1 |
efrain |
1 |
/**
|
|
|
2 |
* @license
|
|
|
3 |
* Video.js 8.10.0 <http://videojs.com/>
|
|
|
4 |
* Copyright Brightcove, Inc. <https://www.brightcove.com/>
|
|
|
5 |
* Available under Apache License Version 2.0
|
|
|
6 |
* <https://github.com/videojs/video.js/blob/main/LICENSE>
|
|
|
7 |
*
|
|
|
8 |
* Includes vtt.js <https://github.com/mozilla/vtt.js>
|
|
|
9 |
* Available under Apache License Version 2.0
|
|
|
10 |
* <https://github.com/mozilla/vtt.js/blob/main/LICENSE>
|
|
|
11 |
*/
|
|
|
12 |
|
|
|
13 |
(function (global, factory) {
|
|
|
14 |
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
|
15 |
typeof define === 'function' && define.amd ? define(factory) :
|
|
|
16 |
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
|
|
|
17 |
})(this, (function () { 'use strict';
|
|
|
18 |
|
|
|
19 |
var version$5 = "8.10.0";
|
|
|
20 |
|
|
|
21 |
/**
|
|
|
22 |
* An Object that contains lifecycle hooks as keys which point to an array
|
|
|
23 |
* of functions that are run when a lifecycle is triggered
|
|
|
24 |
*
|
|
|
25 |
* @private
|
|
|
26 |
*/
|
|
|
27 |
const hooks_ = {};
|
|
|
28 |
|
|
|
29 |
/**
|
|
|
30 |
* Get a list of hooks for a specific lifecycle
|
|
|
31 |
*
|
|
|
32 |
* @param {string} type
|
|
|
33 |
* the lifecycle to get hooks from
|
|
|
34 |
*
|
|
|
35 |
* @param {Function|Function[]} [fn]
|
|
|
36 |
* Optionally add a hook (or hooks) to the lifecycle that your are getting.
|
|
|
37 |
*
|
|
|
38 |
* @return {Array}
|
|
|
39 |
* an array of hooks, or an empty array if there are none.
|
|
|
40 |
*/
|
|
|
41 |
const hooks = function (type, fn) {
|
|
|
42 |
hooks_[type] = hooks_[type] || [];
|
|
|
43 |
if (fn) {
|
|
|
44 |
hooks_[type] = hooks_[type].concat(fn);
|
|
|
45 |
}
|
|
|
46 |
return hooks_[type];
|
|
|
47 |
};
|
|
|
48 |
|
|
|
49 |
/**
|
|
|
50 |
* Add a function hook to a specific videojs lifecycle.
|
|
|
51 |
*
|
|
|
52 |
* @param {string} type
|
|
|
53 |
* the lifecycle to hook the function to.
|
|
|
54 |
*
|
|
|
55 |
* @param {Function|Function[]}
|
|
|
56 |
* The function or array of functions to attach.
|
|
|
57 |
*/
|
|
|
58 |
const hook = function (type, fn) {
|
|
|
59 |
hooks(type, fn);
|
|
|
60 |
};
|
|
|
61 |
|
|
|
62 |
/**
|
|
|
63 |
* Remove a hook from a specific videojs lifecycle.
|
|
|
64 |
*
|
|
|
65 |
* @param {string} type
|
|
|
66 |
* the lifecycle that the function hooked to
|
|
|
67 |
*
|
|
|
68 |
* @param {Function} fn
|
|
|
69 |
* The hooked function to remove
|
|
|
70 |
*
|
|
|
71 |
* @return {boolean}
|
|
|
72 |
* The function that was removed or undef
|
|
|
73 |
*/
|
|
|
74 |
const removeHook = function (type, fn) {
|
|
|
75 |
const index = hooks(type).indexOf(fn);
|
|
|
76 |
if (index <= -1) {
|
|
|
77 |
return false;
|
|
|
78 |
}
|
|
|
79 |
hooks_[type] = hooks_[type].slice();
|
|
|
80 |
hooks_[type].splice(index, 1);
|
|
|
81 |
return true;
|
|
|
82 |
};
|
|
|
83 |
|
|
|
84 |
/**
|
|
|
85 |
* Add a function hook that will only run once to a specific videojs lifecycle.
|
|
|
86 |
*
|
|
|
87 |
* @param {string} type
|
|
|
88 |
* the lifecycle to hook the function to.
|
|
|
89 |
*
|
|
|
90 |
* @param {Function|Function[]}
|
|
|
91 |
* The function or array of functions to attach.
|
|
|
92 |
*/
|
|
|
93 |
const hookOnce = function (type, fn) {
|
|
|
94 |
hooks(type, [].concat(fn).map(original => {
|
|
|
95 |
const wrapper = (...args) => {
|
|
|
96 |
removeHook(type, wrapper);
|
|
|
97 |
return original(...args);
|
|
|
98 |
};
|
|
|
99 |
return wrapper;
|
|
|
100 |
}));
|
|
|
101 |
};
|
|
|
102 |
|
|
|
103 |
/**
|
|
|
104 |
* @file fullscreen-api.js
|
|
|
105 |
* @module fullscreen-api
|
|
|
106 |
*/
|
|
|
107 |
|
|
|
108 |
/**
|
|
|
109 |
* Store the browser-specific methods for the fullscreen API.
|
|
|
110 |
*
|
|
|
111 |
* @type {Object}
|
|
|
112 |
* @see [Specification]{@link https://fullscreen.spec.whatwg.org}
|
|
|
113 |
* @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
|
|
|
114 |
*/
|
|
|
115 |
const FullscreenApi = {
|
|
|
116 |
prefixed: true
|
|
|
117 |
};
|
|
|
118 |
|
|
|
119 |
// browser API methods
|
|
|
120 |
const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
|
|
|
121 |
// WebKit
|
|
|
122 |
['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
|
|
|
123 |
const specApi = apiMap[0];
|
|
|
124 |
let browserApi;
|
|
|
125 |
|
|
|
126 |
// determine the supported set of functions
|
|
|
127 |
for (let i = 0; i < apiMap.length; i++) {
|
|
|
128 |
// check for exitFullscreen function
|
|
|
129 |
if (apiMap[i][1] in document) {
|
|
|
130 |
browserApi = apiMap[i];
|
|
|
131 |
break;
|
|
|
132 |
}
|
|
|
133 |
}
|
|
|
134 |
|
|
|
135 |
// map the browser API names to the spec API names
|
|
|
136 |
if (browserApi) {
|
|
|
137 |
for (let i = 0; i < browserApi.length; i++) {
|
|
|
138 |
FullscreenApi[specApi[i]] = browserApi[i];
|
|
|
139 |
}
|
|
|
140 |
FullscreenApi.prefixed = browserApi[0] !== specApi[0];
|
|
|
141 |
}
|
|
|
142 |
|
|
|
143 |
/**
|
|
|
144 |
* @file create-logger.js
|
|
|
145 |
* @module create-logger
|
|
|
146 |
*/
|
|
|
147 |
|
|
|
148 |
// This is the private tracking variable for the logging history.
|
|
|
149 |
let history = [];
|
|
|
150 |
|
|
|
151 |
/**
|
|
|
152 |
* Log messages to the console and history based on the type of message
|
|
|
153 |
*
|
|
|
154 |
* @private
|
|
|
155 |
* @param {string} name
|
|
|
156 |
* The name of the console method to use.
|
|
|
157 |
*
|
|
|
158 |
* @param {Object} log
|
|
|
159 |
* The arguments to be passed to the matching console method.
|
|
|
160 |
*
|
|
|
161 |
* @param {string} [styles]
|
|
|
162 |
* styles for name
|
|
|
163 |
*/
|
|
|
164 |
const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
|
|
|
165 |
const lvl = log.levels[level];
|
|
|
166 |
const lvlRegExp = new RegExp(`^(${lvl})$`);
|
|
|
167 |
let resultName = name;
|
|
|
168 |
if (type !== 'log') {
|
|
|
169 |
// Add the type to the front of the message when it's not "log".
|
|
|
170 |
args.unshift(type.toUpperCase() + ':');
|
|
|
171 |
}
|
|
|
172 |
if (styles) {
|
|
|
173 |
resultName = `%c${name}`;
|
|
|
174 |
args.unshift(styles);
|
|
|
175 |
}
|
|
|
176 |
|
|
|
177 |
// Add console prefix after adding to history.
|
|
|
178 |
args.unshift(resultName + ':');
|
|
|
179 |
|
|
|
180 |
// Add a clone of the args at this point to history.
|
|
|
181 |
if (history) {
|
|
|
182 |
history.push([].concat(args));
|
|
|
183 |
|
|
|
184 |
// only store 1000 history entries
|
|
|
185 |
const splice = history.length - 1000;
|
|
|
186 |
history.splice(0, splice > 0 ? splice : 0);
|
|
|
187 |
}
|
|
|
188 |
|
|
|
189 |
// If there's no console then don't try to output messages, but they will
|
|
|
190 |
// still be stored in history.
|
|
|
191 |
if (!window.console) {
|
|
|
192 |
return;
|
|
|
193 |
}
|
|
|
194 |
|
|
|
195 |
// Was setting these once outside of this function, but containing them
|
|
|
196 |
// in the function makes it easier to test cases where console doesn't exist
|
|
|
197 |
// when the module is executed.
|
|
|
198 |
let fn = window.console[type];
|
|
|
199 |
if (!fn && type === 'debug') {
|
|
|
200 |
// Certain browsers don't have support for console.debug. For those, we
|
|
|
201 |
// should default to the closest comparable log.
|
|
|
202 |
fn = window.console.info || window.console.log;
|
|
|
203 |
}
|
|
|
204 |
|
|
|
205 |
// Bail out if there's no console or if this type is not allowed by the
|
|
|
206 |
// current logging level.
|
|
|
207 |
if (!fn || !lvl || !lvlRegExp.test(type)) {
|
|
|
208 |
return;
|
|
|
209 |
}
|
|
|
210 |
fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
|
|
|
211 |
};
|
|
|
212 |
function createLogger$1(name, delimiter = ':', styles = '') {
|
|
|
213 |
// This is the private tracking variable for logging level.
|
|
|
214 |
let level = 'info';
|
|
|
215 |
|
|
|
216 |
// the curried logByType bound to the specific log and history
|
|
|
217 |
let logByType;
|
|
|
218 |
|
|
|
219 |
/**
|
|
|
220 |
* Logs plain debug messages. Similar to `console.log`.
|
|
|
221 |
*
|
|
|
222 |
* Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
|
|
|
223 |
* of our JSDoc template, we cannot properly document this as both a function
|
|
|
224 |
* and a namespace, so its function signature is documented here.
|
|
|
225 |
*
|
|
|
226 |
* #### Arguments
|
|
|
227 |
* ##### *args
|
|
|
228 |
* *[]
|
|
|
229 |
*
|
|
|
230 |
* Any combination of values that could be passed to `console.log()`.
|
|
|
231 |
*
|
|
|
232 |
* #### Return Value
|
|
|
233 |
*
|
|
|
234 |
* `undefined`
|
|
|
235 |
*
|
|
|
236 |
* @namespace
|
|
|
237 |
* @param {...*} args
|
|
|
238 |
* One or more messages or objects that should be logged.
|
|
|
239 |
*/
|
|
|
240 |
const log = function (...args) {
|
|
|
241 |
logByType('log', level, args);
|
|
|
242 |
};
|
|
|
243 |
|
|
|
244 |
// This is the logByType helper that the logging methods below use
|
|
|
245 |
logByType = LogByTypeFactory(name, log, styles);
|
|
|
246 |
|
|
|
247 |
/**
|
|
|
248 |
* Create a new subLogger which chains the old name to the new name.
|
|
|
249 |
*
|
|
|
250 |
* For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
|
|
|
251 |
* ```js
|
|
|
252 |
* mylogger('foo');
|
|
|
253 |
* // > VIDEOJS: player: foo
|
|
|
254 |
* ```
|
|
|
255 |
*
|
|
|
256 |
* @param {string} subName
|
|
|
257 |
* The name to add call the new logger
|
|
|
258 |
* @param {string} [subDelimiter]
|
|
|
259 |
* Optional delimiter
|
|
|
260 |
* @param {string} [subStyles]
|
|
|
261 |
* Optional styles
|
|
|
262 |
* @return {Object}
|
|
|
263 |
*/
|
|
|
264 |
log.createLogger = (subName, subDelimiter, subStyles) => {
|
|
|
265 |
const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
|
|
|
266 |
const resultStyles = subStyles !== undefined ? subStyles : styles;
|
|
|
267 |
const resultName = `${name} ${resultDelimiter} ${subName}`;
|
|
|
268 |
return createLogger$1(resultName, resultDelimiter, resultStyles);
|
|
|
269 |
};
|
|
|
270 |
|
|
|
271 |
/**
|
|
|
272 |
* Create a new logger.
|
|
|
273 |
*
|
|
|
274 |
* @param {string} newName
|
|
|
275 |
* The name for the new logger
|
|
|
276 |
* @param {string} [newDelimiter]
|
|
|
277 |
* Optional delimiter
|
|
|
278 |
* @param {string} [newStyles]
|
|
|
279 |
* Optional styles
|
|
|
280 |
* @return {Object}
|
|
|
281 |
*/
|
|
|
282 |
log.createNewLogger = (newName, newDelimiter, newStyles) => {
|
|
|
283 |
return createLogger$1(newName, newDelimiter, newStyles);
|
|
|
284 |
};
|
|
|
285 |
|
|
|
286 |
/**
|
|
|
287 |
* Enumeration of available logging levels, where the keys are the level names
|
|
|
288 |
* and the values are `|`-separated strings containing logging methods allowed
|
|
|
289 |
* in that logging level. These strings are used to create a regular expression
|
|
|
290 |
* matching the function name being called.
|
|
|
291 |
*
|
|
|
292 |
* Levels provided by Video.js are:
|
|
|
293 |
*
|
|
|
294 |
* - `off`: Matches no calls. Any value that can be cast to `false` will have
|
|
|
295 |
* this effect. The most restrictive.
|
|
|
296 |
* - `all`: Matches only Video.js-provided functions (`debug`, `log`,
|
|
|
297 |
* `log.warn`, and `log.error`).
|
|
|
298 |
* - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
|
|
|
299 |
* - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
|
|
|
300 |
* - `warn`: Matches `log.warn` and `log.error` calls.
|
|
|
301 |
* - `error`: Matches only `log.error` calls.
|
|
|
302 |
*
|
|
|
303 |
* @type {Object}
|
|
|
304 |
*/
|
|
|
305 |
log.levels = {
|
|
|
306 |
all: 'debug|log|warn|error',
|
|
|
307 |
off: '',
|
|
|
308 |
debug: 'debug|log|warn|error',
|
|
|
309 |
info: 'log|warn|error',
|
|
|
310 |
warn: 'warn|error',
|
|
|
311 |
error: 'error',
|
|
|
312 |
DEFAULT: level
|
|
|
313 |
};
|
|
|
314 |
|
|
|
315 |
/**
|
|
|
316 |
* Get or set the current logging level.
|
|
|
317 |
*
|
|
|
318 |
* If a string matching a key from {@link module:log.levels} is provided, acts
|
|
|
319 |
* as a setter.
|
|
|
320 |
*
|
|
|
321 |
* @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
|
|
|
322 |
* Pass a valid level to set a new logging level.
|
|
|
323 |
*
|
|
|
324 |
* @return {string}
|
|
|
325 |
* The current logging level.
|
|
|
326 |
*/
|
|
|
327 |
log.level = lvl => {
|
|
|
328 |
if (typeof lvl === 'string') {
|
|
|
329 |
if (!log.levels.hasOwnProperty(lvl)) {
|
|
|
330 |
throw new Error(`"${lvl}" in not a valid log level`);
|
|
|
331 |
}
|
|
|
332 |
level = lvl;
|
|
|
333 |
}
|
|
|
334 |
return level;
|
|
|
335 |
};
|
|
|
336 |
|
|
|
337 |
/**
|
|
|
338 |
* Returns an array containing everything that has been logged to the history.
|
|
|
339 |
*
|
|
|
340 |
* This array is a shallow clone of the internal history record. However, its
|
|
|
341 |
* contents are _not_ cloned; so, mutating objects inside this array will
|
|
|
342 |
* mutate them in history.
|
|
|
343 |
*
|
|
|
344 |
* @return {Array}
|
|
|
345 |
*/
|
|
|
346 |
log.history = () => history ? [].concat(history) : [];
|
|
|
347 |
|
|
|
348 |
/**
|
|
|
349 |
* Allows you to filter the history by the given logger name
|
|
|
350 |
*
|
|
|
351 |
* @param {string} fname
|
|
|
352 |
* The name to filter by
|
|
|
353 |
*
|
|
|
354 |
* @return {Array}
|
|
|
355 |
* The filtered list to return
|
|
|
356 |
*/
|
|
|
357 |
log.history.filter = fname => {
|
|
|
358 |
return (history || []).filter(historyItem => {
|
|
|
359 |
// if the first item in each historyItem includes `fname`, then it's a match
|
|
|
360 |
return new RegExp(`.*${fname}.*`).test(historyItem[0]);
|
|
|
361 |
});
|
|
|
362 |
};
|
|
|
363 |
|
|
|
364 |
/**
|
|
|
365 |
* Clears the internal history tracking, but does not prevent further history
|
|
|
366 |
* tracking.
|
|
|
367 |
*/
|
|
|
368 |
log.history.clear = () => {
|
|
|
369 |
if (history) {
|
|
|
370 |
history.length = 0;
|
|
|
371 |
}
|
|
|
372 |
};
|
|
|
373 |
|
|
|
374 |
/**
|
|
|
375 |
* Disable history tracking if it is currently enabled.
|
|
|
376 |
*/
|
|
|
377 |
log.history.disable = () => {
|
|
|
378 |
if (history !== null) {
|
|
|
379 |
history.length = 0;
|
|
|
380 |
history = null;
|
|
|
381 |
}
|
|
|
382 |
};
|
|
|
383 |
|
|
|
384 |
/**
|
|
|
385 |
* Enable history tracking if it is currently disabled.
|
|
|
386 |
*/
|
|
|
387 |
log.history.enable = () => {
|
|
|
388 |
if (history === null) {
|
|
|
389 |
history = [];
|
|
|
390 |
}
|
|
|
391 |
};
|
|
|
392 |
|
|
|
393 |
/**
|
|
|
394 |
* Logs error messages. Similar to `console.error`.
|
|
|
395 |
*
|
|
|
396 |
* @param {...*} args
|
|
|
397 |
* One or more messages or objects that should be logged as an error
|
|
|
398 |
*/
|
|
|
399 |
log.error = (...args) => logByType('error', level, args);
|
|
|
400 |
|
|
|
401 |
/**
|
|
|
402 |
* Logs warning messages. Similar to `console.warn`.
|
|
|
403 |
*
|
|
|
404 |
* @param {...*} args
|
|
|
405 |
* One or more messages or objects that should be logged as a warning.
|
|
|
406 |
*/
|
|
|
407 |
log.warn = (...args) => logByType('warn', level, args);
|
|
|
408 |
|
|
|
409 |
/**
|
|
|
410 |
* Logs debug messages. Similar to `console.debug`, but may also act as a comparable
|
|
|
411 |
* log if `console.debug` is not available
|
|
|
412 |
*
|
|
|
413 |
* @param {...*} args
|
|
|
414 |
* One or more messages or objects that should be logged as debug.
|
|
|
415 |
*/
|
|
|
416 |
log.debug = (...args) => logByType('debug', level, args);
|
|
|
417 |
return log;
|
|
|
418 |
}
|
|
|
419 |
|
|
|
420 |
/**
|
|
|
421 |
* @file log.js
|
|
|
422 |
* @module log
|
|
|
423 |
*/
|
|
|
424 |
const log$1 = createLogger$1('VIDEOJS');
|
|
|
425 |
const createLogger = log$1.createLogger;
|
|
|
426 |
|
|
|
427 |
/**
|
|
|
428 |
* @file obj.js
|
|
|
429 |
* @module obj
|
|
|
430 |
*/
|
|
|
431 |
|
|
|
432 |
/**
|
|
|
433 |
* @callback obj:EachCallback
|
|
|
434 |
*
|
|
|
435 |
* @param {*} value
|
|
|
436 |
* The current key for the object that is being iterated over.
|
|
|
437 |
*
|
|
|
438 |
* @param {string} key
|
|
|
439 |
* The current key-value for object that is being iterated over
|
|
|
440 |
*/
|
|
|
441 |
|
|
|
442 |
/**
|
|
|
443 |
* @callback obj:ReduceCallback
|
|
|
444 |
*
|
|
|
445 |
* @param {*} accum
|
|
|
446 |
* The value that is accumulating over the reduce loop.
|
|
|
447 |
*
|
|
|
448 |
* @param {*} value
|
|
|
449 |
* The current key for the object that is being iterated over.
|
|
|
450 |
*
|
|
|
451 |
* @param {string} key
|
|
|
452 |
* The current key-value for object that is being iterated over
|
|
|
453 |
*
|
|
|
454 |
* @return {*}
|
|
|
455 |
* The new accumulated value.
|
|
|
456 |
*/
|
|
|
457 |
const toString$1 = Object.prototype.toString;
|
|
|
458 |
|
|
|
459 |
/**
|
|
|
460 |
* Get the keys of an Object
|
|
|
461 |
*
|
|
|
462 |
* @param {Object}
|
|
|
463 |
* The Object to get the keys from
|
|
|
464 |
*
|
|
|
465 |
* @return {string[]}
|
|
|
466 |
* An array of the keys from the object. Returns an empty array if the
|
|
|
467 |
* object passed in was invalid or had no keys.
|
|
|
468 |
*
|
|
|
469 |
* @private
|
|
|
470 |
*/
|
|
|
471 |
const keys = function (object) {
|
|
|
472 |
return isObject$1(object) ? Object.keys(object) : [];
|
|
|
473 |
};
|
|
|
474 |
|
|
|
475 |
/**
|
|
|
476 |
* Array-like iteration for objects.
|
|
|
477 |
*
|
|
|
478 |
* @param {Object} object
|
|
|
479 |
* The object to iterate over
|
|
|
480 |
*
|
|
|
481 |
* @param {obj:EachCallback} fn
|
|
|
482 |
* The callback function which is called for each key in the object.
|
|
|
483 |
*/
|
|
|
484 |
function each(object, fn) {
|
|
|
485 |
keys(object).forEach(key => fn(object[key], key));
|
|
|
486 |
}
|
|
|
487 |
|
|
|
488 |
/**
|
|
|
489 |
* Array-like reduce for objects.
|
|
|
490 |
*
|
|
|
491 |
* @param {Object} object
|
|
|
492 |
* The Object that you want to reduce.
|
|
|
493 |
*
|
|
|
494 |
* @param {Function} fn
|
|
|
495 |
* A callback function which is called for each key in the object. It
|
|
|
496 |
* receives the accumulated value and the per-iteration value and key
|
|
|
497 |
* as arguments.
|
|
|
498 |
*
|
|
|
499 |
* @param {*} [initial = 0]
|
|
|
500 |
* Starting value
|
|
|
501 |
*
|
|
|
502 |
* @return {*}
|
|
|
503 |
* The final accumulated value.
|
|
|
504 |
*/
|
|
|
505 |
function reduce(object, fn, initial = 0) {
|
|
|
506 |
return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
|
|
|
507 |
}
|
|
|
508 |
|
|
|
509 |
/**
|
|
|
510 |
* Returns whether a value is an object of any kind - including DOM nodes,
|
|
|
511 |
* arrays, regular expressions, etc. Not functions, though.
|
|
|
512 |
*
|
|
|
513 |
* This avoids the gotcha where using `typeof` on a `null` value
|
|
|
514 |
* results in `'object'`.
|
|
|
515 |
*
|
|
|
516 |
* @param {Object} value
|
|
|
517 |
* @return {boolean}
|
|
|
518 |
*/
|
|
|
519 |
function isObject$1(value) {
|
|
|
520 |
return !!value && typeof value === 'object';
|
|
|
521 |
}
|
|
|
522 |
|
|
|
523 |
/**
|
|
|
524 |
* Returns whether an object appears to be a "plain" object - that is, a
|
|
|
525 |
* direct instance of `Object`.
|
|
|
526 |
*
|
|
|
527 |
* @param {Object} value
|
|
|
528 |
* @return {boolean}
|
|
|
529 |
*/
|
|
|
530 |
function isPlain(value) {
|
|
|
531 |
return isObject$1(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object;
|
|
|
532 |
}
|
|
|
533 |
|
|
|
534 |
/**
|
|
|
535 |
* Merge two objects recursively.
|
|
|
536 |
*
|
|
|
537 |
* Performs a deep merge like
|
|
|
538 |
* {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
|
|
|
539 |
* plain objects (not arrays, elements, or anything else).
|
|
|
540 |
*
|
|
|
541 |
* Non-plain object values will be copied directly from the right-most
|
|
|
542 |
* argument.
|
|
|
543 |
*
|
|
|
544 |
* @param {Object[]} sources
|
|
|
545 |
* One or more objects to merge into a new object.
|
|
|
546 |
*
|
|
|
547 |
* @return {Object}
|
|
|
548 |
* A new object that is the merged result of all sources.
|
|
|
549 |
*/
|
|
|
550 |
function merge$2(...sources) {
|
|
|
551 |
const result = {};
|
|
|
552 |
sources.forEach(source => {
|
|
|
553 |
if (!source) {
|
|
|
554 |
return;
|
|
|
555 |
}
|
|
|
556 |
each(source, (value, key) => {
|
|
|
557 |
if (!isPlain(value)) {
|
|
|
558 |
result[key] = value;
|
|
|
559 |
return;
|
|
|
560 |
}
|
|
|
561 |
if (!isPlain(result[key])) {
|
|
|
562 |
result[key] = {};
|
|
|
563 |
}
|
|
|
564 |
result[key] = merge$2(result[key], value);
|
|
|
565 |
});
|
|
|
566 |
});
|
|
|
567 |
return result;
|
|
|
568 |
}
|
|
|
569 |
|
|
|
570 |
/**
|
|
|
571 |
* Returns an array of values for a given object
|
|
|
572 |
*
|
|
|
573 |
* @param {Object} source - target object
|
|
|
574 |
* @return {Array<unknown>} - object values
|
|
|
575 |
*/
|
|
|
576 |
function values$1(source = {}) {
|
|
|
577 |
const result = [];
|
|
|
578 |
for (const key in source) {
|
|
|
579 |
if (source.hasOwnProperty(key)) {
|
|
|
580 |
const value = source[key];
|
|
|
581 |
result.push(value);
|
|
|
582 |
}
|
|
|
583 |
}
|
|
|
584 |
return result;
|
|
|
585 |
}
|
|
|
586 |
|
|
|
587 |
/**
|
|
|
588 |
* Object.defineProperty but "lazy", which means that the value is only set after
|
|
|
589 |
* it is retrieved the first time, rather than being set right away.
|
|
|
590 |
*
|
|
|
591 |
* @param {Object} obj the object to set the property on
|
|
|
592 |
* @param {string} key the key for the property to set
|
|
|
593 |
* @param {Function} getValue the function used to get the value when it is needed.
|
|
|
594 |
* @param {boolean} setter whether a setter should be allowed or not
|
|
|
595 |
*/
|
|
|
596 |
function defineLazyProperty(obj, key, getValue, setter = true) {
|
|
|
597 |
const set = value => Object.defineProperty(obj, key, {
|
|
|
598 |
value,
|
|
|
599 |
enumerable: true,
|
|
|
600 |
writable: true
|
|
|
601 |
});
|
|
|
602 |
const options = {
|
|
|
603 |
configurable: true,
|
|
|
604 |
enumerable: true,
|
|
|
605 |
get() {
|
|
|
606 |
const value = getValue();
|
|
|
607 |
set(value);
|
|
|
608 |
return value;
|
|
|
609 |
}
|
|
|
610 |
};
|
|
|
611 |
if (setter) {
|
|
|
612 |
options.set = set;
|
|
|
613 |
}
|
|
|
614 |
return Object.defineProperty(obj, key, options);
|
|
|
615 |
}
|
|
|
616 |
|
|
|
617 |
var Obj = /*#__PURE__*/Object.freeze({
|
|
|
618 |
__proto__: null,
|
|
|
619 |
each: each,
|
|
|
620 |
reduce: reduce,
|
|
|
621 |
isObject: isObject$1,
|
|
|
622 |
isPlain: isPlain,
|
|
|
623 |
merge: merge$2,
|
|
|
624 |
values: values$1,
|
|
|
625 |
defineLazyProperty: defineLazyProperty
|
|
|
626 |
});
|
|
|
627 |
|
|
|
628 |
/**
|
|
|
629 |
* @file browser.js
|
|
|
630 |
* @module browser
|
|
|
631 |
*/
|
|
|
632 |
|
|
|
633 |
/**
|
|
|
634 |
* Whether or not this device is an iPod.
|
|
|
635 |
*
|
|
|
636 |
* @static
|
|
|
637 |
* @type {Boolean}
|
|
|
638 |
*/
|
|
|
639 |
let IS_IPOD = false;
|
|
|
640 |
|
|
|
641 |
/**
|
|
|
642 |
* The detected iOS version - or `null`.
|
|
|
643 |
*
|
|
|
644 |
* @static
|
|
|
645 |
* @type {string|null}
|
|
|
646 |
*/
|
|
|
647 |
let IOS_VERSION = null;
|
|
|
648 |
|
|
|
649 |
/**
|
|
|
650 |
* Whether or not this is an Android device.
|
|
|
651 |
*
|
|
|
652 |
* @static
|
|
|
653 |
* @type {Boolean}
|
|
|
654 |
*/
|
|
|
655 |
let IS_ANDROID = false;
|
|
|
656 |
|
|
|
657 |
/**
|
|
|
658 |
* The detected Android version - or `null` if not Android or indeterminable.
|
|
|
659 |
*
|
|
|
660 |
* @static
|
|
|
661 |
* @type {number|string|null}
|
|
|
662 |
*/
|
|
|
663 |
let ANDROID_VERSION;
|
|
|
664 |
|
|
|
665 |
/**
|
|
|
666 |
* Whether or not this is Mozilla Firefox.
|
|
|
667 |
*
|
|
|
668 |
* @static
|
|
|
669 |
* @type {Boolean}
|
|
|
670 |
*/
|
|
|
671 |
let IS_FIREFOX = false;
|
|
|
672 |
|
|
|
673 |
/**
|
|
|
674 |
* Whether or not this is Microsoft Edge.
|
|
|
675 |
*
|
|
|
676 |
* @static
|
|
|
677 |
* @type {Boolean}
|
|
|
678 |
*/
|
|
|
679 |
let IS_EDGE = false;
|
|
|
680 |
|
|
|
681 |
/**
|
|
|
682 |
* Whether or not this is any Chromium Browser
|
|
|
683 |
*
|
|
|
684 |
* @static
|
|
|
685 |
* @type {Boolean}
|
|
|
686 |
*/
|
|
|
687 |
let IS_CHROMIUM = false;
|
|
|
688 |
|
|
|
689 |
/**
|
|
|
690 |
* Whether or not this is any Chromium browser that is not Edge.
|
|
|
691 |
*
|
|
|
692 |
* This will also be `true` for Chrome on iOS, which will have different support
|
|
|
693 |
* as it is actually Safari under the hood.
|
|
|
694 |
*
|
|
|
695 |
* Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
|
|
|
696 |
* IS_CHROMIUM should be used instead.
|
|
|
697 |
* "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
|
|
|
698 |
*
|
|
|
699 |
* @static
|
|
|
700 |
* @deprecated
|
|
|
701 |
* @type {Boolean}
|
|
|
702 |
*/
|
|
|
703 |
let IS_CHROME = false;
|
|
|
704 |
|
|
|
705 |
/**
|
|
|
706 |
* The detected Chromium version - or `null`.
|
|
|
707 |
*
|
|
|
708 |
* @static
|
|
|
709 |
* @type {number|null}
|
|
|
710 |
*/
|
|
|
711 |
let CHROMIUM_VERSION = null;
|
|
|
712 |
|
|
|
713 |
/**
|
|
|
714 |
* The detected Google Chrome version - or `null`.
|
|
|
715 |
* This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
|
|
|
716 |
* Deprecated, use CHROMIUM_VERSION instead.
|
|
|
717 |
*
|
|
|
718 |
* @static
|
|
|
719 |
* @deprecated
|
|
|
720 |
* @type {number|null}
|
|
|
721 |
*/
|
|
|
722 |
let CHROME_VERSION = null;
|
|
|
723 |
|
|
|
724 |
/**
|
|
|
725 |
* The detected Internet Explorer version - or `null`.
|
|
|
726 |
*
|
|
|
727 |
* @static
|
|
|
728 |
* @deprecated
|
|
|
729 |
* @type {number|null}
|
|
|
730 |
*/
|
|
|
731 |
let IE_VERSION = null;
|
|
|
732 |
|
|
|
733 |
/**
|
|
|
734 |
* Whether or not this is desktop Safari.
|
|
|
735 |
*
|
|
|
736 |
* @static
|
|
|
737 |
* @type {Boolean}
|
|
|
738 |
*/
|
|
|
739 |
let IS_SAFARI = false;
|
|
|
740 |
|
|
|
741 |
/**
|
|
|
742 |
* Whether or not this is a Windows machine.
|
|
|
743 |
*
|
|
|
744 |
* @static
|
|
|
745 |
* @type {Boolean}
|
|
|
746 |
*/
|
|
|
747 |
let IS_WINDOWS = false;
|
|
|
748 |
|
|
|
749 |
/**
|
|
|
750 |
* Whether or not this device is an iPad.
|
|
|
751 |
*
|
|
|
752 |
* @static
|
|
|
753 |
* @type {Boolean}
|
|
|
754 |
*/
|
|
|
755 |
let IS_IPAD = false;
|
|
|
756 |
|
|
|
757 |
/**
|
|
|
758 |
* Whether or not this device is an iPhone.
|
|
|
759 |
*
|
|
|
760 |
* @static
|
|
|
761 |
* @type {Boolean}
|
|
|
762 |
*/
|
|
|
763 |
// The Facebook app's UIWebView identifies as both an iPhone and iPad, so
|
|
|
764 |
// to identify iPhones, we need to exclude iPads.
|
|
|
765 |
// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
|
|
|
766 |
let IS_IPHONE = false;
|
|
|
767 |
|
|
|
768 |
/**
|
|
|
769 |
* Whether or not this device is touch-enabled.
|
|
|
770 |
*
|
|
|
771 |
* @static
|
|
|
772 |
* @const
|
|
|
773 |
* @type {Boolean}
|
|
|
774 |
*/
|
|
|
775 |
const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch));
|
|
|
776 |
const UAD = window.navigator && window.navigator.userAgentData;
|
|
|
777 |
if (UAD && UAD.platform && UAD.brands) {
|
|
|
778 |
// If userAgentData is present, use it instead of userAgent to avoid warnings
|
|
|
779 |
// Currently only implemented on Chromium
|
|
|
780 |
// userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
|
|
|
781 |
|
|
|
782 |
IS_ANDROID = UAD.platform === 'Android';
|
|
|
783 |
IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
|
|
|
784 |
IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
|
|
|
785 |
IS_CHROME = !IS_EDGE && IS_CHROMIUM;
|
|
|
786 |
CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
|
|
|
787 |
IS_WINDOWS = UAD.platform === 'Windows';
|
|
|
788 |
}
|
|
|
789 |
|
|
|
790 |
// If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
|
|
|
791 |
// or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
|
|
|
792 |
// the checks need to be made agiainst the regular userAgent string.
|
|
|
793 |
if (!IS_CHROMIUM) {
|
|
|
794 |
const USER_AGENT = window.navigator && window.navigator.userAgent || '';
|
|
|
795 |
IS_IPOD = /iPod/i.test(USER_AGENT);
|
|
|
796 |
IOS_VERSION = function () {
|
|
|
797 |
const match = USER_AGENT.match(/OS (\d+)_/i);
|
|
|
798 |
if (match && match[1]) {
|
|
|
799 |
return match[1];
|
|
|
800 |
}
|
|
|
801 |
return null;
|
|
|
802 |
}();
|
|
|
803 |
IS_ANDROID = /Android/i.test(USER_AGENT);
|
|
|
804 |
ANDROID_VERSION = function () {
|
|
|
805 |
// This matches Android Major.Minor.Patch versions
|
|
|
806 |
// ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
|
|
|
807 |
const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
|
|
|
808 |
if (!match) {
|
|
|
809 |
return null;
|
|
|
810 |
}
|
|
|
811 |
const major = match[1] && parseFloat(match[1]);
|
|
|
812 |
const minor = match[2] && parseFloat(match[2]);
|
|
|
813 |
if (major && minor) {
|
|
|
814 |
return parseFloat(match[1] + '.' + match[2]);
|
|
|
815 |
} else if (major) {
|
|
|
816 |
return major;
|
|
|
817 |
}
|
|
|
818 |
return null;
|
|
|
819 |
}();
|
|
|
820 |
IS_FIREFOX = /Firefox/i.test(USER_AGENT);
|
|
|
821 |
IS_EDGE = /Edg/i.test(USER_AGENT);
|
|
|
822 |
IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
|
|
|
823 |
IS_CHROME = !IS_EDGE && IS_CHROMIUM;
|
|
|
824 |
CHROMIUM_VERSION = CHROME_VERSION = function () {
|
|
|
825 |
const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
|
|
|
826 |
if (match && match[2]) {
|
|
|
827 |
return parseFloat(match[2]);
|
|
|
828 |
}
|
|
|
829 |
return null;
|
|
|
830 |
}();
|
|
|
831 |
IE_VERSION = function () {
|
|
|
832 |
const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
|
|
|
833 |
let version = result && parseFloat(result[1]);
|
|
|
834 |
if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
|
|
|
835 |
// IE 11 has a different user agent string than other IE versions
|
|
|
836 |
version = 11.0;
|
|
|
837 |
}
|
|
|
838 |
return version;
|
|
|
839 |
}();
|
|
|
840 |
IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE;
|
|
|
841 |
IS_WINDOWS = /Windows/i.test(USER_AGENT);
|
|
|
842 |
IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
|
|
|
843 |
IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
|
|
|
844 |
}
|
|
|
845 |
|
|
|
846 |
/**
|
|
|
847 |
* Whether or not this is an iOS device.
|
|
|
848 |
*
|
|
|
849 |
* @static
|
|
|
850 |
* @const
|
|
|
851 |
* @type {Boolean}
|
|
|
852 |
*/
|
|
|
853 |
const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
|
|
|
854 |
|
|
|
855 |
/**
|
|
|
856 |
* Whether or not this is any flavor of Safari - including iOS.
|
|
|
857 |
*
|
|
|
858 |
* @static
|
|
|
859 |
* @const
|
|
|
860 |
* @type {Boolean}
|
|
|
861 |
*/
|
|
|
862 |
const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
|
|
|
863 |
|
|
|
864 |
var browser = /*#__PURE__*/Object.freeze({
|
|
|
865 |
__proto__: null,
|
|
|
866 |
get IS_IPOD () { return IS_IPOD; },
|
|
|
867 |
get IOS_VERSION () { return IOS_VERSION; },
|
|
|
868 |
get IS_ANDROID () { return IS_ANDROID; },
|
|
|
869 |
get ANDROID_VERSION () { return ANDROID_VERSION; },
|
|
|
870 |
get IS_FIREFOX () { return IS_FIREFOX; },
|
|
|
871 |
get IS_EDGE () { return IS_EDGE; },
|
|
|
872 |
get IS_CHROMIUM () { return IS_CHROMIUM; },
|
|
|
873 |
get IS_CHROME () { return IS_CHROME; },
|
|
|
874 |
get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
|
|
|
875 |
get CHROME_VERSION () { return CHROME_VERSION; },
|
|
|
876 |
get IE_VERSION () { return IE_VERSION; },
|
|
|
877 |
get IS_SAFARI () { return IS_SAFARI; },
|
|
|
878 |
get IS_WINDOWS () { return IS_WINDOWS; },
|
|
|
879 |
get IS_IPAD () { return IS_IPAD; },
|
|
|
880 |
get IS_IPHONE () { return IS_IPHONE; },
|
|
|
881 |
TOUCH_ENABLED: TOUCH_ENABLED,
|
|
|
882 |
IS_IOS: IS_IOS,
|
|
|
883 |
IS_ANY_SAFARI: IS_ANY_SAFARI
|
|
|
884 |
});
|
|
|
885 |
|
|
|
886 |
/**
|
|
|
887 |
* @file dom.js
|
|
|
888 |
* @module dom
|
|
|
889 |
*/
|
|
|
890 |
|
|
|
891 |
/**
|
|
|
892 |
* Detect if a value is a string with any non-whitespace characters.
|
|
|
893 |
*
|
|
|
894 |
* @private
|
|
|
895 |
* @param {string} str
|
|
|
896 |
* The string to check
|
|
|
897 |
*
|
|
|
898 |
* @return {boolean}
|
|
|
899 |
* Will be `true` if the string is non-blank, `false` otherwise.
|
|
|
900 |
*
|
|
|
901 |
*/
|
|
|
902 |
function isNonBlankString(str) {
|
|
|
903 |
// we use str.trim as it will trim any whitespace characters
|
|
|
904 |
// from the front or back of non-whitespace characters. aka
|
|
|
905 |
// Any string that contains non-whitespace characters will
|
|
|
906 |
// still contain them after `trim` but whitespace only strings
|
|
|
907 |
// will have a length of 0, failing this check.
|
|
|
908 |
return typeof str === 'string' && Boolean(str.trim());
|
|
|
909 |
}
|
|
|
910 |
|
|
|
911 |
/**
|
|
|
912 |
* Throws an error if the passed string has whitespace. This is used by
|
|
|
913 |
* class methods to be relatively consistent with the classList API.
|
|
|
914 |
*
|
|
|
915 |
* @private
|
|
|
916 |
* @param {string} str
|
|
|
917 |
* The string to check for whitespace.
|
|
|
918 |
*
|
|
|
919 |
* @throws {Error}
|
|
|
920 |
* Throws an error if there is whitespace in the string.
|
|
|
921 |
*/
|
|
|
922 |
function throwIfWhitespace(str) {
|
|
|
923 |
// str.indexOf instead of regex because str.indexOf is faster performance wise.
|
|
|
924 |
if (str.indexOf(' ') >= 0) {
|
|
|
925 |
throw new Error('class has illegal whitespace characters');
|
|
|
926 |
}
|
|
|
927 |
}
|
|
|
928 |
|
|
|
929 |
/**
|
|
|
930 |
* Whether the current DOM interface appears to be real (i.e. not simulated).
|
|
|
931 |
*
|
|
|
932 |
* @return {boolean}
|
|
|
933 |
* Will be `true` if the DOM appears to be real, `false` otherwise.
|
|
|
934 |
*/
|
|
|
935 |
function isReal() {
|
|
|
936 |
// Both document and window will never be undefined thanks to `global`.
|
|
|
937 |
return document === window.document;
|
|
|
938 |
}
|
|
|
939 |
|
|
|
940 |
/**
|
|
|
941 |
* Determines, via duck typing, whether or not a value is a DOM element.
|
|
|
942 |
*
|
|
|
943 |
* @param {*} value
|
|
|
944 |
* The value to check.
|
|
|
945 |
*
|
|
|
946 |
* @return {boolean}
|
|
|
947 |
* Will be `true` if the value is a DOM element, `false` otherwise.
|
|
|
948 |
*/
|
|
|
949 |
function isEl(value) {
|
|
|
950 |
return isObject$1(value) && value.nodeType === 1;
|
|
|
951 |
}
|
|
|
952 |
|
|
|
953 |
/**
|
|
|
954 |
* Determines if the current DOM is embedded in an iframe.
|
|
|
955 |
*
|
|
|
956 |
* @return {boolean}
|
|
|
957 |
* Will be `true` if the DOM is embedded in an iframe, `false`
|
|
|
958 |
* otherwise.
|
|
|
959 |
*/
|
|
|
960 |
function isInFrame() {
|
|
|
961 |
// We need a try/catch here because Safari will throw errors when attempting
|
|
|
962 |
// to get either `parent` or `self`
|
|
|
963 |
try {
|
|
|
964 |
return window.parent !== window.self;
|
|
|
965 |
} catch (x) {
|
|
|
966 |
return true;
|
|
|
967 |
}
|
|
|
968 |
}
|
|
|
969 |
|
|
|
970 |
/**
|
|
|
971 |
* Creates functions to query the DOM using a given method.
|
|
|
972 |
*
|
|
|
973 |
* @private
|
|
|
974 |
* @param {string} method
|
|
|
975 |
* The method to create the query with.
|
|
|
976 |
*
|
|
|
977 |
* @return {Function}
|
|
|
978 |
* The query method
|
|
|
979 |
*/
|
|
|
980 |
function createQuerier(method) {
|
|
|
981 |
return function (selector, context) {
|
|
|
982 |
if (!isNonBlankString(selector)) {
|
|
|
983 |
return document[method](null);
|
|
|
984 |
}
|
|
|
985 |
if (isNonBlankString(context)) {
|
|
|
986 |
context = document.querySelector(context);
|
|
|
987 |
}
|
|
|
988 |
const ctx = isEl(context) ? context : document;
|
|
|
989 |
return ctx[method] && ctx[method](selector);
|
|
|
990 |
};
|
|
|
991 |
}
|
|
|
992 |
|
|
|
993 |
/**
|
|
|
994 |
* Creates an element and applies properties, attributes, and inserts content.
|
|
|
995 |
*
|
|
|
996 |
* @param {string} [tagName='div']
|
|
|
997 |
* Name of tag to be created.
|
|
|
998 |
*
|
|
|
999 |
* @param {Object} [properties={}]
|
|
|
1000 |
* Element properties to be applied.
|
|
|
1001 |
*
|
|
|
1002 |
* @param {Object} [attributes={}]
|
|
|
1003 |
* Element attributes to be applied.
|
|
|
1004 |
*
|
|
|
1005 |
* @param {ContentDescriptor} [content]
|
|
|
1006 |
* A content descriptor object.
|
|
|
1007 |
*
|
|
|
1008 |
* @return {Element}
|
|
|
1009 |
* The element that was created.
|
|
|
1010 |
*/
|
|
|
1011 |
function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
|
|
|
1012 |
const el = document.createElement(tagName);
|
|
|
1013 |
Object.getOwnPropertyNames(properties).forEach(function (propName) {
|
|
|
1014 |
const val = properties[propName];
|
|
|
1015 |
|
|
|
1016 |
// Handle textContent since it's not supported everywhere and we have a
|
|
|
1017 |
// method for it.
|
|
|
1018 |
if (propName === 'textContent') {
|
|
|
1019 |
textContent(el, val);
|
|
|
1020 |
} else if (el[propName] !== val || propName === 'tabIndex') {
|
|
|
1021 |
el[propName] = val;
|
|
|
1022 |
}
|
|
|
1023 |
});
|
|
|
1024 |
Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
|
|
|
1025 |
el.setAttribute(attrName, attributes[attrName]);
|
|
|
1026 |
});
|
|
|
1027 |
if (content) {
|
|
|
1028 |
appendContent(el, content);
|
|
|
1029 |
}
|
|
|
1030 |
return el;
|
|
|
1031 |
}
|
|
|
1032 |
|
|
|
1033 |
/**
|
|
|
1034 |
* Injects text into an element, replacing any existing contents entirely.
|
|
|
1035 |
*
|
|
|
1036 |
* @param {HTMLElement} el
|
|
|
1037 |
* The element to add text content into
|
|
|
1038 |
*
|
|
|
1039 |
* @param {string} text
|
|
|
1040 |
* The text content to add.
|
|
|
1041 |
*
|
|
|
1042 |
* @return {Element}
|
|
|
1043 |
* The element with added text content.
|
|
|
1044 |
*/
|
|
|
1045 |
function textContent(el, text) {
|
|
|
1046 |
if (typeof el.textContent === 'undefined') {
|
|
|
1047 |
el.innerText = text;
|
|
|
1048 |
} else {
|
|
|
1049 |
el.textContent = text;
|
|
|
1050 |
}
|
|
|
1051 |
return el;
|
|
|
1052 |
}
|
|
|
1053 |
|
|
|
1054 |
/**
|
|
|
1055 |
* Insert an element as the first child node of another
|
|
|
1056 |
*
|
|
|
1057 |
* @param {Element} child
|
|
|
1058 |
* Element to insert
|
|
|
1059 |
*
|
|
|
1060 |
* @param {Element} parent
|
|
|
1061 |
* Element to insert child into
|
|
|
1062 |
*/
|
|
|
1063 |
function prependTo(child, parent) {
|
|
|
1064 |
if (parent.firstChild) {
|
|
|
1065 |
parent.insertBefore(child, parent.firstChild);
|
|
|
1066 |
} else {
|
|
|
1067 |
parent.appendChild(child);
|
|
|
1068 |
}
|
|
|
1069 |
}
|
|
|
1070 |
|
|
|
1071 |
/**
|
|
|
1072 |
* Check if an element has a class name.
|
|
|
1073 |
*
|
|
|
1074 |
* @param {Element} element
|
|
|
1075 |
* Element to check
|
|
|
1076 |
*
|
|
|
1077 |
* @param {string} classToCheck
|
|
|
1078 |
* Class name to check for
|
|
|
1079 |
*
|
|
|
1080 |
* @return {boolean}
|
|
|
1081 |
* Will be `true` if the element has a class, `false` otherwise.
|
|
|
1082 |
*
|
|
|
1083 |
* @throws {Error}
|
|
|
1084 |
* Throws an error if `classToCheck` has white space.
|
|
|
1085 |
*/
|
|
|
1086 |
function hasClass(element, classToCheck) {
|
|
|
1087 |
throwIfWhitespace(classToCheck);
|
|
|
1088 |
return element.classList.contains(classToCheck);
|
|
|
1089 |
}
|
|
|
1090 |
|
|
|
1091 |
/**
|
|
|
1092 |
* Add a class name to an element.
|
|
|
1093 |
*
|
|
|
1094 |
* @param {Element} element
|
|
|
1095 |
* Element to add class name to.
|
|
|
1096 |
*
|
|
|
1097 |
* @param {...string} classesToAdd
|
|
|
1098 |
* One or more class name to add.
|
|
|
1099 |
*
|
|
|
1100 |
* @return {Element}
|
|
|
1101 |
* The DOM element with the added class name.
|
|
|
1102 |
*/
|
|
|
1103 |
function addClass(element, ...classesToAdd) {
|
|
|
1104 |
element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
|
|
|
1105 |
return element;
|
|
|
1106 |
}
|
|
|
1107 |
|
|
|
1108 |
/**
|
|
|
1109 |
* Remove a class name from an element.
|
|
|
1110 |
*
|
|
|
1111 |
* @param {Element} element
|
|
|
1112 |
* Element to remove a class name from.
|
|
|
1113 |
*
|
|
|
1114 |
* @param {...string} classesToRemove
|
|
|
1115 |
* One or more class name to remove.
|
|
|
1116 |
*
|
|
|
1117 |
* @return {Element}
|
|
|
1118 |
* The DOM element with class name removed.
|
|
|
1119 |
*/
|
|
|
1120 |
function removeClass(element, ...classesToRemove) {
|
|
|
1121 |
// Protect in case the player gets disposed
|
|
|
1122 |
if (!element) {
|
|
|
1123 |
log$1.warn("removeClass was called with an element that doesn't exist");
|
|
|
1124 |
return null;
|
|
|
1125 |
}
|
|
|
1126 |
element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
|
|
|
1127 |
return element;
|
|
|
1128 |
}
|
|
|
1129 |
|
|
|
1130 |
/**
|
|
|
1131 |
* The callback definition for toggleClass.
|
|
|
1132 |
*
|
|
|
1133 |
* @callback module:dom~PredicateCallback
|
|
|
1134 |
* @param {Element} element
|
|
|
1135 |
* The DOM element of the Component.
|
|
|
1136 |
*
|
|
|
1137 |
* @param {string} classToToggle
|
|
|
1138 |
* The `className` that wants to be toggled
|
|
|
1139 |
*
|
|
|
1140 |
* @return {boolean|undefined}
|
|
|
1141 |
* If `true` is returned, the `classToToggle` will be added to the
|
|
|
1142 |
* `element`. If `false`, the `classToToggle` will be removed from
|
|
|
1143 |
* the `element`. If `undefined`, the callback will be ignored.
|
|
|
1144 |
*/
|
|
|
1145 |
|
|
|
1146 |
/**
|
|
|
1147 |
* Adds or removes a class name to/from an element depending on an optional
|
|
|
1148 |
* condition or the presence/absence of the class name.
|
|
|
1149 |
*
|
|
|
1150 |
* @param {Element} element
|
|
|
1151 |
* The element to toggle a class name on.
|
|
|
1152 |
*
|
|
|
1153 |
* @param {string} classToToggle
|
|
|
1154 |
* The class that should be toggled.
|
|
|
1155 |
*
|
|
|
1156 |
* @param {boolean|module:dom~PredicateCallback} [predicate]
|
|
|
1157 |
* See the return value for {@link module:dom~PredicateCallback}
|
|
|
1158 |
*
|
|
|
1159 |
* @return {Element}
|
|
|
1160 |
* The element with a class that has been toggled.
|
|
|
1161 |
*/
|
|
|
1162 |
function toggleClass(element, classToToggle, predicate) {
|
|
|
1163 |
if (typeof predicate === 'function') {
|
|
|
1164 |
predicate = predicate(element, classToToggle);
|
|
|
1165 |
}
|
|
|
1166 |
if (typeof predicate !== 'boolean') {
|
|
|
1167 |
predicate = undefined;
|
|
|
1168 |
}
|
|
|
1169 |
classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
|
|
|
1170 |
return element;
|
|
|
1171 |
}
|
|
|
1172 |
|
|
|
1173 |
/**
|
|
|
1174 |
* Apply attributes to an HTML element.
|
|
|
1175 |
*
|
|
|
1176 |
* @param {Element} el
|
|
|
1177 |
* Element to add attributes to.
|
|
|
1178 |
*
|
|
|
1179 |
* @param {Object} [attributes]
|
|
|
1180 |
* Attributes to be applied.
|
|
|
1181 |
*/
|
|
|
1182 |
function setAttributes(el, attributes) {
|
|
|
1183 |
Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
|
|
|
1184 |
const attrValue = attributes[attrName];
|
|
|
1185 |
if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
|
|
|
1186 |
el.removeAttribute(attrName);
|
|
|
1187 |
} else {
|
|
|
1188 |
el.setAttribute(attrName, attrValue === true ? '' : attrValue);
|
|
|
1189 |
}
|
|
|
1190 |
});
|
|
|
1191 |
}
|
|
|
1192 |
|
|
|
1193 |
/**
|
|
|
1194 |
* Get an element's attribute values, as defined on the HTML tag.
|
|
|
1195 |
*
|
|
|
1196 |
* Attributes are not the same as properties. They're defined on the tag
|
|
|
1197 |
* or with setAttribute.
|
|
|
1198 |
*
|
|
|
1199 |
* @param {Element} tag
|
|
|
1200 |
* Element from which to get tag attributes.
|
|
|
1201 |
*
|
|
|
1202 |
* @return {Object}
|
|
|
1203 |
* All attributes of the element. Boolean attributes will be `true` or
|
|
|
1204 |
* `false`, others will be strings.
|
|
|
1205 |
*/
|
|
|
1206 |
function getAttributes(tag) {
|
|
|
1207 |
const obj = {};
|
|
|
1208 |
|
|
|
1209 |
// known boolean attributes
|
|
|
1210 |
// we can check for matching boolean properties, but not all browsers
|
|
|
1211 |
// and not all tags know about these attributes, so, we still want to check them manually
|
|
|
1212 |
const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
|
|
|
1213 |
if (tag && tag.attributes && tag.attributes.length > 0) {
|
|
|
1214 |
const attrs = tag.attributes;
|
|
|
1215 |
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
|
1216 |
const attrName = attrs[i].name;
|
|
|
1217 |
/** @type {boolean|string} */
|
|
|
1218 |
let attrVal = attrs[i].value;
|
|
|
1219 |
|
|
|
1220 |
// check for known booleans
|
|
|
1221 |
// the matching element property will return a value for typeof
|
|
|
1222 |
if (knownBooleans.includes(attrName)) {
|
|
|
1223 |
// the value of an included boolean attribute is typically an empty
|
|
|
1224 |
// string ('') which would equal false if we just check for a false value.
|
|
|
1225 |
// we also don't want support bad code like autoplay='false'
|
|
|
1226 |
attrVal = attrVal !== null ? true : false;
|
|
|
1227 |
}
|
|
|
1228 |
obj[attrName] = attrVal;
|
|
|
1229 |
}
|
|
|
1230 |
}
|
|
|
1231 |
return obj;
|
|
|
1232 |
}
|
|
|
1233 |
|
|
|
1234 |
/**
|
|
|
1235 |
* Get the value of an element's attribute.
|
|
|
1236 |
*
|
|
|
1237 |
* @param {Element} el
|
|
|
1238 |
* A DOM element.
|
|
|
1239 |
*
|
|
|
1240 |
* @param {string} attribute
|
|
|
1241 |
* Attribute to get the value of.
|
|
|
1242 |
*
|
|
|
1243 |
* @return {string}
|
|
|
1244 |
* The value of the attribute.
|
|
|
1245 |
*/
|
|
|
1246 |
function getAttribute(el, attribute) {
|
|
|
1247 |
return el.getAttribute(attribute);
|
|
|
1248 |
}
|
|
|
1249 |
|
|
|
1250 |
/**
|
|
|
1251 |
* Set the value of an element's attribute.
|
|
|
1252 |
*
|
|
|
1253 |
* @param {Element} el
|
|
|
1254 |
* A DOM element.
|
|
|
1255 |
*
|
|
|
1256 |
* @param {string} attribute
|
|
|
1257 |
* Attribute to set.
|
|
|
1258 |
*
|
|
|
1259 |
* @param {string} value
|
|
|
1260 |
* Value to set the attribute to.
|
|
|
1261 |
*/
|
|
|
1262 |
function setAttribute(el, attribute, value) {
|
|
|
1263 |
el.setAttribute(attribute, value);
|
|
|
1264 |
}
|
|
|
1265 |
|
|
|
1266 |
/**
|
|
|
1267 |
* Remove an element's attribute.
|
|
|
1268 |
*
|
|
|
1269 |
* @param {Element} el
|
|
|
1270 |
* A DOM element.
|
|
|
1271 |
*
|
|
|
1272 |
* @param {string} attribute
|
|
|
1273 |
* Attribute to remove.
|
|
|
1274 |
*/
|
|
|
1275 |
function removeAttribute(el, attribute) {
|
|
|
1276 |
el.removeAttribute(attribute);
|
|
|
1277 |
}
|
|
|
1278 |
|
|
|
1279 |
/**
|
|
|
1280 |
* Attempt to block the ability to select text.
|
|
|
1281 |
*/
|
|
|
1282 |
function blockTextSelection() {
|
|
|
1283 |
document.body.focus();
|
|
|
1284 |
document.onselectstart = function () {
|
|
|
1285 |
return false;
|
|
|
1286 |
};
|
|
|
1287 |
}
|
|
|
1288 |
|
|
|
1289 |
/**
|
|
|
1290 |
* Turn off text selection blocking.
|
|
|
1291 |
*/
|
|
|
1292 |
function unblockTextSelection() {
|
|
|
1293 |
document.onselectstart = function () {
|
|
|
1294 |
return true;
|
|
|
1295 |
};
|
|
|
1296 |
}
|
|
|
1297 |
|
|
|
1298 |
/**
|
|
|
1299 |
* Identical to the native `getBoundingClientRect` function, but ensures that
|
|
|
1300 |
* the method is supported at all (it is in all browsers we claim to support)
|
|
|
1301 |
* and that the element is in the DOM before continuing.
|
|
|
1302 |
*
|
|
|
1303 |
* This wrapper function also shims properties which are not provided by some
|
|
|
1304 |
* older browsers (namely, IE8).
|
|
|
1305 |
*
|
|
|
1306 |
* Additionally, some browsers do not support adding properties to a
|
|
|
1307 |
* `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
|
|
|
1308 |
* properties (except `x` and `y` which are not widely supported). This helps
|
|
|
1309 |
* avoid implementations where keys are non-enumerable.
|
|
|
1310 |
*
|
|
|
1311 |
* @param {Element} el
|
|
|
1312 |
* Element whose `ClientRect` we want to calculate.
|
|
|
1313 |
*
|
|
|
1314 |
* @return {Object|undefined}
|
|
|
1315 |
* Always returns a plain object - or `undefined` if it cannot.
|
|
|
1316 |
*/
|
|
|
1317 |
function getBoundingClientRect(el) {
|
|
|
1318 |
if (el && el.getBoundingClientRect && el.parentNode) {
|
|
|
1319 |
const rect = el.getBoundingClientRect();
|
|
|
1320 |
const result = {};
|
|
|
1321 |
['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
|
|
|
1322 |
if (rect[k] !== undefined) {
|
|
|
1323 |
result[k] = rect[k];
|
|
|
1324 |
}
|
|
|
1325 |
});
|
|
|
1326 |
if (!result.height) {
|
|
|
1327 |
result.height = parseFloat(computedStyle(el, 'height'));
|
|
|
1328 |
}
|
|
|
1329 |
if (!result.width) {
|
|
|
1330 |
result.width = parseFloat(computedStyle(el, 'width'));
|
|
|
1331 |
}
|
|
|
1332 |
return result;
|
|
|
1333 |
}
|
|
|
1334 |
}
|
|
|
1335 |
|
|
|
1336 |
/**
|
|
|
1337 |
* Represents the position of a DOM element on the page.
|
|
|
1338 |
*
|
|
|
1339 |
* @typedef {Object} module:dom~Position
|
|
|
1340 |
*
|
|
|
1341 |
* @property {number} left
|
|
|
1342 |
* Pixels to the left.
|
|
|
1343 |
*
|
|
|
1344 |
* @property {number} top
|
|
|
1345 |
* Pixels from the top.
|
|
|
1346 |
*/
|
|
|
1347 |
|
|
|
1348 |
/**
|
|
|
1349 |
* Get the position of an element in the DOM.
|
|
|
1350 |
*
|
|
|
1351 |
* Uses `getBoundingClientRect` technique from John Resig.
|
|
|
1352 |
*
|
|
|
1353 |
* @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
|
|
|
1354 |
*
|
|
|
1355 |
* @param {Element} el
|
|
|
1356 |
* Element from which to get offset.
|
|
|
1357 |
*
|
|
|
1358 |
* @return {module:dom~Position}
|
|
|
1359 |
* The position of the element that was passed in.
|
|
|
1360 |
*/
|
|
|
1361 |
function findPosition(el) {
|
|
|
1362 |
if (!el || el && !el.offsetParent) {
|
|
|
1363 |
return {
|
|
|
1364 |
left: 0,
|
|
|
1365 |
top: 0,
|
|
|
1366 |
width: 0,
|
|
|
1367 |
height: 0
|
|
|
1368 |
};
|
|
|
1369 |
}
|
|
|
1370 |
const width = el.offsetWidth;
|
|
|
1371 |
const height = el.offsetHeight;
|
|
|
1372 |
let left = 0;
|
|
|
1373 |
let top = 0;
|
|
|
1374 |
while (el.offsetParent && el !== document[FullscreenApi.fullscreenElement]) {
|
|
|
1375 |
left += el.offsetLeft;
|
|
|
1376 |
top += el.offsetTop;
|
|
|
1377 |
el = el.offsetParent;
|
|
|
1378 |
}
|
|
|
1379 |
return {
|
|
|
1380 |
left,
|
|
|
1381 |
top,
|
|
|
1382 |
width,
|
|
|
1383 |
height
|
|
|
1384 |
};
|
|
|
1385 |
}
|
|
|
1386 |
|
|
|
1387 |
/**
|
|
|
1388 |
* Represents x and y coordinates for a DOM element or mouse pointer.
|
|
|
1389 |
*
|
|
|
1390 |
* @typedef {Object} module:dom~Coordinates
|
|
|
1391 |
*
|
|
|
1392 |
* @property {number} x
|
|
|
1393 |
* x coordinate in pixels
|
|
|
1394 |
*
|
|
|
1395 |
* @property {number} y
|
|
|
1396 |
* y coordinate in pixels
|
|
|
1397 |
*/
|
|
|
1398 |
|
|
|
1399 |
/**
|
|
|
1400 |
* Get the pointer position within an element.
|
|
|
1401 |
*
|
|
|
1402 |
* The base on the coordinates are the bottom left of the element.
|
|
|
1403 |
*
|
|
|
1404 |
* @param {Element} el
|
|
|
1405 |
* Element on which to get the pointer position on.
|
|
|
1406 |
*
|
|
|
1407 |
* @param {Event} event
|
|
|
1408 |
* Event object.
|
|
|
1409 |
*
|
|
|
1410 |
* @return {module:dom~Coordinates}
|
|
|
1411 |
* A coordinates object corresponding to the mouse position.
|
|
|
1412 |
*
|
|
|
1413 |
*/
|
|
|
1414 |
function getPointerPosition(el, event) {
|
|
|
1415 |
const translated = {
|
|
|
1416 |
x: 0,
|
|
|
1417 |
y: 0
|
|
|
1418 |
};
|
|
|
1419 |
if (IS_IOS) {
|
|
|
1420 |
let item = el;
|
|
|
1421 |
while (item && item.nodeName.toLowerCase() !== 'html') {
|
|
|
1422 |
const transform = computedStyle(item, 'transform');
|
|
|
1423 |
if (/^matrix/.test(transform)) {
|
|
|
1424 |
const values = transform.slice(7, -1).split(/,\s/).map(Number);
|
|
|
1425 |
translated.x += values[4];
|
|
|
1426 |
translated.y += values[5];
|
|
|
1427 |
} else if (/^matrix3d/.test(transform)) {
|
|
|
1428 |
const values = transform.slice(9, -1).split(/,\s/).map(Number);
|
|
|
1429 |
translated.x += values[12];
|
|
|
1430 |
translated.y += values[13];
|
|
|
1431 |
}
|
|
|
1432 |
item = item.parentNode;
|
|
|
1433 |
}
|
|
|
1434 |
}
|
|
|
1435 |
const position = {};
|
|
|
1436 |
const boxTarget = findPosition(event.target);
|
|
|
1437 |
const box = findPosition(el);
|
|
|
1438 |
const boxW = box.width;
|
|
|
1439 |
const boxH = box.height;
|
|
|
1440 |
let offsetY = event.offsetY - (box.top - boxTarget.top);
|
|
|
1441 |
let offsetX = event.offsetX - (box.left - boxTarget.left);
|
|
|
1442 |
if (event.changedTouches) {
|
|
|
1443 |
offsetX = event.changedTouches[0].pageX - box.left;
|
|
|
1444 |
offsetY = event.changedTouches[0].pageY + box.top;
|
|
|
1445 |
if (IS_IOS) {
|
|
|
1446 |
offsetX -= translated.x;
|
|
|
1447 |
offsetY -= translated.y;
|
|
|
1448 |
}
|
|
|
1449 |
}
|
|
|
1450 |
position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
|
|
|
1451 |
position.x = Math.max(0, Math.min(1, offsetX / boxW));
|
|
|
1452 |
return position;
|
|
|
1453 |
}
|
|
|
1454 |
|
|
|
1455 |
/**
|
|
|
1456 |
* Determines, via duck typing, whether or not a value is a text node.
|
|
|
1457 |
*
|
|
|
1458 |
* @param {*} value
|
|
|
1459 |
* Check if this value is a text node.
|
|
|
1460 |
*
|
|
|
1461 |
* @return {boolean}
|
|
|
1462 |
* Will be `true` if the value is a text node, `false` otherwise.
|
|
|
1463 |
*/
|
|
|
1464 |
function isTextNode$1(value) {
|
|
|
1465 |
return isObject$1(value) && value.nodeType === 3;
|
|
|
1466 |
}
|
|
|
1467 |
|
|
|
1468 |
/**
|
|
|
1469 |
* Empties the contents of an element.
|
|
|
1470 |
*
|
|
|
1471 |
* @param {Element} el
|
|
|
1472 |
* The element to empty children from
|
|
|
1473 |
*
|
|
|
1474 |
* @return {Element}
|
|
|
1475 |
* The element with no children
|
|
|
1476 |
*/
|
|
|
1477 |
function emptyEl(el) {
|
|
|
1478 |
while (el.firstChild) {
|
|
|
1479 |
el.removeChild(el.firstChild);
|
|
|
1480 |
}
|
|
|
1481 |
return el;
|
|
|
1482 |
}
|
|
|
1483 |
|
|
|
1484 |
/**
|
|
|
1485 |
* This is a mixed value that describes content to be injected into the DOM
|
|
|
1486 |
* via some method. It can be of the following types:
|
|
|
1487 |
*
|
|
|
1488 |
* Type | Description
|
|
|
1489 |
* -----------|-------------
|
|
|
1490 |
* `string` | The value will be normalized into a text node.
|
|
|
1491 |
* `Element` | The value will be accepted as-is.
|
|
|
1492 |
* `Text` | A TextNode. The value will be accepted as-is.
|
|
|
1493 |
* `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
|
|
|
1494 |
* `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
|
|
|
1495 |
*
|
|
|
1496 |
* @typedef {string|Element|Text|Array|Function} ContentDescriptor
|
|
|
1497 |
*/
|
|
|
1498 |
|
|
|
1499 |
/**
|
|
|
1500 |
* Normalizes content for eventual insertion into the DOM.
|
|
|
1501 |
*
|
|
|
1502 |
* This allows a wide range of content definition methods, but helps protect
|
|
|
1503 |
* from falling into the trap of simply writing to `innerHTML`, which could
|
|
|
1504 |
* be an XSS concern.
|
|
|
1505 |
*
|
|
|
1506 |
* The content for an element can be passed in multiple types and
|
|
|
1507 |
* combinations, whose behavior is as follows:
|
|
|
1508 |
*
|
|
|
1509 |
* @param {ContentDescriptor} content
|
|
|
1510 |
* A content descriptor value.
|
|
|
1511 |
*
|
|
|
1512 |
* @return {Array}
|
|
|
1513 |
* All of the content that was passed in, normalized to an array of
|
|
|
1514 |
* elements or text nodes.
|
|
|
1515 |
*/
|
|
|
1516 |
function normalizeContent(content) {
|
|
|
1517 |
// First, invoke content if it is a function. If it produces an array,
|
|
|
1518 |
// that needs to happen before normalization.
|
|
|
1519 |
if (typeof content === 'function') {
|
|
|
1520 |
content = content();
|
|
|
1521 |
}
|
|
|
1522 |
|
|
|
1523 |
// Next up, normalize to an array, so one or many items can be normalized,
|
|
|
1524 |
// filtered, and returned.
|
|
|
1525 |
return (Array.isArray(content) ? content : [content]).map(value => {
|
|
|
1526 |
// First, invoke value if it is a function to produce a new value,
|
|
|
1527 |
// which will be subsequently normalized to a Node of some kind.
|
|
|
1528 |
if (typeof value === 'function') {
|
|
|
1529 |
value = value();
|
|
|
1530 |
}
|
|
|
1531 |
if (isEl(value) || isTextNode$1(value)) {
|
|
|
1532 |
return value;
|
|
|
1533 |
}
|
|
|
1534 |
if (typeof value === 'string' && /\S/.test(value)) {
|
|
|
1535 |
return document.createTextNode(value);
|
|
|
1536 |
}
|
|
|
1537 |
}).filter(value => value);
|
|
|
1538 |
}
|
|
|
1539 |
|
|
|
1540 |
/**
|
|
|
1541 |
* Normalizes and appends content to an element.
|
|
|
1542 |
*
|
|
|
1543 |
* @param {Element} el
|
|
|
1544 |
* Element to append normalized content to.
|
|
|
1545 |
*
|
|
|
1546 |
* @param {ContentDescriptor} content
|
|
|
1547 |
* A content descriptor value.
|
|
|
1548 |
*
|
|
|
1549 |
* @return {Element}
|
|
|
1550 |
* The element with appended normalized content.
|
|
|
1551 |
*/
|
|
|
1552 |
function appendContent(el, content) {
|
|
|
1553 |
normalizeContent(content).forEach(node => el.appendChild(node));
|
|
|
1554 |
return el;
|
|
|
1555 |
}
|
|
|
1556 |
|
|
|
1557 |
/**
|
|
|
1558 |
* Normalizes and inserts content into an element; this is identical to
|
|
|
1559 |
* `appendContent()`, except it empties the element first.
|
|
|
1560 |
*
|
|
|
1561 |
* @param {Element} el
|
|
|
1562 |
* Element to insert normalized content into.
|
|
|
1563 |
*
|
|
|
1564 |
* @param {ContentDescriptor} content
|
|
|
1565 |
* A content descriptor value.
|
|
|
1566 |
*
|
|
|
1567 |
* @return {Element}
|
|
|
1568 |
* The element with inserted normalized content.
|
|
|
1569 |
*/
|
|
|
1570 |
function insertContent(el, content) {
|
|
|
1571 |
return appendContent(emptyEl(el), content);
|
|
|
1572 |
}
|
|
|
1573 |
|
|
|
1574 |
/**
|
|
|
1575 |
* Check if an event was a single left click.
|
|
|
1576 |
*
|
|
|
1577 |
* @param {MouseEvent} event
|
|
|
1578 |
* Event object.
|
|
|
1579 |
*
|
|
|
1580 |
* @return {boolean}
|
|
|
1581 |
* Will be `true` if a single left click, `false` otherwise.
|
|
|
1582 |
*/
|
|
|
1583 |
function isSingleLeftClick(event) {
|
|
|
1584 |
// Note: if you create something draggable, be sure to
|
|
|
1585 |
// call it on both `mousedown` and `mousemove` event,
|
|
|
1586 |
// otherwise `mousedown` should be enough for a button
|
|
|
1587 |
|
|
|
1588 |
if (event.button === undefined && event.buttons === undefined) {
|
|
|
1589 |
// Why do we need `buttons` ?
|
|
|
1590 |
// Because, middle mouse sometimes have this:
|
|
|
1591 |
// e.button === 0 and e.buttons === 4
|
|
|
1592 |
// Furthermore, we want to prevent combination click, something like
|
|
|
1593 |
// HOLD middlemouse then left click, that would be
|
|
|
1594 |
// e.button === 0, e.buttons === 5
|
|
|
1595 |
// just `button` is not gonna work
|
|
|
1596 |
|
|
|
1597 |
// Alright, then what this block does ?
|
|
|
1598 |
// this is for chrome `simulate mobile devices`
|
|
|
1599 |
// I want to support this as well
|
|
|
1600 |
|
|
|
1601 |
return true;
|
|
|
1602 |
}
|
|
|
1603 |
if (event.button === 0 && event.buttons === undefined) {
|
|
|
1604 |
// Touch screen, sometimes on some specific device, `buttons`
|
|
|
1605 |
// doesn't have anything (safari on ios, blackberry...)
|
|
|
1606 |
|
|
|
1607 |
return true;
|
|
|
1608 |
}
|
|
|
1609 |
|
|
|
1610 |
// `mouseup` event on a single left click has
|
|
|
1611 |
// `button` and `buttons` equal to 0
|
|
|
1612 |
if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
|
|
|
1613 |
return true;
|
|
|
1614 |
}
|
|
|
1615 |
if (event.button !== 0 || event.buttons !== 1) {
|
|
|
1616 |
// This is the reason we have those if else block above
|
|
|
1617 |
// if any special case we can catch and let it slide
|
|
|
1618 |
// we do it above, when get to here, this definitely
|
|
|
1619 |
// is-not-left-click
|
|
|
1620 |
|
|
|
1621 |
return false;
|
|
|
1622 |
}
|
|
|
1623 |
return true;
|
|
|
1624 |
}
|
|
|
1625 |
|
|
|
1626 |
/**
|
|
|
1627 |
* Finds a single DOM element matching `selector` within the optional
|
|
|
1628 |
* `context` of another DOM element (defaulting to `document`).
|
|
|
1629 |
*
|
|
|
1630 |
* @param {string} selector
|
|
|
1631 |
* A valid CSS selector, which will be passed to `querySelector`.
|
|
|
1632 |
*
|
|
|
1633 |
* @param {Element|String} [context=document]
|
|
|
1634 |
* A DOM element within which to query. Can also be a selector
|
|
|
1635 |
* string in which case the first matching element will be used
|
|
|
1636 |
* as context. If missing (or no element matches selector), falls
|
|
|
1637 |
* back to `document`.
|
|
|
1638 |
*
|
|
|
1639 |
* @return {Element|null}
|
|
|
1640 |
* The element that was found or null.
|
|
|
1641 |
*/
|
|
|
1642 |
const $ = createQuerier('querySelector');
|
|
|
1643 |
|
|
|
1644 |
/**
|
|
|
1645 |
* Finds a all DOM elements matching `selector` within the optional
|
|
|
1646 |
* `context` of another DOM element (defaulting to `document`).
|
|
|
1647 |
*
|
|
|
1648 |
* @param {string} selector
|
|
|
1649 |
* A valid CSS selector, which will be passed to `querySelectorAll`.
|
|
|
1650 |
*
|
|
|
1651 |
* @param {Element|String} [context=document]
|
|
|
1652 |
* A DOM element within which to query. Can also be a selector
|
|
|
1653 |
* string in which case the first matching element will be used
|
|
|
1654 |
* as context. If missing (or no element matches selector), falls
|
|
|
1655 |
* back to `document`.
|
|
|
1656 |
*
|
|
|
1657 |
* @return {NodeList}
|
|
|
1658 |
* A element list of elements that were found. Will be empty if none
|
|
|
1659 |
* were found.
|
|
|
1660 |
*
|
|
|
1661 |
*/
|
|
|
1662 |
const $$ = createQuerier('querySelectorAll');
|
|
|
1663 |
|
|
|
1664 |
/**
|
|
|
1665 |
* A safe getComputedStyle.
|
|
|
1666 |
*
|
|
|
1667 |
* This is needed because in Firefox, if the player is loaded in an iframe with
|
|
|
1668 |
* `display:none`, then `getComputedStyle` returns `null`, so, we do a
|
|
|
1669 |
* null-check to make sure that the player doesn't break in these cases.
|
|
|
1670 |
*
|
|
|
1671 |
* @param {Element} el
|
|
|
1672 |
* The element you want the computed style of
|
|
|
1673 |
*
|
|
|
1674 |
* @param {string} prop
|
|
|
1675 |
* The property name you want
|
|
|
1676 |
*
|
|
|
1677 |
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
|
|
1678 |
*/
|
|
|
1679 |
function computedStyle(el, prop) {
|
|
|
1680 |
if (!el || !prop) {
|
|
|
1681 |
return '';
|
|
|
1682 |
}
|
|
|
1683 |
if (typeof window.getComputedStyle === 'function') {
|
|
|
1684 |
let computedStyleValue;
|
|
|
1685 |
try {
|
|
|
1686 |
computedStyleValue = window.getComputedStyle(el);
|
|
|
1687 |
} catch (e) {
|
|
|
1688 |
return '';
|
|
|
1689 |
}
|
|
|
1690 |
return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
|
|
|
1691 |
}
|
|
|
1692 |
return '';
|
|
|
1693 |
}
|
|
|
1694 |
|
|
|
1695 |
/**
|
|
|
1696 |
* Copy document style sheets to another window.
|
|
|
1697 |
*
|
|
|
1698 |
* @param {Window} win
|
|
|
1699 |
* The window element you want to copy the document style sheets to.
|
|
|
1700 |
*
|
|
|
1701 |
*/
|
|
|
1702 |
function copyStyleSheetsToWindow(win) {
|
|
|
1703 |
[...document.styleSheets].forEach(styleSheet => {
|
|
|
1704 |
try {
|
|
|
1705 |
const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
|
|
|
1706 |
const style = document.createElement('style');
|
|
|
1707 |
style.textContent = cssRules;
|
|
|
1708 |
win.document.head.appendChild(style);
|
|
|
1709 |
} catch (e) {
|
|
|
1710 |
const link = document.createElement('link');
|
|
|
1711 |
link.rel = 'stylesheet';
|
|
|
1712 |
link.type = styleSheet.type;
|
|
|
1713 |
// For older Safari this has to be the string; on other browsers setting the MediaList works
|
|
|
1714 |
link.media = styleSheet.media.mediaText;
|
|
|
1715 |
link.href = styleSheet.href;
|
|
|
1716 |
win.document.head.appendChild(link);
|
|
|
1717 |
}
|
|
|
1718 |
});
|
|
|
1719 |
}
|
|
|
1720 |
|
|
|
1721 |
var Dom = /*#__PURE__*/Object.freeze({
|
|
|
1722 |
__proto__: null,
|
|
|
1723 |
isReal: isReal,
|
|
|
1724 |
isEl: isEl,
|
|
|
1725 |
isInFrame: isInFrame,
|
|
|
1726 |
createEl: createEl,
|
|
|
1727 |
textContent: textContent,
|
|
|
1728 |
prependTo: prependTo,
|
|
|
1729 |
hasClass: hasClass,
|
|
|
1730 |
addClass: addClass,
|
|
|
1731 |
removeClass: removeClass,
|
|
|
1732 |
toggleClass: toggleClass,
|
|
|
1733 |
setAttributes: setAttributes,
|
|
|
1734 |
getAttributes: getAttributes,
|
|
|
1735 |
getAttribute: getAttribute,
|
|
|
1736 |
setAttribute: setAttribute,
|
|
|
1737 |
removeAttribute: removeAttribute,
|
|
|
1738 |
blockTextSelection: blockTextSelection,
|
|
|
1739 |
unblockTextSelection: unblockTextSelection,
|
|
|
1740 |
getBoundingClientRect: getBoundingClientRect,
|
|
|
1741 |
findPosition: findPosition,
|
|
|
1742 |
getPointerPosition: getPointerPosition,
|
|
|
1743 |
isTextNode: isTextNode$1,
|
|
|
1744 |
emptyEl: emptyEl,
|
|
|
1745 |
normalizeContent: normalizeContent,
|
|
|
1746 |
appendContent: appendContent,
|
|
|
1747 |
insertContent: insertContent,
|
|
|
1748 |
isSingleLeftClick: isSingleLeftClick,
|
|
|
1749 |
$: $,
|
|
|
1750 |
$$: $$,
|
|
|
1751 |
computedStyle: computedStyle,
|
|
|
1752 |
copyStyleSheetsToWindow: copyStyleSheetsToWindow
|
|
|
1753 |
});
|
|
|
1754 |
|
|
|
1755 |
/**
|
|
|
1756 |
* @file setup.js - Functions for setting up a player without
|
|
|
1757 |
* user interaction based on the data-setup `attribute` of the video tag.
|
|
|
1758 |
*
|
|
|
1759 |
* @module setup
|
|
|
1760 |
*/
|
|
|
1761 |
let _windowLoaded = false;
|
|
|
1762 |
let videojs$1;
|
|
|
1763 |
|
|
|
1764 |
/**
|
|
|
1765 |
* Set up any tags that have a data-setup `attribute` when the player is started.
|
|
|
1766 |
*/
|
|
|
1767 |
const autoSetup = function () {
|
|
|
1768 |
if (videojs$1.options.autoSetup === false) {
|
|
|
1769 |
return;
|
|
|
1770 |
}
|
|
|
1771 |
const vids = Array.prototype.slice.call(document.getElementsByTagName('video'));
|
|
|
1772 |
const audios = Array.prototype.slice.call(document.getElementsByTagName('audio'));
|
|
|
1773 |
const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js'));
|
|
|
1774 |
const mediaEls = vids.concat(audios, divs);
|
|
|
1775 |
|
|
|
1776 |
// Check if any media elements exist
|
|
|
1777 |
if (mediaEls && mediaEls.length > 0) {
|
|
|
1778 |
for (let i = 0, e = mediaEls.length; i < e; i++) {
|
|
|
1779 |
const mediaEl = mediaEls[i];
|
|
|
1780 |
|
|
|
1781 |
// Check if element exists, has getAttribute func.
|
|
|
1782 |
if (mediaEl && mediaEl.getAttribute) {
|
|
|
1783 |
// Make sure this player hasn't already been set up.
|
|
|
1784 |
if (mediaEl.player === undefined) {
|
|
|
1785 |
const options = mediaEl.getAttribute('data-setup');
|
|
|
1786 |
|
|
|
1787 |
// Check if data-setup attr exists.
|
|
|
1788 |
// We only auto-setup if they've added the data-setup attr.
|
|
|
1789 |
if (options !== null) {
|
|
|
1790 |
// Create new video.js instance.
|
|
|
1791 |
videojs$1(mediaEl);
|
|
|
1792 |
}
|
|
|
1793 |
}
|
|
|
1794 |
|
|
|
1795 |
// If getAttribute isn't defined, we need to wait for the DOM.
|
|
|
1796 |
} else {
|
|
|
1797 |
autoSetupTimeout(1);
|
|
|
1798 |
break;
|
|
|
1799 |
}
|
|
|
1800 |
}
|
|
|
1801 |
|
|
|
1802 |
// No videos were found, so keep looping unless page is finished loading.
|
|
|
1803 |
} else if (!_windowLoaded) {
|
|
|
1804 |
autoSetupTimeout(1);
|
|
|
1805 |
}
|
|
|
1806 |
};
|
|
|
1807 |
|
|
|
1808 |
/**
|
|
|
1809 |
* Wait until the page is loaded before running autoSetup. This will be called in
|
|
|
1810 |
* autoSetup if `hasLoaded` returns false.
|
|
|
1811 |
*
|
|
|
1812 |
* @param {number} wait
|
|
|
1813 |
* How long to wait in ms
|
|
|
1814 |
*
|
|
|
1815 |
* @param {module:videojs} [vjs]
|
|
|
1816 |
* The videojs library function
|
|
|
1817 |
*/
|
|
|
1818 |
function autoSetupTimeout(wait, vjs) {
|
|
|
1819 |
// Protect against breakage in non-browser environments
|
|
|
1820 |
if (!isReal()) {
|
|
|
1821 |
return;
|
|
|
1822 |
}
|
|
|
1823 |
if (vjs) {
|
|
|
1824 |
videojs$1 = vjs;
|
|
|
1825 |
}
|
|
|
1826 |
window.setTimeout(autoSetup, wait);
|
|
|
1827 |
}
|
|
|
1828 |
|
|
|
1829 |
/**
|
|
|
1830 |
* Used to set the internal tracking of window loaded state to true.
|
|
|
1831 |
*
|
|
|
1832 |
* @private
|
|
|
1833 |
*/
|
|
|
1834 |
function setWindowLoaded() {
|
|
|
1835 |
_windowLoaded = true;
|
|
|
1836 |
window.removeEventListener('load', setWindowLoaded);
|
|
|
1837 |
}
|
|
|
1838 |
if (isReal()) {
|
|
|
1839 |
if (document.readyState === 'complete') {
|
|
|
1840 |
setWindowLoaded();
|
|
|
1841 |
} else {
|
|
|
1842 |
/**
|
|
|
1843 |
* Listen for the load event on window, and set _windowLoaded to true.
|
|
|
1844 |
*
|
|
|
1845 |
* We use a standard event listener here to avoid incrementing the GUID
|
|
|
1846 |
* before any players are created.
|
|
|
1847 |
*
|
|
|
1848 |
* @listens load
|
|
|
1849 |
*/
|
|
|
1850 |
window.addEventListener('load', setWindowLoaded);
|
|
|
1851 |
}
|
|
|
1852 |
}
|
|
|
1853 |
|
|
|
1854 |
/**
|
|
|
1855 |
* @file stylesheet.js
|
|
|
1856 |
* @module stylesheet
|
|
|
1857 |
*/
|
|
|
1858 |
|
|
|
1859 |
/**
|
|
|
1860 |
* Create a DOM style element given a className for it.
|
|
|
1861 |
*
|
|
|
1862 |
* @param {string} className
|
|
|
1863 |
* The className to add to the created style element.
|
|
|
1864 |
*
|
|
|
1865 |
* @return {Element}
|
|
|
1866 |
* The element that was created.
|
|
|
1867 |
*/
|
|
|
1868 |
const createStyleElement = function (className) {
|
|
|
1869 |
const style = document.createElement('style');
|
|
|
1870 |
style.className = className;
|
|
|
1871 |
return style;
|
|
|
1872 |
};
|
|
|
1873 |
|
|
|
1874 |
/**
|
|
|
1875 |
* Add text to a DOM element.
|
|
|
1876 |
*
|
|
|
1877 |
* @param {Element} el
|
|
|
1878 |
* The Element to add text content to.
|
|
|
1879 |
*
|
|
|
1880 |
* @param {string} content
|
|
|
1881 |
* The text to add to the element.
|
|
|
1882 |
*/
|
|
|
1883 |
const setTextContent = function (el, content) {
|
|
|
1884 |
if (el.styleSheet) {
|
|
|
1885 |
el.styleSheet.cssText = content;
|
|
|
1886 |
} else {
|
|
|
1887 |
el.textContent = content;
|
|
|
1888 |
}
|
|
|
1889 |
};
|
|
|
1890 |
|
|
|
1891 |
/**
|
|
|
1892 |
* @file dom-data.js
|
|
|
1893 |
* @module dom-data
|
|
|
1894 |
*/
|
|
|
1895 |
|
|
|
1896 |
/**
|
|
|
1897 |
* Element Data Store.
|
|
|
1898 |
*
|
|
|
1899 |
* Allows for binding data to an element without putting it directly on the
|
|
|
1900 |
* element. Ex. Event listeners are stored here.
|
|
|
1901 |
* (also from jsninja.com, slightly modified and updated for closure compiler)
|
|
|
1902 |
*
|
|
|
1903 |
* @type {Object}
|
|
|
1904 |
* @private
|
|
|
1905 |
*/
|
|
|
1906 |
var DomData = new WeakMap();
|
|
|
1907 |
|
|
|
1908 |
/**
|
|
|
1909 |
* @file guid.js
|
|
|
1910 |
* @module guid
|
|
|
1911 |
*/
|
|
|
1912 |
|
|
|
1913 |
// Default value for GUIDs. This allows us to reset the GUID counter in tests.
|
|
|
1914 |
//
|
|
|
1915 |
// The initial GUID is 3 because some users have come to rely on the first
|
|
|
1916 |
// default player ID ending up as `vjs_video_3`.
|
|
|
1917 |
//
|
|
|
1918 |
// See: https://github.com/videojs/video.js/pull/6216
|
|
|
1919 |
const _initialGuid = 3;
|
|
|
1920 |
|
|
|
1921 |
/**
|
|
|
1922 |
* Unique ID for an element or function
|
|
|
1923 |
*
|
|
|
1924 |
* @type {Number}
|
|
|
1925 |
*/
|
|
|
1926 |
let _guid = _initialGuid;
|
|
|
1927 |
|
|
|
1928 |
/**
|
|
|
1929 |
* Get a unique auto-incrementing ID by number that has not been returned before.
|
|
|
1930 |
*
|
|
|
1931 |
* @return {number}
|
|
|
1932 |
* A new unique ID.
|
|
|
1933 |
*/
|
|
|
1934 |
function newGUID() {
|
|
|
1935 |
return _guid++;
|
|
|
1936 |
}
|
|
|
1937 |
|
|
|
1938 |
/**
|
|
|
1939 |
* @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
|
|
|
1940 |
* (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
|
|
|
1941 |
* This should work very similarly to jQuery's events, however it's based off the book version which isn't as
|
|
|
1942 |
* robust as jquery's, so there's probably some differences.
|
|
|
1943 |
*
|
|
|
1944 |
* @file events.js
|
|
|
1945 |
* @module events
|
|
|
1946 |
*/
|
|
|
1947 |
|
|
|
1948 |
/**
|
|
|
1949 |
* Clean up the listener cache and dispatchers
|
|
|
1950 |
*
|
|
|
1951 |
* @param {Element|Object} elem
|
|
|
1952 |
* Element to clean up
|
|
|
1953 |
*
|
|
|
1954 |
* @param {string} type
|
|
|
1955 |
* Type of event to clean up
|
|
|
1956 |
*/
|
|
|
1957 |
function _cleanUpEvents(elem, type) {
|
|
|
1958 |
if (!DomData.has(elem)) {
|
|
|
1959 |
return;
|
|
|
1960 |
}
|
|
|
1961 |
const data = DomData.get(elem);
|
|
|
1962 |
|
|
|
1963 |
// Remove the events of a particular type if there are none left
|
|
|
1964 |
if (data.handlers[type].length === 0) {
|
|
|
1965 |
delete data.handlers[type];
|
|
|
1966 |
// data.handlers[type] = null;
|
|
|
1967 |
// Setting to null was causing an error with data.handlers
|
|
|
1968 |
|
|
|
1969 |
// Remove the meta-handler from the element
|
|
|
1970 |
if (elem.removeEventListener) {
|
|
|
1971 |
elem.removeEventListener(type, data.dispatcher, false);
|
|
|
1972 |
} else if (elem.detachEvent) {
|
|
|
1973 |
elem.detachEvent('on' + type, data.dispatcher);
|
|
|
1974 |
}
|
|
|
1975 |
}
|
|
|
1976 |
|
|
|
1977 |
// Remove the events object if there are no types left
|
|
|
1978 |
if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
|
|
|
1979 |
delete data.handlers;
|
|
|
1980 |
delete data.dispatcher;
|
|
|
1981 |
delete data.disabled;
|
|
|
1982 |
}
|
|
|
1983 |
|
|
|
1984 |
// Finally remove the element data if there is no data left
|
|
|
1985 |
if (Object.getOwnPropertyNames(data).length === 0) {
|
|
|
1986 |
DomData.delete(elem);
|
|
|
1987 |
}
|
|
|
1988 |
}
|
|
|
1989 |
|
|
|
1990 |
/**
|
|
|
1991 |
* Loops through an array of event types and calls the requested method for each type.
|
|
|
1992 |
*
|
|
|
1993 |
* @param {Function} fn
|
|
|
1994 |
* The event method we want to use.
|
|
|
1995 |
*
|
|
|
1996 |
* @param {Element|Object} elem
|
|
|
1997 |
* Element or object to bind listeners to
|
|
|
1998 |
*
|
|
|
1999 |
* @param {string[]} types
|
|
|
2000 |
* Type of event to bind to.
|
|
|
2001 |
*
|
|
|
2002 |
* @param {Function} callback
|
|
|
2003 |
* Event listener.
|
|
|
2004 |
*/
|
|
|
2005 |
function _handleMultipleEvents(fn, elem, types, callback) {
|
|
|
2006 |
types.forEach(function (type) {
|
|
|
2007 |
// Call the event method for each one of the types
|
|
|
2008 |
fn(elem, type, callback);
|
|
|
2009 |
});
|
|
|
2010 |
}
|
|
|
2011 |
|
|
|
2012 |
/**
|
|
|
2013 |
* Fix a native event to have standard property values
|
|
|
2014 |
*
|
|
|
2015 |
* @param {Object} event
|
|
|
2016 |
* Event object to fix.
|
|
|
2017 |
*
|
|
|
2018 |
* @return {Object}
|
|
|
2019 |
* Fixed event object.
|
|
|
2020 |
*/
|
|
|
2021 |
function fixEvent(event) {
|
|
|
2022 |
if (event.fixed_) {
|
|
|
2023 |
return event;
|
|
|
2024 |
}
|
|
|
2025 |
function returnTrue() {
|
|
|
2026 |
return true;
|
|
|
2027 |
}
|
|
|
2028 |
function returnFalse() {
|
|
|
2029 |
return false;
|
|
|
2030 |
}
|
|
|
2031 |
|
|
|
2032 |
// Test if fixing up is needed
|
|
|
2033 |
// Used to check if !event.stopPropagation instead of isPropagationStopped
|
|
|
2034 |
// But native events return true for stopPropagation, but don't have
|
|
|
2035 |
// other expected methods like isPropagationStopped. Seems to be a problem
|
|
|
2036 |
// with the Javascript Ninja code. So we're just overriding all events now.
|
|
|
2037 |
if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
|
|
|
2038 |
const old = event || window.event;
|
|
|
2039 |
event = {};
|
|
|
2040 |
// Clone the old object so that we can modify the values event = {};
|
|
|
2041 |
// IE8 Doesn't like when you mess with native event properties
|
|
|
2042 |
// Firefox returns false for event.hasOwnProperty('type') and other props
|
|
|
2043 |
// which makes copying more difficult.
|
|
|
2044 |
// TODO: Probably best to create a whitelist of event props
|
|
|
2045 |
for (const key in old) {
|
|
|
2046 |
// Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
|
|
|
2047 |
// Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
|
|
|
2048 |
// and webkitMovementX/Y
|
|
|
2049 |
// Lighthouse complains if Event.path is copied
|
|
|
2050 |
if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
|
|
|
2051 |
// Chrome 32+ warns if you try to copy deprecated returnValue, but
|
|
|
2052 |
// we still want to if preventDefault isn't supported (IE8).
|
|
|
2053 |
if (!(key === 'returnValue' && old.preventDefault)) {
|
|
|
2054 |
event[key] = old[key];
|
|
|
2055 |
}
|
|
|
2056 |
}
|
|
|
2057 |
}
|
|
|
2058 |
|
|
|
2059 |
// The event occurred on this element
|
|
|
2060 |
if (!event.target) {
|
|
|
2061 |
event.target = event.srcElement || document;
|
|
|
2062 |
}
|
|
|
2063 |
|
|
|
2064 |
// Handle which other element the event is related to
|
|
|
2065 |
if (!event.relatedTarget) {
|
|
|
2066 |
event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
|
|
|
2067 |
}
|
|
|
2068 |
|
|
|
2069 |
// Stop the default browser action
|
|
|
2070 |
event.preventDefault = function () {
|
|
|
2071 |
if (old.preventDefault) {
|
|
|
2072 |
old.preventDefault();
|
|
|
2073 |
}
|
|
|
2074 |
event.returnValue = false;
|
|
|
2075 |
old.returnValue = false;
|
|
|
2076 |
event.defaultPrevented = true;
|
|
|
2077 |
};
|
|
|
2078 |
event.defaultPrevented = false;
|
|
|
2079 |
|
|
|
2080 |
// Stop the event from bubbling
|
|
|
2081 |
event.stopPropagation = function () {
|
|
|
2082 |
if (old.stopPropagation) {
|
|
|
2083 |
old.stopPropagation();
|
|
|
2084 |
}
|
|
|
2085 |
event.cancelBubble = true;
|
|
|
2086 |
old.cancelBubble = true;
|
|
|
2087 |
event.isPropagationStopped = returnTrue;
|
|
|
2088 |
};
|
|
|
2089 |
event.isPropagationStopped = returnFalse;
|
|
|
2090 |
|
|
|
2091 |
// Stop the event from bubbling and executing other handlers
|
|
|
2092 |
event.stopImmediatePropagation = function () {
|
|
|
2093 |
if (old.stopImmediatePropagation) {
|
|
|
2094 |
old.stopImmediatePropagation();
|
|
|
2095 |
}
|
|
|
2096 |
event.isImmediatePropagationStopped = returnTrue;
|
|
|
2097 |
event.stopPropagation();
|
|
|
2098 |
};
|
|
|
2099 |
event.isImmediatePropagationStopped = returnFalse;
|
|
|
2100 |
|
|
|
2101 |
// Handle mouse position
|
|
|
2102 |
if (event.clientX !== null && event.clientX !== undefined) {
|
|
|
2103 |
const doc = document.documentElement;
|
|
|
2104 |
const body = document.body;
|
|
|
2105 |
event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
|
|
|
2106 |
event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
|
|
|
2107 |
}
|
|
|
2108 |
|
|
|
2109 |
// Handle key presses
|
|
|
2110 |
event.which = event.charCode || event.keyCode;
|
|
|
2111 |
|
|
|
2112 |
// Fix button for mouse clicks:
|
|
|
2113 |
// 0 == left; 1 == middle; 2 == right
|
|
|
2114 |
if (event.button !== null && event.button !== undefined) {
|
|
|
2115 |
// The following is disabled because it does not pass videojs-standard
|
|
|
2116 |
// and... yikes.
|
|
|
2117 |
/* eslint-disable */
|
|
|
2118 |
event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
|
|
|
2119 |
/* eslint-enable */
|
|
|
2120 |
}
|
|
|
2121 |
}
|
|
|
2122 |
|
|
|
2123 |
event.fixed_ = true;
|
|
|
2124 |
// Returns fixed-up instance
|
|
|
2125 |
return event;
|
|
|
2126 |
}
|
|
|
2127 |
|
|
|
2128 |
/**
|
|
|
2129 |
* Whether passive event listeners are supported
|
|
|
2130 |
*/
|
|
|
2131 |
let _supportsPassive;
|
|
|
2132 |
const supportsPassive = function () {
|
|
|
2133 |
if (typeof _supportsPassive !== 'boolean') {
|
|
|
2134 |
_supportsPassive = false;
|
|
|
2135 |
try {
|
|
|
2136 |
const opts = Object.defineProperty({}, 'passive', {
|
|
|
2137 |
get() {
|
|
|
2138 |
_supportsPassive = true;
|
|
|
2139 |
}
|
|
|
2140 |
});
|
|
|
2141 |
window.addEventListener('test', null, opts);
|
|
|
2142 |
window.removeEventListener('test', null, opts);
|
|
|
2143 |
} catch (e) {
|
|
|
2144 |
// disregard
|
|
|
2145 |
}
|
|
|
2146 |
}
|
|
|
2147 |
return _supportsPassive;
|
|
|
2148 |
};
|
|
|
2149 |
|
|
|
2150 |
/**
|
|
|
2151 |
* Touch events Chrome expects to be passive
|
|
|
2152 |
*/
|
|
|
2153 |
const passiveEvents = ['touchstart', 'touchmove'];
|
|
|
2154 |
|
|
|
2155 |
/**
|
|
|
2156 |
* Add an event listener to element
|
|
|
2157 |
* It stores the handler function in a separate cache object
|
|
|
2158 |
* and adds a generic handler to the element's event,
|
|
|
2159 |
* along with a unique id (guid) to the element.
|
|
|
2160 |
*
|
|
|
2161 |
* @param {Element|Object} elem
|
|
|
2162 |
* Element or object to bind listeners to
|
|
|
2163 |
*
|
|
|
2164 |
* @param {string|string[]} type
|
|
|
2165 |
* Type of event to bind to.
|
|
|
2166 |
*
|
|
|
2167 |
* @param {Function} fn
|
|
|
2168 |
* Event listener.
|
|
|
2169 |
*/
|
|
|
2170 |
function on(elem, type, fn) {
|
|
|
2171 |
if (Array.isArray(type)) {
|
|
|
2172 |
return _handleMultipleEvents(on, elem, type, fn);
|
|
|
2173 |
}
|
|
|
2174 |
if (!DomData.has(elem)) {
|
|
|
2175 |
DomData.set(elem, {});
|
|
|
2176 |
}
|
|
|
2177 |
const data = DomData.get(elem);
|
|
|
2178 |
|
|
|
2179 |
// We need a place to store all our handler data
|
|
|
2180 |
if (!data.handlers) {
|
|
|
2181 |
data.handlers = {};
|
|
|
2182 |
}
|
|
|
2183 |
if (!data.handlers[type]) {
|
|
|
2184 |
data.handlers[type] = [];
|
|
|
2185 |
}
|
|
|
2186 |
if (!fn.guid) {
|
|
|
2187 |
fn.guid = newGUID();
|
|
|
2188 |
}
|
|
|
2189 |
data.handlers[type].push(fn);
|
|
|
2190 |
if (!data.dispatcher) {
|
|
|
2191 |
data.disabled = false;
|
|
|
2192 |
data.dispatcher = function (event, hash) {
|
|
|
2193 |
if (data.disabled) {
|
|
|
2194 |
return;
|
|
|
2195 |
}
|
|
|
2196 |
event = fixEvent(event);
|
|
|
2197 |
const handlers = data.handlers[event.type];
|
|
|
2198 |
if (handlers) {
|
|
|
2199 |
// Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
|
|
|
2200 |
const handlersCopy = handlers.slice(0);
|
|
|
2201 |
for (let m = 0, n = handlersCopy.length; m < n; m++) {
|
|
|
2202 |
if (event.isImmediatePropagationStopped()) {
|
|
|
2203 |
break;
|
|
|
2204 |
} else {
|
|
|
2205 |
try {
|
|
|
2206 |
handlersCopy[m].call(elem, event, hash);
|
|
|
2207 |
} catch (e) {
|
|
|
2208 |
log$1.error(e);
|
|
|
2209 |
}
|
|
|
2210 |
}
|
|
|
2211 |
}
|
|
|
2212 |
}
|
|
|
2213 |
};
|
|
|
2214 |
}
|
|
|
2215 |
if (data.handlers[type].length === 1) {
|
|
|
2216 |
if (elem.addEventListener) {
|
|
|
2217 |
let options = false;
|
|
|
2218 |
if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
|
|
|
2219 |
options = {
|
|
|
2220 |
passive: true
|
|
|
2221 |
};
|
|
|
2222 |
}
|
|
|
2223 |
elem.addEventListener(type, data.dispatcher, options);
|
|
|
2224 |
} else if (elem.attachEvent) {
|
|
|
2225 |
elem.attachEvent('on' + type, data.dispatcher);
|
|
|
2226 |
}
|
|
|
2227 |
}
|
|
|
2228 |
}
|
|
|
2229 |
|
|
|
2230 |
/**
|
|
|
2231 |
* Removes event listeners from an element
|
|
|
2232 |
*
|
|
|
2233 |
* @param {Element|Object} elem
|
|
|
2234 |
* Object to remove listeners from.
|
|
|
2235 |
*
|
|
|
2236 |
* @param {string|string[]} [type]
|
|
|
2237 |
* Type of listener to remove. Don't include to remove all events from element.
|
|
|
2238 |
*
|
|
|
2239 |
* @param {Function} [fn]
|
|
|
2240 |
* Specific listener to remove. Don't include to remove listeners for an event
|
|
|
2241 |
* type.
|
|
|
2242 |
*/
|
|
|
2243 |
function off(elem, type, fn) {
|
|
|
2244 |
// Don't want to add a cache object through getElData if not needed
|
|
|
2245 |
if (!DomData.has(elem)) {
|
|
|
2246 |
return;
|
|
|
2247 |
}
|
|
|
2248 |
const data = DomData.get(elem);
|
|
|
2249 |
|
|
|
2250 |
// If no events exist, nothing to unbind
|
|
|
2251 |
if (!data.handlers) {
|
|
|
2252 |
return;
|
|
|
2253 |
}
|
|
|
2254 |
if (Array.isArray(type)) {
|
|
|
2255 |
return _handleMultipleEvents(off, elem, type, fn);
|
|
|
2256 |
}
|
|
|
2257 |
|
|
|
2258 |
// Utility function
|
|
|
2259 |
const removeType = function (el, t) {
|
|
|
2260 |
data.handlers[t] = [];
|
|
|
2261 |
_cleanUpEvents(el, t);
|
|
|
2262 |
};
|
|
|
2263 |
|
|
|
2264 |
// Are we removing all bound events?
|
|
|
2265 |
if (type === undefined) {
|
|
|
2266 |
for (const t in data.handlers) {
|
|
|
2267 |
if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
|
|
|
2268 |
removeType(elem, t);
|
|
|
2269 |
}
|
|
|
2270 |
}
|
|
|
2271 |
return;
|
|
|
2272 |
}
|
|
|
2273 |
const handlers = data.handlers[type];
|
|
|
2274 |
|
|
|
2275 |
// If no handlers exist, nothing to unbind
|
|
|
2276 |
if (!handlers) {
|
|
|
2277 |
return;
|
|
|
2278 |
}
|
|
|
2279 |
|
|
|
2280 |
// If no listener was provided, remove all listeners for type
|
|
|
2281 |
if (!fn) {
|
|
|
2282 |
removeType(elem, type);
|
|
|
2283 |
return;
|
|
|
2284 |
}
|
|
|
2285 |
|
|
|
2286 |
// We're only removing a single handler
|
|
|
2287 |
if (fn.guid) {
|
|
|
2288 |
for (let n = 0; n < handlers.length; n++) {
|
|
|
2289 |
if (handlers[n].guid === fn.guid) {
|
|
|
2290 |
handlers.splice(n--, 1);
|
|
|
2291 |
}
|
|
|
2292 |
}
|
|
|
2293 |
}
|
|
|
2294 |
_cleanUpEvents(elem, type);
|
|
|
2295 |
}
|
|
|
2296 |
|
|
|
2297 |
/**
|
|
|
2298 |
* Trigger an event for an element
|
|
|
2299 |
*
|
|
|
2300 |
* @param {Element|Object} elem
|
|
|
2301 |
* Element to trigger an event on
|
|
|
2302 |
*
|
|
|
2303 |
* @param {EventTarget~Event|string} event
|
|
|
2304 |
* A string (the type) or an event object with a type attribute
|
|
|
2305 |
*
|
|
|
2306 |
* @param {Object} [hash]
|
|
|
2307 |
* data hash to pass along with the event
|
|
|
2308 |
*
|
|
|
2309 |
* @return {boolean|undefined}
|
|
|
2310 |
* Returns the opposite of `defaultPrevented` if default was
|
|
|
2311 |
* prevented. Otherwise, returns `undefined`
|
|
|
2312 |
*/
|
|
|
2313 |
function trigger(elem, event, hash) {
|
|
|
2314 |
// Fetches element data and a reference to the parent (for bubbling).
|
|
|
2315 |
// Don't want to add a data object to cache for every parent,
|
|
|
2316 |
// so checking hasElData first.
|
|
|
2317 |
const elemData = DomData.has(elem) ? DomData.get(elem) : {};
|
|
|
2318 |
const parent = elem.parentNode || elem.ownerDocument;
|
|
|
2319 |
// type = event.type || event,
|
|
|
2320 |
// handler;
|
|
|
2321 |
|
|
|
2322 |
// If an event name was passed as a string, creates an event out of it
|
|
|
2323 |
if (typeof event === 'string') {
|
|
|
2324 |
event = {
|
|
|
2325 |
type: event,
|
|
|
2326 |
target: elem
|
|
|
2327 |
};
|
|
|
2328 |
} else if (!event.target) {
|
|
|
2329 |
event.target = elem;
|
|
|
2330 |
}
|
|
|
2331 |
|
|
|
2332 |
// Normalizes the event properties.
|
|
|
2333 |
event = fixEvent(event);
|
|
|
2334 |
|
|
|
2335 |
// If the passed element has a dispatcher, executes the established handlers.
|
|
|
2336 |
if (elemData.dispatcher) {
|
|
|
2337 |
elemData.dispatcher.call(elem, event, hash);
|
|
|
2338 |
}
|
|
|
2339 |
|
|
|
2340 |
// Unless explicitly stopped or the event does not bubble (e.g. media events)
|
|
|
2341 |
// recursively calls this function to bubble the event up the DOM.
|
|
|
2342 |
if (parent && !event.isPropagationStopped() && event.bubbles === true) {
|
|
|
2343 |
trigger.call(null, parent, event, hash);
|
|
|
2344 |
|
|
|
2345 |
// If at the top of the DOM, triggers the default action unless disabled.
|
|
|
2346 |
} else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
|
|
|
2347 |
if (!DomData.has(event.target)) {
|
|
|
2348 |
DomData.set(event.target, {});
|
|
|
2349 |
}
|
|
|
2350 |
const targetData = DomData.get(event.target);
|
|
|
2351 |
|
|
|
2352 |
// Checks if the target has a default action for this event.
|
|
|
2353 |
if (event.target[event.type]) {
|
|
|
2354 |
// Temporarily disables event dispatching on the target as we have already executed the handler.
|
|
|
2355 |
targetData.disabled = true;
|
|
|
2356 |
// Executes the default action.
|
|
|
2357 |
if (typeof event.target[event.type] === 'function') {
|
|
|
2358 |
event.target[event.type]();
|
|
|
2359 |
}
|
|
|
2360 |
// Re-enables event dispatching.
|
|
|
2361 |
targetData.disabled = false;
|
|
|
2362 |
}
|
|
|
2363 |
}
|
|
|
2364 |
|
|
|
2365 |
// Inform the triggerer if the default was prevented by returning false
|
|
|
2366 |
return !event.defaultPrevented;
|
|
|
2367 |
}
|
|
|
2368 |
|
|
|
2369 |
/**
|
|
|
2370 |
* Trigger a listener only once for an event.
|
|
|
2371 |
*
|
|
|
2372 |
* @param {Element|Object} elem
|
|
|
2373 |
* Element or object to bind to.
|
|
|
2374 |
*
|
|
|
2375 |
* @param {string|string[]} type
|
|
|
2376 |
* Name/type of event
|
|
|
2377 |
*
|
|
|
2378 |
* @param {Event~EventListener} fn
|
|
|
2379 |
* Event listener function
|
|
|
2380 |
*/
|
|
|
2381 |
function one(elem, type, fn) {
|
|
|
2382 |
if (Array.isArray(type)) {
|
|
|
2383 |
return _handleMultipleEvents(one, elem, type, fn);
|
|
|
2384 |
}
|
|
|
2385 |
const func = function () {
|
|
|
2386 |
off(elem, type, func);
|
|
|
2387 |
fn.apply(this, arguments);
|
|
|
2388 |
};
|
|
|
2389 |
|
|
|
2390 |
// copy the guid to the new function so it can removed using the original function's ID
|
|
|
2391 |
func.guid = fn.guid = fn.guid || newGUID();
|
|
|
2392 |
on(elem, type, func);
|
|
|
2393 |
}
|
|
|
2394 |
|
|
|
2395 |
/**
|
|
|
2396 |
* Trigger a listener only once and then turn if off for all
|
|
|
2397 |
* configured events
|
|
|
2398 |
*
|
|
|
2399 |
* @param {Element|Object} elem
|
|
|
2400 |
* Element or object to bind to.
|
|
|
2401 |
*
|
|
|
2402 |
* @param {string|string[]} type
|
|
|
2403 |
* Name/type of event
|
|
|
2404 |
*
|
|
|
2405 |
* @param {Event~EventListener} fn
|
|
|
2406 |
* Event listener function
|
|
|
2407 |
*/
|
|
|
2408 |
function any(elem, type, fn) {
|
|
|
2409 |
const func = function () {
|
|
|
2410 |
off(elem, type, func);
|
|
|
2411 |
fn.apply(this, arguments);
|
|
|
2412 |
};
|
|
|
2413 |
|
|
|
2414 |
// copy the guid to the new function so it can removed using the original function's ID
|
|
|
2415 |
func.guid = fn.guid = fn.guid || newGUID();
|
|
|
2416 |
|
|
|
2417 |
// multiple ons, but one off for everything
|
|
|
2418 |
on(elem, type, func);
|
|
|
2419 |
}
|
|
|
2420 |
|
|
|
2421 |
var Events = /*#__PURE__*/Object.freeze({
|
|
|
2422 |
__proto__: null,
|
|
|
2423 |
fixEvent: fixEvent,
|
|
|
2424 |
on: on,
|
|
|
2425 |
off: off,
|
|
|
2426 |
trigger: trigger,
|
|
|
2427 |
one: one,
|
|
|
2428 |
any: any
|
|
|
2429 |
});
|
|
|
2430 |
|
|
|
2431 |
/**
|
|
|
2432 |
* @file fn.js
|
|
|
2433 |
* @module fn
|
|
|
2434 |
*/
|
|
|
2435 |
const UPDATE_REFRESH_INTERVAL = 30;
|
|
|
2436 |
|
|
|
2437 |
/**
|
|
|
2438 |
* A private, internal-only function for changing the context of a function.
|
|
|
2439 |
*
|
|
|
2440 |
* It also stores a unique id on the function so it can be easily removed from
|
|
|
2441 |
* events.
|
|
|
2442 |
*
|
|
|
2443 |
* @private
|
|
|
2444 |
* @function
|
|
|
2445 |
* @param {*} context
|
|
|
2446 |
* The object to bind as scope.
|
|
|
2447 |
*
|
|
|
2448 |
* @param {Function} fn
|
|
|
2449 |
* The function to be bound to a scope.
|
|
|
2450 |
*
|
|
|
2451 |
* @param {number} [uid]
|
|
|
2452 |
* An optional unique ID for the function to be set
|
|
|
2453 |
*
|
|
|
2454 |
* @return {Function}
|
|
|
2455 |
* The new function that will be bound into the context given
|
|
|
2456 |
*/
|
|
|
2457 |
const bind_ = function (context, fn, uid) {
|
|
|
2458 |
// Make sure the function has a unique ID
|
|
|
2459 |
if (!fn.guid) {
|
|
|
2460 |
fn.guid = newGUID();
|
|
|
2461 |
}
|
|
|
2462 |
|
|
|
2463 |
// Create the new function that changes the context
|
|
|
2464 |
const bound = fn.bind(context);
|
|
|
2465 |
|
|
|
2466 |
// Allow for the ability to individualize this function
|
|
|
2467 |
// Needed in the case where multiple objects might share the same prototype
|
|
|
2468 |
// IF both items add an event listener with the same function, then you try to remove just one
|
|
|
2469 |
// it will remove both because they both have the same guid.
|
|
|
2470 |
// when using this, you need to use the bind method when you remove the listener as well.
|
|
|
2471 |
// currently used in text tracks
|
|
|
2472 |
bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
|
|
|
2473 |
return bound;
|
|
|
2474 |
};
|
|
|
2475 |
|
|
|
2476 |
/**
|
|
|
2477 |
* Wraps the given function, `fn`, with a new function that only invokes `fn`
|
|
|
2478 |
* at most once per every `wait` milliseconds.
|
|
|
2479 |
*
|
|
|
2480 |
* @function
|
|
|
2481 |
* @param {Function} fn
|
|
|
2482 |
* The function to be throttled.
|
|
|
2483 |
*
|
|
|
2484 |
* @param {number} wait
|
|
|
2485 |
* The number of milliseconds by which to throttle.
|
|
|
2486 |
*
|
|
|
2487 |
* @return {Function}
|
|
|
2488 |
*/
|
|
|
2489 |
const throttle = function (fn, wait) {
|
|
|
2490 |
let last = window.performance.now();
|
|
|
2491 |
const throttled = function (...args) {
|
|
|
2492 |
const now = window.performance.now();
|
|
|
2493 |
if (now - last >= wait) {
|
|
|
2494 |
fn(...args);
|
|
|
2495 |
last = now;
|
|
|
2496 |
}
|
|
|
2497 |
};
|
|
|
2498 |
return throttled;
|
|
|
2499 |
};
|
|
|
2500 |
|
|
|
2501 |
/**
|
|
|
2502 |
* Creates a debounced function that delays invoking `func` until after `wait`
|
|
|
2503 |
* milliseconds have elapsed since the last time the debounced function was
|
|
|
2504 |
* invoked.
|
|
|
2505 |
*
|
|
|
2506 |
* Inspired by lodash and underscore implementations.
|
|
|
2507 |
*
|
|
|
2508 |
* @function
|
|
|
2509 |
* @param {Function} func
|
|
|
2510 |
* The function to wrap with debounce behavior.
|
|
|
2511 |
*
|
|
|
2512 |
* @param {number} wait
|
|
|
2513 |
* The number of milliseconds to wait after the last invocation.
|
|
|
2514 |
*
|
|
|
2515 |
* @param {boolean} [immediate]
|
|
|
2516 |
* Whether or not to invoke the function immediately upon creation.
|
|
|
2517 |
*
|
|
|
2518 |
* @param {Object} [context=window]
|
|
|
2519 |
* The "context" in which the debounced function should debounce. For
|
|
|
2520 |
* example, if this function should be tied to a Video.js player,
|
|
|
2521 |
* the player can be passed here. Alternatively, defaults to the
|
|
|
2522 |
* global `window` object.
|
|
|
2523 |
*
|
|
|
2524 |
* @return {Function}
|
|
|
2525 |
* A debounced function.
|
|
|
2526 |
*/
|
|
|
2527 |
const debounce = function (func, wait, immediate, context = window) {
|
|
|
2528 |
let timeout;
|
|
|
2529 |
const cancel = () => {
|
|
|
2530 |
context.clearTimeout(timeout);
|
|
|
2531 |
timeout = null;
|
|
|
2532 |
};
|
|
|
2533 |
|
|
|
2534 |
/* eslint-disable consistent-this */
|
|
|
2535 |
const debounced = function () {
|
|
|
2536 |
const self = this;
|
|
|
2537 |
const args = arguments;
|
|
|
2538 |
let later = function () {
|
|
|
2539 |
timeout = null;
|
|
|
2540 |
later = null;
|
|
|
2541 |
if (!immediate) {
|
|
|
2542 |
func.apply(self, args);
|
|
|
2543 |
}
|
|
|
2544 |
};
|
|
|
2545 |
if (!timeout && immediate) {
|
|
|
2546 |
func.apply(self, args);
|
|
|
2547 |
}
|
|
|
2548 |
context.clearTimeout(timeout);
|
|
|
2549 |
timeout = context.setTimeout(later, wait);
|
|
|
2550 |
};
|
|
|
2551 |
/* eslint-enable consistent-this */
|
|
|
2552 |
|
|
|
2553 |
debounced.cancel = cancel;
|
|
|
2554 |
return debounced;
|
|
|
2555 |
};
|
|
|
2556 |
|
|
|
2557 |
var Fn = /*#__PURE__*/Object.freeze({
|
|
|
2558 |
__proto__: null,
|
|
|
2559 |
UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
|
|
|
2560 |
bind_: bind_,
|
|
|
2561 |
throttle: throttle,
|
|
|
2562 |
debounce: debounce
|
|
|
2563 |
});
|
|
|
2564 |
|
|
|
2565 |
/**
|
|
|
2566 |
* @file src/js/event-target.js
|
|
|
2567 |
*/
|
|
|
2568 |
let EVENT_MAP;
|
|
|
2569 |
|
|
|
2570 |
/**
|
|
|
2571 |
* `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
|
|
|
2572 |
* adds shorthand functions that wrap around lengthy functions. For example:
|
|
|
2573 |
* the `on` function is a wrapper around `addEventListener`.
|
|
|
2574 |
*
|
|
|
2575 |
* @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
|
|
|
2576 |
* @class EventTarget
|
|
|
2577 |
*/
|
|
|
2578 |
class EventTarget$2 {
|
|
|
2579 |
/**
|
|
|
2580 |
* Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
|
|
|
2581 |
* function that will get called when an event with a certain name gets triggered.
|
|
|
2582 |
*
|
|
|
2583 |
* @param {string|string[]} type
|
|
|
2584 |
* An event name or an array of event names.
|
|
|
2585 |
*
|
|
|
2586 |
* @param {Function} fn
|
|
|
2587 |
* The function to call with `EventTarget`s
|
|
|
2588 |
*/
|
|
|
2589 |
on(type, fn) {
|
|
|
2590 |
// Remove the addEventListener alias before calling Events.on
|
|
|
2591 |
// so we don't get into an infinite type loop
|
|
|
2592 |
const ael = this.addEventListener;
|
|
|
2593 |
this.addEventListener = () => {};
|
|
|
2594 |
on(this, type, fn);
|
|
|
2595 |
this.addEventListener = ael;
|
|
|
2596 |
}
|
|
|
2597 |
/**
|
|
|
2598 |
* Removes an `event listener` for a specific event from an instance of `EventTarget`.
|
|
|
2599 |
* This makes it so that the `event listener` will no longer get called when the
|
|
|
2600 |
* named event happens.
|
|
|
2601 |
*
|
|
|
2602 |
* @param {string|string[]} type
|
|
|
2603 |
* An event name or an array of event names.
|
|
|
2604 |
*
|
|
|
2605 |
* @param {Function} fn
|
|
|
2606 |
* The function to remove.
|
|
|
2607 |
*/
|
|
|
2608 |
off(type, fn) {
|
|
|
2609 |
off(this, type, fn);
|
|
|
2610 |
}
|
|
|
2611 |
/**
|
|
|
2612 |
* This function will add an `event listener` that gets triggered only once. After the
|
|
|
2613 |
* first trigger it will get removed. This is like adding an `event listener`
|
|
|
2614 |
* with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
|
|
|
2615 |
*
|
|
|
2616 |
* @param {string|string[]} type
|
|
|
2617 |
* An event name or an array of event names.
|
|
|
2618 |
*
|
|
|
2619 |
* @param {Function} fn
|
|
|
2620 |
* The function to be called once for each event name.
|
|
|
2621 |
*/
|
|
|
2622 |
one(type, fn) {
|
|
|
2623 |
// Remove the addEventListener aliasing Events.on
|
|
|
2624 |
// so we don't get into an infinite type loop
|
|
|
2625 |
const ael = this.addEventListener;
|
|
|
2626 |
this.addEventListener = () => {};
|
|
|
2627 |
one(this, type, fn);
|
|
|
2628 |
this.addEventListener = ael;
|
|
|
2629 |
}
|
|
|
2630 |
/**
|
|
|
2631 |
* This function will add an `event listener` that gets triggered only once and is
|
|
|
2632 |
* removed from all events. This is like adding an array of `event listener`s
|
|
|
2633 |
* with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
|
|
|
2634 |
* first time it is triggered.
|
|
|
2635 |
*
|
|
|
2636 |
* @param {string|string[]} type
|
|
|
2637 |
* An event name or an array of event names.
|
|
|
2638 |
*
|
|
|
2639 |
* @param {Function} fn
|
|
|
2640 |
* The function to be called once for each event name.
|
|
|
2641 |
*/
|
|
|
2642 |
any(type, fn) {
|
|
|
2643 |
// Remove the addEventListener aliasing Events.on
|
|
|
2644 |
// so we don't get into an infinite type loop
|
|
|
2645 |
const ael = this.addEventListener;
|
|
|
2646 |
this.addEventListener = () => {};
|
|
|
2647 |
any(this, type, fn);
|
|
|
2648 |
this.addEventListener = ael;
|
|
|
2649 |
}
|
|
|
2650 |
/**
|
|
|
2651 |
* This function causes an event to happen. This will then cause any `event listeners`
|
|
|
2652 |
* that are waiting for that event, to get called. If there are no `event listeners`
|
|
|
2653 |
* for an event then nothing will happen.
|
|
|
2654 |
*
|
|
|
2655 |
* If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
|
|
|
2656 |
* Trigger will also call the `on` + `uppercaseEventName` function.
|
|
|
2657 |
*
|
|
|
2658 |
* Example:
|
|
|
2659 |
* 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
|
|
|
2660 |
* `onClick` if it exists.
|
|
|
2661 |
*
|
|
|
2662 |
* @param {string|EventTarget~Event|Object} event
|
|
|
2663 |
* The name of the event, an `Event`, or an object with a key of type set to
|
|
|
2664 |
* an event name.
|
|
|
2665 |
*/
|
|
|
2666 |
trigger(event) {
|
|
|
2667 |
const type = event.type || event;
|
|
|
2668 |
|
|
|
2669 |
// deprecation
|
|
|
2670 |
// In a future version we should default target to `this`
|
|
|
2671 |
// similar to how we default the target to `elem` in
|
|
|
2672 |
// `Events.trigger`. Right now the default `target` will be
|
|
|
2673 |
// `document` due to the `Event.fixEvent` call.
|
|
|
2674 |
if (typeof event === 'string') {
|
|
|
2675 |
event = {
|
|
|
2676 |
type
|
|
|
2677 |
};
|
|
|
2678 |
}
|
|
|
2679 |
event = fixEvent(event);
|
|
|
2680 |
if (this.allowedEvents_[type] && this['on' + type]) {
|
|
|
2681 |
this['on' + type](event);
|
|
|
2682 |
}
|
|
|
2683 |
trigger(this, event);
|
|
|
2684 |
}
|
|
|
2685 |
queueTrigger(event) {
|
|
|
2686 |
// only set up EVENT_MAP if it'll be used
|
|
|
2687 |
if (!EVENT_MAP) {
|
|
|
2688 |
EVENT_MAP = new Map();
|
|
|
2689 |
}
|
|
|
2690 |
const type = event.type || event;
|
|
|
2691 |
let map = EVENT_MAP.get(this);
|
|
|
2692 |
if (!map) {
|
|
|
2693 |
map = new Map();
|
|
|
2694 |
EVENT_MAP.set(this, map);
|
|
|
2695 |
}
|
|
|
2696 |
const oldTimeout = map.get(type);
|
|
|
2697 |
map.delete(type);
|
|
|
2698 |
window.clearTimeout(oldTimeout);
|
|
|
2699 |
const timeout = window.setTimeout(() => {
|
|
|
2700 |
map.delete(type);
|
|
|
2701 |
// if we cleared out all timeouts for the current target, delete its map
|
|
|
2702 |
if (map.size === 0) {
|
|
|
2703 |
map = null;
|
|
|
2704 |
EVENT_MAP.delete(this);
|
|
|
2705 |
}
|
|
|
2706 |
this.trigger(event);
|
|
|
2707 |
}, 0);
|
|
|
2708 |
map.set(type, timeout);
|
|
|
2709 |
}
|
|
|
2710 |
}
|
|
|
2711 |
|
|
|
2712 |
/**
|
|
|
2713 |
* A Custom DOM event.
|
|
|
2714 |
*
|
|
|
2715 |
* @typedef {CustomEvent} Event
|
|
|
2716 |
* @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
|
|
|
2717 |
*/
|
|
|
2718 |
|
|
|
2719 |
/**
|
|
|
2720 |
* All event listeners should follow the following format.
|
|
|
2721 |
*
|
|
|
2722 |
* @callback EventListener
|
|
|
2723 |
* @this {EventTarget}
|
|
|
2724 |
*
|
|
|
2725 |
* @param {Event} event
|
|
|
2726 |
* the event that triggered this function
|
|
|
2727 |
*
|
|
|
2728 |
* @param {Object} [hash]
|
|
|
2729 |
* hash of data sent during the event
|
|
|
2730 |
*/
|
|
|
2731 |
|
|
|
2732 |
/**
|
|
|
2733 |
* An object containing event names as keys and booleans as values.
|
|
|
2734 |
*
|
|
|
2735 |
* > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
|
|
|
2736 |
* will have extra functionality. See that function for more information.
|
|
|
2737 |
*
|
|
|
2738 |
* @property EventTarget.prototype.allowedEvents_
|
|
|
2739 |
* @protected
|
|
|
2740 |
*/
|
|
|
2741 |
EventTarget$2.prototype.allowedEvents_ = {};
|
|
|
2742 |
|
|
|
2743 |
/**
|
|
|
2744 |
* An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
|
|
|
2745 |
* the standard DOM API.
|
|
|
2746 |
*
|
|
|
2747 |
* @function
|
|
|
2748 |
* @see {@link EventTarget#on}
|
|
|
2749 |
*/
|
|
|
2750 |
EventTarget$2.prototype.addEventListener = EventTarget$2.prototype.on;
|
|
|
2751 |
|
|
|
2752 |
/**
|
|
|
2753 |
* An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
|
|
|
2754 |
* the standard DOM API.
|
|
|
2755 |
*
|
|
|
2756 |
* @function
|
|
|
2757 |
* @see {@link EventTarget#off}
|
|
|
2758 |
*/
|
|
|
2759 |
EventTarget$2.prototype.removeEventListener = EventTarget$2.prototype.off;
|
|
|
2760 |
|
|
|
2761 |
/**
|
|
|
2762 |
* An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
|
|
|
2763 |
* the standard DOM API.
|
|
|
2764 |
*
|
|
|
2765 |
* @function
|
|
|
2766 |
* @see {@link EventTarget#trigger}
|
|
|
2767 |
*/
|
|
|
2768 |
EventTarget$2.prototype.dispatchEvent = EventTarget$2.prototype.trigger;
|
|
|
2769 |
|
|
|
2770 |
/**
|
|
|
2771 |
* @file mixins/evented.js
|
|
|
2772 |
* @module evented
|
|
|
2773 |
*/
|
|
|
2774 |
const objName = obj => {
|
|
|
2775 |
if (typeof obj.name === 'function') {
|
|
|
2776 |
return obj.name();
|
|
|
2777 |
}
|
|
|
2778 |
if (typeof obj.name === 'string') {
|
|
|
2779 |
return obj.name;
|
|
|
2780 |
}
|
|
|
2781 |
if (obj.name_) {
|
|
|
2782 |
return obj.name_;
|
|
|
2783 |
}
|
|
|
2784 |
if (obj.constructor && obj.constructor.name) {
|
|
|
2785 |
return obj.constructor.name;
|
|
|
2786 |
}
|
|
|
2787 |
return typeof obj;
|
|
|
2788 |
};
|
|
|
2789 |
|
|
|
2790 |
/**
|
|
|
2791 |
* Returns whether or not an object has had the evented mixin applied.
|
|
|
2792 |
*
|
|
|
2793 |
* @param {Object} object
|
|
|
2794 |
* An object to test.
|
|
|
2795 |
*
|
|
|
2796 |
* @return {boolean}
|
|
|
2797 |
* Whether or not the object appears to be evented.
|
|
|
2798 |
*/
|
|
|
2799 |
const isEvented = object => object instanceof EventTarget$2 || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
|
|
|
2800 |
|
|
|
2801 |
/**
|
|
|
2802 |
* Adds a callback to run after the evented mixin applied.
|
|
|
2803 |
*
|
|
|
2804 |
* @param {Object} target
|
|
|
2805 |
* An object to Add
|
|
|
2806 |
* @param {Function} callback
|
|
|
2807 |
* The callback to run.
|
|
|
2808 |
*/
|
|
|
2809 |
const addEventedCallback = (target, callback) => {
|
|
|
2810 |
if (isEvented(target)) {
|
|
|
2811 |
callback();
|
|
|
2812 |
} else {
|
|
|
2813 |
if (!target.eventedCallbacks) {
|
|
|
2814 |
target.eventedCallbacks = [];
|
|
|
2815 |
}
|
|
|
2816 |
target.eventedCallbacks.push(callback);
|
|
|
2817 |
}
|
|
|
2818 |
};
|
|
|
2819 |
|
|
|
2820 |
/**
|
|
|
2821 |
* Whether a value is a valid event type - non-empty string or array.
|
|
|
2822 |
*
|
|
|
2823 |
* @private
|
|
|
2824 |
* @param {string|Array} type
|
|
|
2825 |
* The type value to test.
|
|
|
2826 |
*
|
|
|
2827 |
* @return {boolean}
|
|
|
2828 |
* Whether or not the type is a valid event type.
|
|
|
2829 |
*/
|
|
|
2830 |
const isValidEventType = type =>
|
|
|
2831 |
// The regex here verifies that the `type` contains at least one non-
|
|
|
2832 |
// whitespace character.
|
|
|
2833 |
typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
|
|
|
2834 |
|
|
|
2835 |
/**
|
|
|
2836 |
* Validates a value to determine if it is a valid event target. Throws if not.
|
|
|
2837 |
*
|
|
|
2838 |
* @private
|
|
|
2839 |
* @throws {Error}
|
|
|
2840 |
* If the target does not appear to be a valid event target.
|
|
|
2841 |
*
|
|
|
2842 |
* @param {Object} target
|
|
|
2843 |
* The object to test.
|
|
|
2844 |
*
|
|
|
2845 |
* @param {Object} obj
|
|
|
2846 |
* The evented object we are validating for
|
|
|
2847 |
*
|
|
|
2848 |
* @param {string} fnName
|
|
|
2849 |
* The name of the evented mixin function that called this.
|
|
|
2850 |
*/
|
|
|
2851 |
const validateTarget = (target, obj, fnName) => {
|
|
|
2852 |
if (!target || !target.nodeName && !isEvented(target)) {
|
|
|
2853 |
throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
|
|
|
2854 |
}
|
|
|
2855 |
};
|
|
|
2856 |
|
|
|
2857 |
/**
|
|
|
2858 |
* Validates a value to determine if it is a valid event target. Throws if not.
|
|
|
2859 |
*
|
|
|
2860 |
* @private
|
|
|
2861 |
* @throws {Error}
|
|
|
2862 |
* If the type does not appear to be a valid event type.
|
|
|
2863 |
*
|
|
|
2864 |
* @param {string|Array} type
|
|
|
2865 |
* The type to test.
|
|
|
2866 |
*
|
|
|
2867 |
* @param {Object} obj
|
|
|
2868 |
* The evented object we are validating for
|
|
|
2869 |
*
|
|
|
2870 |
* @param {string} fnName
|
|
|
2871 |
* The name of the evented mixin function that called this.
|
|
|
2872 |
*/
|
|
|
2873 |
const validateEventType = (type, obj, fnName) => {
|
|
|
2874 |
if (!isValidEventType(type)) {
|
|
|
2875 |
throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
|
|
|
2876 |
}
|
|
|
2877 |
};
|
|
|
2878 |
|
|
|
2879 |
/**
|
|
|
2880 |
* Validates a value to determine if it is a valid listener. Throws if not.
|
|
|
2881 |
*
|
|
|
2882 |
* @private
|
|
|
2883 |
* @throws {Error}
|
|
|
2884 |
* If the listener is not a function.
|
|
|
2885 |
*
|
|
|
2886 |
* @param {Function} listener
|
|
|
2887 |
* The listener to test.
|
|
|
2888 |
*
|
|
|
2889 |
* @param {Object} obj
|
|
|
2890 |
* The evented object we are validating for
|
|
|
2891 |
*
|
|
|
2892 |
* @param {string} fnName
|
|
|
2893 |
* The name of the evented mixin function that called this.
|
|
|
2894 |
*/
|
|
|
2895 |
const validateListener = (listener, obj, fnName) => {
|
|
|
2896 |
if (typeof listener !== 'function') {
|
|
|
2897 |
throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
|
|
|
2898 |
}
|
|
|
2899 |
};
|
|
|
2900 |
|
|
|
2901 |
/**
|
|
|
2902 |
* Takes an array of arguments given to `on()` or `one()`, validates them, and
|
|
|
2903 |
* normalizes them into an object.
|
|
|
2904 |
*
|
|
|
2905 |
* @private
|
|
|
2906 |
* @param {Object} self
|
|
|
2907 |
* The evented object on which `on()` or `one()` was called. This
|
|
|
2908 |
* object will be bound as the `this` value for the listener.
|
|
|
2909 |
*
|
|
|
2910 |
* @param {Array} args
|
|
|
2911 |
* An array of arguments passed to `on()` or `one()`.
|
|
|
2912 |
*
|
|
|
2913 |
* @param {string} fnName
|
|
|
2914 |
* The name of the evented mixin function that called this.
|
|
|
2915 |
*
|
|
|
2916 |
* @return {Object}
|
|
|
2917 |
* An object containing useful values for `on()` or `one()` calls.
|
|
|
2918 |
*/
|
|
|
2919 |
const normalizeListenArgs = (self, args, fnName) => {
|
|
|
2920 |
// If the number of arguments is less than 3, the target is always the
|
|
|
2921 |
// evented object itself.
|
|
|
2922 |
const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
|
|
|
2923 |
let target;
|
|
|
2924 |
let type;
|
|
|
2925 |
let listener;
|
|
|
2926 |
if (isTargetingSelf) {
|
|
|
2927 |
target = self.eventBusEl_;
|
|
|
2928 |
|
|
|
2929 |
// Deal with cases where we got 3 arguments, but we are still listening to
|
|
|
2930 |
// the evented object itself.
|
|
|
2931 |
if (args.length >= 3) {
|
|
|
2932 |
args.shift();
|
|
|
2933 |
}
|
|
|
2934 |
[type, listener] = args;
|
|
|
2935 |
} else {
|
|
|
2936 |
[target, type, listener] = args;
|
|
|
2937 |
}
|
|
|
2938 |
validateTarget(target, self, fnName);
|
|
|
2939 |
validateEventType(type, self, fnName);
|
|
|
2940 |
validateListener(listener, self, fnName);
|
|
|
2941 |
listener = bind_(self, listener);
|
|
|
2942 |
return {
|
|
|
2943 |
isTargetingSelf,
|
|
|
2944 |
target,
|
|
|
2945 |
type,
|
|
|
2946 |
listener
|
|
|
2947 |
};
|
|
|
2948 |
};
|
|
|
2949 |
|
|
|
2950 |
/**
|
|
|
2951 |
* Adds the listener to the event type(s) on the target, normalizing for
|
|
|
2952 |
* the type of target.
|
|
|
2953 |
*
|
|
|
2954 |
* @private
|
|
|
2955 |
* @param {Element|Object} target
|
|
|
2956 |
* A DOM node or evented object.
|
|
|
2957 |
*
|
|
|
2958 |
* @param {string} method
|
|
|
2959 |
* The event binding method to use ("on" or "one").
|
|
|
2960 |
*
|
|
|
2961 |
* @param {string|Array} type
|
|
|
2962 |
* One or more event type(s).
|
|
|
2963 |
*
|
|
|
2964 |
* @param {Function} listener
|
|
|
2965 |
* A listener function.
|
|
|
2966 |
*/
|
|
|
2967 |
const listen = (target, method, type, listener) => {
|
|
|
2968 |
validateTarget(target, target, method);
|
|
|
2969 |
if (target.nodeName) {
|
|
|
2970 |
Events[method](target, type, listener);
|
|
|
2971 |
} else {
|
|
|
2972 |
target[method](type, listener);
|
|
|
2973 |
}
|
|
|
2974 |
};
|
|
|
2975 |
|
|
|
2976 |
/**
|
|
|
2977 |
* Contains methods that provide event capabilities to an object which is passed
|
|
|
2978 |
* to {@link module:evented|evented}.
|
|
|
2979 |
*
|
|
|
2980 |
* @mixin EventedMixin
|
|
|
2981 |
*/
|
|
|
2982 |
const EventedMixin = {
|
|
|
2983 |
/**
|
|
|
2984 |
* Add a listener to an event (or events) on this object or another evented
|
|
|
2985 |
* object.
|
|
|
2986 |
*
|
|
|
2987 |
* @param {string|Array|Element|Object} targetOrType
|
|
|
2988 |
* If this is a string or array, it represents the event type(s)
|
|
|
2989 |
* that will trigger the listener.
|
|
|
2990 |
*
|
|
|
2991 |
* Another evented object can be passed here instead, which will
|
|
|
2992 |
* cause the listener to listen for events on _that_ object.
|
|
|
2993 |
*
|
|
|
2994 |
* In either case, the listener's `this` value will be bound to
|
|
|
2995 |
* this object.
|
|
|
2996 |
*
|
|
|
2997 |
* @param {string|Array|Function} typeOrListener
|
|
|
2998 |
* If the first argument was a string or array, this should be the
|
|
|
2999 |
* listener function. Otherwise, this is a string or array of event
|
|
|
3000 |
* type(s).
|
|
|
3001 |
*
|
|
|
3002 |
* @param {Function} [listener]
|
|
|
3003 |
* If the first argument was another evented object, this will be
|
|
|
3004 |
* the listener function.
|
|
|
3005 |
*/
|
|
|
3006 |
on(...args) {
|
|
|
3007 |
const {
|
|
|
3008 |
isTargetingSelf,
|
|
|
3009 |
target,
|
|
|
3010 |
type,
|
|
|
3011 |
listener
|
|
|
3012 |
} = normalizeListenArgs(this, args, 'on');
|
|
|
3013 |
listen(target, 'on', type, listener);
|
|
|
3014 |
|
|
|
3015 |
// If this object is listening to another evented object.
|
|
|
3016 |
if (!isTargetingSelf) {
|
|
|
3017 |
// If this object is disposed, remove the listener.
|
|
|
3018 |
const removeListenerOnDispose = () => this.off(target, type, listener);
|
|
|
3019 |
|
|
|
3020 |
// Use the same function ID as the listener so we can remove it later it
|
|
|
3021 |
// using the ID of the original listener.
|
|
|
3022 |
removeListenerOnDispose.guid = listener.guid;
|
|
|
3023 |
|
|
|
3024 |
// Add a listener to the target's dispose event as well. This ensures
|
|
|
3025 |
// that if the target is disposed BEFORE this object, we remove the
|
|
|
3026 |
// removal listener that was just added. Otherwise, we create a memory leak.
|
|
|
3027 |
const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
|
|
|
3028 |
|
|
|
3029 |
// Use the same function ID as the listener so we can remove it later
|
|
|
3030 |
// it using the ID of the original listener.
|
|
|
3031 |
removeRemoverOnTargetDispose.guid = listener.guid;
|
|
|
3032 |
listen(this, 'on', 'dispose', removeListenerOnDispose);
|
|
|
3033 |
listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
|
|
|
3034 |
}
|
|
|
3035 |
},
|
|
|
3036 |
/**
|
|
|
3037 |
* Add a listener to an event (or events) on this object or another evented
|
|
|
3038 |
* object. The listener will be called once per event and then removed.
|
|
|
3039 |
*
|
|
|
3040 |
* @param {string|Array|Element|Object} targetOrType
|
|
|
3041 |
* If this is a string or array, it represents the event type(s)
|
|
|
3042 |
* that will trigger the listener.
|
|
|
3043 |
*
|
|
|
3044 |
* Another evented object can be passed here instead, which will
|
|
|
3045 |
* cause the listener to listen for events on _that_ object.
|
|
|
3046 |
*
|
|
|
3047 |
* In either case, the listener's `this` value will be bound to
|
|
|
3048 |
* this object.
|
|
|
3049 |
*
|
|
|
3050 |
* @param {string|Array|Function} typeOrListener
|
|
|
3051 |
* If the first argument was a string or array, this should be the
|
|
|
3052 |
* listener function. Otherwise, this is a string or array of event
|
|
|
3053 |
* type(s).
|
|
|
3054 |
*
|
|
|
3055 |
* @param {Function} [listener]
|
|
|
3056 |
* If the first argument was another evented object, this will be
|
|
|
3057 |
* the listener function.
|
|
|
3058 |
*/
|
|
|
3059 |
one(...args) {
|
|
|
3060 |
const {
|
|
|
3061 |
isTargetingSelf,
|
|
|
3062 |
target,
|
|
|
3063 |
type,
|
|
|
3064 |
listener
|
|
|
3065 |
} = normalizeListenArgs(this, args, 'one');
|
|
|
3066 |
|
|
|
3067 |
// Targeting this evented object.
|
|
|
3068 |
if (isTargetingSelf) {
|
|
|
3069 |
listen(target, 'one', type, listener);
|
|
|
3070 |
|
|
|
3071 |
// Targeting another evented object.
|
|
|
3072 |
} else {
|
|
|
3073 |
// TODO: This wrapper is incorrect! It should only
|
|
|
3074 |
// remove the wrapper for the event type that called it.
|
|
|
3075 |
// Instead all listeners are removed on the first trigger!
|
|
|
3076 |
// see https://github.com/videojs/video.js/issues/5962
|
|
|
3077 |
const wrapper = (...largs) => {
|
|
|
3078 |
this.off(target, type, wrapper);
|
|
|
3079 |
listener.apply(null, largs);
|
|
|
3080 |
};
|
|
|
3081 |
|
|
|
3082 |
// Use the same function ID as the listener so we can remove it later
|
|
|
3083 |
// it using the ID of the original listener.
|
|
|
3084 |
wrapper.guid = listener.guid;
|
|
|
3085 |
listen(target, 'one', type, wrapper);
|
|
|
3086 |
}
|
|
|
3087 |
},
|
|
|
3088 |
/**
|
|
|
3089 |
* Add a listener to an event (or events) on this object or another evented
|
|
|
3090 |
* object. The listener will only be called once for the first event that is triggered
|
|
|
3091 |
* then removed.
|
|
|
3092 |
*
|
|
|
3093 |
* @param {string|Array|Element|Object} targetOrType
|
|
|
3094 |
* If this is a string or array, it represents the event type(s)
|
|
|
3095 |
* that will trigger the listener.
|
|
|
3096 |
*
|
|
|
3097 |
* Another evented object can be passed here instead, which will
|
|
|
3098 |
* cause the listener to listen for events on _that_ object.
|
|
|
3099 |
*
|
|
|
3100 |
* In either case, the listener's `this` value will be bound to
|
|
|
3101 |
* this object.
|
|
|
3102 |
*
|
|
|
3103 |
* @param {string|Array|Function} typeOrListener
|
|
|
3104 |
* If the first argument was a string or array, this should be the
|
|
|
3105 |
* listener function. Otherwise, this is a string or array of event
|
|
|
3106 |
* type(s).
|
|
|
3107 |
*
|
|
|
3108 |
* @param {Function} [listener]
|
|
|
3109 |
* If the first argument was another evented object, this will be
|
|
|
3110 |
* the listener function.
|
|
|
3111 |
*/
|
|
|
3112 |
any(...args) {
|
|
|
3113 |
const {
|
|
|
3114 |
isTargetingSelf,
|
|
|
3115 |
target,
|
|
|
3116 |
type,
|
|
|
3117 |
listener
|
|
|
3118 |
} = normalizeListenArgs(this, args, 'any');
|
|
|
3119 |
|
|
|
3120 |
// Targeting this evented object.
|
|
|
3121 |
if (isTargetingSelf) {
|
|
|
3122 |
listen(target, 'any', type, listener);
|
|
|
3123 |
|
|
|
3124 |
// Targeting another evented object.
|
|
|
3125 |
} else {
|
|
|
3126 |
const wrapper = (...largs) => {
|
|
|
3127 |
this.off(target, type, wrapper);
|
|
|
3128 |
listener.apply(null, largs);
|
|
|
3129 |
};
|
|
|
3130 |
|
|
|
3131 |
// Use the same function ID as the listener so we can remove it later
|
|
|
3132 |
// it using the ID of the original listener.
|
|
|
3133 |
wrapper.guid = listener.guid;
|
|
|
3134 |
listen(target, 'any', type, wrapper);
|
|
|
3135 |
}
|
|
|
3136 |
},
|
|
|
3137 |
/**
|
|
|
3138 |
* Removes listener(s) from event(s) on an evented object.
|
|
|
3139 |
*
|
|
|
3140 |
* @param {string|Array|Element|Object} [targetOrType]
|
|
|
3141 |
* If this is a string or array, it represents the event type(s).
|
|
|
3142 |
*
|
|
|
3143 |
* Another evented object can be passed here instead, in which case
|
|
|
3144 |
* ALL 3 arguments are _required_.
|
|
|
3145 |
*
|
|
|
3146 |
* @param {string|Array|Function} [typeOrListener]
|
|
|
3147 |
* If the first argument was a string or array, this may be the
|
|
|
3148 |
* listener function. Otherwise, this is a string or array of event
|
|
|
3149 |
* type(s).
|
|
|
3150 |
*
|
|
|
3151 |
* @param {Function} [listener]
|
|
|
3152 |
* If the first argument was another evented object, this will be
|
|
|
3153 |
* the listener function; otherwise, _all_ listeners bound to the
|
|
|
3154 |
* event type(s) will be removed.
|
|
|
3155 |
*/
|
|
|
3156 |
off(targetOrType, typeOrListener, listener) {
|
|
|
3157 |
// Targeting this evented object.
|
|
|
3158 |
if (!targetOrType || isValidEventType(targetOrType)) {
|
|
|
3159 |
off(this.eventBusEl_, targetOrType, typeOrListener);
|
|
|
3160 |
|
|
|
3161 |
// Targeting another evented object.
|
|
|
3162 |
} else {
|
|
|
3163 |
const target = targetOrType;
|
|
|
3164 |
const type = typeOrListener;
|
|
|
3165 |
|
|
|
3166 |
// Fail fast and in a meaningful way!
|
|
|
3167 |
validateTarget(target, this, 'off');
|
|
|
3168 |
validateEventType(type, this, 'off');
|
|
|
3169 |
validateListener(listener, this, 'off');
|
|
|
3170 |
|
|
|
3171 |
// Ensure there's at least a guid, even if the function hasn't been used
|
|
|
3172 |
listener = bind_(this, listener);
|
|
|
3173 |
|
|
|
3174 |
// Remove the dispose listener on this evented object, which was given
|
|
|
3175 |
// the same guid as the event listener in on().
|
|
|
3176 |
this.off('dispose', listener);
|
|
|
3177 |
if (target.nodeName) {
|
|
|
3178 |
off(target, type, listener);
|
|
|
3179 |
off(target, 'dispose', listener);
|
|
|
3180 |
} else if (isEvented(target)) {
|
|
|
3181 |
target.off(type, listener);
|
|
|
3182 |
target.off('dispose', listener);
|
|
|
3183 |
}
|
|
|
3184 |
}
|
|
|
3185 |
},
|
|
|
3186 |
/**
|
|
|
3187 |
* Fire an event on this evented object, causing its listeners to be called.
|
|
|
3188 |
*
|
|
|
3189 |
* @param {string|Object} event
|
|
|
3190 |
* An event type or an object with a type property.
|
|
|
3191 |
*
|
|
|
3192 |
* @param {Object} [hash]
|
|
|
3193 |
* An additional object to pass along to listeners.
|
|
|
3194 |
*
|
|
|
3195 |
* @return {boolean}
|
|
|
3196 |
* Whether or not the default behavior was prevented.
|
|
|
3197 |
*/
|
|
|
3198 |
trigger(event, hash) {
|
|
|
3199 |
validateTarget(this.eventBusEl_, this, 'trigger');
|
|
|
3200 |
const type = event && typeof event !== 'string' ? event.type : event;
|
|
|
3201 |
if (!isValidEventType(type)) {
|
|
|
3202 |
throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
|
|
|
3203 |
}
|
|
|
3204 |
return trigger(this.eventBusEl_, event, hash);
|
|
|
3205 |
}
|
|
|
3206 |
};
|
|
|
3207 |
|
|
|
3208 |
/**
|
|
|
3209 |
* Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
|
|
|
3210 |
*
|
|
|
3211 |
* @param {Object} target
|
|
|
3212 |
* The object to which to add event methods.
|
|
|
3213 |
*
|
|
|
3214 |
* @param {Object} [options={}]
|
|
|
3215 |
* Options for customizing the mixin behavior.
|
|
|
3216 |
*
|
|
|
3217 |
* @param {string} [options.eventBusKey]
|
|
|
3218 |
* By default, adds a `eventBusEl_` DOM element to the target object,
|
|
|
3219 |
* which is used as an event bus. If the target object already has a
|
|
|
3220 |
* DOM element that should be used, pass its key here.
|
|
|
3221 |
*
|
|
|
3222 |
* @return {Object}
|
|
|
3223 |
* The target object.
|
|
|
3224 |
*/
|
|
|
3225 |
function evented(target, options = {}) {
|
|
|
3226 |
const {
|
|
|
3227 |
eventBusKey
|
|
|
3228 |
} = options;
|
|
|
3229 |
|
|
|
3230 |
// Set or create the eventBusEl_.
|
|
|
3231 |
if (eventBusKey) {
|
|
|
3232 |
if (!target[eventBusKey].nodeName) {
|
|
|
3233 |
throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
|
|
|
3234 |
}
|
|
|
3235 |
target.eventBusEl_ = target[eventBusKey];
|
|
|
3236 |
} else {
|
|
|
3237 |
target.eventBusEl_ = createEl('span', {
|
|
|
3238 |
className: 'vjs-event-bus'
|
|
|
3239 |
});
|
|
|
3240 |
}
|
|
|
3241 |
Object.assign(target, EventedMixin);
|
|
|
3242 |
if (target.eventedCallbacks) {
|
|
|
3243 |
target.eventedCallbacks.forEach(callback => {
|
|
|
3244 |
callback();
|
|
|
3245 |
});
|
|
|
3246 |
}
|
|
|
3247 |
|
|
|
3248 |
// When any evented object is disposed, it removes all its listeners.
|
|
|
3249 |
target.on('dispose', () => {
|
|
|
3250 |
target.off();
|
|
|
3251 |
[target, target.el_, target.eventBusEl_].forEach(function (val) {
|
|
|
3252 |
if (val && DomData.has(val)) {
|
|
|
3253 |
DomData.delete(val);
|
|
|
3254 |
}
|
|
|
3255 |
});
|
|
|
3256 |
window.setTimeout(() => {
|
|
|
3257 |
target.eventBusEl_ = null;
|
|
|
3258 |
}, 0);
|
|
|
3259 |
});
|
|
|
3260 |
return target;
|
|
|
3261 |
}
|
|
|
3262 |
|
|
|
3263 |
/**
|
|
|
3264 |
* @file mixins/stateful.js
|
|
|
3265 |
* @module stateful
|
|
|
3266 |
*/
|
|
|
3267 |
|
|
|
3268 |
/**
|
|
|
3269 |
* Contains methods that provide statefulness to an object which is passed
|
|
|
3270 |
* to {@link module:stateful}.
|
|
|
3271 |
*
|
|
|
3272 |
* @mixin StatefulMixin
|
|
|
3273 |
*/
|
|
|
3274 |
const StatefulMixin = {
|
|
|
3275 |
/**
|
|
|
3276 |
* A hash containing arbitrary keys and values representing the state of
|
|
|
3277 |
* the object.
|
|
|
3278 |
*
|
|
|
3279 |
* @type {Object}
|
|
|
3280 |
*/
|
|
|
3281 |
state: {},
|
|
|
3282 |
/**
|
|
|
3283 |
* Set the state of an object by mutating its
|
|
|
3284 |
* {@link module:stateful~StatefulMixin.state|state} object in place.
|
|
|
3285 |
*
|
|
|
3286 |
* @fires module:stateful~StatefulMixin#statechanged
|
|
|
3287 |
* @param {Object|Function} stateUpdates
|
|
|
3288 |
* A new set of properties to shallow-merge into the plugin state.
|
|
|
3289 |
* Can be a plain object or a function returning a plain object.
|
|
|
3290 |
*
|
|
|
3291 |
* @return {Object|undefined}
|
|
|
3292 |
* An object containing changes that occurred. If no changes
|
|
|
3293 |
* occurred, returns `undefined`.
|
|
|
3294 |
*/
|
|
|
3295 |
setState(stateUpdates) {
|
|
|
3296 |
// Support providing the `stateUpdates` state as a function.
|
|
|
3297 |
if (typeof stateUpdates === 'function') {
|
|
|
3298 |
stateUpdates = stateUpdates();
|
|
|
3299 |
}
|
|
|
3300 |
let changes;
|
|
|
3301 |
each(stateUpdates, (value, key) => {
|
|
|
3302 |
// Record the change if the value is different from what's in the
|
|
|
3303 |
// current state.
|
|
|
3304 |
if (this.state[key] !== value) {
|
|
|
3305 |
changes = changes || {};
|
|
|
3306 |
changes[key] = {
|
|
|
3307 |
from: this.state[key],
|
|
|
3308 |
to: value
|
|
|
3309 |
};
|
|
|
3310 |
}
|
|
|
3311 |
this.state[key] = value;
|
|
|
3312 |
});
|
|
|
3313 |
|
|
|
3314 |
// Only trigger "statechange" if there were changes AND we have a trigger
|
|
|
3315 |
// function. This allows us to not require that the target object be an
|
|
|
3316 |
// evented object.
|
|
|
3317 |
if (changes && isEvented(this)) {
|
|
|
3318 |
/**
|
|
|
3319 |
* An event triggered on an object that is both
|
|
|
3320 |
* {@link module:stateful|stateful} and {@link module:evented|evented}
|
|
|
3321 |
* indicating that its state has changed.
|
|
|
3322 |
*
|
|
|
3323 |
* @event module:stateful~StatefulMixin#statechanged
|
|
|
3324 |
* @type {Object}
|
|
|
3325 |
* @property {Object} changes
|
|
|
3326 |
* A hash containing the properties that were changed and
|
|
|
3327 |
* the values they were changed `from` and `to`.
|
|
|
3328 |
*/
|
|
|
3329 |
this.trigger({
|
|
|
3330 |
changes,
|
|
|
3331 |
type: 'statechanged'
|
|
|
3332 |
});
|
|
|
3333 |
}
|
|
|
3334 |
return changes;
|
|
|
3335 |
}
|
|
|
3336 |
};
|
|
|
3337 |
|
|
|
3338 |
/**
|
|
|
3339 |
* Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
|
|
|
3340 |
* object.
|
|
|
3341 |
*
|
|
|
3342 |
* If the target object is {@link module:evented|evented} and has a
|
|
|
3343 |
* `handleStateChanged` method, that method will be automatically bound to the
|
|
|
3344 |
* `statechanged` event on itself.
|
|
|
3345 |
*
|
|
|
3346 |
* @param {Object} target
|
|
|
3347 |
* The object to be made stateful.
|
|
|
3348 |
*
|
|
|
3349 |
* @param {Object} [defaultState]
|
|
|
3350 |
* A default set of properties to populate the newly-stateful object's
|
|
|
3351 |
* `state` property.
|
|
|
3352 |
*
|
|
|
3353 |
* @return {Object}
|
|
|
3354 |
* Returns the `target`.
|
|
|
3355 |
*/
|
|
|
3356 |
function stateful(target, defaultState) {
|
|
|
3357 |
Object.assign(target, StatefulMixin);
|
|
|
3358 |
|
|
|
3359 |
// This happens after the mixing-in because we need to replace the `state`
|
|
|
3360 |
// added in that step.
|
|
|
3361 |
target.state = Object.assign({}, target.state, defaultState);
|
|
|
3362 |
|
|
|
3363 |
// Auto-bind the `handleStateChanged` method of the target object if it exists.
|
|
|
3364 |
if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
|
|
|
3365 |
target.on('statechanged', target.handleStateChanged);
|
|
|
3366 |
}
|
|
|
3367 |
return target;
|
|
|
3368 |
}
|
|
|
3369 |
|
|
|
3370 |
/**
|
|
|
3371 |
* @file str.js
|
|
|
3372 |
* @module to-lower-case
|
|
|
3373 |
*/
|
|
|
3374 |
|
|
|
3375 |
/**
|
|
|
3376 |
* Lowercase the first letter of a string.
|
|
|
3377 |
*
|
|
|
3378 |
* @param {string} string
|
|
|
3379 |
* String to be lowercased
|
|
|
3380 |
*
|
|
|
3381 |
* @return {string}
|
|
|
3382 |
* The string with a lowercased first letter
|
|
|
3383 |
*/
|
|
|
3384 |
const toLowerCase = function (string) {
|
|
|
3385 |
if (typeof string !== 'string') {
|
|
|
3386 |
return string;
|
|
|
3387 |
}
|
|
|
3388 |
return string.replace(/./, w => w.toLowerCase());
|
|
|
3389 |
};
|
|
|
3390 |
|
|
|
3391 |
/**
|
|
|
3392 |
* Uppercase the first letter of a string.
|
|
|
3393 |
*
|
|
|
3394 |
* @param {string} string
|
|
|
3395 |
* String to be uppercased
|
|
|
3396 |
*
|
|
|
3397 |
* @return {string}
|
|
|
3398 |
* The string with an uppercased first letter
|
|
|
3399 |
*/
|
|
|
3400 |
const toTitleCase$1 = function (string) {
|
|
|
3401 |
if (typeof string !== 'string') {
|
|
|
3402 |
return string;
|
|
|
3403 |
}
|
|
|
3404 |
return string.replace(/./, w => w.toUpperCase());
|
|
|
3405 |
};
|
|
|
3406 |
|
|
|
3407 |
/**
|
|
|
3408 |
* Compares the TitleCase versions of the two strings for equality.
|
|
|
3409 |
*
|
|
|
3410 |
* @param {string} str1
|
|
|
3411 |
* The first string to compare
|
|
|
3412 |
*
|
|
|
3413 |
* @param {string} str2
|
|
|
3414 |
* The second string to compare
|
|
|
3415 |
*
|
|
|
3416 |
* @return {boolean}
|
|
|
3417 |
* Whether the TitleCase versions of the strings are equal
|
|
|
3418 |
*/
|
|
|
3419 |
const titleCaseEquals = function (str1, str2) {
|
|
|
3420 |
return toTitleCase$1(str1) === toTitleCase$1(str2);
|
|
|
3421 |
};
|
|
|
3422 |
|
|
|
3423 |
var Str = /*#__PURE__*/Object.freeze({
|
|
|
3424 |
__proto__: null,
|
|
|
3425 |
toLowerCase: toLowerCase,
|
|
|
3426 |
toTitleCase: toTitleCase$1,
|
|
|
3427 |
titleCaseEquals: titleCaseEquals
|
|
|
3428 |
});
|
|
|
3429 |
|
|
|
3430 |
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
|
3431 |
|
|
|
3432 |
function unwrapExports (x) {
|
|
|
3433 |
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
|
3434 |
}
|
|
|
3435 |
|
|
|
3436 |
function createCommonjsModule(fn, module) {
|
|
|
3437 |
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
|
|
3438 |
}
|
|
|
3439 |
|
|
|
3440 |
var keycode = createCommonjsModule(function (module, exports) {
|
|
|
3441 |
// Source: http://jsfiddle.net/vWx8V/
|
|
|
3442 |
// http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes
|
|
|
3443 |
|
|
|
3444 |
/**
|
|
|
3445 |
* Conenience method returns corresponding value for given keyName or keyCode.
|
|
|
3446 |
*
|
|
|
3447 |
* @param {Mixed} keyCode {Number} or keyName {String}
|
|
|
3448 |
* @return {Mixed}
|
|
|
3449 |
* @api public
|
|
|
3450 |
*/
|
|
|
3451 |
|
|
|
3452 |
function keyCode(searchInput) {
|
|
|
3453 |
// Keyboard Events
|
|
|
3454 |
if (searchInput && 'object' === typeof searchInput) {
|
|
|
3455 |
var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
|
|
|
3456 |
if (hasKeyCode) searchInput = hasKeyCode;
|
|
|
3457 |
}
|
|
|
3458 |
|
|
|
3459 |
// Numbers
|
|
|
3460 |
if ('number' === typeof searchInput) return names[searchInput];
|
|
|
3461 |
|
|
|
3462 |
// Everything else (cast to string)
|
|
|
3463 |
var search = String(searchInput);
|
|
|
3464 |
|
|
|
3465 |
// check codes
|
|
|
3466 |
var foundNamedKey = codes[search.toLowerCase()];
|
|
|
3467 |
if (foundNamedKey) return foundNamedKey;
|
|
|
3468 |
|
|
|
3469 |
// check aliases
|
|
|
3470 |
var foundNamedKey = aliases[search.toLowerCase()];
|
|
|
3471 |
if (foundNamedKey) return foundNamedKey;
|
|
|
3472 |
|
|
|
3473 |
// weird character?
|
|
|
3474 |
if (search.length === 1) return search.charCodeAt(0);
|
|
|
3475 |
return undefined;
|
|
|
3476 |
}
|
|
|
3477 |
|
|
|
3478 |
/**
|
|
|
3479 |
* Compares a keyboard event with a given keyCode or keyName.
|
|
|
3480 |
*
|
|
|
3481 |
* @param {Event} event Keyboard event that should be tested
|
|
|
3482 |
* @param {Mixed} keyCode {Number} or keyName {String}
|
|
|
3483 |
* @return {Boolean}
|
|
|
3484 |
* @api public
|
|
|
3485 |
*/
|
|
|
3486 |
keyCode.isEventKey = function isEventKey(event, nameOrCode) {
|
|
|
3487 |
if (event && 'object' === typeof event) {
|
|
|
3488 |
var keyCode = event.which || event.keyCode || event.charCode;
|
|
|
3489 |
if (keyCode === null || keyCode === undefined) {
|
|
|
3490 |
return false;
|
|
|
3491 |
}
|
|
|
3492 |
if (typeof nameOrCode === 'string') {
|
|
|
3493 |
// check codes
|
|
|
3494 |
var foundNamedKey = codes[nameOrCode.toLowerCase()];
|
|
|
3495 |
if (foundNamedKey) {
|
|
|
3496 |
return foundNamedKey === keyCode;
|
|
|
3497 |
}
|
|
|
3498 |
|
|
|
3499 |
// check aliases
|
|
|
3500 |
var foundNamedKey = aliases[nameOrCode.toLowerCase()];
|
|
|
3501 |
if (foundNamedKey) {
|
|
|
3502 |
return foundNamedKey === keyCode;
|
|
|
3503 |
}
|
|
|
3504 |
} else if (typeof nameOrCode === 'number') {
|
|
|
3505 |
return nameOrCode === keyCode;
|
|
|
3506 |
}
|
|
|
3507 |
return false;
|
|
|
3508 |
}
|
|
|
3509 |
};
|
|
|
3510 |
exports = module.exports = keyCode;
|
|
|
3511 |
|
|
|
3512 |
/**
|
|
|
3513 |
* Get by name
|
|
|
3514 |
*
|
|
|
3515 |
* exports.code['enter'] // => 13
|
|
|
3516 |
*/
|
|
|
3517 |
|
|
|
3518 |
var codes = exports.code = exports.codes = {
|
|
|
3519 |
'backspace': 8,
|
|
|
3520 |
'tab': 9,
|
|
|
3521 |
'enter': 13,
|
|
|
3522 |
'shift': 16,
|
|
|
3523 |
'ctrl': 17,
|
|
|
3524 |
'alt': 18,
|
|
|
3525 |
'pause/break': 19,
|
|
|
3526 |
'caps lock': 20,
|
|
|
3527 |
'esc': 27,
|
|
|
3528 |
'space': 32,
|
|
|
3529 |
'page up': 33,
|
|
|
3530 |
'page down': 34,
|
|
|
3531 |
'end': 35,
|
|
|
3532 |
'home': 36,
|
|
|
3533 |
'left': 37,
|
|
|
3534 |
'up': 38,
|
|
|
3535 |
'right': 39,
|
|
|
3536 |
'down': 40,
|
|
|
3537 |
'insert': 45,
|
|
|
3538 |
'delete': 46,
|
|
|
3539 |
'command': 91,
|
|
|
3540 |
'left command': 91,
|
|
|
3541 |
'right command': 93,
|
|
|
3542 |
'numpad *': 106,
|
|
|
3543 |
'numpad +': 107,
|
|
|
3544 |
'numpad -': 109,
|
|
|
3545 |
'numpad .': 110,
|
|
|
3546 |
'numpad /': 111,
|
|
|
3547 |
'num lock': 144,
|
|
|
3548 |
'scroll lock': 145,
|
|
|
3549 |
'my computer': 182,
|
|
|
3550 |
'my calculator': 183,
|
|
|
3551 |
';': 186,
|
|
|
3552 |
'=': 187,
|
|
|
3553 |
',': 188,
|
|
|
3554 |
'-': 189,
|
|
|
3555 |
'.': 190,
|
|
|
3556 |
'/': 191,
|
|
|
3557 |
'`': 192,
|
|
|
3558 |
'[': 219,
|
|
|
3559 |
'\\': 220,
|
|
|
3560 |
']': 221,
|
|
|
3561 |
"'": 222
|
|
|
3562 |
};
|
|
|
3563 |
|
|
|
3564 |
// Helper aliases
|
|
|
3565 |
|
|
|
3566 |
var aliases = exports.aliases = {
|
|
|
3567 |
'windows': 91,
|
|
|
3568 |
'⇧': 16,
|
|
|
3569 |
'⌥': 18,
|
|
|
3570 |
'⌃': 17,
|
|
|
3571 |
'⌘': 91,
|
|
|
3572 |
'ctl': 17,
|
|
|
3573 |
'control': 17,
|
|
|
3574 |
'option': 18,
|
|
|
3575 |
'pause': 19,
|
|
|
3576 |
'break': 19,
|
|
|
3577 |
'caps': 20,
|
|
|
3578 |
'return': 13,
|
|
|
3579 |
'escape': 27,
|
|
|
3580 |
'spc': 32,
|
|
|
3581 |
'spacebar': 32,
|
|
|
3582 |
'pgup': 33,
|
|
|
3583 |
'pgdn': 34,
|
|
|
3584 |
'ins': 45,
|
|
|
3585 |
'del': 46,
|
|
|
3586 |
'cmd': 91
|
|
|
3587 |
};
|
|
|
3588 |
|
|
|
3589 |
/*!
|
|
|
3590 |
* Programatically add the following
|
|
|
3591 |
*/
|
|
|
3592 |
|
|
|
3593 |
// lower case chars
|
|
|
3594 |
for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32;
|
|
|
3595 |
|
|
|
3596 |
// numbers
|
|
|
3597 |
for (var i = 48; i < 58; i++) codes[i - 48] = i;
|
|
|
3598 |
|
|
|
3599 |
// function keys
|
|
|
3600 |
for (i = 1; i < 13; i++) codes['f' + i] = i + 111;
|
|
|
3601 |
|
|
|
3602 |
// numpad keys
|
|
|
3603 |
for (i = 0; i < 10; i++) codes['numpad ' + i] = i + 96;
|
|
|
3604 |
|
|
|
3605 |
/**
|
|
|
3606 |
* Get by code
|
|
|
3607 |
*
|
|
|
3608 |
* exports.name[13] // => 'Enter'
|
|
|
3609 |
*/
|
|
|
3610 |
|
|
|
3611 |
var names = exports.names = exports.title = {}; // title for backward compat
|
|
|
3612 |
|
|
|
3613 |
// Create reverse mapping
|
|
|
3614 |
for (i in codes) names[codes[i]] = i;
|
|
|
3615 |
|
|
|
3616 |
// Add aliases
|
|
|
3617 |
for (var alias in aliases) {
|
|
|
3618 |
codes[alias] = aliases[alias];
|
|
|
3619 |
}
|
|
|
3620 |
});
|
|
|
3621 |
keycode.code;
|
|
|
3622 |
keycode.codes;
|
|
|
3623 |
keycode.aliases;
|
|
|
3624 |
keycode.names;
|
|
|
3625 |
keycode.title;
|
|
|
3626 |
|
|
|
3627 |
/**
|
|
|
3628 |
* Player Component - Base class for all UI objects
|
|
|
3629 |
*
|
|
|
3630 |
* @file component.js
|
|
|
3631 |
*/
|
|
|
3632 |
|
|
|
3633 |
/**
|
|
|
3634 |
* Base class for all UI Components.
|
|
|
3635 |
* Components are UI objects which represent both a javascript object and an element
|
|
|
3636 |
* in the DOM. They can be children of other components, and can have
|
|
|
3637 |
* children themselves.
|
|
|
3638 |
*
|
|
|
3639 |
* Components can also use methods from {@link EventTarget}
|
|
|
3640 |
*/
|
|
|
3641 |
class Component$1 {
|
|
|
3642 |
/**
|
|
|
3643 |
* A callback that is called when a component is ready. Does not have any
|
|
|
3644 |
* parameters and any callback value will be ignored.
|
|
|
3645 |
*
|
|
|
3646 |
* @callback ReadyCallback
|
|
|
3647 |
* @this Component
|
|
|
3648 |
*/
|
|
|
3649 |
|
|
|
3650 |
/**
|
|
|
3651 |
* Creates an instance of this class.
|
|
|
3652 |
*
|
|
|
3653 |
* @param { import('./player').default } player
|
|
|
3654 |
* The `Player` that this class should be attached to.
|
|
|
3655 |
*
|
|
|
3656 |
* @param {Object} [options]
|
|
|
3657 |
* The key/value store of component options.
|
|
|
3658 |
*
|
|
|
3659 |
* @param {Object[]} [options.children]
|
|
|
3660 |
* An array of children objects to initialize this component with. Children objects have
|
|
|
3661 |
* a name property that will be used if more than one component of the same type needs to be
|
|
|
3662 |
* added.
|
|
|
3663 |
*
|
|
|
3664 |
* @param {string} [options.className]
|
|
|
3665 |
* A class or space separated list of classes to add the component
|
|
|
3666 |
*
|
|
|
3667 |
* @param {ReadyCallback} [ready]
|
|
|
3668 |
* Function that gets called when the `Component` is ready.
|
|
|
3669 |
*/
|
|
|
3670 |
constructor(player, options, ready) {
|
|
|
3671 |
// The component might be the player itself and we can't pass `this` to super
|
|
|
3672 |
if (!player && this.play) {
|
|
|
3673 |
this.player_ = player = this; // eslint-disable-line
|
|
|
3674 |
} else {
|
|
|
3675 |
this.player_ = player;
|
|
|
3676 |
}
|
|
|
3677 |
this.isDisposed_ = false;
|
|
|
3678 |
|
|
|
3679 |
// Hold the reference to the parent component via `addChild` method
|
|
|
3680 |
this.parentComponent_ = null;
|
|
|
3681 |
|
|
|
3682 |
// Make a copy of prototype.options_ to protect against overriding defaults
|
|
|
3683 |
this.options_ = merge$2({}, this.options_);
|
|
|
3684 |
|
|
|
3685 |
// Updated options with supplied options
|
|
|
3686 |
options = this.options_ = merge$2(this.options_, options);
|
|
|
3687 |
|
|
|
3688 |
// Get ID from options or options element if one is supplied
|
|
|
3689 |
this.id_ = options.id || options.el && options.el.id;
|
|
|
3690 |
|
|
|
3691 |
// If there was no ID from the options, generate one
|
|
|
3692 |
if (!this.id_) {
|
|
|
3693 |
// Don't require the player ID function in the case of mock players
|
|
|
3694 |
const id = player && player.id && player.id() || 'no_player';
|
|
|
3695 |
this.id_ = `${id}_component_${newGUID()}`;
|
|
|
3696 |
}
|
|
|
3697 |
this.name_ = options.name || null;
|
|
|
3698 |
|
|
|
3699 |
// Create element if one wasn't provided in options
|
|
|
3700 |
if (options.el) {
|
|
|
3701 |
this.el_ = options.el;
|
|
|
3702 |
} else if (options.createEl !== false) {
|
|
|
3703 |
this.el_ = this.createEl();
|
|
|
3704 |
}
|
|
|
3705 |
if (options.className && this.el_) {
|
|
|
3706 |
options.className.split(' ').forEach(c => this.addClass(c));
|
|
|
3707 |
}
|
|
|
3708 |
|
|
|
3709 |
// Remove the placeholder event methods. If the component is evented, the
|
|
|
3710 |
// real methods are added next
|
|
|
3711 |
['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
|
|
|
3712 |
this[fn] = undefined;
|
|
|
3713 |
});
|
|
|
3714 |
|
|
|
3715 |
// if evented is anything except false, we want to mixin in evented
|
|
|
3716 |
if (options.evented !== false) {
|
|
|
3717 |
// Make this an evented object and use `el_`, if available, as its event bus
|
|
|
3718 |
evented(this, {
|
|
|
3719 |
eventBusKey: this.el_ ? 'el_' : null
|
|
|
3720 |
});
|
|
|
3721 |
this.handleLanguagechange = this.handleLanguagechange.bind(this);
|
|
|
3722 |
this.on(this.player_, 'languagechange', this.handleLanguagechange);
|
|
|
3723 |
}
|
|
|
3724 |
stateful(this, this.constructor.defaultState);
|
|
|
3725 |
this.children_ = [];
|
|
|
3726 |
this.childIndex_ = {};
|
|
|
3727 |
this.childNameIndex_ = {};
|
|
|
3728 |
this.setTimeoutIds_ = new Set();
|
|
|
3729 |
this.setIntervalIds_ = new Set();
|
|
|
3730 |
this.rafIds_ = new Set();
|
|
|
3731 |
this.namedRafs_ = new Map();
|
|
|
3732 |
this.clearingTimersOnDispose_ = false;
|
|
|
3733 |
|
|
|
3734 |
// Add any child components in options
|
|
|
3735 |
if (options.initChildren !== false) {
|
|
|
3736 |
this.initChildren();
|
|
|
3737 |
}
|
|
|
3738 |
|
|
|
3739 |
// Don't want to trigger ready here or it will go before init is actually
|
|
|
3740 |
// finished for all children that run this constructor
|
|
|
3741 |
this.ready(ready);
|
|
|
3742 |
if (options.reportTouchActivity !== false) {
|
|
|
3743 |
this.enableTouchActivity();
|
|
|
3744 |
}
|
|
|
3745 |
}
|
|
|
3746 |
|
|
|
3747 |
// `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
|
|
|
3748 |
// They are replaced or removed in the constructor
|
|
|
3749 |
|
|
|
3750 |
/**
|
|
|
3751 |
* Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
|
|
|
3752 |
* function that will get called when an event with a certain name gets triggered.
|
|
|
3753 |
*
|
|
|
3754 |
* @param {string|string[]} type
|
|
|
3755 |
* An event name or an array of event names.
|
|
|
3756 |
*
|
|
|
3757 |
* @param {Function} fn
|
|
|
3758 |
* The function to call with `EventTarget`s
|
|
|
3759 |
*/
|
|
|
3760 |
on(type, fn) {}
|
|
|
3761 |
|
|
|
3762 |
/**
|
|
|
3763 |
* Removes an `event listener` for a specific event from an instance of `EventTarget`.
|
|
|
3764 |
* This makes it so that the `event listener` will no longer get called when the
|
|
|
3765 |
* named event happens.
|
|
|
3766 |
*
|
|
|
3767 |
* @param {string|string[]} type
|
|
|
3768 |
* An event name or an array of event names.
|
|
|
3769 |
*
|
|
|
3770 |
* @param {Function} [fn]
|
|
|
3771 |
* The function to remove. If not specified, all listeners managed by Video.js will be removed.
|
|
|
3772 |
*/
|
|
|
3773 |
off(type, fn) {}
|
|
|
3774 |
|
|
|
3775 |
/**
|
|
|
3776 |
* This function will add an `event listener` that gets triggered only once. After the
|
|
|
3777 |
* first trigger it will get removed. This is like adding an `event listener`
|
|
|
3778 |
* with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
|
|
|
3779 |
*
|
|
|
3780 |
* @param {string|string[]} type
|
|
|
3781 |
* An event name or an array of event names.
|
|
|
3782 |
*
|
|
|
3783 |
* @param {Function} fn
|
|
|
3784 |
* The function to be called once for each event name.
|
|
|
3785 |
*/
|
|
|
3786 |
one(type, fn) {}
|
|
|
3787 |
|
|
|
3788 |
/**
|
|
|
3789 |
* This function will add an `event listener` that gets triggered only once and is
|
|
|
3790 |
* removed from all events. This is like adding an array of `event listener`s
|
|
|
3791 |
* with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
|
|
|
3792 |
* first time it is triggered.
|
|
|
3793 |
*
|
|
|
3794 |
* @param {string|string[]} type
|
|
|
3795 |
* An event name or an array of event names.
|
|
|
3796 |
*
|
|
|
3797 |
* @param {Function} fn
|
|
|
3798 |
* The function to be called once for each event name.
|
|
|
3799 |
*/
|
|
|
3800 |
any(type, fn) {}
|
|
|
3801 |
|
|
|
3802 |
/**
|
|
|
3803 |
* This function causes an event to happen. This will then cause any `event listeners`
|
|
|
3804 |
* that are waiting for that event, to get called. If there are no `event listeners`
|
|
|
3805 |
* for an event then nothing will happen.
|
|
|
3806 |
*
|
|
|
3807 |
* If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
|
|
|
3808 |
* Trigger will also call the `on` + `uppercaseEventName` function.
|
|
|
3809 |
*
|
|
|
3810 |
* Example:
|
|
|
3811 |
* 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
|
|
|
3812 |
* `onClick` if it exists.
|
|
|
3813 |
*
|
|
|
3814 |
* @param {string|Event|Object} event
|
|
|
3815 |
* The name of the event, an `Event`, or an object with a key of type set to
|
|
|
3816 |
* an event name.
|
|
|
3817 |
*
|
|
|
3818 |
* @param {Object} [hash]
|
|
|
3819 |
* Optionally extra argument to pass through to an event listener
|
|
|
3820 |
*/
|
|
|
3821 |
trigger(event, hash) {}
|
|
|
3822 |
|
|
|
3823 |
/**
|
|
|
3824 |
* Dispose of the `Component` and all child components.
|
|
|
3825 |
*
|
|
|
3826 |
* @fires Component#dispose
|
|
|
3827 |
*
|
|
|
3828 |
* @param {Object} options
|
|
|
3829 |
* @param {Element} options.originalEl element with which to replace player element
|
|
|
3830 |
*/
|
|
|
3831 |
dispose(options = {}) {
|
|
|
3832 |
// Bail out if the component has already been disposed.
|
|
|
3833 |
if (this.isDisposed_) {
|
|
|
3834 |
return;
|
|
|
3835 |
}
|
|
|
3836 |
if (this.readyQueue_) {
|
|
|
3837 |
this.readyQueue_.length = 0;
|
|
|
3838 |
}
|
|
|
3839 |
|
|
|
3840 |
/**
|
|
|
3841 |
* Triggered when a `Component` is disposed.
|
|
|
3842 |
*
|
|
|
3843 |
* @event Component#dispose
|
|
|
3844 |
* @type {Event}
|
|
|
3845 |
*
|
|
|
3846 |
* @property {boolean} [bubbles=false]
|
|
|
3847 |
* set to false so that the dispose event does not
|
|
|
3848 |
* bubble up
|
|
|
3849 |
*/
|
|
|
3850 |
this.trigger({
|
|
|
3851 |
type: 'dispose',
|
|
|
3852 |
bubbles: false
|
|
|
3853 |
});
|
|
|
3854 |
this.isDisposed_ = true;
|
|
|
3855 |
|
|
|
3856 |
// Dispose all children.
|
|
|
3857 |
if (this.children_) {
|
|
|
3858 |
for (let i = this.children_.length - 1; i >= 0; i--) {
|
|
|
3859 |
if (this.children_[i].dispose) {
|
|
|
3860 |
this.children_[i].dispose();
|
|
|
3861 |
}
|
|
|
3862 |
}
|
|
|
3863 |
}
|
|
|
3864 |
|
|
|
3865 |
// Delete child references
|
|
|
3866 |
this.children_ = null;
|
|
|
3867 |
this.childIndex_ = null;
|
|
|
3868 |
this.childNameIndex_ = null;
|
|
|
3869 |
this.parentComponent_ = null;
|
|
|
3870 |
if (this.el_) {
|
|
|
3871 |
// Remove element from DOM
|
|
|
3872 |
if (this.el_.parentNode) {
|
|
|
3873 |
if (options.restoreEl) {
|
|
|
3874 |
this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
|
|
|
3875 |
} else {
|
|
|
3876 |
this.el_.parentNode.removeChild(this.el_);
|
|
|
3877 |
}
|
|
|
3878 |
}
|
|
|
3879 |
this.el_ = null;
|
|
|
3880 |
}
|
|
|
3881 |
|
|
|
3882 |
// remove reference to the player after disposing of the element
|
|
|
3883 |
this.player_ = null;
|
|
|
3884 |
}
|
|
|
3885 |
|
|
|
3886 |
/**
|
|
|
3887 |
* Determine whether or not this component has been disposed.
|
|
|
3888 |
*
|
|
|
3889 |
* @return {boolean}
|
|
|
3890 |
* If the component has been disposed, will be `true`. Otherwise, `false`.
|
|
|
3891 |
*/
|
|
|
3892 |
isDisposed() {
|
|
|
3893 |
return Boolean(this.isDisposed_);
|
|
|
3894 |
}
|
|
|
3895 |
|
|
|
3896 |
/**
|
|
|
3897 |
* Return the {@link Player} that the `Component` has attached to.
|
|
|
3898 |
*
|
|
|
3899 |
* @return { import('./player').default }
|
|
|
3900 |
* The player that this `Component` has attached to.
|
|
|
3901 |
*/
|
|
|
3902 |
player() {
|
|
|
3903 |
return this.player_;
|
|
|
3904 |
}
|
|
|
3905 |
|
|
|
3906 |
/**
|
|
|
3907 |
* Deep merge of options objects with new options.
|
|
|
3908 |
* > Note: When both `obj` and `options` contain properties whose values are objects.
|
|
|
3909 |
* The two properties get merged using {@link module:obj.merge}
|
|
|
3910 |
*
|
|
|
3911 |
* @param {Object} obj
|
|
|
3912 |
* The object that contains new options.
|
|
|
3913 |
*
|
|
|
3914 |
* @return {Object}
|
|
|
3915 |
* A new object of `this.options_` and `obj` merged together.
|
|
|
3916 |
*/
|
|
|
3917 |
options(obj) {
|
|
|
3918 |
if (!obj) {
|
|
|
3919 |
return this.options_;
|
|
|
3920 |
}
|
|
|
3921 |
this.options_ = merge$2(this.options_, obj);
|
|
|
3922 |
return this.options_;
|
|
|
3923 |
}
|
|
|
3924 |
|
|
|
3925 |
/**
|
|
|
3926 |
* Get the `Component`s DOM element
|
|
|
3927 |
*
|
|
|
3928 |
* @return {Element}
|
|
|
3929 |
* The DOM element for this `Component`.
|
|
|
3930 |
*/
|
|
|
3931 |
el() {
|
|
|
3932 |
return this.el_;
|
|
|
3933 |
}
|
|
|
3934 |
|
|
|
3935 |
/**
|
|
|
3936 |
* Create the `Component`s DOM element.
|
|
|
3937 |
*
|
|
|
3938 |
* @param {string} [tagName]
|
|
|
3939 |
* Element's DOM node type. e.g. 'div'
|
|
|
3940 |
*
|
|
|
3941 |
* @param {Object} [properties]
|
|
|
3942 |
* An object of properties that should be set.
|
|
|
3943 |
*
|
|
|
3944 |
* @param {Object} [attributes]
|
|
|
3945 |
* An object of attributes that should be set.
|
|
|
3946 |
*
|
|
|
3947 |
* @return {Element}
|
|
|
3948 |
* The element that gets created.
|
|
|
3949 |
*/
|
|
|
3950 |
createEl(tagName, properties, attributes) {
|
|
|
3951 |
return createEl(tagName, properties, attributes);
|
|
|
3952 |
}
|
|
|
3953 |
|
|
|
3954 |
/**
|
|
|
3955 |
* Localize a string given the string in english.
|
|
|
3956 |
*
|
|
|
3957 |
* If tokens are provided, it'll try and run a simple token replacement on the provided string.
|
|
|
3958 |
* The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
|
|
|
3959 |
*
|
|
|
3960 |
* If a `defaultValue` is provided, it'll use that over `string`,
|
|
|
3961 |
* if a value isn't found in provided language files.
|
|
|
3962 |
* This is useful if you want to have a descriptive key for token replacement
|
|
|
3963 |
* but have a succinct localized string and not require `en.json` to be included.
|
|
|
3964 |
*
|
|
|
3965 |
* Currently, it is used for the progress bar timing.
|
|
|
3966 |
* ```js
|
|
|
3967 |
* {
|
|
|
3968 |
* "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
|
|
|
3969 |
* }
|
|
|
3970 |
* ```
|
|
|
3971 |
* It is then used like so:
|
|
|
3972 |
* ```js
|
|
|
3973 |
* this.localize('progress bar timing: currentTime={1} duration{2}',
|
|
|
3974 |
* [this.player_.currentTime(), this.player_.duration()],
|
|
|
3975 |
* '{1} of {2}');
|
|
|
3976 |
* ```
|
|
|
3977 |
*
|
|
|
3978 |
* Which outputs something like: `01:23 of 24:56`.
|
|
|
3979 |
*
|
|
|
3980 |
*
|
|
|
3981 |
* @param {string} string
|
|
|
3982 |
* The string to localize and the key to lookup in the language files.
|
|
|
3983 |
* @param {string[]} [tokens]
|
|
|
3984 |
* If the current item has token replacements, provide the tokens here.
|
|
|
3985 |
* @param {string} [defaultValue]
|
|
|
3986 |
* Defaults to `string`. Can be a default value to use for token replacement
|
|
|
3987 |
* if the lookup key is needed to be separate.
|
|
|
3988 |
*
|
|
|
3989 |
* @return {string}
|
|
|
3990 |
* The localized string or if no localization exists the english string.
|
|
|
3991 |
*/
|
|
|
3992 |
localize(string, tokens, defaultValue = string) {
|
|
|
3993 |
const code = this.player_.language && this.player_.language();
|
|
|
3994 |
const languages = this.player_.languages && this.player_.languages();
|
|
|
3995 |
const language = languages && languages[code];
|
|
|
3996 |
const primaryCode = code && code.split('-')[0];
|
|
|
3997 |
const primaryLang = languages && languages[primaryCode];
|
|
|
3998 |
let localizedString = defaultValue;
|
|
|
3999 |
if (language && language[string]) {
|
|
|
4000 |
localizedString = language[string];
|
|
|
4001 |
} else if (primaryLang && primaryLang[string]) {
|
|
|
4002 |
localizedString = primaryLang[string];
|
|
|
4003 |
}
|
|
|
4004 |
if (tokens) {
|
|
|
4005 |
localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
|
|
|
4006 |
const value = tokens[index - 1];
|
|
|
4007 |
let ret = value;
|
|
|
4008 |
if (typeof value === 'undefined') {
|
|
|
4009 |
ret = match;
|
|
|
4010 |
}
|
|
|
4011 |
return ret;
|
|
|
4012 |
});
|
|
|
4013 |
}
|
|
|
4014 |
return localizedString;
|
|
|
4015 |
}
|
|
|
4016 |
|
|
|
4017 |
/**
|
|
|
4018 |
* Handles language change for the player in components. Should be overridden by sub-components.
|
|
|
4019 |
*
|
|
|
4020 |
* @abstract
|
|
|
4021 |
*/
|
|
|
4022 |
handleLanguagechange() {}
|
|
|
4023 |
|
|
|
4024 |
/**
|
|
|
4025 |
* Return the `Component`s DOM element. This is where children get inserted.
|
|
|
4026 |
* This will usually be the the same as the element returned in {@link Component#el}.
|
|
|
4027 |
*
|
|
|
4028 |
* @return {Element}
|
|
|
4029 |
* The content element for this `Component`.
|
|
|
4030 |
*/
|
|
|
4031 |
contentEl() {
|
|
|
4032 |
return this.contentEl_ || this.el_;
|
|
|
4033 |
}
|
|
|
4034 |
|
|
|
4035 |
/**
|
|
|
4036 |
* Get this `Component`s ID
|
|
|
4037 |
*
|
|
|
4038 |
* @return {string}
|
|
|
4039 |
* The id of this `Component`
|
|
|
4040 |
*/
|
|
|
4041 |
id() {
|
|
|
4042 |
return this.id_;
|
|
|
4043 |
}
|
|
|
4044 |
|
|
|
4045 |
/**
|
|
|
4046 |
* Get the `Component`s name. The name gets used to reference the `Component`
|
|
|
4047 |
* and is set during registration.
|
|
|
4048 |
*
|
|
|
4049 |
* @return {string}
|
|
|
4050 |
* The name of this `Component`.
|
|
|
4051 |
*/
|
|
|
4052 |
name() {
|
|
|
4053 |
return this.name_;
|
|
|
4054 |
}
|
|
|
4055 |
|
|
|
4056 |
/**
|
|
|
4057 |
* Get an array of all child components
|
|
|
4058 |
*
|
|
|
4059 |
* @return {Array}
|
|
|
4060 |
* The children
|
|
|
4061 |
*/
|
|
|
4062 |
children() {
|
|
|
4063 |
return this.children_;
|
|
|
4064 |
}
|
|
|
4065 |
|
|
|
4066 |
/**
|
|
|
4067 |
* Returns the child `Component` with the given `id`.
|
|
|
4068 |
*
|
|
|
4069 |
* @param {string} id
|
|
|
4070 |
* The id of the child `Component` to get.
|
|
|
4071 |
*
|
|
|
4072 |
* @return {Component|undefined}
|
|
|
4073 |
* The child `Component` with the given `id` or undefined.
|
|
|
4074 |
*/
|
|
|
4075 |
getChildById(id) {
|
|
|
4076 |
return this.childIndex_[id];
|
|
|
4077 |
}
|
|
|
4078 |
|
|
|
4079 |
/**
|
|
|
4080 |
* Returns the child `Component` with the given `name`.
|
|
|
4081 |
*
|
|
|
4082 |
* @param {string} name
|
|
|
4083 |
* The name of the child `Component` to get.
|
|
|
4084 |
*
|
|
|
4085 |
* @return {Component|undefined}
|
|
|
4086 |
* The child `Component` with the given `name` or undefined.
|
|
|
4087 |
*/
|
|
|
4088 |
getChild(name) {
|
|
|
4089 |
if (!name) {
|
|
|
4090 |
return;
|
|
|
4091 |
}
|
|
|
4092 |
return this.childNameIndex_[name];
|
|
|
4093 |
}
|
|
|
4094 |
|
|
|
4095 |
/**
|
|
|
4096 |
* Returns the descendant `Component` following the givent
|
|
|
4097 |
* descendant `names`. For instance ['foo', 'bar', 'baz'] would
|
|
|
4098 |
* try to get 'foo' on the current component, 'bar' on the 'foo'
|
|
|
4099 |
* component and 'baz' on the 'bar' component and return undefined
|
|
|
4100 |
* if any of those don't exist.
|
|
|
4101 |
*
|
|
|
4102 |
* @param {...string[]|...string} names
|
|
|
4103 |
* The name of the child `Component` to get.
|
|
|
4104 |
*
|
|
|
4105 |
* @return {Component|undefined}
|
|
|
4106 |
* The descendant `Component` following the given descendant
|
|
|
4107 |
* `names` or undefined.
|
|
|
4108 |
*/
|
|
|
4109 |
getDescendant(...names) {
|
|
|
4110 |
// flatten array argument into the main array
|
|
|
4111 |
names = names.reduce((acc, n) => acc.concat(n), []);
|
|
|
4112 |
let currentChild = this;
|
|
|
4113 |
for (let i = 0; i < names.length; i++) {
|
|
|
4114 |
currentChild = currentChild.getChild(names[i]);
|
|
|
4115 |
if (!currentChild || !currentChild.getChild) {
|
|
|
4116 |
return;
|
|
|
4117 |
}
|
|
|
4118 |
}
|
|
|
4119 |
return currentChild;
|
|
|
4120 |
}
|
|
|
4121 |
|
|
|
4122 |
/**
|
|
|
4123 |
* Adds an SVG icon element to another element or component.
|
|
|
4124 |
*
|
|
|
4125 |
* @param {string} iconName
|
|
|
4126 |
* The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
|
|
|
4127 |
*
|
|
|
4128 |
* @param {Element} [el=this.el()]
|
|
|
4129 |
* Element to set the title on. Defaults to the current Component's element.
|
|
|
4130 |
*
|
|
|
4131 |
* @return {Element}
|
|
|
4132 |
* The newly created icon element.
|
|
|
4133 |
*/
|
|
|
4134 |
setIcon(iconName, el = this.el()) {
|
|
|
4135 |
// TODO: In v9 of video.js, we will want to remove font icons entirely.
|
|
|
4136 |
// This means this check, as well as the others throughout the code, and
|
|
|
4137 |
// the unecessary CSS for font icons, will need to be removed.
|
|
|
4138 |
// See https://github.com/videojs/video.js/pull/8260 as to which components
|
|
|
4139 |
// need updating.
|
|
|
4140 |
if (!this.player_.options_.experimentalSvgIcons) {
|
|
|
4141 |
return;
|
|
|
4142 |
}
|
|
|
4143 |
const xmlnsURL = 'http://www.w3.org/2000/svg';
|
|
|
4144 |
|
|
|
4145 |
// The below creates an element in the format of:
|
|
|
4146 |
// <span><svg><use>....</use></svg></span>
|
|
|
4147 |
const iconContainer = createEl('span', {
|
|
|
4148 |
className: 'vjs-icon-placeholder vjs-svg-icon'
|
|
|
4149 |
}, {
|
|
|
4150 |
'aria-hidden': 'true'
|
|
|
4151 |
});
|
|
|
4152 |
const svgEl = document.createElementNS(xmlnsURL, 'svg');
|
|
|
4153 |
svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
|
|
|
4154 |
const useEl = document.createElementNS(xmlnsURL, 'use');
|
|
|
4155 |
svgEl.appendChild(useEl);
|
|
|
4156 |
useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
|
|
|
4157 |
iconContainer.appendChild(svgEl);
|
|
|
4158 |
|
|
|
4159 |
// Replace a pre-existing icon if one exists.
|
|
|
4160 |
if (this.iconIsSet_) {
|
|
|
4161 |
el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
|
|
|
4162 |
} else {
|
|
|
4163 |
el.appendChild(iconContainer);
|
|
|
4164 |
}
|
|
|
4165 |
this.iconIsSet_ = true;
|
|
|
4166 |
return iconContainer;
|
|
|
4167 |
}
|
|
|
4168 |
|
|
|
4169 |
/**
|
|
|
4170 |
* Add a child `Component` inside the current `Component`.
|
|
|
4171 |
*
|
|
|
4172 |
* @param {string|Component} child
|
|
|
4173 |
* The name or instance of a child to add.
|
|
|
4174 |
*
|
|
|
4175 |
* @param {Object} [options={}]
|
|
|
4176 |
* The key/value store of options that will get passed to children of
|
|
|
4177 |
* the child.
|
|
|
4178 |
*
|
|
|
4179 |
* @param {number} [index=this.children_.length]
|
|
|
4180 |
* The index to attempt to add a child into.
|
|
|
4181 |
*
|
|
|
4182 |
*
|
|
|
4183 |
* @return {Component}
|
|
|
4184 |
* The `Component` that gets added as a child. When using a string the
|
|
|
4185 |
* `Component` will get created by this process.
|
|
|
4186 |
*/
|
|
|
4187 |
addChild(child, options = {}, index = this.children_.length) {
|
|
|
4188 |
let component;
|
|
|
4189 |
let componentName;
|
|
|
4190 |
|
|
|
4191 |
// If child is a string, create component with options
|
|
|
4192 |
if (typeof child === 'string') {
|
|
|
4193 |
componentName = toTitleCase$1(child);
|
|
|
4194 |
const componentClassName = options.componentClass || componentName;
|
|
|
4195 |
|
|
|
4196 |
// Set name through options
|
|
|
4197 |
options.name = componentName;
|
|
|
4198 |
|
|
|
4199 |
// Create a new object & element for this controls set
|
|
|
4200 |
// If there's no .player_, this is a player
|
|
|
4201 |
const ComponentClass = Component$1.getComponent(componentClassName);
|
|
|
4202 |
if (!ComponentClass) {
|
|
|
4203 |
throw new Error(`Component ${componentClassName} does not exist`);
|
|
|
4204 |
}
|
|
|
4205 |
|
|
|
4206 |
// data stored directly on the videojs object may be
|
|
|
4207 |
// misidentified as a component to retain
|
|
|
4208 |
// backwards-compatibility with 4.x. check to make sure the
|
|
|
4209 |
// component class can be instantiated.
|
|
|
4210 |
if (typeof ComponentClass !== 'function') {
|
|
|
4211 |
return null;
|
|
|
4212 |
}
|
|
|
4213 |
component = new ComponentClass(this.player_ || this, options);
|
|
|
4214 |
|
|
|
4215 |
// child is a component instance
|
|
|
4216 |
} else {
|
|
|
4217 |
component = child;
|
|
|
4218 |
}
|
|
|
4219 |
if (component.parentComponent_) {
|
|
|
4220 |
component.parentComponent_.removeChild(component);
|
|
|
4221 |
}
|
|
|
4222 |
this.children_.splice(index, 0, component);
|
|
|
4223 |
component.parentComponent_ = this;
|
|
|
4224 |
if (typeof component.id === 'function') {
|
|
|
4225 |
this.childIndex_[component.id()] = component;
|
|
|
4226 |
}
|
|
|
4227 |
|
|
|
4228 |
// If a name wasn't used to create the component, check if we can use the
|
|
|
4229 |
// name function of the component
|
|
|
4230 |
componentName = componentName || component.name && toTitleCase$1(component.name());
|
|
|
4231 |
if (componentName) {
|
|
|
4232 |
this.childNameIndex_[componentName] = component;
|
|
|
4233 |
this.childNameIndex_[toLowerCase(componentName)] = component;
|
|
|
4234 |
}
|
|
|
4235 |
|
|
|
4236 |
// Add the UI object's element to the container div (box)
|
|
|
4237 |
// Having an element is not required
|
|
|
4238 |
if (typeof component.el === 'function' && component.el()) {
|
|
|
4239 |
// If inserting before a component, insert before that component's element
|
|
|
4240 |
let refNode = null;
|
|
|
4241 |
if (this.children_[index + 1]) {
|
|
|
4242 |
// Most children are components, but the video tech is an HTML element
|
|
|
4243 |
if (this.children_[index + 1].el_) {
|
|
|
4244 |
refNode = this.children_[index + 1].el_;
|
|
|
4245 |
} else if (isEl(this.children_[index + 1])) {
|
|
|
4246 |
refNode = this.children_[index + 1];
|
|
|
4247 |
}
|
|
|
4248 |
}
|
|
|
4249 |
this.contentEl().insertBefore(component.el(), refNode);
|
|
|
4250 |
}
|
|
|
4251 |
|
|
|
4252 |
// Return so it can stored on parent object if desired.
|
|
|
4253 |
return component;
|
|
|
4254 |
}
|
|
|
4255 |
|
|
|
4256 |
/**
|
|
|
4257 |
* Remove a child `Component` from this `Component`s list of children. Also removes
|
|
|
4258 |
* the child `Component`s element from this `Component`s element.
|
|
|
4259 |
*
|
|
|
4260 |
* @param {Component} component
|
|
|
4261 |
* The child `Component` to remove.
|
|
|
4262 |
*/
|
|
|
4263 |
removeChild(component) {
|
|
|
4264 |
if (typeof component === 'string') {
|
|
|
4265 |
component = this.getChild(component);
|
|
|
4266 |
}
|
|
|
4267 |
if (!component || !this.children_) {
|
|
|
4268 |
return;
|
|
|
4269 |
}
|
|
|
4270 |
let childFound = false;
|
|
|
4271 |
for (let i = this.children_.length - 1; i >= 0; i--) {
|
|
|
4272 |
if (this.children_[i] === component) {
|
|
|
4273 |
childFound = true;
|
|
|
4274 |
this.children_.splice(i, 1);
|
|
|
4275 |
break;
|
|
|
4276 |
}
|
|
|
4277 |
}
|
|
|
4278 |
if (!childFound) {
|
|
|
4279 |
return;
|
|
|
4280 |
}
|
|
|
4281 |
component.parentComponent_ = null;
|
|
|
4282 |
this.childIndex_[component.id()] = null;
|
|
|
4283 |
this.childNameIndex_[toTitleCase$1(component.name())] = null;
|
|
|
4284 |
this.childNameIndex_[toLowerCase(component.name())] = null;
|
|
|
4285 |
const compEl = component.el();
|
|
|
4286 |
if (compEl && compEl.parentNode === this.contentEl()) {
|
|
|
4287 |
this.contentEl().removeChild(component.el());
|
|
|
4288 |
}
|
|
|
4289 |
}
|
|
|
4290 |
|
|
|
4291 |
/**
|
|
|
4292 |
* Add and initialize default child `Component`s based upon options.
|
|
|
4293 |
*/
|
|
|
4294 |
initChildren() {
|
|
|
4295 |
const children = this.options_.children;
|
|
|
4296 |
if (children) {
|
|
|
4297 |
// `this` is `parent`
|
|
|
4298 |
const parentOptions = this.options_;
|
|
|
4299 |
const handleAdd = child => {
|
|
|
4300 |
const name = child.name;
|
|
|
4301 |
let opts = child.opts;
|
|
|
4302 |
|
|
|
4303 |
// Allow options for children to be set at the parent options
|
|
|
4304 |
// e.g. videojs(id, { controlBar: false });
|
|
|
4305 |
// instead of videojs(id, { children: { controlBar: false });
|
|
|
4306 |
if (parentOptions[name] !== undefined) {
|
|
|
4307 |
opts = parentOptions[name];
|
|
|
4308 |
}
|
|
|
4309 |
|
|
|
4310 |
// Allow for disabling default components
|
|
|
4311 |
// e.g. options['children']['posterImage'] = false
|
|
|
4312 |
if (opts === false) {
|
|
|
4313 |
return;
|
|
|
4314 |
}
|
|
|
4315 |
|
|
|
4316 |
// Allow options to be passed as a simple boolean if no configuration
|
|
|
4317 |
// is necessary.
|
|
|
4318 |
if (opts === true) {
|
|
|
4319 |
opts = {};
|
|
|
4320 |
}
|
|
|
4321 |
|
|
|
4322 |
// We also want to pass the original player options
|
|
|
4323 |
// to each component as well so they don't need to
|
|
|
4324 |
// reach back into the player for options later.
|
|
|
4325 |
opts.playerOptions = this.options_.playerOptions;
|
|
|
4326 |
|
|
|
4327 |
// Create and add the child component.
|
|
|
4328 |
// Add a direct reference to the child by name on the parent instance.
|
|
|
4329 |
// If two of the same component are used, different names should be supplied
|
|
|
4330 |
// for each
|
|
|
4331 |
const newChild = this.addChild(name, opts);
|
|
|
4332 |
if (newChild) {
|
|
|
4333 |
this[name] = newChild;
|
|
|
4334 |
}
|
|
|
4335 |
};
|
|
|
4336 |
|
|
|
4337 |
// Allow for an array of children details to passed in the options
|
|
|
4338 |
let workingChildren;
|
|
|
4339 |
const Tech = Component$1.getComponent('Tech');
|
|
|
4340 |
if (Array.isArray(children)) {
|
|
|
4341 |
workingChildren = children;
|
|
|
4342 |
} else {
|
|
|
4343 |
workingChildren = Object.keys(children);
|
|
|
4344 |
}
|
|
|
4345 |
workingChildren
|
|
|
4346 |
// children that are in this.options_ but also in workingChildren would
|
|
|
4347 |
// give us extra children we do not want. So, we want to filter them out.
|
|
|
4348 |
.concat(Object.keys(this.options_).filter(function (child) {
|
|
|
4349 |
return !workingChildren.some(function (wchild) {
|
|
|
4350 |
if (typeof wchild === 'string') {
|
|
|
4351 |
return child === wchild;
|
|
|
4352 |
}
|
|
|
4353 |
return child === wchild.name;
|
|
|
4354 |
});
|
|
|
4355 |
})).map(child => {
|
|
|
4356 |
let name;
|
|
|
4357 |
let opts;
|
|
|
4358 |
if (typeof child === 'string') {
|
|
|
4359 |
name = child;
|
|
|
4360 |
opts = children[name] || this.options_[name] || {};
|
|
|
4361 |
} else {
|
|
|
4362 |
name = child.name;
|
|
|
4363 |
opts = child;
|
|
|
4364 |
}
|
|
|
4365 |
return {
|
|
|
4366 |
name,
|
|
|
4367 |
opts
|
|
|
4368 |
};
|
|
|
4369 |
}).filter(child => {
|
|
|
4370 |
// we have to make sure that child.name isn't in the techOrder since
|
|
|
4371 |
// techs are registered as Components but can't aren't compatible
|
|
|
4372 |
// See https://github.com/videojs/video.js/issues/2772
|
|
|
4373 |
const c = Component$1.getComponent(child.opts.componentClass || toTitleCase$1(child.name));
|
|
|
4374 |
return c && !Tech.isTech(c);
|
|
|
4375 |
}).forEach(handleAdd);
|
|
|
4376 |
}
|
|
|
4377 |
}
|
|
|
4378 |
|
|
|
4379 |
/**
|
|
|
4380 |
* Builds the default DOM class name. Should be overridden by sub-components.
|
|
|
4381 |
*
|
|
|
4382 |
* @return {string}
|
|
|
4383 |
* The DOM class name for this object.
|
|
|
4384 |
*
|
|
|
4385 |
* @abstract
|
|
|
4386 |
*/
|
|
|
4387 |
buildCSSClass() {
|
|
|
4388 |
// Child classes can include a function that does:
|
|
|
4389 |
// return 'CLASS NAME' + this._super();
|
|
|
4390 |
return '';
|
|
|
4391 |
}
|
|
|
4392 |
|
|
|
4393 |
/**
|
|
|
4394 |
* Bind a listener to the component's ready state.
|
|
|
4395 |
* Different from event listeners in that if the ready event has already happened
|
|
|
4396 |
* it will trigger the function immediately.
|
|
|
4397 |
*
|
|
|
4398 |
* @param {ReadyCallback} fn
|
|
|
4399 |
* Function that gets called when the `Component` is ready.
|
|
|
4400 |
*
|
|
|
4401 |
* @return {Component}
|
|
|
4402 |
* Returns itself; method can be chained.
|
|
|
4403 |
*/
|
|
|
4404 |
ready(fn, sync = false) {
|
|
|
4405 |
if (!fn) {
|
|
|
4406 |
return;
|
|
|
4407 |
}
|
|
|
4408 |
if (!this.isReady_) {
|
|
|
4409 |
this.readyQueue_ = this.readyQueue_ || [];
|
|
|
4410 |
this.readyQueue_.push(fn);
|
|
|
4411 |
return;
|
|
|
4412 |
}
|
|
|
4413 |
if (sync) {
|
|
|
4414 |
fn.call(this);
|
|
|
4415 |
} else {
|
|
|
4416 |
// Call the function asynchronously by default for consistency
|
|
|
4417 |
this.setTimeout(fn, 1);
|
|
|
4418 |
}
|
|
|
4419 |
}
|
|
|
4420 |
|
|
|
4421 |
/**
|
|
|
4422 |
* Trigger all the ready listeners for this `Component`.
|
|
|
4423 |
*
|
|
|
4424 |
* @fires Component#ready
|
|
|
4425 |
*/
|
|
|
4426 |
triggerReady() {
|
|
|
4427 |
this.isReady_ = true;
|
|
|
4428 |
|
|
|
4429 |
// Ensure ready is triggered asynchronously
|
|
|
4430 |
this.setTimeout(function () {
|
|
|
4431 |
const readyQueue = this.readyQueue_;
|
|
|
4432 |
|
|
|
4433 |
// Reset Ready Queue
|
|
|
4434 |
this.readyQueue_ = [];
|
|
|
4435 |
if (readyQueue && readyQueue.length > 0) {
|
|
|
4436 |
readyQueue.forEach(function (fn) {
|
|
|
4437 |
fn.call(this);
|
|
|
4438 |
}, this);
|
|
|
4439 |
}
|
|
|
4440 |
|
|
|
4441 |
// Allow for using event listeners also
|
|
|
4442 |
/**
|
|
|
4443 |
* Triggered when a `Component` is ready.
|
|
|
4444 |
*
|
|
|
4445 |
* @event Component#ready
|
|
|
4446 |
* @type {Event}
|
|
|
4447 |
*/
|
|
|
4448 |
this.trigger('ready');
|
|
|
4449 |
}, 1);
|
|
|
4450 |
}
|
|
|
4451 |
|
|
|
4452 |
/**
|
|
|
4453 |
* Find a single DOM element matching a `selector`. This can be within the `Component`s
|
|
|
4454 |
* `contentEl()` or another custom context.
|
|
|
4455 |
*
|
|
|
4456 |
* @param {string} selector
|
|
|
4457 |
* A valid CSS selector, which will be passed to `querySelector`.
|
|
|
4458 |
*
|
|
|
4459 |
* @param {Element|string} [context=this.contentEl()]
|
|
|
4460 |
* A DOM element within which to query. Can also be a selector string in
|
|
|
4461 |
* which case the first matching element will get used as context. If
|
|
|
4462 |
* missing `this.contentEl()` gets used. If `this.contentEl()` returns
|
|
|
4463 |
* nothing it falls back to `document`.
|
|
|
4464 |
*
|
|
|
4465 |
* @return {Element|null}
|
|
|
4466 |
* the dom element that was found, or null
|
|
|
4467 |
*
|
|
|
4468 |
* @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
|
|
|
4469 |
*/
|
|
|
4470 |
$(selector, context) {
|
|
|
4471 |
return $(selector, context || this.contentEl());
|
|
|
4472 |
}
|
|
|
4473 |
|
|
|
4474 |
/**
|
|
|
4475 |
* Finds all DOM element matching a `selector`. This can be within the `Component`s
|
|
|
4476 |
* `contentEl()` or another custom context.
|
|
|
4477 |
*
|
|
|
4478 |
* @param {string} selector
|
|
|
4479 |
* A valid CSS selector, which will be passed to `querySelectorAll`.
|
|
|
4480 |
*
|
|
|
4481 |
* @param {Element|string} [context=this.contentEl()]
|
|
|
4482 |
* A DOM element within which to query. Can also be a selector string in
|
|
|
4483 |
* which case the first matching element will get used as context. If
|
|
|
4484 |
* missing `this.contentEl()` gets used. If `this.contentEl()` returns
|
|
|
4485 |
* nothing it falls back to `document`.
|
|
|
4486 |
*
|
|
|
4487 |
* @return {NodeList}
|
|
|
4488 |
* a list of dom elements that were found
|
|
|
4489 |
*
|
|
|
4490 |
* @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
|
|
|
4491 |
*/
|
|
|
4492 |
$$(selector, context) {
|
|
|
4493 |
return $$(selector, context || this.contentEl());
|
|
|
4494 |
}
|
|
|
4495 |
|
|
|
4496 |
/**
|
|
|
4497 |
* Check if a component's element has a CSS class name.
|
|
|
4498 |
*
|
|
|
4499 |
* @param {string} classToCheck
|
|
|
4500 |
* CSS class name to check.
|
|
|
4501 |
*
|
|
|
4502 |
* @return {boolean}
|
|
|
4503 |
* - True if the `Component` has the class.
|
|
|
4504 |
* - False if the `Component` does not have the class`
|
|
|
4505 |
*/
|
|
|
4506 |
hasClass(classToCheck) {
|
|
|
4507 |
return hasClass(this.el_, classToCheck);
|
|
|
4508 |
}
|
|
|
4509 |
|
|
|
4510 |
/**
|
|
|
4511 |
* Add a CSS class name to the `Component`s element.
|
|
|
4512 |
*
|
|
|
4513 |
* @param {...string} classesToAdd
|
|
|
4514 |
* One or more CSS class name to add.
|
|
|
4515 |
*/
|
|
|
4516 |
addClass(...classesToAdd) {
|
|
|
4517 |
addClass(this.el_, ...classesToAdd);
|
|
|
4518 |
}
|
|
|
4519 |
|
|
|
4520 |
/**
|
|
|
4521 |
* Remove a CSS class name from the `Component`s element.
|
|
|
4522 |
*
|
|
|
4523 |
* @param {...string} classesToRemove
|
|
|
4524 |
* One or more CSS class name to remove.
|
|
|
4525 |
*/
|
|
|
4526 |
removeClass(...classesToRemove) {
|
|
|
4527 |
removeClass(this.el_, ...classesToRemove);
|
|
|
4528 |
}
|
|
|
4529 |
|
|
|
4530 |
/**
|
|
|
4531 |
* Add or remove a CSS class name from the component's element.
|
|
|
4532 |
* - `classToToggle` gets added when {@link Component#hasClass} would return false.
|
|
|
4533 |
* - `classToToggle` gets removed when {@link Component#hasClass} would return true.
|
|
|
4534 |
*
|
|
|
4535 |
* @param {string} classToToggle
|
|
|
4536 |
* The class to add or remove based on (@link Component#hasClass}
|
|
|
4537 |
*
|
|
|
4538 |
* @param {boolean|Dom~predicate} [predicate]
|
|
|
4539 |
* An {@link Dom~predicate} function or a boolean
|
|
|
4540 |
*/
|
|
|
4541 |
toggleClass(classToToggle, predicate) {
|
|
|
4542 |
toggleClass(this.el_, classToToggle, predicate);
|
|
|
4543 |
}
|
|
|
4544 |
|
|
|
4545 |
/**
|
|
|
4546 |
* Show the `Component`s element if it is hidden by removing the
|
|
|
4547 |
* 'vjs-hidden' class name from it.
|
|
|
4548 |
*/
|
|
|
4549 |
show() {
|
|
|
4550 |
this.removeClass('vjs-hidden');
|
|
|
4551 |
}
|
|
|
4552 |
|
|
|
4553 |
/**
|
|
|
4554 |
* Hide the `Component`s element if it is currently showing by adding the
|
|
|
4555 |
* 'vjs-hidden` class name to it.
|
|
|
4556 |
*/
|
|
|
4557 |
hide() {
|
|
|
4558 |
this.addClass('vjs-hidden');
|
|
|
4559 |
}
|
|
|
4560 |
|
|
|
4561 |
/**
|
|
|
4562 |
* Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
|
|
|
4563 |
* class name to it. Used during fadeIn/fadeOut.
|
|
|
4564 |
*
|
|
|
4565 |
* @private
|
|
|
4566 |
*/
|
|
|
4567 |
lockShowing() {
|
|
|
4568 |
this.addClass('vjs-lock-showing');
|
|
|
4569 |
}
|
|
|
4570 |
|
|
|
4571 |
/**
|
|
|
4572 |
* Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
|
|
|
4573 |
* class name from it. Used during fadeIn/fadeOut.
|
|
|
4574 |
*
|
|
|
4575 |
* @private
|
|
|
4576 |
*/
|
|
|
4577 |
unlockShowing() {
|
|
|
4578 |
this.removeClass('vjs-lock-showing');
|
|
|
4579 |
}
|
|
|
4580 |
|
|
|
4581 |
/**
|
|
|
4582 |
* Get the value of an attribute on the `Component`s element.
|
|
|
4583 |
*
|
|
|
4584 |
* @param {string} attribute
|
|
|
4585 |
* Name of the attribute to get the value from.
|
|
|
4586 |
*
|
|
|
4587 |
* @return {string|null}
|
|
|
4588 |
* - The value of the attribute that was asked for.
|
|
|
4589 |
* - Can be an empty string on some browsers if the attribute does not exist
|
|
|
4590 |
* or has no value
|
|
|
4591 |
* - Most browsers will return null if the attribute does not exist or has
|
|
|
4592 |
* no value.
|
|
|
4593 |
*
|
|
|
4594 |
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
|
|
|
4595 |
*/
|
|
|
4596 |
getAttribute(attribute) {
|
|
|
4597 |
return getAttribute(this.el_, attribute);
|
|
|
4598 |
}
|
|
|
4599 |
|
|
|
4600 |
/**
|
|
|
4601 |
* Set the value of an attribute on the `Component`'s element
|
|
|
4602 |
*
|
|
|
4603 |
* @param {string} attribute
|
|
|
4604 |
* Name of the attribute to set.
|
|
|
4605 |
*
|
|
|
4606 |
* @param {string} value
|
|
|
4607 |
* Value to set the attribute to.
|
|
|
4608 |
*
|
|
|
4609 |
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
|
|
|
4610 |
*/
|
|
|
4611 |
setAttribute(attribute, value) {
|
|
|
4612 |
setAttribute(this.el_, attribute, value);
|
|
|
4613 |
}
|
|
|
4614 |
|
|
|
4615 |
/**
|
|
|
4616 |
* Remove an attribute from the `Component`s element.
|
|
|
4617 |
*
|
|
|
4618 |
* @param {string} attribute
|
|
|
4619 |
* Name of the attribute to remove.
|
|
|
4620 |
*
|
|
|
4621 |
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
|
|
|
4622 |
*/
|
|
|
4623 |
removeAttribute(attribute) {
|
|
|
4624 |
removeAttribute(this.el_, attribute);
|
|
|
4625 |
}
|
|
|
4626 |
|
|
|
4627 |
/**
|
|
|
4628 |
* Get or set the width of the component based upon the CSS styles.
|
|
|
4629 |
* See {@link Component#dimension} for more detailed information.
|
|
|
4630 |
*
|
|
|
4631 |
* @param {number|string} [num]
|
|
|
4632 |
* The width that you want to set postfixed with '%', 'px' or nothing.
|
|
|
4633 |
*
|
|
|
4634 |
* @param {boolean} [skipListeners]
|
|
|
4635 |
* Skip the componentresize event trigger
|
|
|
4636 |
*
|
|
|
4637 |
* @return {number|undefined}
|
|
|
4638 |
* The width when getting, zero if there is no width
|
|
|
4639 |
*/
|
|
|
4640 |
width(num, skipListeners) {
|
|
|
4641 |
return this.dimension('width', num, skipListeners);
|
|
|
4642 |
}
|
|
|
4643 |
|
|
|
4644 |
/**
|
|
|
4645 |
* Get or set the height of the component based upon the CSS styles.
|
|
|
4646 |
* See {@link Component#dimension} for more detailed information.
|
|
|
4647 |
*
|
|
|
4648 |
* @param {number|string} [num]
|
|
|
4649 |
* The height that you want to set postfixed with '%', 'px' or nothing.
|
|
|
4650 |
*
|
|
|
4651 |
* @param {boolean} [skipListeners]
|
|
|
4652 |
* Skip the componentresize event trigger
|
|
|
4653 |
*
|
|
|
4654 |
* @return {number|undefined}
|
|
|
4655 |
* The height when getting, zero if there is no height
|
|
|
4656 |
*/
|
|
|
4657 |
height(num, skipListeners) {
|
|
|
4658 |
return this.dimension('height', num, skipListeners);
|
|
|
4659 |
}
|
|
|
4660 |
|
|
|
4661 |
/**
|
|
|
4662 |
* Set both the width and height of the `Component` element at the same time.
|
|
|
4663 |
*
|
|
|
4664 |
* @param {number|string} width
|
|
|
4665 |
* Width to set the `Component`s element to.
|
|
|
4666 |
*
|
|
|
4667 |
* @param {number|string} height
|
|
|
4668 |
* Height to set the `Component`s element to.
|
|
|
4669 |
*/
|
|
|
4670 |
dimensions(width, height) {
|
|
|
4671 |
// Skip componentresize listeners on width for optimization
|
|
|
4672 |
this.width(width, true);
|
|
|
4673 |
this.height(height);
|
|
|
4674 |
}
|
|
|
4675 |
|
|
|
4676 |
/**
|
|
|
4677 |
* Get or set width or height of the `Component` element. This is the shared code
|
|
|
4678 |
* for the {@link Component#width} and {@link Component#height}.
|
|
|
4679 |
*
|
|
|
4680 |
* Things to know:
|
|
|
4681 |
* - If the width or height in an number this will return the number postfixed with 'px'.
|
|
|
4682 |
* - If the width/height is a percent this will return the percent postfixed with '%'
|
|
|
4683 |
* - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
|
|
|
4684 |
* defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
|
|
|
4685 |
* See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
|
|
|
4686 |
* for more information
|
|
|
4687 |
* - If you want the computed style of the component, use {@link Component#currentWidth}
|
|
|
4688 |
* and {@link {Component#currentHeight}
|
|
|
4689 |
*
|
|
|
4690 |
* @fires Component#componentresize
|
|
|
4691 |
*
|
|
|
4692 |
* @param {string} widthOrHeight
|
|
|
4693 |
8 'width' or 'height'
|
|
|
4694 |
*
|
|
|
4695 |
* @param {number|string} [num]
|
|
|
4696 |
8 New dimension
|
|
|
4697 |
*
|
|
|
4698 |
* @param {boolean} [skipListeners]
|
|
|
4699 |
* Skip componentresize event trigger
|
|
|
4700 |
*
|
|
|
4701 |
* @return {number|undefined}
|
|
|
4702 |
* The dimension when getting or 0 if unset
|
|
|
4703 |
*/
|
|
|
4704 |
dimension(widthOrHeight, num, skipListeners) {
|
|
|
4705 |
if (num !== undefined) {
|
|
|
4706 |
// Set to zero if null or literally NaN (NaN !== NaN)
|
|
|
4707 |
if (num === null || num !== num) {
|
|
|
4708 |
num = 0;
|
|
|
4709 |
}
|
|
|
4710 |
|
|
|
4711 |
// Check if using css width/height (% or px) and adjust
|
|
|
4712 |
if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
|
|
|
4713 |
this.el_.style[widthOrHeight] = num;
|
|
|
4714 |
} else if (num === 'auto') {
|
|
|
4715 |
this.el_.style[widthOrHeight] = '';
|
|
|
4716 |
} else {
|
|
|
4717 |
this.el_.style[widthOrHeight] = num + 'px';
|
|
|
4718 |
}
|
|
|
4719 |
|
|
|
4720 |
// skipListeners allows us to avoid triggering the resize event when setting both width and height
|
|
|
4721 |
if (!skipListeners) {
|
|
|
4722 |
/**
|
|
|
4723 |
* Triggered when a component is resized.
|
|
|
4724 |
*
|
|
|
4725 |
* @event Component#componentresize
|
|
|
4726 |
* @type {Event}
|
|
|
4727 |
*/
|
|
|
4728 |
this.trigger('componentresize');
|
|
|
4729 |
}
|
|
|
4730 |
return;
|
|
|
4731 |
}
|
|
|
4732 |
|
|
|
4733 |
// Not setting a value, so getting it
|
|
|
4734 |
// Make sure element exists
|
|
|
4735 |
if (!this.el_) {
|
|
|
4736 |
return 0;
|
|
|
4737 |
}
|
|
|
4738 |
|
|
|
4739 |
// Get dimension value from style
|
|
|
4740 |
const val = this.el_.style[widthOrHeight];
|
|
|
4741 |
const pxIndex = val.indexOf('px');
|
|
|
4742 |
if (pxIndex !== -1) {
|
|
|
4743 |
// Return the pixel value with no 'px'
|
|
|
4744 |
return parseInt(val.slice(0, pxIndex), 10);
|
|
|
4745 |
}
|
|
|
4746 |
|
|
|
4747 |
// No px so using % or no style was set, so falling back to offsetWidth/height
|
|
|
4748 |
// If component has display:none, offset will return 0
|
|
|
4749 |
// TODO: handle display:none and no dimension style using px
|
|
|
4750 |
return parseInt(this.el_['offset' + toTitleCase$1(widthOrHeight)], 10);
|
|
|
4751 |
}
|
|
|
4752 |
|
|
|
4753 |
/**
|
|
|
4754 |
* Get the computed width or the height of the component's element.
|
|
|
4755 |
*
|
|
|
4756 |
* Uses `window.getComputedStyle`.
|
|
|
4757 |
*
|
|
|
4758 |
* @param {string} widthOrHeight
|
|
|
4759 |
* A string containing 'width' or 'height'. Whichever one you want to get.
|
|
|
4760 |
*
|
|
|
4761 |
* @return {number}
|
|
|
4762 |
* The dimension that gets asked for or 0 if nothing was set
|
|
|
4763 |
* for that dimension.
|
|
|
4764 |
*/
|
|
|
4765 |
currentDimension(widthOrHeight) {
|
|
|
4766 |
let computedWidthOrHeight = 0;
|
|
|
4767 |
if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
|
|
|
4768 |
throw new Error('currentDimension only accepts width or height value');
|
|
|
4769 |
}
|
|
|
4770 |
computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
|
|
|
4771 |
|
|
|
4772 |
// remove 'px' from variable and parse as integer
|
|
|
4773 |
computedWidthOrHeight = parseFloat(computedWidthOrHeight);
|
|
|
4774 |
|
|
|
4775 |
// if the computed value is still 0, it's possible that the browser is lying
|
|
|
4776 |
// and we want to check the offset values.
|
|
|
4777 |
// This code also runs wherever getComputedStyle doesn't exist.
|
|
|
4778 |
if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
|
|
|
4779 |
const rule = `offset${toTitleCase$1(widthOrHeight)}`;
|
|
|
4780 |
computedWidthOrHeight = this.el_[rule];
|
|
|
4781 |
}
|
|
|
4782 |
return computedWidthOrHeight;
|
|
|
4783 |
}
|
|
|
4784 |
|
|
|
4785 |
/**
|
|
|
4786 |
* An object that contains width and height values of the `Component`s
|
|
|
4787 |
* computed style. Uses `window.getComputedStyle`.
|
|
|
4788 |
*
|
|
|
4789 |
* @typedef {Object} Component~DimensionObject
|
|
|
4790 |
*
|
|
|
4791 |
* @property {number} width
|
|
|
4792 |
* The width of the `Component`s computed style.
|
|
|
4793 |
*
|
|
|
4794 |
* @property {number} height
|
|
|
4795 |
* The height of the `Component`s computed style.
|
|
|
4796 |
*/
|
|
|
4797 |
|
|
|
4798 |
/**
|
|
|
4799 |
* Get an object that contains computed width and height values of the
|
|
|
4800 |
* component's element.
|
|
|
4801 |
*
|
|
|
4802 |
* Uses `window.getComputedStyle`.
|
|
|
4803 |
*
|
|
|
4804 |
* @return {Component~DimensionObject}
|
|
|
4805 |
* The computed dimensions of the component's element.
|
|
|
4806 |
*/
|
|
|
4807 |
currentDimensions() {
|
|
|
4808 |
return {
|
|
|
4809 |
width: this.currentDimension('width'),
|
|
|
4810 |
height: this.currentDimension('height')
|
|
|
4811 |
};
|
|
|
4812 |
}
|
|
|
4813 |
|
|
|
4814 |
/**
|
|
|
4815 |
* Get the computed width of the component's element.
|
|
|
4816 |
*
|
|
|
4817 |
* Uses `window.getComputedStyle`.
|
|
|
4818 |
*
|
|
|
4819 |
* @return {number}
|
|
|
4820 |
* The computed width of the component's element.
|
|
|
4821 |
*/
|
|
|
4822 |
currentWidth() {
|
|
|
4823 |
return this.currentDimension('width');
|
|
|
4824 |
}
|
|
|
4825 |
|
|
|
4826 |
/**
|
|
|
4827 |
* Get the computed height of the component's element.
|
|
|
4828 |
*
|
|
|
4829 |
* Uses `window.getComputedStyle`.
|
|
|
4830 |
*
|
|
|
4831 |
* @return {number}
|
|
|
4832 |
* The computed height of the component's element.
|
|
|
4833 |
*/
|
|
|
4834 |
currentHeight() {
|
|
|
4835 |
return this.currentDimension('height');
|
|
|
4836 |
}
|
|
|
4837 |
|
|
|
4838 |
/**
|
|
|
4839 |
* Set the focus to this component
|
|
|
4840 |
*/
|
|
|
4841 |
focus() {
|
|
|
4842 |
this.el_.focus();
|
|
|
4843 |
}
|
|
|
4844 |
|
|
|
4845 |
/**
|
|
|
4846 |
* Remove the focus from this component
|
|
|
4847 |
*/
|
|
|
4848 |
blur() {
|
|
|
4849 |
this.el_.blur();
|
|
|
4850 |
}
|
|
|
4851 |
|
|
|
4852 |
/**
|
|
|
4853 |
* When this Component receives a `keydown` event which it does not process,
|
|
|
4854 |
* it passes the event to the Player for handling.
|
|
|
4855 |
*
|
|
|
4856 |
* @param {KeyboardEvent} event
|
|
|
4857 |
* The `keydown` event that caused this function to be called.
|
|
|
4858 |
*/
|
|
|
4859 |
handleKeyDown(event) {
|
|
|
4860 |
if (this.player_) {
|
|
|
4861 |
// We only stop propagation here because we want unhandled events to fall
|
|
|
4862 |
// back to the browser. Exclude Tab for focus trapping.
|
|
|
4863 |
if (!keycode.isEventKey(event, 'Tab')) {
|
|
|
4864 |
event.stopPropagation();
|
|
|
4865 |
}
|
|
|
4866 |
this.player_.handleKeyDown(event);
|
|
|
4867 |
}
|
|
|
4868 |
}
|
|
|
4869 |
|
|
|
4870 |
/**
|
|
|
4871 |
* Many components used to have a `handleKeyPress` method, which was poorly
|
|
|
4872 |
* named because it listened to a `keydown` event. This method name now
|
|
|
4873 |
* delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
|
|
|
4874 |
* will not see their method calls stop working.
|
|
|
4875 |
*
|
|
|
4876 |
* @param {KeyboardEvent} event
|
|
|
4877 |
* The event that caused this function to be called.
|
|
|
4878 |
*/
|
|
|
4879 |
handleKeyPress(event) {
|
|
|
4880 |
this.handleKeyDown(event);
|
|
|
4881 |
}
|
|
|
4882 |
|
|
|
4883 |
/**
|
|
|
4884 |
* Emit a 'tap' events when touch event support gets detected. This gets used to
|
|
|
4885 |
* support toggling the controls through a tap on the video. They get enabled
|
|
|
4886 |
* because every sub-component would have extra overhead otherwise.
|
|
|
4887 |
*
|
|
|
4888 |
* @protected
|
|
|
4889 |
* @fires Component#tap
|
|
|
4890 |
* @listens Component#touchstart
|
|
|
4891 |
* @listens Component#touchmove
|
|
|
4892 |
* @listens Component#touchleave
|
|
|
4893 |
* @listens Component#touchcancel
|
|
|
4894 |
* @listens Component#touchend
|
|
|
4895 |
*/
|
|
|
4896 |
emitTapEvents() {
|
|
|
4897 |
// Track the start time so we can determine how long the touch lasted
|
|
|
4898 |
let touchStart = 0;
|
|
|
4899 |
let firstTouch = null;
|
|
|
4900 |
|
|
|
4901 |
// Maximum movement allowed during a touch event to still be considered a tap
|
|
|
4902 |
// Other popular libs use anywhere from 2 (hammer.js) to 15,
|
|
|
4903 |
// so 10 seems like a nice, round number.
|
|
|
4904 |
const tapMovementThreshold = 10;
|
|
|
4905 |
|
|
|
4906 |
// The maximum length a touch can be while still being considered a tap
|
|
|
4907 |
const touchTimeThreshold = 200;
|
|
|
4908 |
let couldBeTap;
|
|
|
4909 |
this.on('touchstart', function (event) {
|
|
|
4910 |
// If more than one finger, don't consider treating this as a click
|
|
|
4911 |
if (event.touches.length === 1) {
|
|
|
4912 |
// Copy pageX/pageY from the object
|
|
|
4913 |
firstTouch = {
|
|
|
4914 |
pageX: event.touches[0].pageX,
|
|
|
4915 |
pageY: event.touches[0].pageY
|
|
|
4916 |
};
|
|
|
4917 |
// Record start time so we can detect a tap vs. "touch and hold"
|
|
|
4918 |
touchStart = window.performance.now();
|
|
|
4919 |
// Reset couldBeTap tracking
|
|
|
4920 |
couldBeTap = true;
|
|
|
4921 |
}
|
|
|
4922 |
});
|
|
|
4923 |
this.on('touchmove', function (event) {
|
|
|
4924 |
// If more than one finger, don't consider treating this as a click
|
|
|
4925 |
if (event.touches.length > 1) {
|
|
|
4926 |
couldBeTap = false;
|
|
|
4927 |
} else if (firstTouch) {
|
|
|
4928 |
// Some devices will throw touchmoves for all but the slightest of taps.
|
|
|
4929 |
// So, if we moved only a small distance, this could still be a tap
|
|
|
4930 |
const xdiff = event.touches[0].pageX - firstTouch.pageX;
|
|
|
4931 |
const ydiff = event.touches[0].pageY - firstTouch.pageY;
|
|
|
4932 |
const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
|
|
|
4933 |
if (touchDistance > tapMovementThreshold) {
|
|
|
4934 |
couldBeTap = false;
|
|
|
4935 |
}
|
|
|
4936 |
}
|
|
|
4937 |
});
|
|
|
4938 |
const noTap = function () {
|
|
|
4939 |
couldBeTap = false;
|
|
|
4940 |
};
|
|
|
4941 |
|
|
|
4942 |
// TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
|
|
|
4943 |
this.on('touchleave', noTap);
|
|
|
4944 |
this.on('touchcancel', noTap);
|
|
|
4945 |
|
|
|
4946 |
// When the touch ends, measure how long it took and trigger the appropriate
|
|
|
4947 |
// event
|
|
|
4948 |
this.on('touchend', function (event) {
|
|
|
4949 |
firstTouch = null;
|
|
|
4950 |
// Proceed only if the touchmove/leave/cancel event didn't happen
|
|
|
4951 |
if (couldBeTap === true) {
|
|
|
4952 |
// Measure how long the touch lasted
|
|
|
4953 |
const touchTime = window.performance.now() - touchStart;
|
|
|
4954 |
|
|
|
4955 |
// Make sure the touch was less than the threshold to be considered a tap
|
|
|
4956 |
if (touchTime < touchTimeThreshold) {
|
|
|
4957 |
// Don't let browser turn this into a click
|
|
|
4958 |
event.preventDefault();
|
|
|
4959 |
/**
|
|
|
4960 |
* Triggered when a `Component` is tapped.
|
|
|
4961 |
*
|
|
|
4962 |
* @event Component#tap
|
|
|
4963 |
* @type {MouseEvent}
|
|
|
4964 |
*/
|
|
|
4965 |
this.trigger('tap');
|
|
|
4966 |
// It may be good to copy the touchend event object and change the
|
|
|
4967 |
// type to tap, if the other event properties aren't exact after
|
|
|
4968 |
// Events.fixEvent runs (e.g. event.target)
|
|
|
4969 |
}
|
|
|
4970 |
}
|
|
|
4971 |
});
|
|
|
4972 |
}
|
|
|
4973 |
|
|
|
4974 |
/**
|
|
|
4975 |
* This function reports user activity whenever touch events happen. This can get
|
|
|
4976 |
* turned off by any sub-components that wants touch events to act another way.
|
|
|
4977 |
*
|
|
|
4978 |
* Report user touch activity when touch events occur. User activity gets used to
|
|
|
4979 |
* determine when controls should show/hide. It is simple when it comes to mouse
|
|
|
4980 |
* events, because any mouse event should show the controls. So we capture mouse
|
|
|
4981 |
* events that bubble up to the player and report activity when that happens.
|
|
|
4982 |
* With touch events it isn't as easy as `touchstart` and `touchend` toggle player
|
|
|
4983 |
* controls. So touch events can't help us at the player level either.
|
|
|
4984 |
*
|
|
|
4985 |
* User activity gets checked asynchronously. So what could happen is a tap event
|
|
|
4986 |
* on the video turns the controls off. Then the `touchend` event bubbles up to
|
|
|
4987 |
* the player. Which, if it reported user activity, would turn the controls right
|
|
|
4988 |
* back on. We also don't want to completely block touch events from bubbling up.
|
|
|
4989 |
* Furthermore a `touchmove` event and anything other than a tap, should not turn
|
|
|
4990 |
* controls back on.
|
|
|
4991 |
*
|
|
|
4992 |
* @listens Component#touchstart
|
|
|
4993 |
* @listens Component#touchmove
|
|
|
4994 |
* @listens Component#touchend
|
|
|
4995 |
* @listens Component#touchcancel
|
|
|
4996 |
*/
|
|
|
4997 |
enableTouchActivity() {
|
|
|
4998 |
// Don't continue if the root player doesn't support reporting user activity
|
|
|
4999 |
if (!this.player() || !this.player().reportUserActivity) {
|
|
|
5000 |
return;
|
|
|
5001 |
}
|
|
|
5002 |
|
|
|
5003 |
// listener for reporting that the user is active
|
|
|
5004 |
const report = bind_(this.player(), this.player().reportUserActivity);
|
|
|
5005 |
let touchHolding;
|
|
|
5006 |
this.on('touchstart', function () {
|
|
|
5007 |
report();
|
|
|
5008 |
// For as long as the they are touching the device or have their mouse down,
|
|
|
5009 |
// we consider them active even if they're not moving their finger or mouse.
|
|
|
5010 |
// So we want to continue to update that they are active
|
|
|
5011 |
this.clearInterval(touchHolding);
|
|
|
5012 |
// report at the same interval as activityCheck
|
|
|
5013 |
touchHolding = this.setInterval(report, 250);
|
|
|
5014 |
});
|
|
|
5015 |
const touchEnd = function (event) {
|
|
|
5016 |
report();
|
|
|
5017 |
// stop the interval that maintains activity if the touch is holding
|
|
|
5018 |
this.clearInterval(touchHolding);
|
|
|
5019 |
};
|
|
|
5020 |
this.on('touchmove', report);
|
|
|
5021 |
this.on('touchend', touchEnd);
|
|
|
5022 |
this.on('touchcancel', touchEnd);
|
|
|
5023 |
}
|
|
|
5024 |
|
|
|
5025 |
/**
|
|
|
5026 |
* A callback that has no parameters and is bound into `Component`s context.
|
|
|
5027 |
*
|
|
|
5028 |
* @callback Component~GenericCallback
|
|
|
5029 |
* @this Component
|
|
|
5030 |
*/
|
|
|
5031 |
|
|
|
5032 |
/**
|
|
|
5033 |
* Creates a function that runs after an `x` millisecond timeout. This function is a
|
|
|
5034 |
* wrapper around `window.setTimeout`. There are a few reasons to use this one
|
|
|
5035 |
* instead though:
|
|
|
5036 |
* 1. It gets cleared via {@link Component#clearTimeout} when
|
|
|
5037 |
* {@link Component#dispose} gets called.
|
|
|
5038 |
* 2. The function callback will gets turned into a {@link Component~GenericCallback}
|
|
|
5039 |
*
|
|
|
5040 |
* > Note: You can't use `window.clearTimeout` on the id returned by this function. This
|
|
|
5041 |
* will cause its dispose listener not to get cleaned up! Please use
|
|
|
5042 |
* {@link Component#clearTimeout} or {@link Component#dispose} instead.
|
|
|
5043 |
*
|
|
|
5044 |
* @param {Component~GenericCallback} fn
|
|
|
5045 |
* The function that will be run after `timeout`.
|
|
|
5046 |
*
|
|
|
5047 |
* @param {number} timeout
|
|
|
5048 |
* Timeout in milliseconds to delay before executing the specified function.
|
|
|
5049 |
*
|
|
|
5050 |
* @return {number}
|
|
|
5051 |
* Returns a timeout ID that gets used to identify the timeout. It can also
|
|
|
5052 |
* get used in {@link Component#clearTimeout} to clear the timeout that
|
|
|
5053 |
* was set.
|
|
|
5054 |
*
|
|
|
5055 |
* @listens Component#dispose
|
|
|
5056 |
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
|
|
|
5057 |
*/
|
|
|
5058 |
setTimeout(fn, timeout) {
|
|
|
5059 |
// declare as variables so they are properly available in timeout function
|
|
|
5060 |
// eslint-disable-next-line
|
|
|
5061 |
var timeoutId;
|
|
|
5062 |
fn = bind_(this, fn);
|
|
|
5063 |
this.clearTimersOnDispose_();
|
|
|
5064 |
timeoutId = window.setTimeout(() => {
|
|
|
5065 |
if (this.setTimeoutIds_.has(timeoutId)) {
|
|
|
5066 |
this.setTimeoutIds_.delete(timeoutId);
|
|
|
5067 |
}
|
|
|
5068 |
fn();
|
|
|
5069 |
}, timeout);
|
|
|
5070 |
this.setTimeoutIds_.add(timeoutId);
|
|
|
5071 |
return timeoutId;
|
|
|
5072 |
}
|
|
|
5073 |
|
|
|
5074 |
/**
|
|
|
5075 |
* Clears a timeout that gets created via `window.setTimeout` or
|
|
|
5076 |
* {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
|
|
|
5077 |
* use this function instead of `window.clearTimout`. If you don't your dispose
|
|
|
5078 |
* listener will not get cleaned up until {@link Component#dispose}!
|
|
|
5079 |
*
|
|
|
5080 |
* @param {number} timeoutId
|
|
|
5081 |
* The id of the timeout to clear. The return value of
|
|
|
5082 |
* {@link Component#setTimeout} or `window.setTimeout`.
|
|
|
5083 |
*
|
|
|
5084 |
* @return {number}
|
|
|
5085 |
* Returns the timeout id that was cleared.
|
|
|
5086 |
*
|
|
|
5087 |
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
|
|
|
5088 |
*/
|
|
|
5089 |
clearTimeout(timeoutId) {
|
|
|
5090 |
if (this.setTimeoutIds_.has(timeoutId)) {
|
|
|
5091 |
this.setTimeoutIds_.delete(timeoutId);
|
|
|
5092 |
window.clearTimeout(timeoutId);
|
|
|
5093 |
}
|
|
|
5094 |
return timeoutId;
|
|
|
5095 |
}
|
|
|
5096 |
|
|
|
5097 |
/**
|
|
|
5098 |
* Creates a function that gets run every `x` milliseconds. This function is a wrapper
|
|
|
5099 |
* around `window.setInterval`. There are a few reasons to use this one instead though.
|
|
|
5100 |
* 1. It gets cleared via {@link Component#clearInterval} when
|
|
|
5101 |
* {@link Component#dispose} gets called.
|
|
|
5102 |
* 2. The function callback will be a {@link Component~GenericCallback}
|
|
|
5103 |
*
|
|
|
5104 |
* @param {Component~GenericCallback} fn
|
|
|
5105 |
* The function to run every `x` seconds.
|
|
|
5106 |
*
|
|
|
5107 |
* @param {number} interval
|
|
|
5108 |
* Execute the specified function every `x` milliseconds.
|
|
|
5109 |
*
|
|
|
5110 |
* @return {number}
|
|
|
5111 |
* Returns an id that can be used to identify the interval. It can also be be used in
|
|
|
5112 |
* {@link Component#clearInterval} to clear the interval.
|
|
|
5113 |
*
|
|
|
5114 |
* @listens Component#dispose
|
|
|
5115 |
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
|
|
|
5116 |
*/
|
|
|
5117 |
setInterval(fn, interval) {
|
|
|
5118 |
fn = bind_(this, fn);
|
|
|
5119 |
this.clearTimersOnDispose_();
|
|
|
5120 |
const intervalId = window.setInterval(fn, interval);
|
|
|
5121 |
this.setIntervalIds_.add(intervalId);
|
|
|
5122 |
return intervalId;
|
|
|
5123 |
}
|
|
|
5124 |
|
|
|
5125 |
/**
|
|
|
5126 |
* Clears an interval that gets created via `window.setInterval` or
|
|
|
5127 |
* {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
|
|
|
5128 |
* use this function instead of `window.clearInterval`. If you don't your dispose
|
|
|
5129 |
* listener will not get cleaned up until {@link Component#dispose}!
|
|
|
5130 |
*
|
|
|
5131 |
* @param {number} intervalId
|
|
|
5132 |
* The id of the interval to clear. The return value of
|
|
|
5133 |
* {@link Component#setInterval} or `window.setInterval`.
|
|
|
5134 |
*
|
|
|
5135 |
* @return {number}
|
|
|
5136 |
* Returns the interval id that was cleared.
|
|
|
5137 |
*
|
|
|
5138 |
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
|
|
|
5139 |
*/
|
|
|
5140 |
clearInterval(intervalId) {
|
|
|
5141 |
if (this.setIntervalIds_.has(intervalId)) {
|
|
|
5142 |
this.setIntervalIds_.delete(intervalId);
|
|
|
5143 |
window.clearInterval(intervalId);
|
|
|
5144 |
}
|
|
|
5145 |
return intervalId;
|
|
|
5146 |
}
|
|
|
5147 |
|
|
|
5148 |
/**
|
|
|
5149 |
* Queues up a callback to be passed to requestAnimationFrame (rAF), but
|
|
|
5150 |
* with a few extra bonuses:
|
|
|
5151 |
*
|
|
|
5152 |
* - Supports browsers that do not support rAF by falling back to
|
|
|
5153 |
* {@link Component#setTimeout}.
|
|
|
5154 |
*
|
|
|
5155 |
* - The callback is turned into a {@link Component~GenericCallback} (i.e.
|
|
|
5156 |
* bound to the component).
|
|
|
5157 |
*
|
|
|
5158 |
* - Automatic cancellation of the rAF callback is handled if the component
|
|
|
5159 |
* is disposed before it is called.
|
|
|
5160 |
*
|
|
|
5161 |
* @param {Component~GenericCallback} fn
|
|
|
5162 |
* A function that will be bound to this component and executed just
|
|
|
5163 |
* before the browser's next repaint.
|
|
|
5164 |
*
|
|
|
5165 |
* @return {number}
|
|
|
5166 |
* Returns an rAF ID that gets used to identify the timeout. It can
|
|
|
5167 |
* also be used in {@link Component#cancelAnimationFrame} to cancel
|
|
|
5168 |
* the animation frame callback.
|
|
|
5169 |
*
|
|
|
5170 |
* @listens Component#dispose
|
|
|
5171 |
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
|
|
|
5172 |
*/
|
|
|
5173 |
requestAnimationFrame(fn) {
|
|
|
5174 |
this.clearTimersOnDispose_();
|
|
|
5175 |
|
|
|
5176 |
// declare as variables so they are properly available in rAF function
|
|
|
5177 |
// eslint-disable-next-line
|
|
|
5178 |
var id;
|
|
|
5179 |
fn = bind_(this, fn);
|
|
|
5180 |
id = window.requestAnimationFrame(() => {
|
|
|
5181 |
if (this.rafIds_.has(id)) {
|
|
|
5182 |
this.rafIds_.delete(id);
|
|
|
5183 |
}
|
|
|
5184 |
fn();
|
|
|
5185 |
});
|
|
|
5186 |
this.rafIds_.add(id);
|
|
|
5187 |
return id;
|
|
|
5188 |
}
|
|
|
5189 |
|
|
|
5190 |
/**
|
|
|
5191 |
* Request an animation frame, but only one named animation
|
|
|
5192 |
* frame will be queued. Another will never be added until
|
|
|
5193 |
* the previous one finishes.
|
|
|
5194 |
*
|
|
|
5195 |
* @param {string} name
|
|
|
5196 |
* The name to give this requestAnimationFrame
|
|
|
5197 |
*
|
|
|
5198 |
* @param {Component~GenericCallback} fn
|
|
|
5199 |
* A function that will be bound to this component and executed just
|
|
|
5200 |
* before the browser's next repaint.
|
|
|
5201 |
*/
|
|
|
5202 |
requestNamedAnimationFrame(name, fn) {
|
|
|
5203 |
if (this.namedRafs_.has(name)) {
|
|
|
5204 |
return;
|
|
|
5205 |
}
|
|
|
5206 |
this.clearTimersOnDispose_();
|
|
|
5207 |
fn = bind_(this, fn);
|
|
|
5208 |
const id = this.requestAnimationFrame(() => {
|
|
|
5209 |
fn();
|
|
|
5210 |
if (this.namedRafs_.has(name)) {
|
|
|
5211 |
this.namedRafs_.delete(name);
|
|
|
5212 |
}
|
|
|
5213 |
});
|
|
|
5214 |
this.namedRafs_.set(name, id);
|
|
|
5215 |
return name;
|
|
|
5216 |
}
|
|
|
5217 |
|
|
|
5218 |
/**
|
|
|
5219 |
* Cancels a current named animation frame if it exists.
|
|
|
5220 |
*
|
|
|
5221 |
* @param {string} name
|
|
|
5222 |
* The name of the requestAnimationFrame to cancel.
|
|
|
5223 |
*/
|
|
|
5224 |
cancelNamedAnimationFrame(name) {
|
|
|
5225 |
if (!this.namedRafs_.has(name)) {
|
|
|
5226 |
return;
|
|
|
5227 |
}
|
|
|
5228 |
this.cancelAnimationFrame(this.namedRafs_.get(name));
|
|
|
5229 |
this.namedRafs_.delete(name);
|
|
|
5230 |
}
|
|
|
5231 |
|
|
|
5232 |
/**
|
|
|
5233 |
* Cancels a queued callback passed to {@link Component#requestAnimationFrame}
|
|
|
5234 |
* (rAF).
|
|
|
5235 |
*
|
|
|
5236 |
* If you queue an rAF callback via {@link Component#requestAnimationFrame},
|
|
|
5237 |
* use this function instead of `window.cancelAnimationFrame`. If you don't,
|
|
|
5238 |
* your dispose listener will not get cleaned up until {@link Component#dispose}!
|
|
|
5239 |
*
|
|
|
5240 |
* @param {number} id
|
|
|
5241 |
* The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
|
|
|
5242 |
*
|
|
|
5243 |
* @return {number}
|
|
|
5244 |
* Returns the rAF ID that was cleared.
|
|
|
5245 |
*
|
|
|
5246 |
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
|
|
|
5247 |
*/
|
|
|
5248 |
cancelAnimationFrame(id) {
|
|
|
5249 |
if (this.rafIds_.has(id)) {
|
|
|
5250 |
this.rafIds_.delete(id);
|
|
|
5251 |
window.cancelAnimationFrame(id);
|
|
|
5252 |
}
|
|
|
5253 |
return id;
|
|
|
5254 |
}
|
|
|
5255 |
|
|
|
5256 |
/**
|
|
|
5257 |
* A function to setup `requestAnimationFrame`, `setTimeout`,
|
|
|
5258 |
* and `setInterval`, clearing on dispose.
|
|
|
5259 |
*
|
|
|
5260 |
* > Previously each timer added and removed dispose listeners on it's own.
|
|
|
5261 |
* For better performance it was decided to batch them all, and use `Set`s
|
|
|
5262 |
* to track outstanding timer ids.
|
|
|
5263 |
*
|
|
|
5264 |
* @private
|
|
|
5265 |
*/
|
|
|
5266 |
clearTimersOnDispose_() {
|
|
|
5267 |
if (this.clearingTimersOnDispose_) {
|
|
|
5268 |
return;
|
|
|
5269 |
}
|
|
|
5270 |
this.clearingTimersOnDispose_ = true;
|
|
|
5271 |
this.one('dispose', () => {
|
|
|
5272 |
[['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
|
|
|
5273 |
// for a `Set` key will actually be the value again
|
|
|
5274 |
// so forEach((val, val) =>` but for maps we want to use
|
|
|
5275 |
// the key.
|
|
|
5276 |
this[idName].forEach((val, key) => this[cancelName](key));
|
|
|
5277 |
});
|
|
|
5278 |
this.clearingTimersOnDispose_ = false;
|
|
|
5279 |
});
|
|
|
5280 |
}
|
|
|
5281 |
|
|
|
5282 |
/**
|
|
|
5283 |
* Register a `Component` with `videojs` given the name and the component.
|
|
|
5284 |
*
|
|
|
5285 |
* > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
|
|
|
5286 |
* should be registered using {@link Tech.registerTech} or
|
|
|
5287 |
* {@link videojs:videojs.registerTech}.
|
|
|
5288 |
*
|
|
|
5289 |
* > NOTE: This function can also be seen on videojs as
|
|
|
5290 |
* {@link videojs:videojs.registerComponent}.
|
|
|
5291 |
*
|
|
|
5292 |
* @param {string} name
|
|
|
5293 |
* The name of the `Component` to register.
|
|
|
5294 |
*
|
|
|
5295 |
* @param {Component} ComponentToRegister
|
|
|
5296 |
* The `Component` class to register.
|
|
|
5297 |
*
|
|
|
5298 |
* @return {Component}
|
|
|
5299 |
* The `Component` that was registered.
|
|
|
5300 |
*/
|
|
|
5301 |
static registerComponent(name, ComponentToRegister) {
|
|
|
5302 |
if (typeof name !== 'string' || !name) {
|
|
|
5303 |
throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
|
|
|
5304 |
}
|
|
|
5305 |
const Tech = Component$1.getComponent('Tech');
|
|
|
5306 |
|
|
|
5307 |
// We need to make sure this check is only done if Tech has been registered.
|
|
|
5308 |
const isTech = Tech && Tech.isTech(ComponentToRegister);
|
|
|
5309 |
const isComp = Component$1 === ComponentToRegister || Component$1.prototype.isPrototypeOf(ComponentToRegister.prototype);
|
|
|
5310 |
if (isTech || !isComp) {
|
|
|
5311 |
let reason;
|
|
|
5312 |
if (isTech) {
|
|
|
5313 |
reason = 'techs must be registered using Tech.registerTech()';
|
|
|
5314 |
} else {
|
|
|
5315 |
reason = 'must be a Component subclass';
|
|
|
5316 |
}
|
|
|
5317 |
throw new Error(`Illegal component, "${name}"; ${reason}.`);
|
|
|
5318 |
}
|
|
|
5319 |
name = toTitleCase$1(name);
|
|
|
5320 |
if (!Component$1.components_) {
|
|
|
5321 |
Component$1.components_ = {};
|
|
|
5322 |
}
|
|
|
5323 |
const Player = Component$1.getComponent('Player');
|
|
|
5324 |
if (name === 'Player' && Player && Player.players) {
|
|
|
5325 |
const players = Player.players;
|
|
|
5326 |
const playerNames = Object.keys(players);
|
|
|
5327 |
|
|
|
5328 |
// If we have players that were disposed, then their name will still be
|
|
|
5329 |
// in Players.players. So, we must loop through and verify that the value
|
|
|
5330 |
// for each item is not null. This allows registration of the Player component
|
|
|
5331 |
// after all players have been disposed or before any were created.
|
|
|
5332 |
if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
|
|
|
5333 |
throw new Error('Can not register Player component after player has been created.');
|
|
|
5334 |
}
|
|
|
5335 |
}
|
|
|
5336 |
Component$1.components_[name] = ComponentToRegister;
|
|
|
5337 |
Component$1.components_[toLowerCase(name)] = ComponentToRegister;
|
|
|
5338 |
return ComponentToRegister;
|
|
|
5339 |
}
|
|
|
5340 |
|
|
|
5341 |
/**
|
|
|
5342 |
* Get a `Component` based on the name it was registered with.
|
|
|
5343 |
*
|
|
|
5344 |
* @param {string} name
|
|
|
5345 |
* The Name of the component to get.
|
|
|
5346 |
*
|
|
|
5347 |
* @return {typeof Component}
|
|
|
5348 |
* The `Component` that got registered under the given name.
|
|
|
5349 |
*/
|
|
|
5350 |
static getComponent(name) {
|
|
|
5351 |
if (!name || !Component$1.components_) {
|
|
|
5352 |
return;
|
|
|
5353 |
}
|
|
|
5354 |
return Component$1.components_[name];
|
|
|
5355 |
}
|
|
|
5356 |
}
|
|
|
5357 |
Component$1.registerComponent('Component', Component$1);
|
|
|
5358 |
|
|
|
5359 |
/**
|
|
|
5360 |
* @file time.js
|
|
|
5361 |
* @module time
|
|
|
5362 |
*/
|
|
|
5363 |
|
|
|
5364 |
/**
|
|
|
5365 |
* Returns the time for the specified index at the start or end
|
|
|
5366 |
* of a TimeRange object.
|
|
|
5367 |
*
|
|
|
5368 |
* @typedef {Function} TimeRangeIndex
|
|
|
5369 |
*
|
|
|
5370 |
* @param {number} [index=0]
|
|
|
5371 |
* The range number to return the time for.
|
|
|
5372 |
*
|
|
|
5373 |
* @return {number}
|
|
|
5374 |
* The time offset at the specified index.
|
|
|
5375 |
*
|
|
|
5376 |
* @deprecated The index argument must be provided.
|
|
|
5377 |
* In the future, leaving it out will throw an error.
|
|
|
5378 |
*/
|
|
|
5379 |
|
|
|
5380 |
/**
|
|
|
5381 |
* An object that contains ranges of time, which mimics {@link TimeRanges}.
|
|
|
5382 |
*
|
|
|
5383 |
* @typedef {Object} TimeRange
|
|
|
5384 |
*
|
|
|
5385 |
* @property {number} length
|
|
|
5386 |
* The number of time ranges represented by this object.
|
|
|
5387 |
*
|
|
|
5388 |
* @property {module:time~TimeRangeIndex} start
|
|
|
5389 |
* Returns the time offset at which a specified time range begins.
|
|
|
5390 |
*
|
|
|
5391 |
* @property {module:time~TimeRangeIndex} end
|
|
|
5392 |
* Returns the time offset at which a specified time range ends.
|
|
|
5393 |
*
|
|
|
5394 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
|
|
|
5395 |
*/
|
|
|
5396 |
|
|
|
5397 |
/**
|
|
|
5398 |
* Check if any of the time ranges are over the maximum index.
|
|
|
5399 |
*
|
|
|
5400 |
* @private
|
|
|
5401 |
* @param {string} fnName
|
|
|
5402 |
* The function name to use for logging
|
|
|
5403 |
*
|
|
|
5404 |
* @param {number} index
|
|
|
5405 |
* The index to check
|
|
|
5406 |
*
|
|
|
5407 |
* @param {number} maxIndex
|
|
|
5408 |
* The maximum possible index
|
|
|
5409 |
*
|
|
|
5410 |
* @throws {Error} if the timeRanges provided are over the maxIndex
|
|
|
5411 |
*/
|
|
|
5412 |
function rangeCheck(fnName, index, maxIndex) {
|
|
|
5413 |
if (typeof index !== 'number' || index < 0 || index > maxIndex) {
|
|
|
5414 |
throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
|
|
|
5415 |
}
|
|
|
5416 |
}
|
|
|
5417 |
|
|
|
5418 |
/**
|
|
|
5419 |
* Get the time for the specified index at the start or end
|
|
|
5420 |
* of a TimeRange object.
|
|
|
5421 |
*
|
|
|
5422 |
* @private
|
|
|
5423 |
* @param {string} fnName
|
|
|
5424 |
* The function name to use for logging
|
|
|
5425 |
*
|
|
|
5426 |
* @param {string} valueIndex
|
|
|
5427 |
* The property that should be used to get the time. should be
|
|
|
5428 |
* 'start' or 'end'
|
|
|
5429 |
*
|
|
|
5430 |
* @param {Array} ranges
|
|
|
5431 |
* An array of time ranges
|
|
|
5432 |
*
|
|
|
5433 |
* @param {Array} [rangeIndex=0]
|
|
|
5434 |
* The index to start the search at
|
|
|
5435 |
*
|
|
|
5436 |
* @return {number}
|
|
|
5437 |
* The time that offset at the specified index.
|
|
|
5438 |
*
|
|
|
5439 |
* @deprecated rangeIndex must be set to a value, in the future this will throw an error.
|
|
|
5440 |
* @throws {Error} if rangeIndex is more than the length of ranges
|
|
|
5441 |
*/
|
|
|
5442 |
function getRange(fnName, valueIndex, ranges, rangeIndex) {
|
|
|
5443 |
rangeCheck(fnName, rangeIndex, ranges.length - 1);
|
|
|
5444 |
return ranges[rangeIndex][valueIndex];
|
|
|
5445 |
}
|
|
|
5446 |
|
|
|
5447 |
/**
|
|
|
5448 |
* Create a time range object given ranges of time.
|
|
|
5449 |
*
|
|
|
5450 |
* @private
|
|
|
5451 |
* @param {Array} [ranges]
|
|
|
5452 |
* An array of time ranges.
|
|
|
5453 |
*
|
|
|
5454 |
* @return {TimeRange}
|
|
|
5455 |
*/
|
|
|
5456 |
function createTimeRangesObj(ranges) {
|
|
|
5457 |
let timeRangesObj;
|
|
|
5458 |
if (ranges === undefined || ranges.length === 0) {
|
|
|
5459 |
timeRangesObj = {
|
|
|
5460 |
length: 0,
|
|
|
5461 |
start() {
|
|
|
5462 |
throw new Error('This TimeRanges object is empty');
|
|
|
5463 |
},
|
|
|
5464 |
end() {
|
|
|
5465 |
throw new Error('This TimeRanges object is empty');
|
|
|
5466 |
}
|
|
|
5467 |
};
|
|
|
5468 |
} else {
|
|
|
5469 |
timeRangesObj = {
|
|
|
5470 |
length: ranges.length,
|
|
|
5471 |
start: getRange.bind(null, 'start', 0, ranges),
|
|
|
5472 |
end: getRange.bind(null, 'end', 1, ranges)
|
|
|
5473 |
};
|
|
|
5474 |
}
|
|
|
5475 |
if (window.Symbol && window.Symbol.iterator) {
|
|
|
5476 |
timeRangesObj[window.Symbol.iterator] = () => (ranges || []).values();
|
|
|
5477 |
}
|
|
|
5478 |
return timeRangesObj;
|
|
|
5479 |
}
|
|
|
5480 |
|
|
|
5481 |
/**
|
|
|
5482 |
* Create a `TimeRange` object which mimics an
|
|
|
5483 |
* {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
|
|
|
5484 |
*
|
|
|
5485 |
* @param {number|Array[]} start
|
|
|
5486 |
* The start of a single range (a number) or an array of ranges (an
|
|
|
5487 |
* array of arrays of two numbers each).
|
|
|
5488 |
*
|
|
|
5489 |
* @param {number} end
|
|
|
5490 |
* The end of a single range. Cannot be used with the array form of
|
|
|
5491 |
* the `start` argument.
|
|
|
5492 |
*
|
|
|
5493 |
* @return {TimeRange}
|
|
|
5494 |
*/
|
|
|
5495 |
function createTimeRanges$1(start, end) {
|
|
|
5496 |
if (Array.isArray(start)) {
|
|
|
5497 |
return createTimeRangesObj(start);
|
|
|
5498 |
} else if (start === undefined || end === undefined) {
|
|
|
5499 |
return createTimeRangesObj();
|
|
|
5500 |
}
|
|
|
5501 |
return createTimeRangesObj([[start, end]]);
|
|
|
5502 |
}
|
|
|
5503 |
|
|
|
5504 |
/**
|
|
|
5505 |
* Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
|
|
|
5506 |
* seconds) will force a number of leading zeros to cover the length of the
|
|
|
5507 |
* guide.
|
|
|
5508 |
*
|
|
|
5509 |
* @private
|
|
|
5510 |
* @param {number} seconds
|
|
|
5511 |
* Number of seconds to be turned into a string
|
|
|
5512 |
*
|
|
|
5513 |
* @param {number} guide
|
|
|
5514 |
* Number (in seconds) to model the string after
|
|
|
5515 |
*
|
|
|
5516 |
* @return {string}
|
|
|
5517 |
* Time formatted as H:MM:SS or M:SS
|
|
|
5518 |
*/
|
|
|
5519 |
const defaultImplementation = function (seconds, guide) {
|
|
|
5520 |
seconds = seconds < 0 ? 0 : seconds;
|
|
|
5521 |
let s = Math.floor(seconds % 60);
|
|
|
5522 |
let m = Math.floor(seconds / 60 % 60);
|
|
|
5523 |
let h = Math.floor(seconds / 3600);
|
|
|
5524 |
const gm = Math.floor(guide / 60 % 60);
|
|
|
5525 |
const gh = Math.floor(guide / 3600);
|
|
|
5526 |
|
|
|
5527 |
// handle invalid times
|
|
|
5528 |
if (isNaN(seconds) || seconds === Infinity) {
|
|
|
5529 |
// '-' is false for all relational operators (e.g. <, >=) so this setting
|
|
|
5530 |
// will add the minimum number of fields specified by the guide
|
|
|
5531 |
h = m = s = '-';
|
|
|
5532 |
}
|
|
|
5533 |
|
|
|
5534 |
// Check if we need to show hours
|
|
|
5535 |
h = h > 0 || gh > 0 ? h + ':' : '';
|
|
|
5536 |
|
|
|
5537 |
// If hours are showing, we may need to add a leading zero.
|
|
|
5538 |
// Always show at least one digit of minutes.
|
|
|
5539 |
m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
|
|
|
5540 |
|
|
|
5541 |
// Check if leading zero is need for seconds
|
|
|
5542 |
s = s < 10 ? '0' + s : s;
|
|
|
5543 |
return h + m + s;
|
|
|
5544 |
};
|
|
|
5545 |
|
|
|
5546 |
// Internal pointer to the current implementation.
|
|
|
5547 |
let implementation = defaultImplementation;
|
|
|
5548 |
|
|
|
5549 |
/**
|
|
|
5550 |
* Replaces the default formatTime implementation with a custom implementation.
|
|
|
5551 |
*
|
|
|
5552 |
* @param {Function} customImplementation
|
|
|
5553 |
* A function which will be used in place of the default formatTime
|
|
|
5554 |
* implementation. Will receive the current time in seconds and the
|
|
|
5555 |
* guide (in seconds) as arguments.
|
|
|
5556 |
*/
|
|
|
5557 |
function setFormatTime(customImplementation) {
|
|
|
5558 |
implementation = customImplementation;
|
|
|
5559 |
}
|
|
|
5560 |
|
|
|
5561 |
/**
|
|
|
5562 |
* Resets formatTime to the default implementation.
|
|
|
5563 |
*/
|
|
|
5564 |
function resetFormatTime() {
|
|
|
5565 |
implementation = defaultImplementation;
|
|
|
5566 |
}
|
|
|
5567 |
|
|
|
5568 |
/**
|
|
|
5569 |
* Delegates to either the default time formatting function or a custom
|
|
|
5570 |
* function supplied via `setFormatTime`.
|
|
|
5571 |
*
|
|
|
5572 |
* Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
|
|
|
5573 |
* guide (in seconds) will force a number of leading zeros to cover the
|
|
|
5574 |
* length of the guide.
|
|
|
5575 |
*
|
|
|
5576 |
* @example formatTime(125, 600) === "02:05"
|
|
|
5577 |
* @param {number} seconds
|
|
|
5578 |
* Number of seconds to be turned into a string
|
|
|
5579 |
*
|
|
|
5580 |
* @param {number} guide
|
|
|
5581 |
* Number (in seconds) to model the string after
|
|
|
5582 |
*
|
|
|
5583 |
* @return {string}
|
|
|
5584 |
* Time formatted as H:MM:SS or M:SS
|
|
|
5585 |
*/
|
|
|
5586 |
function formatTime(seconds, guide = seconds) {
|
|
|
5587 |
return implementation(seconds, guide);
|
|
|
5588 |
}
|
|
|
5589 |
|
|
|
5590 |
var Time = /*#__PURE__*/Object.freeze({
|
|
|
5591 |
__proto__: null,
|
|
|
5592 |
createTimeRanges: createTimeRanges$1,
|
|
|
5593 |
createTimeRange: createTimeRanges$1,
|
|
|
5594 |
setFormatTime: setFormatTime,
|
|
|
5595 |
resetFormatTime: resetFormatTime,
|
|
|
5596 |
formatTime: formatTime
|
|
|
5597 |
});
|
|
|
5598 |
|
|
|
5599 |
/**
|
|
|
5600 |
* @file buffer.js
|
|
|
5601 |
* @module buffer
|
|
|
5602 |
*/
|
|
|
5603 |
|
|
|
5604 |
/**
|
|
|
5605 |
* Compute the percentage of the media that has been buffered.
|
|
|
5606 |
*
|
|
|
5607 |
* @param { import('./time').TimeRange } buffered
|
|
|
5608 |
* The current `TimeRanges` object representing buffered time ranges
|
|
|
5609 |
*
|
|
|
5610 |
* @param {number} duration
|
|
|
5611 |
* Total duration of the media
|
|
|
5612 |
*
|
|
|
5613 |
* @return {number}
|
|
|
5614 |
* Percent buffered of the total duration in decimal form.
|
|
|
5615 |
*/
|
|
|
5616 |
function bufferedPercent(buffered, duration) {
|
|
|
5617 |
let bufferedDuration = 0;
|
|
|
5618 |
let start;
|
|
|
5619 |
let end;
|
|
|
5620 |
if (!duration) {
|
|
|
5621 |
return 0;
|
|
|
5622 |
}
|
|
|
5623 |
if (!buffered || !buffered.length) {
|
|
|
5624 |
buffered = createTimeRanges$1(0, 0);
|
|
|
5625 |
}
|
|
|
5626 |
for (let i = 0; i < buffered.length; i++) {
|
|
|
5627 |
start = buffered.start(i);
|
|
|
5628 |
end = buffered.end(i);
|
|
|
5629 |
|
|
|
5630 |
// buffered end can be bigger than duration by a very small fraction
|
|
|
5631 |
if (end > duration) {
|
|
|
5632 |
end = duration;
|
|
|
5633 |
}
|
|
|
5634 |
bufferedDuration += end - start;
|
|
|
5635 |
}
|
|
|
5636 |
return bufferedDuration / duration;
|
|
|
5637 |
}
|
|
|
5638 |
|
|
|
5639 |
/**
|
|
|
5640 |
* @file media-error.js
|
|
|
5641 |
*/
|
|
|
5642 |
|
|
|
5643 |
/**
|
|
|
5644 |
* A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
|
|
|
5645 |
*
|
|
|
5646 |
* @param {number|string|Object|MediaError} value
|
|
|
5647 |
* This can be of multiple types:
|
|
|
5648 |
* - number: should be a standard error code
|
|
|
5649 |
* - string: an error message (the code will be 0)
|
|
|
5650 |
* - Object: arbitrary properties
|
|
|
5651 |
* - `MediaError` (native): used to populate a video.js `MediaError` object
|
|
|
5652 |
* - `MediaError` (video.js): will return itself if it's already a
|
|
|
5653 |
* video.js `MediaError` object.
|
|
|
5654 |
*
|
|
|
5655 |
* @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
|
|
|
5656 |
* @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
|
|
|
5657 |
*
|
|
|
5658 |
* @class MediaError
|
|
|
5659 |
*/
|
|
|
5660 |
function MediaError(value) {
|
|
|
5661 |
// Allow redundant calls to this constructor to avoid having `instanceof`
|
|
|
5662 |
// checks peppered around the code.
|
|
|
5663 |
if (value instanceof MediaError) {
|
|
|
5664 |
return value;
|
|
|
5665 |
}
|
|
|
5666 |
if (typeof value === 'number') {
|
|
|
5667 |
this.code = value;
|
|
|
5668 |
} else if (typeof value === 'string') {
|
|
|
5669 |
// default code is zero, so this is a custom error
|
|
|
5670 |
this.message = value;
|
|
|
5671 |
} else if (isObject$1(value)) {
|
|
|
5672 |
// We assign the `code` property manually because native `MediaError` objects
|
|
|
5673 |
// do not expose it as an own/enumerable property of the object.
|
|
|
5674 |
if (typeof value.code === 'number') {
|
|
|
5675 |
this.code = value.code;
|
|
|
5676 |
}
|
|
|
5677 |
Object.assign(this, value);
|
|
|
5678 |
}
|
|
|
5679 |
if (!this.message) {
|
|
|
5680 |
this.message = MediaError.defaultMessages[this.code] || '';
|
|
|
5681 |
}
|
|
|
5682 |
}
|
|
|
5683 |
|
|
|
5684 |
/**
|
|
|
5685 |
* The error code that refers two one of the defined `MediaError` types
|
|
|
5686 |
*
|
|
|
5687 |
* @type {Number}
|
|
|
5688 |
*/
|
|
|
5689 |
MediaError.prototype.code = 0;
|
|
|
5690 |
|
|
|
5691 |
/**
|
|
|
5692 |
* An optional message that to show with the error. Message is not part of the HTML5
|
|
|
5693 |
* video spec but allows for more informative custom errors.
|
|
|
5694 |
*
|
|
|
5695 |
* @type {String}
|
|
|
5696 |
*/
|
|
|
5697 |
MediaError.prototype.message = '';
|
|
|
5698 |
|
|
|
5699 |
/**
|
|
|
5700 |
* An optional status code that can be set by plugins to allow even more detail about
|
|
|
5701 |
* the error. For example a plugin might provide a specific HTTP status code and an
|
|
|
5702 |
* error message for that code. Then when the plugin gets that error this class will
|
|
|
5703 |
* know how to display an error message for it. This allows a custom message to show
|
|
|
5704 |
* up on the `Player` error overlay.
|
|
|
5705 |
*
|
|
|
5706 |
* @type {Array}
|
|
|
5707 |
*/
|
|
|
5708 |
MediaError.prototype.status = null;
|
|
|
5709 |
|
|
|
5710 |
/**
|
|
|
5711 |
* Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
|
|
|
5712 |
* specification listed under {@link MediaError} for more information.
|
|
|
5713 |
*
|
|
|
5714 |
* @enum {array}
|
|
|
5715 |
* @readonly
|
|
|
5716 |
* @property {string} 0 - MEDIA_ERR_CUSTOM
|
|
|
5717 |
* @property {string} 1 - MEDIA_ERR_ABORTED
|
|
|
5718 |
* @property {string} 2 - MEDIA_ERR_NETWORK
|
|
|
5719 |
* @property {string} 3 - MEDIA_ERR_DECODE
|
|
|
5720 |
* @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
|
|
|
5721 |
* @property {string} 5 - MEDIA_ERR_ENCRYPTED
|
|
|
5722 |
*/
|
|
|
5723 |
MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
|
|
|
5724 |
|
|
|
5725 |
/**
|
|
|
5726 |
* The default `MediaError` messages based on the {@link MediaError.errorTypes}.
|
|
|
5727 |
*
|
|
|
5728 |
* @type {Array}
|
|
|
5729 |
* @constant
|
|
|
5730 |
*/
|
|
|
5731 |
MediaError.defaultMessages = {
|
|
|
5732 |
1: 'You aborted the media playback',
|
|
|
5733 |
2: 'A network error caused the media download to fail part-way.',
|
|
|
5734 |
3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
|
|
|
5735 |
4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
|
|
|
5736 |
5: 'The media is encrypted and we do not have the keys to decrypt it.'
|
|
|
5737 |
};
|
|
|
5738 |
|
|
|
5739 |
// Add types as properties on MediaError
|
|
|
5740 |
// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
|
|
|
5741 |
for (let errNum = 0; errNum < MediaError.errorTypes.length; errNum++) {
|
|
|
5742 |
MediaError[MediaError.errorTypes[errNum]] = errNum;
|
|
|
5743 |
// values should be accessible on both the class and instance
|
|
|
5744 |
MediaError.prototype[MediaError.errorTypes[errNum]] = errNum;
|
|
|
5745 |
}
|
|
|
5746 |
|
|
|
5747 |
var tuple = SafeParseTuple;
|
|
|
5748 |
function SafeParseTuple(obj, reviver) {
|
|
|
5749 |
var json;
|
|
|
5750 |
var error = null;
|
|
|
5751 |
try {
|
|
|
5752 |
json = JSON.parse(obj, reviver);
|
|
|
5753 |
} catch (err) {
|
|
|
5754 |
error = err;
|
|
|
5755 |
}
|
|
|
5756 |
return [error, json];
|
|
|
5757 |
}
|
|
|
5758 |
|
|
|
5759 |
/**
|
|
|
5760 |
* Returns whether an object is `Promise`-like (i.e. has a `then` method).
|
|
|
5761 |
*
|
|
|
5762 |
* @param {Object} value
|
|
|
5763 |
* An object that may or may not be `Promise`-like.
|
|
|
5764 |
*
|
|
|
5765 |
* @return {boolean}
|
|
|
5766 |
* Whether or not the object is `Promise`-like.
|
|
|
5767 |
*/
|
|
|
5768 |
function isPromise(value) {
|
|
|
5769 |
return value !== undefined && value !== null && typeof value.then === 'function';
|
|
|
5770 |
}
|
|
|
5771 |
|
|
|
5772 |
/**
|
|
|
5773 |
* Silence a Promise-like object.
|
|
|
5774 |
*
|
|
|
5775 |
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
|
|
|
5776 |
* play promise" rejection error messages.
|
|
|
5777 |
*
|
|
|
5778 |
* @param {Object} value
|
|
|
5779 |
* An object that may or may not be `Promise`-like.
|
|
|
5780 |
*/
|
|
|
5781 |
function silencePromise(value) {
|
|
|
5782 |
if (isPromise(value)) {
|
|
|
5783 |
value.then(null, e => {});
|
|
|
5784 |
}
|
|
|
5785 |
}
|
|
|
5786 |
|
|
|
5787 |
/**
|
|
|
5788 |
* @file text-track-list-converter.js Utilities for capturing text track state and
|
|
|
5789 |
* re-creating tracks based on a capture.
|
|
|
5790 |
*
|
|
|
5791 |
* @module text-track-list-converter
|
|
|
5792 |
*/
|
|
|
5793 |
|
|
|
5794 |
/**
|
|
|
5795 |
* Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
|
|
|
5796 |
* represents the {@link TextTrack}'s state.
|
|
|
5797 |
*
|
|
|
5798 |
* @param {TextTrack} track
|
|
|
5799 |
* The text track to query.
|
|
|
5800 |
*
|
|
|
5801 |
* @return {Object}
|
|
|
5802 |
* A serializable javascript representation of the TextTrack.
|
|
|
5803 |
* @private
|
|
|
5804 |
*/
|
|
|
5805 |
const trackToJson_ = function (track) {
|
|
|
5806 |
const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
|
|
|
5807 |
if (track[prop]) {
|
|
|
5808 |
acc[prop] = track[prop];
|
|
|
5809 |
}
|
|
|
5810 |
return acc;
|
|
|
5811 |
}, {
|
|
|
5812 |
cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
|
|
|
5813 |
return {
|
|
|
5814 |
startTime: cue.startTime,
|
|
|
5815 |
endTime: cue.endTime,
|
|
|
5816 |
text: cue.text,
|
|
|
5817 |
id: cue.id
|
|
|
5818 |
};
|
|
|
5819 |
})
|
|
|
5820 |
});
|
|
|
5821 |
return ret;
|
|
|
5822 |
};
|
|
|
5823 |
|
|
|
5824 |
/**
|
|
|
5825 |
* Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
|
|
|
5826 |
* state of all {@link TextTrack}s currently configured. The return array is compatible with
|
|
|
5827 |
* {@link text-track-list-converter:jsonToTextTracks}.
|
|
|
5828 |
*
|
|
|
5829 |
* @param { import('../tech/tech').default } tech
|
|
|
5830 |
* The tech object to query
|
|
|
5831 |
*
|
|
|
5832 |
* @return {Array}
|
|
|
5833 |
* A serializable javascript representation of the {@link Tech}s
|
|
|
5834 |
* {@link TextTrackList}.
|
|
|
5835 |
*/
|
|
|
5836 |
const textTracksToJson = function (tech) {
|
|
|
5837 |
const trackEls = tech.$$('track');
|
|
|
5838 |
const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
|
|
|
5839 |
const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
|
|
|
5840 |
const json = trackToJson_(trackEl.track);
|
|
|
5841 |
if (trackEl.src) {
|
|
|
5842 |
json.src = trackEl.src;
|
|
|
5843 |
}
|
|
|
5844 |
return json;
|
|
|
5845 |
});
|
|
|
5846 |
return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
|
|
|
5847 |
return trackObjs.indexOf(track) === -1;
|
|
|
5848 |
}).map(trackToJson_));
|
|
|
5849 |
};
|
|
|
5850 |
|
|
|
5851 |
/**
|
|
|
5852 |
* Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
|
|
|
5853 |
* object {@link TextTrack} representations.
|
|
|
5854 |
*
|
|
|
5855 |
* @param {Array} json
|
|
|
5856 |
* An array of `TextTrack` representation objects, like those that would be
|
|
|
5857 |
* produced by `textTracksToJson`.
|
|
|
5858 |
*
|
|
|
5859 |
* @param {Tech} tech
|
|
|
5860 |
* The `Tech` to create the `TextTrack`s on.
|
|
|
5861 |
*/
|
|
|
5862 |
const jsonToTextTracks = function (json, tech) {
|
|
|
5863 |
json.forEach(function (track) {
|
|
|
5864 |
const addedTrack = tech.addRemoteTextTrack(track).track;
|
|
|
5865 |
if (!track.src && track.cues) {
|
|
|
5866 |
track.cues.forEach(cue => addedTrack.addCue(cue));
|
|
|
5867 |
}
|
|
|
5868 |
});
|
|
|
5869 |
return tech.textTracks();
|
|
|
5870 |
};
|
|
|
5871 |
var textTrackConverter = {
|
|
|
5872 |
textTracksToJson,
|
|
|
5873 |
jsonToTextTracks,
|
|
|
5874 |
trackToJson_
|
|
|
5875 |
};
|
|
|
5876 |
|
|
|
5877 |
/**
|
|
|
5878 |
* @file modal-dialog.js
|
|
|
5879 |
*/
|
|
|
5880 |
const MODAL_CLASS_NAME = 'vjs-modal-dialog';
|
|
|
5881 |
|
|
|
5882 |
/**
|
|
|
5883 |
* The `ModalDialog` displays over the video and its controls, which blocks
|
|
|
5884 |
* interaction with the player until it is closed.
|
|
|
5885 |
*
|
|
|
5886 |
* Modal dialogs include a "Close" button and will close when that button
|
|
|
5887 |
* is activated - or when ESC is pressed anywhere.
|
|
|
5888 |
*
|
|
|
5889 |
* @extends Component
|
|
|
5890 |
*/
|
|
|
5891 |
class ModalDialog extends Component$1 {
|
|
|
5892 |
/**
|
|
|
5893 |
* Create an instance of this class.
|
|
|
5894 |
*
|
|
|
5895 |
* @param { import('./player').default } player
|
|
|
5896 |
* The `Player` that this class should be attached to.
|
|
|
5897 |
*
|
|
|
5898 |
* @param {Object} [options]
|
|
|
5899 |
* The key/value store of player options.
|
|
|
5900 |
*
|
|
|
5901 |
* @param { import('./utils/dom').ContentDescriptor} [options.content=undefined]
|
|
|
5902 |
* Provide customized content for this modal.
|
|
|
5903 |
*
|
|
|
5904 |
* @param {string} [options.description]
|
|
|
5905 |
* A text description for the modal, primarily for accessibility.
|
|
|
5906 |
*
|
|
|
5907 |
* @param {boolean} [options.fillAlways=false]
|
|
|
5908 |
* Normally, modals are automatically filled only the first time
|
|
|
5909 |
* they open. This tells the modal to refresh its content
|
|
|
5910 |
* every time it opens.
|
|
|
5911 |
*
|
|
|
5912 |
* @param {string} [options.label]
|
|
|
5913 |
* A text label for the modal, primarily for accessibility.
|
|
|
5914 |
*
|
|
|
5915 |
* @param {boolean} [options.pauseOnOpen=true]
|
|
|
5916 |
* If `true`, playback will will be paused if playing when
|
|
|
5917 |
* the modal opens, and resumed when it closes.
|
|
|
5918 |
*
|
|
|
5919 |
* @param {boolean} [options.temporary=true]
|
|
|
5920 |
* If `true`, the modal can only be opened once; it will be
|
|
|
5921 |
* disposed as soon as it's closed.
|
|
|
5922 |
*
|
|
|
5923 |
* @param {boolean} [options.uncloseable=false]
|
|
|
5924 |
* If `true`, the user will not be able to close the modal
|
|
|
5925 |
* through the UI in the normal ways. Programmatic closing is
|
|
|
5926 |
* still possible.
|
|
|
5927 |
*/
|
|
|
5928 |
constructor(player, options) {
|
|
|
5929 |
super(player, options);
|
|
|
5930 |
this.handleKeyDown_ = e => this.handleKeyDown(e);
|
|
|
5931 |
this.close_ = e => this.close(e);
|
|
|
5932 |
this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
|
|
|
5933 |
this.closeable(!this.options_.uncloseable);
|
|
|
5934 |
this.content(this.options_.content);
|
|
|
5935 |
|
|
|
5936 |
// Make sure the contentEl is defined AFTER any children are initialized
|
|
|
5937 |
// because we only want the contents of the modal in the contentEl
|
|
|
5938 |
// (not the UI elements like the close button).
|
|
|
5939 |
this.contentEl_ = createEl('div', {
|
|
|
5940 |
className: `${MODAL_CLASS_NAME}-content`
|
|
|
5941 |
}, {
|
|
|
5942 |
role: 'document'
|
|
|
5943 |
});
|
|
|
5944 |
this.descEl_ = createEl('p', {
|
|
|
5945 |
className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
|
|
|
5946 |
id: this.el().getAttribute('aria-describedby')
|
|
|
5947 |
});
|
|
|
5948 |
textContent(this.descEl_, this.description());
|
|
|
5949 |
this.el_.appendChild(this.descEl_);
|
|
|
5950 |
this.el_.appendChild(this.contentEl_);
|
|
|
5951 |
}
|
|
|
5952 |
|
|
|
5953 |
/**
|
|
|
5954 |
* Create the `ModalDialog`'s DOM element
|
|
|
5955 |
*
|
|
|
5956 |
* @return {Element}
|
|
|
5957 |
* The DOM element that gets created.
|
|
|
5958 |
*/
|
|
|
5959 |
createEl() {
|
|
|
5960 |
return super.createEl('div', {
|
|
|
5961 |
className: this.buildCSSClass(),
|
|
|
5962 |
tabIndex: -1
|
|
|
5963 |
}, {
|
|
|
5964 |
'aria-describedby': `${this.id()}_description`,
|
|
|
5965 |
'aria-hidden': 'true',
|
|
|
5966 |
'aria-label': this.label(),
|
|
|
5967 |
'role': 'dialog'
|
|
|
5968 |
});
|
|
|
5969 |
}
|
|
|
5970 |
dispose() {
|
|
|
5971 |
this.contentEl_ = null;
|
|
|
5972 |
this.descEl_ = null;
|
|
|
5973 |
this.previouslyActiveEl_ = null;
|
|
|
5974 |
super.dispose();
|
|
|
5975 |
}
|
|
|
5976 |
|
|
|
5977 |
/**
|
|
|
5978 |
* Builds the default DOM `className`.
|
|
|
5979 |
*
|
|
|
5980 |
* @return {string}
|
|
|
5981 |
* The DOM `className` for this object.
|
|
|
5982 |
*/
|
|
|
5983 |
buildCSSClass() {
|
|
|
5984 |
return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
|
|
|
5985 |
}
|
|
|
5986 |
|
|
|
5987 |
/**
|
|
|
5988 |
* Returns the label string for this modal. Primarily used for accessibility.
|
|
|
5989 |
*
|
|
|
5990 |
* @return {string}
|
|
|
5991 |
* the localized or raw label of this modal.
|
|
|
5992 |
*/
|
|
|
5993 |
label() {
|
|
|
5994 |
return this.localize(this.options_.label || 'Modal Window');
|
|
|
5995 |
}
|
|
|
5996 |
|
|
|
5997 |
/**
|
|
|
5998 |
* Returns the description string for this modal. Primarily used for
|
|
|
5999 |
* accessibility.
|
|
|
6000 |
*
|
|
|
6001 |
* @return {string}
|
|
|
6002 |
* The localized or raw description of this modal.
|
|
|
6003 |
*/
|
|
|
6004 |
description() {
|
|
|
6005 |
let desc = this.options_.description || this.localize('This is a modal window.');
|
|
|
6006 |
|
|
|
6007 |
// Append a universal closeability message if the modal is closeable.
|
|
|
6008 |
if (this.closeable()) {
|
|
|
6009 |
desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
|
|
|
6010 |
}
|
|
|
6011 |
return desc;
|
|
|
6012 |
}
|
|
|
6013 |
|
|
|
6014 |
/**
|
|
|
6015 |
* Opens the modal.
|
|
|
6016 |
*
|
|
|
6017 |
* @fires ModalDialog#beforemodalopen
|
|
|
6018 |
* @fires ModalDialog#modalopen
|
|
|
6019 |
*/
|
|
|
6020 |
open() {
|
|
|
6021 |
if (!this.opened_) {
|
|
|
6022 |
const player = this.player();
|
|
|
6023 |
|
|
|
6024 |
/**
|
|
|
6025 |
* Fired just before a `ModalDialog` is opened.
|
|
|
6026 |
*
|
|
|
6027 |
* @event ModalDialog#beforemodalopen
|
|
|
6028 |
* @type {Event}
|
|
|
6029 |
*/
|
|
|
6030 |
this.trigger('beforemodalopen');
|
|
|
6031 |
this.opened_ = true;
|
|
|
6032 |
|
|
|
6033 |
// Fill content if the modal has never opened before and
|
|
|
6034 |
// never been filled.
|
|
|
6035 |
if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
|
|
|
6036 |
this.fill();
|
|
|
6037 |
}
|
|
|
6038 |
|
|
|
6039 |
// If the player was playing, pause it and take note of its previously
|
|
|
6040 |
// playing state.
|
|
|
6041 |
this.wasPlaying_ = !player.paused();
|
|
|
6042 |
if (this.options_.pauseOnOpen && this.wasPlaying_) {
|
|
|
6043 |
player.pause();
|
|
|
6044 |
}
|
|
|
6045 |
this.on('keydown', this.handleKeyDown_);
|
|
|
6046 |
|
|
|
6047 |
// Hide controls and note if they were enabled.
|
|
|
6048 |
this.hadControls_ = player.controls();
|
|
|
6049 |
player.controls(false);
|
|
|
6050 |
this.show();
|
|
|
6051 |
this.conditionalFocus_();
|
|
|
6052 |
this.el().setAttribute('aria-hidden', 'false');
|
|
|
6053 |
|
|
|
6054 |
/**
|
|
|
6055 |
* Fired just after a `ModalDialog` is opened.
|
|
|
6056 |
*
|
|
|
6057 |
* @event ModalDialog#modalopen
|
|
|
6058 |
* @type {Event}
|
|
|
6059 |
*/
|
|
|
6060 |
this.trigger('modalopen');
|
|
|
6061 |
this.hasBeenOpened_ = true;
|
|
|
6062 |
}
|
|
|
6063 |
}
|
|
|
6064 |
|
|
|
6065 |
/**
|
|
|
6066 |
* If the `ModalDialog` is currently open or closed.
|
|
|
6067 |
*
|
|
|
6068 |
* @param {boolean} [value]
|
|
|
6069 |
* If given, it will open (`true`) or close (`false`) the modal.
|
|
|
6070 |
*
|
|
|
6071 |
* @return {boolean}
|
|
|
6072 |
* the current open state of the modaldialog
|
|
|
6073 |
*/
|
|
|
6074 |
opened(value) {
|
|
|
6075 |
if (typeof value === 'boolean') {
|
|
|
6076 |
this[value ? 'open' : 'close']();
|
|
|
6077 |
}
|
|
|
6078 |
return this.opened_;
|
|
|
6079 |
}
|
|
|
6080 |
|
|
|
6081 |
/**
|
|
|
6082 |
* Closes the modal, does nothing if the `ModalDialog` is
|
|
|
6083 |
* not open.
|
|
|
6084 |
*
|
|
|
6085 |
* @fires ModalDialog#beforemodalclose
|
|
|
6086 |
* @fires ModalDialog#modalclose
|
|
|
6087 |
*/
|
|
|
6088 |
close() {
|
|
|
6089 |
if (!this.opened_) {
|
|
|
6090 |
return;
|
|
|
6091 |
}
|
|
|
6092 |
const player = this.player();
|
|
|
6093 |
|
|
|
6094 |
/**
|
|
|
6095 |
* Fired just before a `ModalDialog` is closed.
|
|
|
6096 |
*
|
|
|
6097 |
* @event ModalDialog#beforemodalclose
|
|
|
6098 |
* @type {Event}
|
|
|
6099 |
*/
|
|
|
6100 |
this.trigger('beforemodalclose');
|
|
|
6101 |
this.opened_ = false;
|
|
|
6102 |
if (this.wasPlaying_ && this.options_.pauseOnOpen) {
|
|
|
6103 |
player.play();
|
|
|
6104 |
}
|
|
|
6105 |
this.off('keydown', this.handleKeyDown_);
|
|
|
6106 |
if (this.hadControls_) {
|
|
|
6107 |
player.controls(true);
|
|
|
6108 |
}
|
|
|
6109 |
this.hide();
|
|
|
6110 |
this.el().setAttribute('aria-hidden', 'true');
|
|
|
6111 |
|
|
|
6112 |
/**
|
|
|
6113 |
* Fired just after a `ModalDialog` is closed.
|
|
|
6114 |
*
|
|
|
6115 |
* @event ModalDialog#modalclose
|
|
|
6116 |
* @type {Event}
|
|
|
6117 |
*/
|
|
|
6118 |
this.trigger('modalclose');
|
|
|
6119 |
this.conditionalBlur_();
|
|
|
6120 |
if (this.options_.temporary) {
|
|
|
6121 |
this.dispose();
|
|
|
6122 |
}
|
|
|
6123 |
}
|
|
|
6124 |
|
|
|
6125 |
/**
|
|
|
6126 |
* Check to see if the `ModalDialog` is closeable via the UI.
|
|
|
6127 |
*
|
|
|
6128 |
* @param {boolean} [value]
|
|
|
6129 |
* If given as a boolean, it will set the `closeable` option.
|
|
|
6130 |
*
|
|
|
6131 |
* @return {boolean}
|
|
|
6132 |
* Returns the final value of the closable option.
|
|
|
6133 |
*/
|
|
|
6134 |
closeable(value) {
|
|
|
6135 |
if (typeof value === 'boolean') {
|
|
|
6136 |
const closeable = this.closeable_ = !!value;
|
|
|
6137 |
let close = this.getChild('closeButton');
|
|
|
6138 |
|
|
|
6139 |
// If this is being made closeable and has no close button, add one.
|
|
|
6140 |
if (closeable && !close) {
|
|
|
6141 |
// The close button should be a child of the modal - not its
|
|
|
6142 |
// content element, so temporarily change the content element.
|
|
|
6143 |
const temp = this.contentEl_;
|
|
|
6144 |
this.contentEl_ = this.el_;
|
|
|
6145 |
close = this.addChild('closeButton', {
|
|
|
6146 |
controlText: 'Close Modal Dialog'
|
|
|
6147 |
});
|
|
|
6148 |
this.contentEl_ = temp;
|
|
|
6149 |
this.on(close, 'close', this.close_);
|
|
|
6150 |
}
|
|
|
6151 |
|
|
|
6152 |
// If this is being made uncloseable and has a close button, remove it.
|
|
|
6153 |
if (!closeable && close) {
|
|
|
6154 |
this.off(close, 'close', this.close_);
|
|
|
6155 |
this.removeChild(close);
|
|
|
6156 |
close.dispose();
|
|
|
6157 |
}
|
|
|
6158 |
}
|
|
|
6159 |
return this.closeable_;
|
|
|
6160 |
}
|
|
|
6161 |
|
|
|
6162 |
/**
|
|
|
6163 |
* Fill the modal's content element with the modal's "content" option.
|
|
|
6164 |
* The content element will be emptied before this change takes place.
|
|
|
6165 |
*/
|
|
|
6166 |
fill() {
|
|
|
6167 |
this.fillWith(this.content());
|
|
|
6168 |
}
|
|
|
6169 |
|
|
|
6170 |
/**
|
|
|
6171 |
* Fill the modal's content element with arbitrary content.
|
|
|
6172 |
* The content element will be emptied before this change takes place.
|
|
|
6173 |
*
|
|
|
6174 |
* @fires ModalDialog#beforemodalfill
|
|
|
6175 |
* @fires ModalDialog#modalfill
|
|
|
6176 |
*
|
|
|
6177 |
* @param { import('./utils/dom').ContentDescriptor} [content]
|
|
|
6178 |
* The same rules apply to this as apply to the `content` option.
|
|
|
6179 |
*/
|
|
|
6180 |
fillWith(content) {
|
|
|
6181 |
const contentEl = this.contentEl();
|
|
|
6182 |
const parentEl = contentEl.parentNode;
|
|
|
6183 |
const nextSiblingEl = contentEl.nextSibling;
|
|
|
6184 |
|
|
|
6185 |
/**
|
|
|
6186 |
* Fired just before a `ModalDialog` is filled with content.
|
|
|
6187 |
*
|
|
|
6188 |
* @event ModalDialog#beforemodalfill
|
|
|
6189 |
* @type {Event}
|
|
|
6190 |
*/
|
|
|
6191 |
this.trigger('beforemodalfill');
|
|
|
6192 |
this.hasBeenFilled_ = true;
|
|
|
6193 |
|
|
|
6194 |
// Detach the content element from the DOM before performing
|
|
|
6195 |
// manipulation to avoid modifying the live DOM multiple times.
|
|
|
6196 |
parentEl.removeChild(contentEl);
|
|
|
6197 |
this.empty();
|
|
|
6198 |
insertContent(contentEl, content);
|
|
|
6199 |
/**
|
|
|
6200 |
* Fired just after a `ModalDialog` is filled with content.
|
|
|
6201 |
*
|
|
|
6202 |
* @event ModalDialog#modalfill
|
|
|
6203 |
* @type {Event}
|
|
|
6204 |
*/
|
|
|
6205 |
this.trigger('modalfill');
|
|
|
6206 |
|
|
|
6207 |
// Re-inject the re-filled content element.
|
|
|
6208 |
if (nextSiblingEl) {
|
|
|
6209 |
parentEl.insertBefore(contentEl, nextSiblingEl);
|
|
|
6210 |
} else {
|
|
|
6211 |
parentEl.appendChild(contentEl);
|
|
|
6212 |
}
|
|
|
6213 |
|
|
|
6214 |
// make sure that the close button is last in the dialog DOM
|
|
|
6215 |
const closeButton = this.getChild('closeButton');
|
|
|
6216 |
if (closeButton) {
|
|
|
6217 |
parentEl.appendChild(closeButton.el_);
|
|
|
6218 |
}
|
|
|
6219 |
}
|
|
|
6220 |
|
|
|
6221 |
/**
|
|
|
6222 |
* Empties the content element. This happens anytime the modal is filled.
|
|
|
6223 |
*
|
|
|
6224 |
* @fires ModalDialog#beforemodalempty
|
|
|
6225 |
* @fires ModalDialog#modalempty
|
|
|
6226 |
*/
|
|
|
6227 |
empty() {
|
|
|
6228 |
/**
|
|
|
6229 |
* Fired just before a `ModalDialog` is emptied.
|
|
|
6230 |
*
|
|
|
6231 |
* @event ModalDialog#beforemodalempty
|
|
|
6232 |
* @type {Event}
|
|
|
6233 |
*/
|
|
|
6234 |
this.trigger('beforemodalempty');
|
|
|
6235 |
emptyEl(this.contentEl());
|
|
|
6236 |
|
|
|
6237 |
/**
|
|
|
6238 |
* Fired just after a `ModalDialog` is emptied.
|
|
|
6239 |
*
|
|
|
6240 |
* @event ModalDialog#modalempty
|
|
|
6241 |
* @type {Event}
|
|
|
6242 |
*/
|
|
|
6243 |
this.trigger('modalempty');
|
|
|
6244 |
}
|
|
|
6245 |
|
|
|
6246 |
/**
|
|
|
6247 |
* Gets or sets the modal content, which gets normalized before being
|
|
|
6248 |
* rendered into the DOM.
|
|
|
6249 |
*
|
|
|
6250 |
* This does not update the DOM or fill the modal, but it is called during
|
|
|
6251 |
* that process.
|
|
|
6252 |
*
|
|
|
6253 |
* @param { import('./utils/dom').ContentDescriptor} [value]
|
|
|
6254 |
* If defined, sets the internal content value to be used on the
|
|
|
6255 |
* next call(s) to `fill`. This value is normalized before being
|
|
|
6256 |
* inserted. To "clear" the internal content value, pass `null`.
|
|
|
6257 |
*
|
|
|
6258 |
* @return { import('./utils/dom').ContentDescriptor}
|
|
|
6259 |
* The current content of the modal dialog
|
|
|
6260 |
*/
|
|
|
6261 |
content(value) {
|
|
|
6262 |
if (typeof value !== 'undefined') {
|
|
|
6263 |
this.content_ = value;
|
|
|
6264 |
}
|
|
|
6265 |
return this.content_;
|
|
|
6266 |
}
|
|
|
6267 |
|
|
|
6268 |
/**
|
|
|
6269 |
* conditionally focus the modal dialog if focus was previously on the player.
|
|
|
6270 |
*
|
|
|
6271 |
* @private
|
|
|
6272 |
*/
|
|
|
6273 |
conditionalFocus_() {
|
|
|
6274 |
const activeEl = document.activeElement;
|
|
|
6275 |
const playerEl = this.player_.el_;
|
|
|
6276 |
this.previouslyActiveEl_ = null;
|
|
|
6277 |
if (playerEl.contains(activeEl) || playerEl === activeEl) {
|
|
|
6278 |
this.previouslyActiveEl_ = activeEl;
|
|
|
6279 |
this.focus();
|
|
|
6280 |
}
|
|
|
6281 |
}
|
|
|
6282 |
|
|
|
6283 |
/**
|
|
|
6284 |
* conditionally blur the element and refocus the last focused element
|
|
|
6285 |
*
|
|
|
6286 |
* @private
|
|
|
6287 |
*/
|
|
|
6288 |
conditionalBlur_() {
|
|
|
6289 |
if (this.previouslyActiveEl_) {
|
|
|
6290 |
this.previouslyActiveEl_.focus();
|
|
|
6291 |
this.previouslyActiveEl_ = null;
|
|
|
6292 |
}
|
|
|
6293 |
}
|
|
|
6294 |
|
|
|
6295 |
/**
|
|
|
6296 |
* Keydown handler. Attached when modal is focused.
|
|
|
6297 |
*
|
|
|
6298 |
* @listens keydown
|
|
|
6299 |
*/
|
|
|
6300 |
handleKeyDown(event) {
|
|
|
6301 |
// Do not allow keydowns to reach out of the modal dialog.
|
|
|
6302 |
event.stopPropagation();
|
|
|
6303 |
if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
|
|
|
6304 |
event.preventDefault();
|
|
|
6305 |
this.close();
|
|
|
6306 |
return;
|
|
|
6307 |
}
|
|
|
6308 |
|
|
|
6309 |
// exit early if it isn't a tab key
|
|
|
6310 |
if (!keycode.isEventKey(event, 'Tab')) {
|
|
|
6311 |
return;
|
|
|
6312 |
}
|
|
|
6313 |
const focusableEls = this.focusableEls_();
|
|
|
6314 |
const activeEl = this.el_.querySelector(':focus');
|
|
|
6315 |
let focusIndex;
|
|
|
6316 |
for (let i = 0; i < focusableEls.length; i++) {
|
|
|
6317 |
if (activeEl === focusableEls[i]) {
|
|
|
6318 |
focusIndex = i;
|
|
|
6319 |
break;
|
|
|
6320 |
}
|
|
|
6321 |
}
|
|
|
6322 |
if (document.activeElement === this.el_) {
|
|
|
6323 |
focusIndex = 0;
|
|
|
6324 |
}
|
|
|
6325 |
if (event.shiftKey && focusIndex === 0) {
|
|
|
6326 |
focusableEls[focusableEls.length - 1].focus();
|
|
|
6327 |
event.preventDefault();
|
|
|
6328 |
} else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
|
|
|
6329 |
focusableEls[0].focus();
|
|
|
6330 |
event.preventDefault();
|
|
|
6331 |
}
|
|
|
6332 |
}
|
|
|
6333 |
|
|
|
6334 |
/**
|
|
|
6335 |
* get all focusable elements
|
|
|
6336 |
*
|
|
|
6337 |
* @private
|
|
|
6338 |
*/
|
|
|
6339 |
focusableEls_() {
|
|
|
6340 |
const allChildren = this.el_.querySelectorAll('*');
|
|
|
6341 |
return Array.prototype.filter.call(allChildren, child => {
|
|
|
6342 |
return (child instanceof window.HTMLAnchorElement || child instanceof window.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window.HTMLInputElement || child instanceof window.HTMLSelectElement || child instanceof window.HTMLTextAreaElement || child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window.HTMLIFrameElement || child instanceof window.HTMLObjectElement || child instanceof window.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
|
|
|
6343 |
});
|
|
|
6344 |
}
|
|
|
6345 |
}
|
|
|
6346 |
|
|
|
6347 |
/**
|
|
|
6348 |
* Default options for `ModalDialog` default options.
|
|
|
6349 |
*
|
|
|
6350 |
* @type {Object}
|
|
|
6351 |
* @private
|
|
|
6352 |
*/
|
|
|
6353 |
ModalDialog.prototype.options_ = {
|
|
|
6354 |
pauseOnOpen: true,
|
|
|
6355 |
temporary: true
|
|
|
6356 |
};
|
|
|
6357 |
Component$1.registerComponent('ModalDialog', ModalDialog);
|
|
|
6358 |
|
|
|
6359 |
/**
|
|
|
6360 |
* @file track-list.js
|
|
|
6361 |
*/
|
|
|
6362 |
|
|
|
6363 |
/**
|
|
|
6364 |
* Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
|
|
|
6365 |
* {@link VideoTrackList}
|
|
|
6366 |
*
|
|
|
6367 |
* @extends EventTarget
|
|
|
6368 |
*/
|
|
|
6369 |
class TrackList extends EventTarget$2 {
|
|
|
6370 |
/**
|
|
|
6371 |
* Create an instance of this class
|
|
|
6372 |
*
|
|
|
6373 |
* @param { import('./track').default[] } tracks
|
|
|
6374 |
* A list of tracks to initialize the list with.
|
|
|
6375 |
*
|
|
|
6376 |
* @abstract
|
|
|
6377 |
*/
|
|
|
6378 |
constructor(tracks = []) {
|
|
|
6379 |
super();
|
|
|
6380 |
this.tracks_ = [];
|
|
|
6381 |
|
|
|
6382 |
/**
|
|
|
6383 |
* @memberof TrackList
|
|
|
6384 |
* @member {number} length
|
|
|
6385 |
* The current number of `Track`s in the this Trackist.
|
|
|
6386 |
* @instance
|
|
|
6387 |
*/
|
|
|
6388 |
Object.defineProperty(this, 'length', {
|
|
|
6389 |
get() {
|
|
|
6390 |
return this.tracks_.length;
|
|
|
6391 |
}
|
|
|
6392 |
});
|
|
|
6393 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
6394 |
this.addTrack(tracks[i]);
|
|
|
6395 |
}
|
|
|
6396 |
}
|
|
|
6397 |
|
|
|
6398 |
/**
|
|
|
6399 |
* Add a {@link Track} to the `TrackList`
|
|
|
6400 |
*
|
|
|
6401 |
* @param { import('./track').default } track
|
|
|
6402 |
* The audio, video, or text track to add to the list.
|
|
|
6403 |
*
|
|
|
6404 |
* @fires TrackList#addtrack
|
|
|
6405 |
*/
|
|
|
6406 |
addTrack(track) {
|
|
|
6407 |
const index = this.tracks_.length;
|
|
|
6408 |
if (!('' + index in this)) {
|
|
|
6409 |
Object.defineProperty(this, index, {
|
|
|
6410 |
get() {
|
|
|
6411 |
return this.tracks_[index];
|
|
|
6412 |
}
|
|
|
6413 |
});
|
|
|
6414 |
}
|
|
|
6415 |
|
|
|
6416 |
// Do not add duplicate tracks
|
|
|
6417 |
if (this.tracks_.indexOf(track) === -1) {
|
|
|
6418 |
this.tracks_.push(track);
|
|
|
6419 |
/**
|
|
|
6420 |
* Triggered when a track is added to a track list.
|
|
|
6421 |
*
|
|
|
6422 |
* @event TrackList#addtrack
|
|
|
6423 |
* @type {Event}
|
|
|
6424 |
* @property {Track} track
|
|
|
6425 |
* A reference to track that was added.
|
|
|
6426 |
*/
|
|
|
6427 |
this.trigger({
|
|
|
6428 |
track,
|
|
|
6429 |
type: 'addtrack',
|
|
|
6430 |
target: this
|
|
|
6431 |
});
|
|
|
6432 |
}
|
|
|
6433 |
|
|
|
6434 |
/**
|
|
|
6435 |
* Triggered when a track label is changed.
|
|
|
6436 |
*
|
|
|
6437 |
* @event TrackList#addtrack
|
|
|
6438 |
* @type {Event}
|
|
|
6439 |
* @property {Track} track
|
|
|
6440 |
* A reference to track that was added.
|
|
|
6441 |
*/
|
|
|
6442 |
track.labelchange_ = () => {
|
|
|
6443 |
this.trigger({
|
|
|
6444 |
track,
|
|
|
6445 |
type: 'labelchange',
|
|
|
6446 |
target: this
|
|
|
6447 |
});
|
|
|
6448 |
};
|
|
|
6449 |
if (isEvented(track)) {
|
|
|
6450 |
track.addEventListener('labelchange', track.labelchange_);
|
|
|
6451 |
}
|
|
|
6452 |
}
|
|
|
6453 |
|
|
|
6454 |
/**
|
|
|
6455 |
* Remove a {@link Track} from the `TrackList`
|
|
|
6456 |
*
|
|
|
6457 |
* @param { import('./track').default } rtrack
|
|
|
6458 |
* The audio, video, or text track to remove from the list.
|
|
|
6459 |
*
|
|
|
6460 |
* @fires TrackList#removetrack
|
|
|
6461 |
*/
|
|
|
6462 |
removeTrack(rtrack) {
|
|
|
6463 |
let track;
|
|
|
6464 |
for (let i = 0, l = this.length; i < l; i++) {
|
|
|
6465 |
if (this[i] === rtrack) {
|
|
|
6466 |
track = this[i];
|
|
|
6467 |
if (track.off) {
|
|
|
6468 |
track.off();
|
|
|
6469 |
}
|
|
|
6470 |
this.tracks_.splice(i, 1);
|
|
|
6471 |
break;
|
|
|
6472 |
}
|
|
|
6473 |
}
|
|
|
6474 |
if (!track) {
|
|
|
6475 |
return;
|
|
|
6476 |
}
|
|
|
6477 |
|
|
|
6478 |
/**
|
|
|
6479 |
* Triggered when a track is removed from track list.
|
|
|
6480 |
*
|
|
|
6481 |
* @event TrackList#removetrack
|
|
|
6482 |
* @type {Event}
|
|
|
6483 |
* @property {Track} track
|
|
|
6484 |
* A reference to track that was removed.
|
|
|
6485 |
*/
|
|
|
6486 |
this.trigger({
|
|
|
6487 |
track,
|
|
|
6488 |
type: 'removetrack',
|
|
|
6489 |
target: this
|
|
|
6490 |
});
|
|
|
6491 |
}
|
|
|
6492 |
|
|
|
6493 |
/**
|
|
|
6494 |
* Get a Track from the TrackList by a tracks id
|
|
|
6495 |
*
|
|
|
6496 |
* @param {string} id - the id of the track to get
|
|
|
6497 |
* @method getTrackById
|
|
|
6498 |
* @return { import('./track').default }
|
|
|
6499 |
* @private
|
|
|
6500 |
*/
|
|
|
6501 |
getTrackById(id) {
|
|
|
6502 |
let result = null;
|
|
|
6503 |
for (let i = 0, l = this.length; i < l; i++) {
|
|
|
6504 |
const track = this[i];
|
|
|
6505 |
if (track.id === id) {
|
|
|
6506 |
result = track;
|
|
|
6507 |
break;
|
|
|
6508 |
}
|
|
|
6509 |
}
|
|
|
6510 |
return result;
|
|
|
6511 |
}
|
|
|
6512 |
}
|
|
|
6513 |
|
|
|
6514 |
/**
|
|
|
6515 |
* Triggered when a different track is selected/enabled.
|
|
|
6516 |
*
|
|
|
6517 |
* @event TrackList#change
|
|
|
6518 |
* @type {Event}
|
|
|
6519 |
*/
|
|
|
6520 |
|
|
|
6521 |
/**
|
|
|
6522 |
* Events that can be called with on + eventName. See {@link EventHandler}.
|
|
|
6523 |
*
|
|
|
6524 |
* @property {Object} TrackList#allowedEvents_
|
|
|
6525 |
* @protected
|
|
|
6526 |
*/
|
|
|
6527 |
TrackList.prototype.allowedEvents_ = {
|
|
|
6528 |
change: 'change',
|
|
|
6529 |
addtrack: 'addtrack',
|
|
|
6530 |
removetrack: 'removetrack',
|
|
|
6531 |
labelchange: 'labelchange'
|
|
|
6532 |
};
|
|
|
6533 |
|
|
|
6534 |
// emulate attribute EventHandler support to allow for feature detection
|
|
|
6535 |
for (const event in TrackList.prototype.allowedEvents_) {
|
|
|
6536 |
TrackList.prototype['on' + event] = null;
|
|
|
6537 |
}
|
|
|
6538 |
|
|
|
6539 |
/**
|
|
|
6540 |
* @file audio-track-list.js
|
|
|
6541 |
*/
|
|
|
6542 |
|
|
|
6543 |
/**
|
|
|
6544 |
* Anywhere we call this function we diverge from the spec
|
|
|
6545 |
* as we only support one enabled audiotrack at a time
|
|
|
6546 |
*
|
|
|
6547 |
* @param {AudioTrackList} list
|
|
|
6548 |
* list to work on
|
|
|
6549 |
*
|
|
|
6550 |
* @param { import('./audio-track').default } track
|
|
|
6551 |
* The track to skip
|
|
|
6552 |
*
|
|
|
6553 |
* @private
|
|
|
6554 |
*/
|
|
|
6555 |
const disableOthers$1 = function (list, track) {
|
|
|
6556 |
for (let i = 0; i < list.length; i++) {
|
|
|
6557 |
if (!Object.keys(list[i]).length || track.id === list[i].id) {
|
|
|
6558 |
continue;
|
|
|
6559 |
}
|
|
|
6560 |
// another audio track is enabled, disable it
|
|
|
6561 |
list[i].enabled = false;
|
|
|
6562 |
}
|
|
|
6563 |
};
|
|
|
6564 |
|
|
|
6565 |
/**
|
|
|
6566 |
* The current list of {@link AudioTrack} for a media file.
|
|
|
6567 |
*
|
|
|
6568 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
|
|
|
6569 |
* @extends TrackList
|
|
|
6570 |
*/
|
|
|
6571 |
class AudioTrackList extends TrackList {
|
|
|
6572 |
/**
|
|
|
6573 |
* Create an instance of this class.
|
|
|
6574 |
*
|
|
|
6575 |
* @param { import('./audio-track').default[] } [tracks=[]]
|
|
|
6576 |
* A list of `AudioTrack` to instantiate the list with.
|
|
|
6577 |
*/
|
|
|
6578 |
constructor(tracks = []) {
|
|
|
6579 |
// make sure only 1 track is enabled
|
|
|
6580 |
// sorted from last index to first index
|
|
|
6581 |
for (let i = tracks.length - 1; i >= 0; i--) {
|
|
|
6582 |
if (tracks[i].enabled) {
|
|
|
6583 |
disableOthers$1(tracks, tracks[i]);
|
|
|
6584 |
break;
|
|
|
6585 |
}
|
|
|
6586 |
}
|
|
|
6587 |
super(tracks);
|
|
|
6588 |
this.changing_ = false;
|
|
|
6589 |
}
|
|
|
6590 |
|
|
|
6591 |
/**
|
|
|
6592 |
* Add an {@link AudioTrack} to the `AudioTrackList`.
|
|
|
6593 |
*
|
|
|
6594 |
* @param { import('./audio-track').default } track
|
|
|
6595 |
* The AudioTrack to add to the list
|
|
|
6596 |
*
|
|
|
6597 |
* @fires TrackList#addtrack
|
|
|
6598 |
*/
|
|
|
6599 |
addTrack(track) {
|
|
|
6600 |
if (track.enabled) {
|
|
|
6601 |
disableOthers$1(this, track);
|
|
|
6602 |
}
|
|
|
6603 |
super.addTrack(track);
|
|
|
6604 |
// native tracks don't have this
|
|
|
6605 |
if (!track.addEventListener) {
|
|
|
6606 |
return;
|
|
|
6607 |
}
|
|
|
6608 |
track.enabledChange_ = () => {
|
|
|
6609 |
// when we are disabling other tracks (since we don't support
|
|
|
6610 |
// more than one track at a time) we will set changing_
|
|
|
6611 |
// to true so that we don't trigger additional change events
|
|
|
6612 |
if (this.changing_) {
|
|
|
6613 |
return;
|
|
|
6614 |
}
|
|
|
6615 |
this.changing_ = true;
|
|
|
6616 |
disableOthers$1(this, track);
|
|
|
6617 |
this.changing_ = false;
|
|
|
6618 |
this.trigger('change');
|
|
|
6619 |
};
|
|
|
6620 |
|
|
|
6621 |
/**
|
|
|
6622 |
* @listens AudioTrack#enabledchange
|
|
|
6623 |
* @fires TrackList#change
|
|
|
6624 |
*/
|
|
|
6625 |
track.addEventListener('enabledchange', track.enabledChange_);
|
|
|
6626 |
}
|
|
|
6627 |
removeTrack(rtrack) {
|
|
|
6628 |
super.removeTrack(rtrack);
|
|
|
6629 |
if (rtrack.removeEventListener && rtrack.enabledChange_) {
|
|
|
6630 |
rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
|
|
|
6631 |
rtrack.enabledChange_ = null;
|
|
|
6632 |
}
|
|
|
6633 |
}
|
|
|
6634 |
}
|
|
|
6635 |
|
|
|
6636 |
/**
|
|
|
6637 |
* @file video-track-list.js
|
|
|
6638 |
*/
|
|
|
6639 |
|
|
|
6640 |
/**
|
|
|
6641 |
* Un-select all other {@link VideoTrack}s that are selected.
|
|
|
6642 |
*
|
|
|
6643 |
* @param {VideoTrackList} list
|
|
|
6644 |
* list to work on
|
|
|
6645 |
*
|
|
|
6646 |
* @param { import('./video-track').default } track
|
|
|
6647 |
* The track to skip
|
|
|
6648 |
*
|
|
|
6649 |
* @private
|
|
|
6650 |
*/
|
|
|
6651 |
const disableOthers = function (list, track) {
|
|
|
6652 |
for (let i = 0; i < list.length; i++) {
|
|
|
6653 |
if (!Object.keys(list[i]).length || track.id === list[i].id) {
|
|
|
6654 |
continue;
|
|
|
6655 |
}
|
|
|
6656 |
// another video track is enabled, disable it
|
|
|
6657 |
list[i].selected = false;
|
|
|
6658 |
}
|
|
|
6659 |
};
|
|
|
6660 |
|
|
|
6661 |
/**
|
|
|
6662 |
* The current list of {@link VideoTrack} for a video.
|
|
|
6663 |
*
|
|
|
6664 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
|
|
|
6665 |
* @extends TrackList
|
|
|
6666 |
*/
|
|
|
6667 |
class VideoTrackList extends TrackList {
|
|
|
6668 |
/**
|
|
|
6669 |
* Create an instance of this class.
|
|
|
6670 |
*
|
|
|
6671 |
* @param {VideoTrack[]} [tracks=[]]
|
|
|
6672 |
* A list of `VideoTrack` to instantiate the list with.
|
|
|
6673 |
*/
|
|
|
6674 |
constructor(tracks = []) {
|
|
|
6675 |
// make sure only 1 track is enabled
|
|
|
6676 |
// sorted from last index to first index
|
|
|
6677 |
for (let i = tracks.length - 1; i >= 0; i--) {
|
|
|
6678 |
if (tracks[i].selected) {
|
|
|
6679 |
disableOthers(tracks, tracks[i]);
|
|
|
6680 |
break;
|
|
|
6681 |
}
|
|
|
6682 |
}
|
|
|
6683 |
super(tracks);
|
|
|
6684 |
this.changing_ = false;
|
|
|
6685 |
|
|
|
6686 |
/**
|
|
|
6687 |
* @member {number} VideoTrackList#selectedIndex
|
|
|
6688 |
* The current index of the selected {@link VideoTrack`}.
|
|
|
6689 |
*/
|
|
|
6690 |
Object.defineProperty(this, 'selectedIndex', {
|
|
|
6691 |
get() {
|
|
|
6692 |
for (let i = 0; i < this.length; i++) {
|
|
|
6693 |
if (this[i].selected) {
|
|
|
6694 |
return i;
|
|
|
6695 |
}
|
|
|
6696 |
}
|
|
|
6697 |
return -1;
|
|
|
6698 |
},
|
|
|
6699 |
set() {}
|
|
|
6700 |
});
|
|
|
6701 |
}
|
|
|
6702 |
|
|
|
6703 |
/**
|
|
|
6704 |
* Add a {@link VideoTrack} to the `VideoTrackList`.
|
|
|
6705 |
*
|
|
|
6706 |
* @param { import('./video-track').default } track
|
|
|
6707 |
* The VideoTrack to add to the list
|
|
|
6708 |
*
|
|
|
6709 |
* @fires TrackList#addtrack
|
|
|
6710 |
*/
|
|
|
6711 |
addTrack(track) {
|
|
|
6712 |
if (track.selected) {
|
|
|
6713 |
disableOthers(this, track);
|
|
|
6714 |
}
|
|
|
6715 |
super.addTrack(track);
|
|
|
6716 |
// native tracks don't have this
|
|
|
6717 |
if (!track.addEventListener) {
|
|
|
6718 |
return;
|
|
|
6719 |
}
|
|
|
6720 |
track.selectedChange_ = () => {
|
|
|
6721 |
if (this.changing_) {
|
|
|
6722 |
return;
|
|
|
6723 |
}
|
|
|
6724 |
this.changing_ = true;
|
|
|
6725 |
disableOthers(this, track);
|
|
|
6726 |
this.changing_ = false;
|
|
|
6727 |
this.trigger('change');
|
|
|
6728 |
};
|
|
|
6729 |
|
|
|
6730 |
/**
|
|
|
6731 |
* @listens VideoTrack#selectedchange
|
|
|
6732 |
* @fires TrackList#change
|
|
|
6733 |
*/
|
|
|
6734 |
track.addEventListener('selectedchange', track.selectedChange_);
|
|
|
6735 |
}
|
|
|
6736 |
removeTrack(rtrack) {
|
|
|
6737 |
super.removeTrack(rtrack);
|
|
|
6738 |
if (rtrack.removeEventListener && rtrack.selectedChange_) {
|
|
|
6739 |
rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
|
|
|
6740 |
rtrack.selectedChange_ = null;
|
|
|
6741 |
}
|
|
|
6742 |
}
|
|
|
6743 |
}
|
|
|
6744 |
|
|
|
6745 |
/**
|
|
|
6746 |
* @file text-track-list.js
|
|
|
6747 |
*/
|
|
|
6748 |
|
|
|
6749 |
/**
|
|
|
6750 |
* The current list of {@link TextTrack} for a media file.
|
|
|
6751 |
*
|
|
|
6752 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
|
|
|
6753 |
* @extends TrackList
|
|
|
6754 |
*/
|
|
|
6755 |
class TextTrackList extends TrackList {
|
|
|
6756 |
/**
|
|
|
6757 |
* Add a {@link TextTrack} to the `TextTrackList`
|
|
|
6758 |
*
|
|
|
6759 |
* @param { import('./text-track').default } track
|
|
|
6760 |
* The text track to add to the list.
|
|
|
6761 |
*
|
|
|
6762 |
* @fires TrackList#addtrack
|
|
|
6763 |
*/
|
|
|
6764 |
addTrack(track) {
|
|
|
6765 |
super.addTrack(track);
|
|
|
6766 |
if (!this.queueChange_) {
|
|
|
6767 |
this.queueChange_ = () => this.queueTrigger('change');
|
|
|
6768 |
}
|
|
|
6769 |
if (!this.triggerSelectedlanguagechange) {
|
|
|
6770 |
this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
|
|
|
6771 |
}
|
|
|
6772 |
|
|
|
6773 |
/**
|
|
|
6774 |
* @listens TextTrack#modechange
|
|
|
6775 |
* @fires TrackList#change
|
|
|
6776 |
*/
|
|
|
6777 |
track.addEventListener('modechange', this.queueChange_);
|
|
|
6778 |
const nonLanguageTextTrackKind = ['metadata', 'chapters'];
|
|
|
6779 |
if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
|
|
|
6780 |
track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
|
|
|
6781 |
}
|
|
|
6782 |
}
|
|
|
6783 |
removeTrack(rtrack) {
|
|
|
6784 |
super.removeTrack(rtrack);
|
|
|
6785 |
|
|
|
6786 |
// manually remove the event handlers we added
|
|
|
6787 |
if (rtrack.removeEventListener) {
|
|
|
6788 |
if (this.queueChange_) {
|
|
|
6789 |
rtrack.removeEventListener('modechange', this.queueChange_);
|
|
|
6790 |
}
|
|
|
6791 |
if (this.selectedlanguagechange_) {
|
|
|
6792 |
rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
|
|
|
6793 |
}
|
|
|
6794 |
}
|
|
|
6795 |
}
|
|
|
6796 |
}
|
|
|
6797 |
|
|
|
6798 |
/**
|
|
|
6799 |
* @file html-track-element-list.js
|
|
|
6800 |
*/
|
|
|
6801 |
|
|
|
6802 |
/**
|
|
|
6803 |
* The current list of {@link HtmlTrackElement}s.
|
|
|
6804 |
*/
|
|
|
6805 |
class HtmlTrackElementList {
|
|
|
6806 |
/**
|
|
|
6807 |
* Create an instance of this class.
|
|
|
6808 |
*
|
|
|
6809 |
* @param {HtmlTrackElement[]} [tracks=[]]
|
|
|
6810 |
* A list of `HtmlTrackElement` to instantiate the list with.
|
|
|
6811 |
*/
|
|
|
6812 |
constructor(trackElements = []) {
|
|
|
6813 |
this.trackElements_ = [];
|
|
|
6814 |
|
|
|
6815 |
/**
|
|
|
6816 |
* @memberof HtmlTrackElementList
|
|
|
6817 |
* @member {number} length
|
|
|
6818 |
* The current number of `Track`s in the this Trackist.
|
|
|
6819 |
* @instance
|
|
|
6820 |
*/
|
|
|
6821 |
Object.defineProperty(this, 'length', {
|
|
|
6822 |
get() {
|
|
|
6823 |
return this.trackElements_.length;
|
|
|
6824 |
}
|
|
|
6825 |
});
|
|
|
6826 |
for (let i = 0, length = trackElements.length; i < length; i++) {
|
|
|
6827 |
this.addTrackElement_(trackElements[i]);
|
|
|
6828 |
}
|
|
|
6829 |
}
|
|
|
6830 |
|
|
|
6831 |
/**
|
|
|
6832 |
* Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
|
|
|
6833 |
*
|
|
|
6834 |
* @param {HtmlTrackElement} trackElement
|
|
|
6835 |
* The track element to add to the list.
|
|
|
6836 |
*
|
|
|
6837 |
* @private
|
|
|
6838 |
*/
|
|
|
6839 |
addTrackElement_(trackElement) {
|
|
|
6840 |
const index = this.trackElements_.length;
|
|
|
6841 |
if (!('' + index in this)) {
|
|
|
6842 |
Object.defineProperty(this, index, {
|
|
|
6843 |
get() {
|
|
|
6844 |
return this.trackElements_[index];
|
|
|
6845 |
}
|
|
|
6846 |
});
|
|
|
6847 |
}
|
|
|
6848 |
|
|
|
6849 |
// Do not add duplicate elements
|
|
|
6850 |
if (this.trackElements_.indexOf(trackElement) === -1) {
|
|
|
6851 |
this.trackElements_.push(trackElement);
|
|
|
6852 |
}
|
|
|
6853 |
}
|
|
|
6854 |
|
|
|
6855 |
/**
|
|
|
6856 |
* Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
|
|
|
6857 |
* {@link TextTrack}.
|
|
|
6858 |
*
|
|
|
6859 |
* @param {TextTrack} track
|
|
|
6860 |
* The track associated with a track element.
|
|
|
6861 |
*
|
|
|
6862 |
* @return {HtmlTrackElement|undefined}
|
|
|
6863 |
* The track element that was found or undefined.
|
|
|
6864 |
*
|
|
|
6865 |
* @private
|
|
|
6866 |
*/
|
|
|
6867 |
getTrackElementByTrack_(track) {
|
|
|
6868 |
let trackElement_;
|
|
|
6869 |
for (let i = 0, length = this.trackElements_.length; i < length; i++) {
|
|
|
6870 |
if (track === this.trackElements_[i].track) {
|
|
|
6871 |
trackElement_ = this.trackElements_[i];
|
|
|
6872 |
break;
|
|
|
6873 |
}
|
|
|
6874 |
}
|
|
|
6875 |
return trackElement_;
|
|
|
6876 |
}
|
|
|
6877 |
|
|
|
6878 |
/**
|
|
|
6879 |
* Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
|
|
|
6880 |
*
|
|
|
6881 |
* @param {HtmlTrackElement} trackElement
|
|
|
6882 |
* The track element to remove from the list.
|
|
|
6883 |
*
|
|
|
6884 |
* @private
|
|
|
6885 |
*/
|
|
|
6886 |
removeTrackElement_(trackElement) {
|
|
|
6887 |
for (let i = 0, length = this.trackElements_.length; i < length; i++) {
|
|
|
6888 |
if (trackElement === this.trackElements_[i]) {
|
|
|
6889 |
if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
|
|
|
6890 |
this.trackElements_[i].track.off();
|
|
|
6891 |
}
|
|
|
6892 |
if (typeof this.trackElements_[i].off === 'function') {
|
|
|
6893 |
this.trackElements_[i].off();
|
|
|
6894 |
}
|
|
|
6895 |
this.trackElements_.splice(i, 1);
|
|
|
6896 |
break;
|
|
|
6897 |
}
|
|
|
6898 |
}
|
|
|
6899 |
}
|
|
|
6900 |
}
|
|
|
6901 |
|
|
|
6902 |
/**
|
|
|
6903 |
* @file text-track-cue-list.js
|
|
|
6904 |
*/
|
|
|
6905 |
|
|
|
6906 |
/**
|
|
|
6907 |
* @typedef {Object} TextTrackCueList~TextTrackCue
|
|
|
6908 |
*
|
|
|
6909 |
* @property {string} id
|
|
|
6910 |
* The unique id for this text track cue
|
|
|
6911 |
*
|
|
|
6912 |
* @property {number} startTime
|
|
|
6913 |
* The start time for this text track cue
|
|
|
6914 |
*
|
|
|
6915 |
* @property {number} endTime
|
|
|
6916 |
* The end time for this text track cue
|
|
|
6917 |
*
|
|
|
6918 |
* @property {boolean} pauseOnExit
|
|
|
6919 |
* Pause when the end time is reached if true.
|
|
|
6920 |
*
|
|
|
6921 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
|
|
|
6922 |
*/
|
|
|
6923 |
|
|
|
6924 |
/**
|
|
|
6925 |
* A List of TextTrackCues.
|
|
|
6926 |
*
|
|
|
6927 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
|
|
|
6928 |
*/
|
|
|
6929 |
class TextTrackCueList {
|
|
|
6930 |
/**
|
|
|
6931 |
* Create an instance of this class..
|
|
|
6932 |
*
|
|
|
6933 |
* @param {Array} cues
|
|
|
6934 |
* A list of cues to be initialized with
|
|
|
6935 |
*/
|
|
|
6936 |
constructor(cues) {
|
|
|
6937 |
TextTrackCueList.prototype.setCues_.call(this, cues);
|
|
|
6938 |
|
|
|
6939 |
/**
|
|
|
6940 |
* @memberof TextTrackCueList
|
|
|
6941 |
* @member {number} length
|
|
|
6942 |
* The current number of `TextTrackCue`s in the TextTrackCueList.
|
|
|
6943 |
* @instance
|
|
|
6944 |
*/
|
|
|
6945 |
Object.defineProperty(this, 'length', {
|
|
|
6946 |
get() {
|
|
|
6947 |
return this.length_;
|
|
|
6948 |
}
|
|
|
6949 |
});
|
|
|
6950 |
}
|
|
|
6951 |
|
|
|
6952 |
/**
|
|
|
6953 |
* A setter for cues in this list. Creates getters
|
|
|
6954 |
* an an index for the cues.
|
|
|
6955 |
*
|
|
|
6956 |
* @param {Array} cues
|
|
|
6957 |
* An array of cues to set
|
|
|
6958 |
*
|
|
|
6959 |
* @private
|
|
|
6960 |
*/
|
|
|
6961 |
setCues_(cues) {
|
|
|
6962 |
const oldLength = this.length || 0;
|
|
|
6963 |
let i = 0;
|
|
|
6964 |
const l = cues.length;
|
|
|
6965 |
this.cues_ = cues;
|
|
|
6966 |
this.length_ = cues.length;
|
|
|
6967 |
const defineProp = function (index) {
|
|
|
6968 |
if (!('' + index in this)) {
|
|
|
6969 |
Object.defineProperty(this, '' + index, {
|
|
|
6970 |
get() {
|
|
|
6971 |
return this.cues_[index];
|
|
|
6972 |
}
|
|
|
6973 |
});
|
|
|
6974 |
}
|
|
|
6975 |
};
|
|
|
6976 |
if (oldLength < l) {
|
|
|
6977 |
i = oldLength;
|
|
|
6978 |
for (; i < l; i++) {
|
|
|
6979 |
defineProp.call(this, i);
|
|
|
6980 |
}
|
|
|
6981 |
}
|
|
|
6982 |
}
|
|
|
6983 |
|
|
|
6984 |
/**
|
|
|
6985 |
* Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
|
|
|
6986 |
*
|
|
|
6987 |
* @param {string} id
|
|
|
6988 |
* The id of the cue that should be searched for.
|
|
|
6989 |
*
|
|
|
6990 |
* @return {TextTrackCueList~TextTrackCue|null}
|
|
|
6991 |
* A single cue or null if none was found.
|
|
|
6992 |
*/
|
|
|
6993 |
getCueById(id) {
|
|
|
6994 |
let result = null;
|
|
|
6995 |
for (let i = 0, l = this.length; i < l; i++) {
|
|
|
6996 |
const cue = this[i];
|
|
|
6997 |
if (cue.id === id) {
|
|
|
6998 |
result = cue;
|
|
|
6999 |
break;
|
|
|
7000 |
}
|
|
|
7001 |
}
|
|
|
7002 |
return result;
|
|
|
7003 |
}
|
|
|
7004 |
}
|
|
|
7005 |
|
|
|
7006 |
/**
|
|
|
7007 |
* @file track-kinds.js
|
|
|
7008 |
*/
|
|
|
7009 |
|
|
|
7010 |
/**
|
|
|
7011 |
* All possible `VideoTrackKind`s
|
|
|
7012 |
*
|
|
|
7013 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
|
|
|
7014 |
* @typedef VideoTrack~Kind
|
|
|
7015 |
* @enum
|
|
|
7016 |
*/
|
|
|
7017 |
const VideoTrackKind = {
|
|
|
7018 |
alternative: 'alternative',
|
|
|
7019 |
captions: 'captions',
|
|
|
7020 |
main: 'main',
|
|
|
7021 |
sign: 'sign',
|
|
|
7022 |
subtitles: 'subtitles',
|
|
|
7023 |
commentary: 'commentary'
|
|
|
7024 |
};
|
|
|
7025 |
|
|
|
7026 |
/**
|
|
|
7027 |
* All possible `AudioTrackKind`s
|
|
|
7028 |
*
|
|
|
7029 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
|
|
|
7030 |
* @typedef AudioTrack~Kind
|
|
|
7031 |
* @enum
|
|
|
7032 |
*/
|
|
|
7033 |
const AudioTrackKind = {
|
|
|
7034 |
'alternative': 'alternative',
|
|
|
7035 |
'descriptions': 'descriptions',
|
|
|
7036 |
'main': 'main',
|
|
|
7037 |
'main-desc': 'main-desc',
|
|
|
7038 |
'translation': 'translation',
|
|
|
7039 |
'commentary': 'commentary'
|
|
|
7040 |
};
|
|
|
7041 |
|
|
|
7042 |
/**
|
|
|
7043 |
* All possible `TextTrackKind`s
|
|
|
7044 |
*
|
|
|
7045 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
|
|
|
7046 |
* @typedef TextTrack~Kind
|
|
|
7047 |
* @enum
|
|
|
7048 |
*/
|
|
|
7049 |
const TextTrackKind = {
|
|
|
7050 |
subtitles: 'subtitles',
|
|
|
7051 |
captions: 'captions',
|
|
|
7052 |
descriptions: 'descriptions',
|
|
|
7053 |
chapters: 'chapters',
|
|
|
7054 |
metadata: 'metadata'
|
|
|
7055 |
};
|
|
|
7056 |
|
|
|
7057 |
/**
|
|
|
7058 |
* All possible `TextTrackMode`s
|
|
|
7059 |
*
|
|
|
7060 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
|
|
|
7061 |
* @typedef TextTrack~Mode
|
|
|
7062 |
* @enum
|
|
|
7063 |
*/
|
|
|
7064 |
const TextTrackMode = {
|
|
|
7065 |
disabled: 'disabled',
|
|
|
7066 |
hidden: 'hidden',
|
|
|
7067 |
showing: 'showing'
|
|
|
7068 |
};
|
|
|
7069 |
|
|
|
7070 |
/**
|
|
|
7071 |
* @file track.js
|
|
|
7072 |
*/
|
|
|
7073 |
|
|
|
7074 |
/**
|
|
|
7075 |
* A Track class that contains all of the common functionality for {@link AudioTrack},
|
|
|
7076 |
* {@link VideoTrack}, and {@link TextTrack}.
|
|
|
7077 |
*
|
|
|
7078 |
* > Note: This class should not be used directly
|
|
|
7079 |
*
|
|
|
7080 |
* @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
|
|
|
7081 |
* @extends EventTarget
|
|
|
7082 |
* @abstract
|
|
|
7083 |
*/
|
|
|
7084 |
class Track extends EventTarget$2 {
|
|
|
7085 |
/**
|
|
|
7086 |
* Create an instance of this class.
|
|
|
7087 |
*
|
|
|
7088 |
* @param {Object} [options={}]
|
|
|
7089 |
* Object of option names and values
|
|
|
7090 |
*
|
|
|
7091 |
* @param {string} [options.kind='']
|
|
|
7092 |
* A valid kind for the track type you are creating.
|
|
|
7093 |
*
|
|
|
7094 |
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
|
|
|
7095 |
* A unique id for this AudioTrack.
|
|
|
7096 |
*
|
|
|
7097 |
* @param {string} [options.label='']
|
|
|
7098 |
* The menu label for this track.
|
|
|
7099 |
*
|
|
|
7100 |
* @param {string} [options.language='']
|
|
|
7101 |
* A valid two character language code.
|
|
|
7102 |
*
|
|
|
7103 |
* @abstract
|
|
|
7104 |
*/
|
|
|
7105 |
constructor(options = {}) {
|
|
|
7106 |
super();
|
|
|
7107 |
const trackProps = {
|
|
|
7108 |
id: options.id || 'vjs_track_' + newGUID(),
|
|
|
7109 |
kind: options.kind || '',
|
|
|
7110 |
language: options.language || ''
|
|
|
7111 |
};
|
|
|
7112 |
let label = options.label || '';
|
|
|
7113 |
|
|
|
7114 |
/**
|
|
|
7115 |
* @memberof Track
|
|
|
7116 |
* @member {string} id
|
|
|
7117 |
* The id of this track. Cannot be changed after creation.
|
|
|
7118 |
* @instance
|
|
|
7119 |
*
|
|
|
7120 |
* @readonly
|
|
|
7121 |
*/
|
|
|
7122 |
|
|
|
7123 |
/**
|
|
|
7124 |
* @memberof Track
|
|
|
7125 |
* @member {string} kind
|
|
|
7126 |
* The kind of track that this is. Cannot be changed after creation.
|
|
|
7127 |
* @instance
|
|
|
7128 |
*
|
|
|
7129 |
* @readonly
|
|
|
7130 |
*/
|
|
|
7131 |
|
|
|
7132 |
/**
|
|
|
7133 |
* @memberof Track
|
|
|
7134 |
* @member {string} language
|
|
|
7135 |
* The two letter language code for this track. Cannot be changed after
|
|
|
7136 |
* creation.
|
|
|
7137 |
* @instance
|
|
|
7138 |
*
|
|
|
7139 |
* @readonly
|
|
|
7140 |
*/
|
|
|
7141 |
|
|
|
7142 |
for (const key in trackProps) {
|
|
|
7143 |
Object.defineProperty(this, key, {
|
|
|
7144 |
get() {
|
|
|
7145 |
return trackProps[key];
|
|
|
7146 |
},
|
|
|
7147 |
set() {}
|
|
|
7148 |
});
|
|
|
7149 |
}
|
|
|
7150 |
|
|
|
7151 |
/**
|
|
|
7152 |
* @memberof Track
|
|
|
7153 |
* @member {string} label
|
|
|
7154 |
* The label of this track. Cannot be changed after creation.
|
|
|
7155 |
* @instance
|
|
|
7156 |
*
|
|
|
7157 |
* @fires Track#labelchange
|
|
|
7158 |
*/
|
|
|
7159 |
Object.defineProperty(this, 'label', {
|
|
|
7160 |
get() {
|
|
|
7161 |
return label;
|
|
|
7162 |
},
|
|
|
7163 |
set(newLabel) {
|
|
|
7164 |
if (newLabel !== label) {
|
|
|
7165 |
label = newLabel;
|
|
|
7166 |
|
|
|
7167 |
/**
|
|
|
7168 |
* An event that fires when label changes on this track.
|
|
|
7169 |
*
|
|
|
7170 |
* > Note: This is not part of the spec!
|
|
|
7171 |
*
|
|
|
7172 |
* @event Track#labelchange
|
|
|
7173 |
* @type {Event}
|
|
|
7174 |
*/
|
|
|
7175 |
this.trigger('labelchange');
|
|
|
7176 |
}
|
|
|
7177 |
}
|
|
|
7178 |
});
|
|
|
7179 |
}
|
|
|
7180 |
}
|
|
|
7181 |
|
|
|
7182 |
/**
|
|
|
7183 |
* @file url.js
|
|
|
7184 |
* @module url
|
|
|
7185 |
*/
|
|
|
7186 |
|
|
|
7187 |
/**
|
|
|
7188 |
* @typedef {Object} url:URLObject
|
|
|
7189 |
*
|
|
|
7190 |
* @property {string} protocol
|
|
|
7191 |
* The protocol of the url that was parsed.
|
|
|
7192 |
*
|
|
|
7193 |
* @property {string} hostname
|
|
|
7194 |
* The hostname of the url that was parsed.
|
|
|
7195 |
*
|
|
|
7196 |
* @property {string} port
|
|
|
7197 |
* The port of the url that was parsed.
|
|
|
7198 |
*
|
|
|
7199 |
* @property {string} pathname
|
|
|
7200 |
* The pathname of the url that was parsed.
|
|
|
7201 |
*
|
|
|
7202 |
* @property {string} search
|
|
|
7203 |
* The search query of the url that was parsed.
|
|
|
7204 |
*
|
|
|
7205 |
* @property {string} hash
|
|
|
7206 |
* The hash of the url that was parsed.
|
|
|
7207 |
*
|
|
|
7208 |
* @property {string} host
|
|
|
7209 |
* The host of the url that was parsed.
|
|
|
7210 |
*/
|
|
|
7211 |
|
|
|
7212 |
/**
|
|
|
7213 |
* Resolve and parse the elements of a URL.
|
|
|
7214 |
*
|
|
|
7215 |
* @function
|
|
|
7216 |
* @param {String} url
|
|
|
7217 |
* The url to parse
|
|
|
7218 |
*
|
|
|
7219 |
* @return {url:URLObject}
|
|
|
7220 |
* An object of url details
|
|
|
7221 |
*/
|
|
|
7222 |
const parseUrl = function (url) {
|
|
|
7223 |
// This entire method can be replace with URL once we are able to drop IE11
|
|
|
7224 |
|
|
|
7225 |
const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
|
|
|
7226 |
|
|
|
7227 |
// add the url to an anchor and let the browser parse the URL
|
|
|
7228 |
const a = document.createElement('a');
|
|
|
7229 |
a.href = url;
|
|
|
7230 |
|
|
|
7231 |
// Copy the specific URL properties to a new object
|
|
|
7232 |
// This is also needed for IE because the anchor loses its
|
|
|
7233 |
// properties when it's removed from the dom
|
|
|
7234 |
const details = {};
|
|
|
7235 |
for (let i = 0; i < props.length; i++) {
|
|
|
7236 |
details[props[i]] = a[props[i]];
|
|
|
7237 |
}
|
|
|
7238 |
|
|
|
7239 |
// IE adds the port to the host property unlike everyone else. If
|
|
|
7240 |
// a port identifier is added for standard ports, strip it.
|
|
|
7241 |
if (details.protocol === 'http:') {
|
|
|
7242 |
details.host = details.host.replace(/:80$/, '');
|
|
|
7243 |
}
|
|
|
7244 |
if (details.protocol === 'https:') {
|
|
|
7245 |
details.host = details.host.replace(/:443$/, '');
|
|
|
7246 |
}
|
|
|
7247 |
if (!details.protocol) {
|
|
|
7248 |
details.protocol = window.location.protocol;
|
|
|
7249 |
}
|
|
|
7250 |
|
|
|
7251 |
/* istanbul ignore if */
|
|
|
7252 |
if (!details.host) {
|
|
|
7253 |
details.host = window.location.host;
|
|
|
7254 |
}
|
|
|
7255 |
return details;
|
|
|
7256 |
};
|
|
|
7257 |
|
|
|
7258 |
/**
|
|
|
7259 |
* Get absolute version of relative URL.
|
|
|
7260 |
*
|
|
|
7261 |
* @function
|
|
|
7262 |
* @param {string} url
|
|
|
7263 |
* URL to make absolute
|
|
|
7264 |
*
|
|
|
7265 |
* @return {string}
|
|
|
7266 |
* Absolute URL
|
|
|
7267 |
*
|
|
|
7268 |
* @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
|
|
|
7269 |
*/
|
|
|
7270 |
const getAbsoluteURL = function (url) {
|
|
|
7271 |
// Check if absolute URL
|
|
|
7272 |
if (!url.match(/^https?:\/\//)) {
|
|
|
7273 |
// Add the url to an anchor and let the browser parse it to convert to an absolute url
|
|
|
7274 |
const a = document.createElement('a');
|
|
|
7275 |
a.href = url;
|
|
|
7276 |
url = a.href;
|
|
|
7277 |
}
|
|
|
7278 |
return url;
|
|
|
7279 |
};
|
|
|
7280 |
|
|
|
7281 |
/**
|
|
|
7282 |
* Returns the extension of the passed file name. It will return an empty string
|
|
|
7283 |
* if passed an invalid path.
|
|
|
7284 |
*
|
|
|
7285 |
* @function
|
|
|
7286 |
* @param {string} path
|
|
|
7287 |
* The fileName path like '/path/to/file.mp4'
|
|
|
7288 |
*
|
|
|
7289 |
* @return {string}
|
|
|
7290 |
* The extension in lower case or an empty string if no
|
|
|
7291 |
* extension could be found.
|
|
|
7292 |
*/
|
|
|
7293 |
const getFileExtension = function (path) {
|
|
|
7294 |
if (typeof path === 'string') {
|
|
|
7295 |
const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
|
|
|
7296 |
const pathParts = splitPathRe.exec(path);
|
|
|
7297 |
if (pathParts) {
|
|
|
7298 |
return pathParts.pop().toLowerCase();
|
|
|
7299 |
}
|
|
|
7300 |
}
|
|
|
7301 |
return '';
|
|
|
7302 |
};
|
|
|
7303 |
|
|
|
7304 |
/**
|
|
|
7305 |
* Returns whether the url passed is a cross domain request or not.
|
|
|
7306 |
*
|
|
|
7307 |
* @function
|
|
|
7308 |
* @param {string} url
|
|
|
7309 |
* The url to check.
|
|
|
7310 |
*
|
|
|
7311 |
* @param {Object} [winLoc]
|
|
|
7312 |
* the domain to check the url against, defaults to window.location
|
|
|
7313 |
*
|
|
|
7314 |
* @param {string} [winLoc.protocol]
|
|
|
7315 |
* The window location protocol defaults to window.location.protocol
|
|
|
7316 |
*
|
|
|
7317 |
* @param {string} [winLoc.host]
|
|
|
7318 |
* The window location host defaults to window.location.host
|
|
|
7319 |
*
|
|
|
7320 |
* @return {boolean}
|
|
|
7321 |
* Whether it is a cross domain request or not.
|
|
|
7322 |
*/
|
|
|
7323 |
const isCrossOrigin = function (url, winLoc = window.location) {
|
|
|
7324 |
const urlInfo = parseUrl(url);
|
|
|
7325 |
|
|
|
7326 |
// IE8 protocol relative urls will return ':' for protocol
|
|
|
7327 |
const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
|
|
|
7328 |
|
|
|
7329 |
// Check if url is for another domain/origin
|
|
|
7330 |
// IE8 doesn't know location.origin, so we won't rely on it here
|
|
|
7331 |
const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
|
|
|
7332 |
return crossOrigin;
|
|
|
7333 |
};
|
|
|
7334 |
|
|
|
7335 |
var Url = /*#__PURE__*/Object.freeze({
|
|
|
7336 |
__proto__: null,
|
|
|
7337 |
parseUrl: parseUrl,
|
|
|
7338 |
getAbsoluteURL: getAbsoluteURL,
|
|
|
7339 |
getFileExtension: getFileExtension,
|
|
|
7340 |
isCrossOrigin: isCrossOrigin
|
|
|
7341 |
});
|
|
|
7342 |
|
|
|
7343 |
var win;
|
|
|
7344 |
if (typeof window !== "undefined") {
|
|
|
7345 |
win = window;
|
|
|
7346 |
} else if (typeof commonjsGlobal !== "undefined") {
|
|
|
7347 |
win = commonjsGlobal;
|
|
|
7348 |
} else if (typeof self !== "undefined") {
|
|
|
7349 |
win = self;
|
|
|
7350 |
} else {
|
|
|
7351 |
win = {};
|
|
|
7352 |
}
|
|
|
7353 |
var window_1 = win;
|
|
|
7354 |
|
|
|
7355 |
var _extends_1 = createCommonjsModule(function (module) {
|
|
|
7356 |
function _extends() {
|
|
|
7357 |
module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
|
7358 |
for (var i = 1; i < arguments.length; i++) {
|
|
|
7359 |
var source = arguments[i];
|
|
|
7360 |
for (var key in source) {
|
|
|
7361 |
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
|
7362 |
target[key] = source[key];
|
|
|
7363 |
}
|
|
|
7364 |
}
|
|
|
7365 |
}
|
|
|
7366 |
return target;
|
|
|
7367 |
}, module.exports.__esModule = true, module.exports["default"] = module.exports;
|
|
|
7368 |
return _extends.apply(this, arguments);
|
|
|
7369 |
}
|
|
|
7370 |
module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports;
|
|
|
7371 |
});
|
|
|
7372 |
var _extends$1 = unwrapExports(_extends_1);
|
|
|
7373 |
|
|
|
7374 |
var isFunction_1 = isFunction;
|
|
|
7375 |
var toString = Object.prototype.toString;
|
|
|
7376 |
function isFunction(fn) {
|
|
|
7377 |
if (!fn) {
|
|
|
7378 |
return false;
|
|
|
7379 |
}
|
|
|
7380 |
var string = toString.call(fn);
|
|
|
7381 |
return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && (
|
|
|
7382 |
// IE8 and below
|
|
|
7383 |
fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt);
|
|
|
7384 |
}
|
|
|
7385 |
|
|
|
7386 |
var httpResponseHandler = function httpResponseHandler(callback, decodeResponseBody) {
|
|
|
7387 |
if (decodeResponseBody === void 0) {
|
|
|
7388 |
decodeResponseBody = false;
|
|
|
7389 |
}
|
|
|
7390 |
return function (err, response, responseBody) {
|
|
|
7391 |
// if the XHR failed, return that error
|
|
|
7392 |
if (err) {
|
|
|
7393 |
callback(err);
|
|
|
7394 |
return;
|
|
|
7395 |
} // if the HTTP status code is 4xx or 5xx, the request also failed
|
|
|
7396 |
|
|
|
7397 |
if (response.statusCode >= 400 && response.statusCode <= 599) {
|
|
|
7398 |
var cause = responseBody;
|
|
|
7399 |
if (decodeResponseBody) {
|
|
|
7400 |
if (window_1.TextDecoder) {
|
|
|
7401 |
var charset = getCharset(response.headers && response.headers['content-type']);
|
|
|
7402 |
try {
|
|
|
7403 |
cause = new TextDecoder(charset).decode(responseBody);
|
|
|
7404 |
} catch (e) {}
|
|
|
7405 |
} else {
|
|
|
7406 |
cause = String.fromCharCode.apply(null, new Uint8Array(responseBody));
|
|
|
7407 |
}
|
|
|
7408 |
}
|
|
|
7409 |
callback({
|
|
|
7410 |
cause: cause
|
|
|
7411 |
});
|
|
|
7412 |
return;
|
|
|
7413 |
} // otherwise, request succeeded
|
|
|
7414 |
|
|
|
7415 |
callback(null, responseBody);
|
|
|
7416 |
};
|
|
|
7417 |
};
|
|
|
7418 |
function getCharset(contentTypeHeader) {
|
|
|
7419 |
if (contentTypeHeader === void 0) {
|
|
|
7420 |
contentTypeHeader = '';
|
|
|
7421 |
}
|
|
|
7422 |
return contentTypeHeader.toLowerCase().split(';').reduce(function (charset, contentType) {
|
|
|
7423 |
var _contentType$split = contentType.split('='),
|
|
|
7424 |
type = _contentType$split[0],
|
|
|
7425 |
value = _contentType$split[1];
|
|
|
7426 |
if (type.trim() === 'charset') {
|
|
|
7427 |
return value.trim();
|
|
|
7428 |
}
|
|
|
7429 |
return charset;
|
|
|
7430 |
}, 'utf-8');
|
|
|
7431 |
}
|
|
|
7432 |
var httpHandler = httpResponseHandler;
|
|
|
7433 |
|
|
|
7434 |
createXHR.httpHandler = httpHandler;
|
|
|
7435 |
/**
|
|
|
7436 |
* @license
|
|
|
7437 |
* slighly modified parse-headers 2.0.2 <https://github.com/kesla/parse-headers/>
|
|
|
7438 |
* Copyright (c) 2014 David Björklund
|
|
|
7439 |
* Available under the MIT license
|
|
|
7440 |
* <https://github.com/kesla/parse-headers/blob/master/LICENCE>
|
|
|
7441 |
*/
|
|
|
7442 |
|
|
|
7443 |
var parseHeaders = function parseHeaders(headers) {
|
|
|
7444 |
var result = {};
|
|
|
7445 |
if (!headers) {
|
|
|
7446 |
return result;
|
|
|
7447 |
}
|
|
|
7448 |
headers.trim().split('\n').forEach(function (row) {
|
|
|
7449 |
var index = row.indexOf(':');
|
|
|
7450 |
var key = row.slice(0, index).trim().toLowerCase();
|
|
|
7451 |
var value = row.slice(index + 1).trim();
|
|
|
7452 |
if (typeof result[key] === 'undefined') {
|
|
|
7453 |
result[key] = value;
|
|
|
7454 |
} else if (Array.isArray(result[key])) {
|
|
|
7455 |
result[key].push(value);
|
|
|
7456 |
} else {
|
|
|
7457 |
result[key] = [result[key], value];
|
|
|
7458 |
}
|
|
|
7459 |
});
|
|
|
7460 |
return result;
|
|
|
7461 |
};
|
|
|
7462 |
var lib = createXHR; // Allow use of default import syntax in TypeScript
|
|
|
7463 |
|
|
|
7464 |
var default_1 = createXHR;
|
|
|
7465 |
createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop$1;
|
|
|
7466 |
createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest;
|
|
|
7467 |
forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) {
|
|
|
7468 |
createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) {
|
|
|
7469 |
options = initParams(uri, options, callback);
|
|
|
7470 |
options.method = method.toUpperCase();
|
|
|
7471 |
return _createXHR(options);
|
|
|
7472 |
};
|
|
|
7473 |
});
|
|
|
7474 |
function forEachArray(array, iterator) {
|
|
|
7475 |
for (var i = 0; i < array.length; i++) {
|
|
|
7476 |
iterator(array[i]);
|
|
|
7477 |
}
|
|
|
7478 |
}
|
|
|
7479 |
function isEmpty(obj) {
|
|
|
7480 |
for (var i in obj) {
|
|
|
7481 |
if (obj.hasOwnProperty(i)) return false;
|
|
|
7482 |
}
|
|
|
7483 |
return true;
|
|
|
7484 |
}
|
|
|
7485 |
function initParams(uri, options, callback) {
|
|
|
7486 |
var params = uri;
|
|
|
7487 |
if (isFunction_1(options)) {
|
|
|
7488 |
callback = options;
|
|
|
7489 |
if (typeof uri === "string") {
|
|
|
7490 |
params = {
|
|
|
7491 |
uri: uri
|
|
|
7492 |
};
|
|
|
7493 |
}
|
|
|
7494 |
} else {
|
|
|
7495 |
params = _extends_1({}, options, {
|
|
|
7496 |
uri: uri
|
|
|
7497 |
});
|
|
|
7498 |
}
|
|
|
7499 |
params.callback = callback;
|
|
|
7500 |
return params;
|
|
|
7501 |
}
|
|
|
7502 |
function createXHR(uri, options, callback) {
|
|
|
7503 |
options = initParams(uri, options, callback);
|
|
|
7504 |
return _createXHR(options);
|
|
|
7505 |
}
|
|
|
7506 |
function _createXHR(options) {
|
|
|
7507 |
if (typeof options.callback === "undefined") {
|
|
|
7508 |
throw new Error("callback argument missing");
|
|
|
7509 |
}
|
|
|
7510 |
var called = false;
|
|
|
7511 |
var callback = function cbOnce(err, response, body) {
|
|
|
7512 |
if (!called) {
|
|
|
7513 |
called = true;
|
|
|
7514 |
options.callback(err, response, body);
|
|
|
7515 |
}
|
|
|
7516 |
};
|
|
|
7517 |
function readystatechange() {
|
|
|
7518 |
if (xhr.readyState === 4) {
|
|
|
7519 |
setTimeout(loadFunc, 0);
|
|
|
7520 |
}
|
|
|
7521 |
}
|
|
|
7522 |
function getBody() {
|
|
|
7523 |
// Chrome with requestType=blob throws errors arround when even testing access to responseText
|
|
|
7524 |
var body = undefined;
|
|
|
7525 |
if (xhr.response) {
|
|
|
7526 |
body = xhr.response;
|
|
|
7527 |
} else {
|
|
|
7528 |
body = xhr.responseText || getXml(xhr);
|
|
|
7529 |
}
|
|
|
7530 |
if (isJson) {
|
|
|
7531 |
try {
|
|
|
7532 |
body = JSON.parse(body);
|
|
|
7533 |
} catch (e) {}
|
|
|
7534 |
}
|
|
|
7535 |
return body;
|
|
|
7536 |
}
|
|
|
7537 |
function errorFunc(evt) {
|
|
|
7538 |
clearTimeout(timeoutTimer);
|
|
|
7539 |
if (!(evt instanceof Error)) {
|
|
|
7540 |
evt = new Error("" + (evt || "Unknown XMLHttpRequest Error"));
|
|
|
7541 |
}
|
|
|
7542 |
evt.statusCode = 0;
|
|
|
7543 |
return callback(evt, failureResponse);
|
|
|
7544 |
} // will load the data & process the response in a special response object
|
|
|
7545 |
|
|
|
7546 |
function loadFunc() {
|
|
|
7547 |
if (aborted) return;
|
|
|
7548 |
var status;
|
|
|
7549 |
clearTimeout(timeoutTimer);
|
|
|
7550 |
if (options.useXDR && xhr.status === undefined) {
|
|
|
7551 |
//IE8 CORS GET successful response doesn't have a status field, but body is fine
|
|
|
7552 |
status = 200;
|
|
|
7553 |
} else {
|
|
|
7554 |
status = xhr.status === 1223 ? 204 : xhr.status;
|
|
|
7555 |
}
|
|
|
7556 |
var response = failureResponse;
|
|
|
7557 |
var err = null;
|
|
|
7558 |
if (status !== 0) {
|
|
|
7559 |
response = {
|
|
|
7560 |
body: getBody(),
|
|
|
7561 |
statusCode: status,
|
|
|
7562 |
method: method,
|
|
|
7563 |
headers: {},
|
|
|
7564 |
url: uri,
|
|
|
7565 |
rawRequest: xhr
|
|
|
7566 |
};
|
|
|
7567 |
if (xhr.getAllResponseHeaders) {
|
|
|
7568 |
//remember xhr can in fact be XDR for CORS in IE
|
|
|
7569 |
response.headers = parseHeaders(xhr.getAllResponseHeaders());
|
|
|
7570 |
}
|
|
|
7571 |
} else {
|
|
|
7572 |
err = new Error("Internal XMLHttpRequest Error");
|
|
|
7573 |
}
|
|
|
7574 |
return callback(err, response, response.body);
|
|
|
7575 |
}
|
|
|
7576 |
var xhr = options.xhr || null;
|
|
|
7577 |
if (!xhr) {
|
|
|
7578 |
if (options.cors || options.useXDR) {
|
|
|
7579 |
xhr = new createXHR.XDomainRequest();
|
|
|
7580 |
} else {
|
|
|
7581 |
xhr = new createXHR.XMLHttpRequest();
|
|
|
7582 |
}
|
|
|
7583 |
}
|
|
|
7584 |
var key;
|
|
|
7585 |
var aborted;
|
|
|
7586 |
var uri = xhr.url = options.uri || options.url;
|
|
|
7587 |
var method = xhr.method = options.method || "GET";
|
|
|
7588 |
var body = options.body || options.data;
|
|
|
7589 |
var headers = xhr.headers = options.headers || {};
|
|
|
7590 |
var sync = !!options.sync;
|
|
|
7591 |
var isJson = false;
|
|
|
7592 |
var timeoutTimer;
|
|
|
7593 |
var failureResponse = {
|
|
|
7594 |
body: undefined,
|
|
|
7595 |
headers: {},
|
|
|
7596 |
statusCode: 0,
|
|
|
7597 |
method: method,
|
|
|
7598 |
url: uri,
|
|
|
7599 |
rawRequest: xhr
|
|
|
7600 |
};
|
|
|
7601 |
if ("json" in options && options.json !== false) {
|
|
|
7602 |
isJson = true;
|
|
|
7603 |
headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
|
|
|
7604 |
|
|
|
7605 |
if (method !== "GET" && method !== "HEAD") {
|
|
|
7606 |
headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
|
|
|
7607 |
|
|
|
7608 |
body = JSON.stringify(options.json === true ? body : options.json);
|
|
|
7609 |
}
|
|
|
7610 |
}
|
|
|
7611 |
xhr.onreadystatechange = readystatechange;
|
|
|
7612 |
xhr.onload = loadFunc;
|
|
|
7613 |
xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function.
|
|
|
7614 |
|
|
|
7615 |
xhr.onprogress = function () {// IE must die
|
|
|
7616 |
};
|
|
|
7617 |
xhr.onabort = function () {
|
|
|
7618 |
aborted = true;
|
|
|
7619 |
};
|
|
|
7620 |
xhr.ontimeout = errorFunc;
|
|
|
7621 |
xhr.open(method, uri, !sync, options.username, options.password); //has to be after open
|
|
|
7622 |
|
|
|
7623 |
if (!sync) {
|
|
|
7624 |
xhr.withCredentials = !!options.withCredentials;
|
|
|
7625 |
} // Cannot set timeout with sync request
|
|
|
7626 |
// not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
|
|
|
7627 |
// both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
|
|
|
7628 |
|
|
|
7629 |
if (!sync && options.timeout > 0) {
|
|
|
7630 |
timeoutTimer = setTimeout(function () {
|
|
|
7631 |
if (aborted) return;
|
|
|
7632 |
aborted = true; //IE9 may still call readystatechange
|
|
|
7633 |
|
|
|
7634 |
xhr.abort("timeout");
|
|
|
7635 |
var e = new Error("XMLHttpRequest timeout");
|
|
|
7636 |
e.code = "ETIMEDOUT";
|
|
|
7637 |
errorFunc(e);
|
|
|
7638 |
}, options.timeout);
|
|
|
7639 |
}
|
|
|
7640 |
if (xhr.setRequestHeader) {
|
|
|
7641 |
for (key in headers) {
|
|
|
7642 |
if (headers.hasOwnProperty(key)) {
|
|
|
7643 |
xhr.setRequestHeader(key, headers[key]);
|
|
|
7644 |
}
|
|
|
7645 |
}
|
|
|
7646 |
} else if (options.headers && !isEmpty(options.headers)) {
|
|
|
7647 |
throw new Error("Headers cannot be set on an XDomainRequest object");
|
|
|
7648 |
}
|
|
|
7649 |
if ("responseType" in options) {
|
|
|
7650 |
xhr.responseType = options.responseType;
|
|
|
7651 |
}
|
|
|
7652 |
if ("beforeSend" in options && typeof options.beforeSend === "function") {
|
|
|
7653 |
options.beforeSend(xhr);
|
|
|
7654 |
} // Microsoft Edge browser sends "undefined" when send is called with undefined value.
|
|
|
7655 |
// XMLHttpRequest spec says to pass null as body to indicate no body
|
|
|
7656 |
// See https://github.com/naugtur/xhr/issues/100.
|
|
|
7657 |
|
|
|
7658 |
xhr.send(body || null);
|
|
|
7659 |
return xhr;
|
|
|
7660 |
}
|
|
|
7661 |
function getXml(xhr) {
|
|
|
7662 |
// xhr.responseXML will throw Exception "InvalidStateError" or "DOMException"
|
|
|
7663 |
// See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML.
|
|
|
7664 |
try {
|
|
|
7665 |
if (xhr.responseType === "document") {
|
|
|
7666 |
return xhr.responseXML;
|
|
|
7667 |
}
|
|
|
7668 |
var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
|
|
|
7669 |
if (xhr.responseType === "" && !firefoxBugTakenEffect) {
|
|
|
7670 |
return xhr.responseXML;
|
|
|
7671 |
}
|
|
|
7672 |
} catch (e) {}
|
|
|
7673 |
return null;
|
|
|
7674 |
}
|
|
|
7675 |
function noop$1() {}
|
|
|
7676 |
lib.default = default_1;
|
|
|
7677 |
|
|
|
7678 |
/**
|
|
|
7679 |
* @file text-track.js
|
|
|
7680 |
*/
|
|
|
7681 |
|
|
|
7682 |
/**
|
|
|
7683 |
* Takes a webvtt file contents and parses it into cues
|
|
|
7684 |
*
|
|
|
7685 |
* @param {string} srcContent
|
|
|
7686 |
* webVTT file contents
|
|
|
7687 |
*
|
|
|
7688 |
* @param {TextTrack} track
|
|
|
7689 |
* TextTrack to add cues to. Cues come from the srcContent.
|
|
|
7690 |
*
|
|
|
7691 |
* @private
|
|
|
7692 |
*/
|
|
|
7693 |
const parseCues = function (srcContent, track) {
|
|
|
7694 |
const parser = new window.WebVTT.Parser(window, window.vttjs, window.WebVTT.StringDecoder());
|
|
|
7695 |
const errors = [];
|
|
|
7696 |
parser.oncue = function (cue) {
|
|
|
7697 |
track.addCue(cue);
|
|
|
7698 |
};
|
|
|
7699 |
parser.onparsingerror = function (error) {
|
|
|
7700 |
errors.push(error);
|
|
|
7701 |
};
|
|
|
7702 |
parser.onflush = function () {
|
|
|
7703 |
track.trigger({
|
|
|
7704 |
type: 'loadeddata',
|
|
|
7705 |
target: track
|
|
|
7706 |
});
|
|
|
7707 |
};
|
|
|
7708 |
parser.parse(srcContent);
|
|
|
7709 |
if (errors.length > 0) {
|
|
|
7710 |
if (window.console && window.console.groupCollapsed) {
|
|
|
7711 |
window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
|
|
|
7712 |
}
|
|
|
7713 |
errors.forEach(error => log$1.error(error));
|
|
|
7714 |
if (window.console && window.console.groupEnd) {
|
|
|
7715 |
window.console.groupEnd();
|
|
|
7716 |
}
|
|
|
7717 |
}
|
|
|
7718 |
parser.flush();
|
|
|
7719 |
};
|
|
|
7720 |
|
|
|
7721 |
/**
|
|
|
7722 |
* Load a `TextTrack` from a specified url.
|
|
|
7723 |
*
|
|
|
7724 |
* @param {string} src
|
|
|
7725 |
* Url to load track from.
|
|
|
7726 |
*
|
|
|
7727 |
* @param {TextTrack} track
|
|
|
7728 |
* Track to add cues to. Comes from the content at the end of `url`.
|
|
|
7729 |
*
|
|
|
7730 |
* @private
|
|
|
7731 |
*/
|
|
|
7732 |
const loadTrack = function (src, track) {
|
|
|
7733 |
const opts = {
|
|
|
7734 |
uri: src
|
|
|
7735 |
};
|
|
|
7736 |
const crossOrigin = isCrossOrigin(src);
|
|
|
7737 |
if (crossOrigin) {
|
|
|
7738 |
opts.cors = crossOrigin;
|
|
|
7739 |
}
|
|
|
7740 |
const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
|
|
|
7741 |
if (withCredentials) {
|
|
|
7742 |
opts.withCredentials = withCredentials;
|
|
|
7743 |
}
|
|
|
7744 |
lib(opts, bind_(this, function (err, response, responseBody) {
|
|
|
7745 |
if (err) {
|
|
|
7746 |
return log$1.error(err, response);
|
|
|
7747 |
}
|
|
|
7748 |
track.loaded_ = true;
|
|
|
7749 |
|
|
|
7750 |
// Make sure that vttjs has loaded, otherwise, wait till it finished loading
|
|
|
7751 |
// NOTE: this is only used for the alt/video.novtt.js build
|
|
|
7752 |
if (typeof window.WebVTT !== 'function') {
|
|
|
7753 |
if (track.tech_) {
|
|
|
7754 |
// to prevent use before define eslint error, we define loadHandler
|
|
|
7755 |
// as a let here
|
|
|
7756 |
track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
|
|
|
7757 |
if (event.type === 'vttjserror') {
|
|
|
7758 |
log$1.error(`vttjs failed to load, stopping trying to process ${track.src}`);
|
|
|
7759 |
return;
|
|
|
7760 |
}
|
|
|
7761 |
return parseCues(responseBody, track);
|
|
|
7762 |
});
|
|
|
7763 |
}
|
|
|
7764 |
} else {
|
|
|
7765 |
parseCues(responseBody, track);
|
|
|
7766 |
}
|
|
|
7767 |
}));
|
|
|
7768 |
};
|
|
|
7769 |
|
|
|
7770 |
/**
|
|
|
7771 |
* A representation of a single `TextTrack`.
|
|
|
7772 |
*
|
|
|
7773 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
|
|
|
7774 |
* @extends Track
|
|
|
7775 |
*/
|
|
|
7776 |
class TextTrack extends Track {
|
|
|
7777 |
/**
|
|
|
7778 |
* Create an instance of this class.
|
|
|
7779 |
*
|
|
|
7780 |
* @param {Object} options={}
|
|
|
7781 |
* Object of option names and values
|
|
|
7782 |
*
|
|
|
7783 |
* @param { import('../tech/tech').default } options.tech
|
|
|
7784 |
* A reference to the tech that owns this TextTrack.
|
|
|
7785 |
*
|
|
|
7786 |
* @param {TextTrack~Kind} [options.kind='subtitles']
|
|
|
7787 |
* A valid text track kind.
|
|
|
7788 |
*
|
|
|
7789 |
* @param {TextTrack~Mode} [options.mode='disabled']
|
|
|
7790 |
* A valid text track mode.
|
|
|
7791 |
*
|
|
|
7792 |
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
|
|
|
7793 |
* A unique id for this TextTrack.
|
|
|
7794 |
*
|
|
|
7795 |
* @param {string} [options.label='']
|
|
|
7796 |
* The menu label for this track.
|
|
|
7797 |
*
|
|
|
7798 |
* @param {string} [options.language='']
|
|
|
7799 |
* A valid two character language code.
|
|
|
7800 |
*
|
|
|
7801 |
* @param {string} [options.srclang='']
|
|
|
7802 |
* A valid two character language code. An alternative, but deprioritized
|
|
|
7803 |
* version of `options.language`
|
|
|
7804 |
*
|
|
|
7805 |
* @param {string} [options.src]
|
|
|
7806 |
* A url to TextTrack cues.
|
|
|
7807 |
*
|
|
|
7808 |
* @param {boolean} [options.default]
|
|
|
7809 |
* If this track should default to on or off.
|
|
|
7810 |
*/
|
|
|
7811 |
constructor(options = {}) {
|
|
|
7812 |
if (!options.tech) {
|
|
|
7813 |
throw new Error('A tech was not provided.');
|
|
|
7814 |
}
|
|
|
7815 |
const settings = merge$2(options, {
|
|
|
7816 |
kind: TextTrackKind[options.kind] || 'subtitles',
|
|
|
7817 |
language: options.language || options.srclang || ''
|
|
|
7818 |
});
|
|
|
7819 |
let mode = TextTrackMode[settings.mode] || 'disabled';
|
|
|
7820 |
const default_ = settings.default;
|
|
|
7821 |
if (settings.kind === 'metadata' || settings.kind === 'chapters') {
|
|
|
7822 |
mode = 'hidden';
|
|
|
7823 |
}
|
|
|
7824 |
super(settings);
|
|
|
7825 |
this.tech_ = settings.tech;
|
|
|
7826 |
this.cues_ = [];
|
|
|
7827 |
this.activeCues_ = [];
|
|
|
7828 |
this.preload_ = this.tech_.preloadTextTracks !== false;
|
|
|
7829 |
const cues = new TextTrackCueList(this.cues_);
|
|
|
7830 |
const activeCues = new TextTrackCueList(this.activeCues_);
|
|
|
7831 |
let changed = false;
|
|
|
7832 |
this.timeupdateHandler = bind_(this, function (event = {}) {
|
|
|
7833 |
if (this.tech_.isDisposed()) {
|
|
|
7834 |
return;
|
|
|
7835 |
}
|
|
|
7836 |
if (!this.tech_.isReady_) {
|
|
|
7837 |
if (event.type !== 'timeupdate') {
|
|
|
7838 |
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
|
|
|
7839 |
}
|
|
|
7840 |
return;
|
|
|
7841 |
}
|
|
|
7842 |
|
|
|
7843 |
// Accessing this.activeCues for the side-effects of updating itself
|
|
|
7844 |
// due to its nature as a getter function. Do not remove or cues will
|
|
|
7845 |
// stop updating!
|
|
|
7846 |
// Use the setter to prevent deletion from uglify (pure_getters rule)
|
|
|
7847 |
this.activeCues = this.activeCues;
|
|
|
7848 |
if (changed) {
|
|
|
7849 |
this.trigger('cuechange');
|
|
|
7850 |
changed = false;
|
|
|
7851 |
}
|
|
|
7852 |
if (event.type !== 'timeupdate') {
|
|
|
7853 |
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
|
|
|
7854 |
}
|
|
|
7855 |
});
|
|
|
7856 |
const disposeHandler = () => {
|
|
|
7857 |
this.stopTracking();
|
|
|
7858 |
};
|
|
|
7859 |
this.tech_.one('dispose', disposeHandler);
|
|
|
7860 |
if (mode !== 'disabled') {
|
|
|
7861 |
this.startTracking();
|
|
|
7862 |
}
|
|
|
7863 |
Object.defineProperties(this, {
|
|
|
7864 |
/**
|
|
|
7865 |
* @memberof TextTrack
|
|
|
7866 |
* @member {boolean} default
|
|
|
7867 |
* If this track was set to be on or off by default. Cannot be changed after
|
|
|
7868 |
* creation.
|
|
|
7869 |
* @instance
|
|
|
7870 |
*
|
|
|
7871 |
* @readonly
|
|
|
7872 |
*/
|
|
|
7873 |
default: {
|
|
|
7874 |
get() {
|
|
|
7875 |
return default_;
|
|
|
7876 |
},
|
|
|
7877 |
set() {}
|
|
|
7878 |
},
|
|
|
7879 |
/**
|
|
|
7880 |
* @memberof TextTrack
|
|
|
7881 |
* @member {string} mode
|
|
|
7882 |
* Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
|
|
|
7883 |
* not be set if setting to an invalid mode.
|
|
|
7884 |
* @instance
|
|
|
7885 |
*
|
|
|
7886 |
* @fires TextTrack#modechange
|
|
|
7887 |
*/
|
|
|
7888 |
mode: {
|
|
|
7889 |
get() {
|
|
|
7890 |
return mode;
|
|
|
7891 |
},
|
|
|
7892 |
set(newMode) {
|
|
|
7893 |
if (!TextTrackMode[newMode]) {
|
|
|
7894 |
return;
|
|
|
7895 |
}
|
|
|
7896 |
if (mode === newMode) {
|
|
|
7897 |
return;
|
|
|
7898 |
}
|
|
|
7899 |
mode = newMode;
|
|
|
7900 |
if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
|
|
|
7901 |
// On-demand load.
|
|
|
7902 |
loadTrack(this.src, this);
|
|
|
7903 |
}
|
|
|
7904 |
this.stopTracking();
|
|
|
7905 |
if (mode !== 'disabled') {
|
|
|
7906 |
this.startTracking();
|
|
|
7907 |
}
|
|
|
7908 |
/**
|
|
|
7909 |
* An event that fires when mode changes on this track. This allows
|
|
|
7910 |
* the TextTrackList that holds this track to act accordingly.
|
|
|
7911 |
*
|
|
|
7912 |
* > Note: This is not part of the spec!
|
|
|
7913 |
*
|
|
|
7914 |
* @event TextTrack#modechange
|
|
|
7915 |
* @type {Event}
|
|
|
7916 |
*/
|
|
|
7917 |
this.trigger('modechange');
|
|
|
7918 |
}
|
|
|
7919 |
},
|
|
|
7920 |
/**
|
|
|
7921 |
* @memberof TextTrack
|
|
|
7922 |
* @member {TextTrackCueList} cues
|
|
|
7923 |
* The text track cue list for this TextTrack.
|
|
|
7924 |
* @instance
|
|
|
7925 |
*/
|
|
|
7926 |
cues: {
|
|
|
7927 |
get() {
|
|
|
7928 |
if (!this.loaded_) {
|
|
|
7929 |
return null;
|
|
|
7930 |
}
|
|
|
7931 |
return cues;
|
|
|
7932 |
},
|
|
|
7933 |
set() {}
|
|
|
7934 |
},
|
|
|
7935 |
/**
|
|
|
7936 |
* @memberof TextTrack
|
|
|
7937 |
* @member {TextTrackCueList} activeCues
|
|
|
7938 |
* The list text track cues that are currently active for this TextTrack.
|
|
|
7939 |
* @instance
|
|
|
7940 |
*/
|
|
|
7941 |
activeCues: {
|
|
|
7942 |
get() {
|
|
|
7943 |
if (!this.loaded_) {
|
|
|
7944 |
return null;
|
|
|
7945 |
}
|
|
|
7946 |
|
|
|
7947 |
// nothing to do
|
|
|
7948 |
if (this.cues.length === 0) {
|
|
|
7949 |
return activeCues;
|
|
|
7950 |
}
|
|
|
7951 |
const ct = this.tech_.currentTime();
|
|
|
7952 |
const active = [];
|
|
|
7953 |
for (let i = 0, l = this.cues.length; i < l; i++) {
|
|
|
7954 |
const cue = this.cues[i];
|
|
|
7955 |
if (cue.startTime <= ct && cue.endTime >= ct) {
|
|
|
7956 |
active.push(cue);
|
|
|
7957 |
}
|
|
|
7958 |
}
|
|
|
7959 |
changed = false;
|
|
|
7960 |
if (active.length !== this.activeCues_.length) {
|
|
|
7961 |
changed = true;
|
|
|
7962 |
} else {
|
|
|
7963 |
for (let i = 0; i < active.length; i++) {
|
|
|
7964 |
if (this.activeCues_.indexOf(active[i]) === -1) {
|
|
|
7965 |
changed = true;
|
|
|
7966 |
}
|
|
|
7967 |
}
|
|
|
7968 |
}
|
|
|
7969 |
this.activeCues_ = active;
|
|
|
7970 |
activeCues.setCues_(this.activeCues_);
|
|
|
7971 |
return activeCues;
|
|
|
7972 |
},
|
|
|
7973 |
// /!\ Keep this setter empty (see the timeupdate handler above)
|
|
|
7974 |
set() {}
|
|
|
7975 |
}
|
|
|
7976 |
});
|
|
|
7977 |
if (settings.src) {
|
|
|
7978 |
this.src = settings.src;
|
|
|
7979 |
if (!this.preload_) {
|
|
|
7980 |
// Tracks will load on-demand.
|
|
|
7981 |
// Act like we're loaded for other purposes.
|
|
|
7982 |
this.loaded_ = true;
|
|
|
7983 |
}
|
|
|
7984 |
if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
|
|
|
7985 |
loadTrack(this.src, this);
|
|
|
7986 |
}
|
|
|
7987 |
} else {
|
|
|
7988 |
this.loaded_ = true;
|
|
|
7989 |
}
|
|
|
7990 |
}
|
|
|
7991 |
startTracking() {
|
|
|
7992 |
// More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
|
|
|
7993 |
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
|
|
|
7994 |
// Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
|
|
|
7995 |
this.tech_.on('timeupdate', this.timeupdateHandler);
|
|
|
7996 |
}
|
|
|
7997 |
stopTracking() {
|
|
|
7998 |
if (this.rvf_) {
|
|
|
7999 |
this.tech_.cancelVideoFrameCallback(this.rvf_);
|
|
|
8000 |
this.rvf_ = undefined;
|
|
|
8001 |
}
|
|
|
8002 |
this.tech_.off('timeupdate', this.timeupdateHandler);
|
|
|
8003 |
}
|
|
|
8004 |
|
|
|
8005 |
/**
|
|
|
8006 |
* Add a cue to the internal list of cues.
|
|
|
8007 |
*
|
|
|
8008 |
* @param {TextTrack~Cue} cue
|
|
|
8009 |
* The cue to add to our internal list
|
|
|
8010 |
*/
|
|
|
8011 |
addCue(originalCue) {
|
|
|
8012 |
let cue = originalCue;
|
|
|
8013 |
|
|
|
8014 |
// Testing if the cue is a VTTCue in a way that survives minification
|
|
|
8015 |
if (!('getCueAsHTML' in cue)) {
|
|
|
8016 |
cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
|
|
|
8017 |
for (const prop in originalCue) {
|
|
|
8018 |
if (!(prop in cue)) {
|
|
|
8019 |
cue[prop] = originalCue[prop];
|
|
|
8020 |
}
|
|
|
8021 |
}
|
|
|
8022 |
|
|
|
8023 |
// make sure that `id` is copied over
|
|
|
8024 |
cue.id = originalCue.id;
|
|
|
8025 |
cue.originalCue_ = originalCue;
|
|
|
8026 |
}
|
|
|
8027 |
const tracks = this.tech_.textTracks();
|
|
|
8028 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
8029 |
if (tracks[i] !== this) {
|
|
|
8030 |
tracks[i].removeCue(cue);
|
|
|
8031 |
}
|
|
|
8032 |
}
|
|
|
8033 |
this.cues_.push(cue);
|
|
|
8034 |
this.cues.setCues_(this.cues_);
|
|
|
8035 |
}
|
|
|
8036 |
|
|
|
8037 |
/**
|
|
|
8038 |
* Remove a cue from our internal list
|
|
|
8039 |
*
|
|
|
8040 |
* @param {TextTrack~Cue} removeCue
|
|
|
8041 |
* The cue to remove from our internal list
|
|
|
8042 |
*/
|
|
|
8043 |
removeCue(removeCue) {
|
|
|
8044 |
let i = this.cues_.length;
|
|
|
8045 |
while (i--) {
|
|
|
8046 |
const cue = this.cues_[i];
|
|
|
8047 |
if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
|
|
|
8048 |
this.cues_.splice(i, 1);
|
|
|
8049 |
this.cues.setCues_(this.cues_);
|
|
|
8050 |
break;
|
|
|
8051 |
}
|
|
|
8052 |
}
|
|
|
8053 |
}
|
|
|
8054 |
}
|
|
|
8055 |
|
|
|
8056 |
/**
|
|
|
8057 |
* cuechange - One or more cues in the track have become active or stopped being active.
|
|
|
8058 |
* @protected
|
|
|
8059 |
*/
|
|
|
8060 |
TextTrack.prototype.allowedEvents_ = {
|
|
|
8061 |
cuechange: 'cuechange'
|
|
|
8062 |
};
|
|
|
8063 |
|
|
|
8064 |
/**
|
|
|
8065 |
* A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
|
|
|
8066 |
* only one `AudioTrack` in the list will be enabled at a time.
|
|
|
8067 |
*
|
|
|
8068 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
|
|
|
8069 |
* @extends Track
|
|
|
8070 |
*/
|
|
|
8071 |
class AudioTrack extends Track {
|
|
|
8072 |
/**
|
|
|
8073 |
* Create an instance of this class.
|
|
|
8074 |
*
|
|
|
8075 |
* @param {Object} [options={}]
|
|
|
8076 |
* Object of option names and values
|
|
|
8077 |
*
|
|
|
8078 |
* @param {AudioTrack~Kind} [options.kind='']
|
|
|
8079 |
* A valid audio track kind
|
|
|
8080 |
*
|
|
|
8081 |
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
|
|
|
8082 |
* A unique id for this AudioTrack.
|
|
|
8083 |
*
|
|
|
8084 |
* @param {string} [options.label='']
|
|
|
8085 |
* The menu label for this track.
|
|
|
8086 |
*
|
|
|
8087 |
* @param {string} [options.language='']
|
|
|
8088 |
* A valid two character language code.
|
|
|
8089 |
*
|
|
|
8090 |
* @param {boolean} [options.enabled]
|
|
|
8091 |
* If this track is the one that is currently playing. If this track is part of
|
|
|
8092 |
* an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
|
|
|
8093 |
*/
|
|
|
8094 |
constructor(options = {}) {
|
|
|
8095 |
const settings = merge$2(options, {
|
|
|
8096 |
kind: AudioTrackKind[options.kind] || ''
|
|
|
8097 |
});
|
|
|
8098 |
super(settings);
|
|
|
8099 |
let enabled = false;
|
|
|
8100 |
|
|
|
8101 |
/**
|
|
|
8102 |
* @memberof AudioTrack
|
|
|
8103 |
* @member {boolean} enabled
|
|
|
8104 |
* If this `AudioTrack` is enabled or not. When setting this will
|
|
|
8105 |
* fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
|
|
|
8106 |
* @instance
|
|
|
8107 |
*
|
|
|
8108 |
* @fires VideoTrack#selectedchange
|
|
|
8109 |
*/
|
|
|
8110 |
Object.defineProperty(this, 'enabled', {
|
|
|
8111 |
get() {
|
|
|
8112 |
return enabled;
|
|
|
8113 |
},
|
|
|
8114 |
set(newEnabled) {
|
|
|
8115 |
// an invalid or unchanged value
|
|
|
8116 |
if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
|
|
|
8117 |
return;
|
|
|
8118 |
}
|
|
|
8119 |
enabled = newEnabled;
|
|
|
8120 |
|
|
|
8121 |
/**
|
|
|
8122 |
* An event that fires when enabled changes on this track. This allows
|
|
|
8123 |
* the AudioTrackList that holds this track to act accordingly.
|
|
|
8124 |
*
|
|
|
8125 |
* > Note: This is not part of the spec! Native tracks will do
|
|
|
8126 |
* this internally without an event.
|
|
|
8127 |
*
|
|
|
8128 |
* @event AudioTrack#enabledchange
|
|
|
8129 |
* @type {Event}
|
|
|
8130 |
*/
|
|
|
8131 |
this.trigger('enabledchange');
|
|
|
8132 |
}
|
|
|
8133 |
});
|
|
|
8134 |
|
|
|
8135 |
// if the user sets this track to selected then
|
|
|
8136 |
// set selected to that true value otherwise
|
|
|
8137 |
// we keep it false
|
|
|
8138 |
if (settings.enabled) {
|
|
|
8139 |
this.enabled = settings.enabled;
|
|
|
8140 |
}
|
|
|
8141 |
this.loaded_ = true;
|
|
|
8142 |
}
|
|
|
8143 |
}
|
|
|
8144 |
|
|
|
8145 |
/**
|
|
|
8146 |
* A representation of a single `VideoTrack`.
|
|
|
8147 |
*
|
|
|
8148 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
|
|
|
8149 |
* @extends Track
|
|
|
8150 |
*/
|
|
|
8151 |
class VideoTrack extends Track {
|
|
|
8152 |
/**
|
|
|
8153 |
* Create an instance of this class.
|
|
|
8154 |
*
|
|
|
8155 |
* @param {Object} [options={}]
|
|
|
8156 |
* Object of option names and values
|
|
|
8157 |
*
|
|
|
8158 |
* @param {string} [options.kind='']
|
|
|
8159 |
* A valid {@link VideoTrack~Kind}
|
|
|
8160 |
*
|
|
|
8161 |
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
|
|
|
8162 |
* A unique id for this AudioTrack.
|
|
|
8163 |
*
|
|
|
8164 |
* @param {string} [options.label='']
|
|
|
8165 |
* The menu label for this track.
|
|
|
8166 |
*
|
|
|
8167 |
* @param {string} [options.language='']
|
|
|
8168 |
* A valid two character language code.
|
|
|
8169 |
*
|
|
|
8170 |
* @param {boolean} [options.selected]
|
|
|
8171 |
* If this track is the one that is currently playing.
|
|
|
8172 |
*/
|
|
|
8173 |
constructor(options = {}) {
|
|
|
8174 |
const settings = merge$2(options, {
|
|
|
8175 |
kind: VideoTrackKind[options.kind] || ''
|
|
|
8176 |
});
|
|
|
8177 |
super(settings);
|
|
|
8178 |
let selected = false;
|
|
|
8179 |
|
|
|
8180 |
/**
|
|
|
8181 |
* @memberof VideoTrack
|
|
|
8182 |
* @member {boolean} selected
|
|
|
8183 |
* If this `VideoTrack` is selected or not. When setting this will
|
|
|
8184 |
* fire {@link VideoTrack#selectedchange} if the state of selected changed.
|
|
|
8185 |
* @instance
|
|
|
8186 |
*
|
|
|
8187 |
* @fires VideoTrack#selectedchange
|
|
|
8188 |
*/
|
|
|
8189 |
Object.defineProperty(this, 'selected', {
|
|
|
8190 |
get() {
|
|
|
8191 |
return selected;
|
|
|
8192 |
},
|
|
|
8193 |
set(newSelected) {
|
|
|
8194 |
// an invalid or unchanged value
|
|
|
8195 |
if (typeof newSelected !== 'boolean' || newSelected === selected) {
|
|
|
8196 |
return;
|
|
|
8197 |
}
|
|
|
8198 |
selected = newSelected;
|
|
|
8199 |
|
|
|
8200 |
/**
|
|
|
8201 |
* An event that fires when selected changes on this track. This allows
|
|
|
8202 |
* the VideoTrackList that holds this track to act accordingly.
|
|
|
8203 |
*
|
|
|
8204 |
* > Note: This is not part of the spec! Native tracks will do
|
|
|
8205 |
* this internally without an event.
|
|
|
8206 |
*
|
|
|
8207 |
* @event VideoTrack#selectedchange
|
|
|
8208 |
* @type {Event}
|
|
|
8209 |
*/
|
|
|
8210 |
this.trigger('selectedchange');
|
|
|
8211 |
}
|
|
|
8212 |
});
|
|
|
8213 |
|
|
|
8214 |
// if the user sets this track to selected then
|
|
|
8215 |
// set selected to that true value otherwise
|
|
|
8216 |
// we keep it false
|
|
|
8217 |
if (settings.selected) {
|
|
|
8218 |
this.selected = settings.selected;
|
|
|
8219 |
}
|
|
|
8220 |
}
|
|
|
8221 |
}
|
|
|
8222 |
|
|
|
8223 |
/**
|
|
|
8224 |
* @file html-track-element.js
|
|
|
8225 |
*/
|
|
|
8226 |
|
|
|
8227 |
/**
|
|
|
8228 |
* A single track represented in the DOM.
|
|
|
8229 |
*
|
|
|
8230 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
|
|
|
8231 |
* @extends EventTarget
|
|
|
8232 |
*/
|
|
|
8233 |
class HTMLTrackElement extends EventTarget$2 {
|
|
|
8234 |
/**
|
|
|
8235 |
* Create an instance of this class.
|
|
|
8236 |
*
|
|
|
8237 |
* @param {Object} options={}
|
|
|
8238 |
* Object of option names and values
|
|
|
8239 |
*
|
|
|
8240 |
* @param { import('../tech/tech').default } options.tech
|
|
|
8241 |
* A reference to the tech that owns this HTMLTrackElement.
|
|
|
8242 |
*
|
|
|
8243 |
* @param {TextTrack~Kind} [options.kind='subtitles']
|
|
|
8244 |
* A valid text track kind.
|
|
|
8245 |
*
|
|
|
8246 |
* @param {TextTrack~Mode} [options.mode='disabled']
|
|
|
8247 |
* A valid text track mode.
|
|
|
8248 |
*
|
|
|
8249 |
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
|
|
|
8250 |
* A unique id for this TextTrack.
|
|
|
8251 |
*
|
|
|
8252 |
* @param {string} [options.label='']
|
|
|
8253 |
* The menu label for this track.
|
|
|
8254 |
*
|
|
|
8255 |
* @param {string} [options.language='']
|
|
|
8256 |
* A valid two character language code.
|
|
|
8257 |
*
|
|
|
8258 |
* @param {string} [options.srclang='']
|
|
|
8259 |
* A valid two character language code. An alternative, but deprioritized
|
|
|
8260 |
* version of `options.language`
|
|
|
8261 |
*
|
|
|
8262 |
* @param {string} [options.src]
|
|
|
8263 |
* A url to TextTrack cues.
|
|
|
8264 |
*
|
|
|
8265 |
* @param {boolean} [options.default]
|
|
|
8266 |
* If this track should default to on or off.
|
|
|
8267 |
*/
|
|
|
8268 |
constructor(options = {}) {
|
|
|
8269 |
super();
|
|
|
8270 |
let readyState;
|
|
|
8271 |
const track = new TextTrack(options);
|
|
|
8272 |
this.kind = track.kind;
|
|
|
8273 |
this.src = track.src;
|
|
|
8274 |
this.srclang = track.language;
|
|
|
8275 |
this.label = track.label;
|
|
|
8276 |
this.default = track.default;
|
|
|
8277 |
Object.defineProperties(this, {
|
|
|
8278 |
/**
|
|
|
8279 |
* @memberof HTMLTrackElement
|
|
|
8280 |
* @member {HTMLTrackElement~ReadyState} readyState
|
|
|
8281 |
* The current ready state of the track element.
|
|
|
8282 |
* @instance
|
|
|
8283 |
*/
|
|
|
8284 |
readyState: {
|
|
|
8285 |
get() {
|
|
|
8286 |
return readyState;
|
|
|
8287 |
}
|
|
|
8288 |
},
|
|
|
8289 |
/**
|
|
|
8290 |
* @memberof HTMLTrackElement
|
|
|
8291 |
* @member {TextTrack} track
|
|
|
8292 |
* The underlying TextTrack object.
|
|
|
8293 |
* @instance
|
|
|
8294 |
*
|
|
|
8295 |
*/
|
|
|
8296 |
track: {
|
|
|
8297 |
get() {
|
|
|
8298 |
return track;
|
|
|
8299 |
}
|
|
|
8300 |
}
|
|
|
8301 |
});
|
|
|
8302 |
readyState = HTMLTrackElement.NONE;
|
|
|
8303 |
|
|
|
8304 |
/**
|
|
|
8305 |
* @listens TextTrack#loadeddata
|
|
|
8306 |
* @fires HTMLTrackElement#load
|
|
|
8307 |
*/
|
|
|
8308 |
track.addEventListener('loadeddata', () => {
|
|
|
8309 |
readyState = HTMLTrackElement.LOADED;
|
|
|
8310 |
this.trigger({
|
|
|
8311 |
type: 'load',
|
|
|
8312 |
target: this
|
|
|
8313 |
});
|
|
|
8314 |
});
|
|
|
8315 |
}
|
|
|
8316 |
}
|
|
|
8317 |
|
|
|
8318 |
/**
|
|
|
8319 |
* @protected
|
|
|
8320 |
*/
|
|
|
8321 |
HTMLTrackElement.prototype.allowedEvents_ = {
|
|
|
8322 |
load: 'load'
|
|
|
8323 |
};
|
|
|
8324 |
|
|
|
8325 |
/**
|
|
|
8326 |
* The text track not loaded state.
|
|
|
8327 |
*
|
|
|
8328 |
* @type {number}
|
|
|
8329 |
* @static
|
|
|
8330 |
*/
|
|
|
8331 |
HTMLTrackElement.NONE = 0;
|
|
|
8332 |
|
|
|
8333 |
/**
|
|
|
8334 |
* The text track loading state.
|
|
|
8335 |
*
|
|
|
8336 |
* @type {number}
|
|
|
8337 |
* @static
|
|
|
8338 |
*/
|
|
|
8339 |
HTMLTrackElement.LOADING = 1;
|
|
|
8340 |
|
|
|
8341 |
/**
|
|
|
8342 |
* The text track loaded state.
|
|
|
8343 |
*
|
|
|
8344 |
* @type {number}
|
|
|
8345 |
* @static
|
|
|
8346 |
*/
|
|
|
8347 |
HTMLTrackElement.LOADED = 2;
|
|
|
8348 |
|
|
|
8349 |
/**
|
|
|
8350 |
* The text track failed to load state.
|
|
|
8351 |
*
|
|
|
8352 |
* @type {number}
|
|
|
8353 |
* @static
|
|
|
8354 |
*/
|
|
|
8355 |
HTMLTrackElement.ERROR = 3;
|
|
|
8356 |
|
|
|
8357 |
/*
|
|
|
8358 |
* This file contains all track properties that are used in
|
|
|
8359 |
* player.js, tech.js, html5.js and possibly other techs in the future.
|
|
|
8360 |
*/
|
|
|
8361 |
|
|
|
8362 |
const NORMAL = {
|
|
|
8363 |
audio: {
|
|
|
8364 |
ListClass: AudioTrackList,
|
|
|
8365 |
TrackClass: AudioTrack,
|
|
|
8366 |
capitalName: 'Audio'
|
|
|
8367 |
},
|
|
|
8368 |
video: {
|
|
|
8369 |
ListClass: VideoTrackList,
|
|
|
8370 |
TrackClass: VideoTrack,
|
|
|
8371 |
capitalName: 'Video'
|
|
|
8372 |
},
|
|
|
8373 |
text: {
|
|
|
8374 |
ListClass: TextTrackList,
|
|
|
8375 |
TrackClass: TextTrack,
|
|
|
8376 |
capitalName: 'Text'
|
|
|
8377 |
}
|
|
|
8378 |
};
|
|
|
8379 |
Object.keys(NORMAL).forEach(function (type) {
|
|
|
8380 |
NORMAL[type].getterName = `${type}Tracks`;
|
|
|
8381 |
NORMAL[type].privateName = `${type}Tracks_`;
|
|
|
8382 |
});
|
|
|
8383 |
const REMOTE = {
|
|
|
8384 |
remoteText: {
|
|
|
8385 |
ListClass: TextTrackList,
|
|
|
8386 |
TrackClass: TextTrack,
|
|
|
8387 |
capitalName: 'RemoteText',
|
|
|
8388 |
getterName: 'remoteTextTracks',
|
|
|
8389 |
privateName: 'remoteTextTracks_'
|
|
|
8390 |
},
|
|
|
8391 |
remoteTextEl: {
|
|
|
8392 |
ListClass: HtmlTrackElementList,
|
|
|
8393 |
TrackClass: HTMLTrackElement,
|
|
|
8394 |
capitalName: 'RemoteTextTrackEls',
|
|
|
8395 |
getterName: 'remoteTextTrackEls',
|
|
|
8396 |
privateName: 'remoteTextTrackEls_'
|
|
|
8397 |
}
|
|
|
8398 |
};
|
|
|
8399 |
const ALL = Object.assign({}, NORMAL, REMOTE);
|
|
|
8400 |
REMOTE.names = Object.keys(REMOTE);
|
|
|
8401 |
NORMAL.names = Object.keys(NORMAL);
|
|
|
8402 |
ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
|
|
|
8403 |
|
|
|
8404 |
var minDoc = {};
|
|
|
8405 |
|
|
|
8406 |
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {};
|
|
|
8407 |
var doccy;
|
|
|
8408 |
if (typeof document !== 'undefined') {
|
|
|
8409 |
doccy = document;
|
|
|
8410 |
} else {
|
|
|
8411 |
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
|
|
|
8412 |
if (!doccy) {
|
|
|
8413 |
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
|
|
|
8414 |
}
|
|
|
8415 |
}
|
|
|
8416 |
var document_1 = doccy;
|
|
|
8417 |
|
|
|
8418 |
/**
|
|
|
8419 |
* Copyright 2013 vtt.js Contributors
|
|
|
8420 |
*
|
|
|
8421 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
8422 |
* you may not use this file except in compliance with the License.
|
|
|
8423 |
* You may obtain a copy of the License at
|
|
|
8424 |
*
|
|
|
8425 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
8426 |
*
|
|
|
8427 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
8428 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
8429 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
8430 |
* See the License for the specific language governing permissions and
|
|
|
8431 |
* limitations under the License.
|
|
|
8432 |
*/
|
|
|
8433 |
|
|
|
8434 |
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
8435 |
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
|
|
8436 |
|
|
|
8437 |
var _objCreate = Object.create || function () {
|
|
|
8438 |
function F() {}
|
|
|
8439 |
return function (o) {
|
|
|
8440 |
if (arguments.length !== 1) {
|
|
|
8441 |
throw new Error('Object.create shim only accepts one parameter.');
|
|
|
8442 |
}
|
|
|
8443 |
F.prototype = o;
|
|
|
8444 |
return new F();
|
|
|
8445 |
};
|
|
|
8446 |
}();
|
|
|
8447 |
|
|
|
8448 |
// Creates a new ParserError object from an errorData object. The errorData
|
|
|
8449 |
// object should have default code and message properties. The default message
|
|
|
8450 |
// property can be overriden by passing in a message parameter.
|
|
|
8451 |
// See ParsingError.Errors below for acceptable errors.
|
|
|
8452 |
function ParsingError(errorData, message) {
|
|
|
8453 |
this.name = "ParsingError";
|
|
|
8454 |
this.code = errorData.code;
|
|
|
8455 |
this.message = message || errorData.message;
|
|
|
8456 |
}
|
|
|
8457 |
ParsingError.prototype = _objCreate(Error.prototype);
|
|
|
8458 |
ParsingError.prototype.constructor = ParsingError;
|
|
|
8459 |
|
|
|
8460 |
// ParsingError metadata for acceptable ParsingErrors.
|
|
|
8461 |
ParsingError.Errors = {
|
|
|
8462 |
BadSignature: {
|
|
|
8463 |
code: 0,
|
|
|
8464 |
message: "Malformed WebVTT signature."
|
|
|
8465 |
},
|
|
|
8466 |
BadTimeStamp: {
|
|
|
8467 |
code: 1,
|
|
|
8468 |
message: "Malformed time stamp."
|
|
|
8469 |
}
|
|
|
8470 |
};
|
|
|
8471 |
|
|
|
8472 |
// Try to parse input as a time stamp.
|
|
|
8473 |
function parseTimeStamp(input) {
|
|
|
8474 |
function computeSeconds(h, m, s, f) {
|
|
|
8475 |
return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
|
|
|
8476 |
}
|
|
|
8477 |
var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/);
|
|
|
8478 |
if (!m) {
|
|
|
8479 |
return null;
|
|
|
8480 |
}
|
|
|
8481 |
if (m[3]) {
|
|
|
8482 |
// Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
|
|
|
8483 |
return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]);
|
|
|
8484 |
} else if (m[1] > 59) {
|
|
|
8485 |
// Timestamp takes the form of [hours]:[minutes].[milliseconds]
|
|
|
8486 |
// First position is hours as it's over 59.
|
|
|
8487 |
return computeSeconds(m[1], m[2], 0, m[4]);
|
|
|
8488 |
} else {
|
|
|
8489 |
// Timestamp takes the form of [minutes]:[seconds].[milliseconds]
|
|
|
8490 |
return computeSeconds(0, m[1], m[2], m[4]);
|
|
|
8491 |
}
|
|
|
8492 |
}
|
|
|
8493 |
|
|
|
8494 |
// A settings object holds key/value pairs and will ignore anything but the first
|
|
|
8495 |
// assignment to a specific key.
|
|
|
8496 |
function Settings() {
|
|
|
8497 |
this.values = _objCreate(null);
|
|
|
8498 |
}
|
|
|
8499 |
Settings.prototype = {
|
|
|
8500 |
// Only accept the first assignment to any key.
|
|
|
8501 |
set: function (k, v) {
|
|
|
8502 |
if (!this.get(k) && v !== "") {
|
|
|
8503 |
this.values[k] = v;
|
|
|
8504 |
}
|
|
|
8505 |
},
|
|
|
8506 |
// Return the value for a key, or a default value.
|
|
|
8507 |
// If 'defaultKey' is passed then 'dflt' is assumed to be an object with
|
|
|
8508 |
// a number of possible default values as properties where 'defaultKey' is
|
|
|
8509 |
// the key of the property that will be chosen; otherwise it's assumed to be
|
|
|
8510 |
// a single value.
|
|
|
8511 |
get: function (k, dflt, defaultKey) {
|
|
|
8512 |
if (defaultKey) {
|
|
|
8513 |
return this.has(k) ? this.values[k] : dflt[defaultKey];
|
|
|
8514 |
}
|
|
|
8515 |
return this.has(k) ? this.values[k] : dflt;
|
|
|
8516 |
},
|
|
|
8517 |
// Check whether we have a value for a key.
|
|
|
8518 |
has: function (k) {
|
|
|
8519 |
return k in this.values;
|
|
|
8520 |
},
|
|
|
8521 |
// Accept a setting if its one of the given alternatives.
|
|
|
8522 |
alt: function (k, v, a) {
|
|
|
8523 |
for (var n = 0; n < a.length; ++n) {
|
|
|
8524 |
if (v === a[n]) {
|
|
|
8525 |
this.set(k, v);
|
|
|
8526 |
break;
|
|
|
8527 |
}
|
|
|
8528 |
}
|
|
|
8529 |
},
|
|
|
8530 |
// Accept a setting if its a valid (signed) integer.
|
|
|
8531 |
integer: function (k, v) {
|
|
|
8532 |
if (/^-?\d+$/.test(v)) {
|
|
|
8533 |
// integer
|
|
|
8534 |
this.set(k, parseInt(v, 10));
|
|
|
8535 |
}
|
|
|
8536 |
},
|
|
|
8537 |
// Accept a setting if its a valid percentage.
|
|
|
8538 |
percent: function (k, v) {
|
|
|
8539 |
if (v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) {
|
|
|
8540 |
v = parseFloat(v);
|
|
|
8541 |
if (v >= 0 && v <= 100) {
|
|
|
8542 |
this.set(k, v);
|
|
|
8543 |
return true;
|
|
|
8544 |
}
|
|
|
8545 |
}
|
|
|
8546 |
return false;
|
|
|
8547 |
}
|
|
|
8548 |
};
|
|
|
8549 |
|
|
|
8550 |
// Helper function to parse input into groups separated by 'groupDelim', and
|
|
|
8551 |
// interprete each group as a key/value pair separated by 'keyValueDelim'.
|
|
|
8552 |
function parseOptions(input, callback, keyValueDelim, groupDelim) {
|
|
|
8553 |
var groups = groupDelim ? input.split(groupDelim) : [input];
|
|
|
8554 |
for (var i in groups) {
|
|
|
8555 |
if (typeof groups[i] !== "string") {
|
|
|
8556 |
continue;
|
|
|
8557 |
}
|
|
|
8558 |
var kv = groups[i].split(keyValueDelim);
|
|
|
8559 |
if (kv.length !== 2) {
|
|
|
8560 |
continue;
|
|
|
8561 |
}
|
|
|
8562 |
var k = kv[0].trim();
|
|
|
8563 |
var v = kv[1].trim();
|
|
|
8564 |
callback(k, v);
|
|
|
8565 |
}
|
|
|
8566 |
}
|
|
|
8567 |
function parseCue(input, cue, regionList) {
|
|
|
8568 |
// Remember the original input if we need to throw an error.
|
|
|
8569 |
var oInput = input;
|
|
|
8570 |
// 4.1 WebVTT timestamp
|
|
|
8571 |
function consumeTimeStamp() {
|
|
|
8572 |
var ts = parseTimeStamp(input);
|
|
|
8573 |
if (ts === null) {
|
|
|
8574 |
throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput);
|
|
|
8575 |
}
|
|
|
8576 |
// Remove time stamp from input.
|
|
|
8577 |
input = input.replace(/^[^\sa-zA-Z-]+/, "");
|
|
|
8578 |
return ts;
|
|
|
8579 |
}
|
|
|
8580 |
|
|
|
8581 |
// 4.4.2 WebVTT cue settings
|
|
|
8582 |
function consumeCueSettings(input, cue) {
|
|
|
8583 |
var settings = new Settings();
|
|
|
8584 |
parseOptions(input, function (k, v) {
|
|
|
8585 |
switch (k) {
|
|
|
8586 |
case "region":
|
|
|
8587 |
// Find the last region we parsed with the same region id.
|
|
|
8588 |
for (var i = regionList.length - 1; i >= 0; i--) {
|
|
|
8589 |
if (regionList[i].id === v) {
|
|
|
8590 |
settings.set(k, regionList[i].region);
|
|
|
8591 |
break;
|
|
|
8592 |
}
|
|
|
8593 |
}
|
|
|
8594 |
break;
|
|
|
8595 |
case "vertical":
|
|
|
8596 |
settings.alt(k, v, ["rl", "lr"]);
|
|
|
8597 |
break;
|
|
|
8598 |
case "line":
|
|
|
8599 |
var vals = v.split(","),
|
|
|
8600 |
vals0 = vals[0];
|
|
|
8601 |
settings.integer(k, vals0);
|
|
|
8602 |
settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
|
|
|
8603 |
settings.alt(k, vals0, ["auto"]);
|
|
|
8604 |
if (vals.length === 2) {
|
|
|
8605 |
settings.alt("lineAlign", vals[1], ["start", "center", "end"]);
|
|
|
8606 |
}
|
|
|
8607 |
break;
|
|
|
8608 |
case "position":
|
|
|
8609 |
vals = v.split(",");
|
|
|
8610 |
settings.percent(k, vals[0]);
|
|
|
8611 |
if (vals.length === 2) {
|
|
|
8612 |
settings.alt("positionAlign", vals[1], ["start", "center", "end"]);
|
|
|
8613 |
}
|
|
|
8614 |
break;
|
|
|
8615 |
case "size":
|
|
|
8616 |
settings.percent(k, v);
|
|
|
8617 |
break;
|
|
|
8618 |
case "align":
|
|
|
8619 |
settings.alt(k, v, ["start", "center", "end", "left", "right"]);
|
|
|
8620 |
break;
|
|
|
8621 |
}
|
|
|
8622 |
}, /:/, /\s/);
|
|
|
8623 |
|
|
|
8624 |
// Apply default values for any missing fields.
|
|
|
8625 |
cue.region = settings.get("region", null);
|
|
|
8626 |
cue.vertical = settings.get("vertical", "");
|
|
|
8627 |
try {
|
|
|
8628 |
cue.line = settings.get("line", "auto");
|
|
|
8629 |
} catch (e) {}
|
|
|
8630 |
cue.lineAlign = settings.get("lineAlign", "start");
|
|
|
8631 |
cue.snapToLines = settings.get("snapToLines", true);
|
|
|
8632 |
cue.size = settings.get("size", 100);
|
|
|
8633 |
// Safari still uses the old middle value and won't accept center
|
|
|
8634 |
try {
|
|
|
8635 |
cue.align = settings.get("align", "center");
|
|
|
8636 |
} catch (e) {
|
|
|
8637 |
cue.align = settings.get("align", "middle");
|
|
|
8638 |
}
|
|
|
8639 |
try {
|
|
|
8640 |
cue.position = settings.get("position", "auto");
|
|
|
8641 |
} catch (e) {
|
|
|
8642 |
cue.position = settings.get("position", {
|
|
|
8643 |
start: 0,
|
|
|
8644 |
left: 0,
|
|
|
8645 |
center: 50,
|
|
|
8646 |
middle: 50,
|
|
|
8647 |
end: 100,
|
|
|
8648 |
right: 100
|
|
|
8649 |
}, cue.align);
|
|
|
8650 |
}
|
|
|
8651 |
cue.positionAlign = settings.get("positionAlign", {
|
|
|
8652 |
start: "start",
|
|
|
8653 |
left: "start",
|
|
|
8654 |
center: "center",
|
|
|
8655 |
middle: "center",
|
|
|
8656 |
end: "end",
|
|
|
8657 |
right: "end"
|
|
|
8658 |
}, cue.align);
|
|
|
8659 |
}
|
|
|
8660 |
function skipWhitespace() {
|
|
|
8661 |
input = input.replace(/^\s+/, "");
|
|
|
8662 |
}
|
|
|
8663 |
|
|
|
8664 |
// 4.1 WebVTT cue timings.
|
|
|
8665 |
skipWhitespace();
|
|
|
8666 |
cue.startTime = consumeTimeStamp(); // (1) collect cue start time
|
|
|
8667 |
skipWhitespace();
|
|
|
8668 |
if (input.substr(0, 3) !== "-->") {
|
|
|
8669 |
// (3) next characters must match "-->"
|
|
|
8670 |
throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput);
|
|
|
8671 |
}
|
|
|
8672 |
input = input.substr(3);
|
|
|
8673 |
skipWhitespace();
|
|
|
8674 |
cue.endTime = consumeTimeStamp(); // (5) collect cue end time
|
|
|
8675 |
|
|
|
8676 |
// 4.1 WebVTT cue settings list.
|
|
|
8677 |
skipWhitespace();
|
|
|
8678 |
consumeCueSettings(input, cue);
|
|
|
8679 |
}
|
|
|
8680 |
|
|
|
8681 |
// When evaluating this file as part of a Webpack bundle for server
|
|
|
8682 |
// side rendering, `document` is an empty object.
|
|
|
8683 |
var TEXTAREA_ELEMENT = document_1.createElement && document_1.createElement("textarea");
|
|
|
8684 |
var TAG_NAME = {
|
|
|
8685 |
c: "span",
|
|
|
8686 |
i: "i",
|
|
|
8687 |
b: "b",
|
|
|
8688 |
u: "u",
|
|
|
8689 |
ruby: "ruby",
|
|
|
8690 |
rt: "rt",
|
|
|
8691 |
v: "span",
|
|
|
8692 |
lang: "span"
|
|
|
8693 |
};
|
|
|
8694 |
|
|
|
8695 |
// 5.1 default text color
|
|
|
8696 |
// 5.2 default text background color is equivalent to text color with bg_ prefix
|
|
|
8697 |
var DEFAULT_COLOR_CLASS = {
|
|
|
8698 |
white: 'rgba(255,255,255,1)',
|
|
|
8699 |
lime: 'rgba(0,255,0,1)',
|
|
|
8700 |
cyan: 'rgba(0,255,255,1)',
|
|
|
8701 |
red: 'rgba(255,0,0,1)',
|
|
|
8702 |
yellow: 'rgba(255,255,0,1)',
|
|
|
8703 |
magenta: 'rgba(255,0,255,1)',
|
|
|
8704 |
blue: 'rgba(0,0,255,1)',
|
|
|
8705 |
black: 'rgba(0,0,0,1)'
|
|
|
8706 |
};
|
|
|
8707 |
var TAG_ANNOTATION = {
|
|
|
8708 |
v: "title",
|
|
|
8709 |
lang: "lang"
|
|
|
8710 |
};
|
|
|
8711 |
var NEEDS_PARENT = {
|
|
|
8712 |
rt: "ruby"
|
|
|
8713 |
};
|
|
|
8714 |
|
|
|
8715 |
// Parse content into a document fragment.
|
|
|
8716 |
function parseContent(window, input) {
|
|
|
8717 |
function nextToken() {
|
|
|
8718 |
// Check for end-of-string.
|
|
|
8719 |
if (!input) {
|
|
|
8720 |
return null;
|
|
|
8721 |
}
|
|
|
8722 |
|
|
|
8723 |
// Consume 'n' characters from the input.
|
|
|
8724 |
function consume(result) {
|
|
|
8725 |
input = input.substr(result.length);
|
|
|
8726 |
return result;
|
|
|
8727 |
}
|
|
|
8728 |
var m = input.match(/^([^<]*)(<[^>]*>?)?/);
|
|
|
8729 |
// If there is some text before the next tag, return it, otherwise return
|
|
|
8730 |
// the tag.
|
|
|
8731 |
return consume(m[1] ? m[1] : m[2]);
|
|
|
8732 |
}
|
|
|
8733 |
function unescape(s) {
|
|
|
8734 |
TEXTAREA_ELEMENT.innerHTML = s;
|
|
|
8735 |
s = TEXTAREA_ELEMENT.textContent;
|
|
|
8736 |
TEXTAREA_ELEMENT.textContent = "";
|
|
|
8737 |
return s;
|
|
|
8738 |
}
|
|
|
8739 |
function shouldAdd(current, element) {
|
|
|
8740 |
return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName;
|
|
|
8741 |
}
|
|
|
8742 |
|
|
|
8743 |
// Create an element for this tag.
|
|
|
8744 |
function createElement(type, annotation) {
|
|
|
8745 |
var tagName = TAG_NAME[type];
|
|
|
8746 |
if (!tagName) {
|
|
|
8747 |
return null;
|
|
|
8748 |
}
|
|
|
8749 |
var element = window.document.createElement(tagName);
|
|
|
8750 |
var name = TAG_ANNOTATION[type];
|
|
|
8751 |
if (name && annotation) {
|
|
|
8752 |
element[name] = annotation.trim();
|
|
|
8753 |
}
|
|
|
8754 |
return element;
|
|
|
8755 |
}
|
|
|
8756 |
var rootDiv = window.document.createElement("div"),
|
|
|
8757 |
current = rootDiv,
|
|
|
8758 |
t,
|
|
|
8759 |
tagStack = [];
|
|
|
8760 |
while ((t = nextToken()) !== null) {
|
|
|
8761 |
if (t[0] === '<') {
|
|
|
8762 |
if (t[1] === "/") {
|
|
|
8763 |
// If the closing tag matches, move back up to the parent node.
|
|
|
8764 |
if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
|
|
|
8765 |
tagStack.pop();
|
|
|
8766 |
current = current.parentNode;
|
|
|
8767 |
}
|
|
|
8768 |
// Otherwise just ignore the end tag.
|
|
|
8769 |
continue;
|
|
|
8770 |
}
|
|
|
8771 |
var ts = parseTimeStamp(t.substr(1, t.length - 2));
|
|
|
8772 |
var node;
|
|
|
8773 |
if (ts) {
|
|
|
8774 |
// Timestamps are lead nodes as well.
|
|
|
8775 |
node = window.document.createProcessingInstruction("timestamp", ts);
|
|
|
8776 |
current.appendChild(node);
|
|
|
8777 |
continue;
|
|
|
8778 |
}
|
|
|
8779 |
var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
|
|
|
8780 |
// If we can't parse the tag, skip to the next tag.
|
|
|
8781 |
if (!m) {
|
|
|
8782 |
continue;
|
|
|
8783 |
}
|
|
|
8784 |
// Try to construct an element, and ignore the tag if we couldn't.
|
|
|
8785 |
node = createElement(m[1], m[3]);
|
|
|
8786 |
if (!node) {
|
|
|
8787 |
continue;
|
|
|
8788 |
}
|
|
|
8789 |
// Determine if the tag should be added based on the context of where it
|
|
|
8790 |
// is placed in the cuetext.
|
|
|
8791 |
if (!shouldAdd(current, node)) {
|
|
|
8792 |
continue;
|
|
|
8793 |
}
|
|
|
8794 |
// Set the class list (as a list of classes, separated by space).
|
|
|
8795 |
if (m[2]) {
|
|
|
8796 |
var classes = m[2].split('.');
|
|
|
8797 |
classes.forEach(function (cl) {
|
|
|
8798 |
var bgColor = /^bg_/.test(cl);
|
|
|
8799 |
// slice out `bg_` if it's a background color
|
|
|
8800 |
var colorName = bgColor ? cl.slice(3) : cl;
|
|
|
8801 |
if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) {
|
|
|
8802 |
var propName = bgColor ? 'background-color' : 'color';
|
|
|
8803 |
var propValue = DEFAULT_COLOR_CLASS[colorName];
|
|
|
8804 |
node.style[propName] = propValue;
|
|
|
8805 |
}
|
|
|
8806 |
});
|
|
|
8807 |
node.className = classes.join(' ');
|
|
|
8808 |
}
|
|
|
8809 |
// Append the node to the current node, and enter the scope of the new
|
|
|
8810 |
// node.
|
|
|
8811 |
tagStack.push(m[1]);
|
|
|
8812 |
current.appendChild(node);
|
|
|
8813 |
current = node;
|
|
|
8814 |
continue;
|
|
|
8815 |
}
|
|
|
8816 |
|
|
|
8817 |
// Text nodes are leaf nodes.
|
|
|
8818 |
current.appendChild(window.document.createTextNode(unescape(t)));
|
|
|
8819 |
}
|
|
|
8820 |
return rootDiv;
|
|
|
8821 |
}
|
|
|
8822 |
|
|
|
8823 |
// This is a list of all the Unicode characters that have a strong
|
|
|
8824 |
// right-to-left category. What this means is that these characters are
|
|
|
8825 |
// written right-to-left for sure. It was generated by pulling all the strong
|
|
|
8826 |
// right-to-left characters out of the Unicode data table. That table can
|
|
|
8827 |
// found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
|
|
8828 |
var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6], [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d], [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6], [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5], [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815], [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858], [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f], [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c], [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1], [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc], [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808], [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855], [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f], [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13], [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58], [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72], [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f], [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32], [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42], [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f], [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59], [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62], [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77], [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b], [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]];
|
|
|
8829 |
function isStrongRTLChar(charCode) {
|
|
|
8830 |
for (var i = 0; i < strongRTLRanges.length; i++) {
|
|
|
8831 |
var currentRange = strongRTLRanges[i];
|
|
|
8832 |
if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
|
|
|
8833 |
return true;
|
|
|
8834 |
}
|
|
|
8835 |
}
|
|
|
8836 |
return false;
|
|
|
8837 |
}
|
|
|
8838 |
function determineBidi(cueDiv) {
|
|
|
8839 |
var nodeStack = [],
|
|
|
8840 |
text = "",
|
|
|
8841 |
charCode;
|
|
|
8842 |
if (!cueDiv || !cueDiv.childNodes) {
|
|
|
8843 |
return "ltr";
|
|
|
8844 |
}
|
|
|
8845 |
function pushNodes(nodeStack, node) {
|
|
|
8846 |
for (var i = node.childNodes.length - 1; i >= 0; i--) {
|
|
|
8847 |
nodeStack.push(node.childNodes[i]);
|
|
|
8848 |
}
|
|
|
8849 |
}
|
|
|
8850 |
function nextTextNode(nodeStack) {
|
|
|
8851 |
if (!nodeStack || !nodeStack.length) {
|
|
|
8852 |
return null;
|
|
|
8853 |
}
|
|
|
8854 |
var node = nodeStack.pop(),
|
|
|
8855 |
text = node.textContent || node.innerText;
|
|
|
8856 |
if (text) {
|
|
|
8857 |
// TODO: This should match all unicode type B characters (paragraph
|
|
|
8858 |
// separator characters). See issue #115.
|
|
|
8859 |
var m = text.match(/^.*(\n|\r)/);
|
|
|
8860 |
if (m) {
|
|
|
8861 |
nodeStack.length = 0;
|
|
|
8862 |
return m[0];
|
|
|
8863 |
}
|
|
|
8864 |
return text;
|
|
|
8865 |
}
|
|
|
8866 |
if (node.tagName === "ruby") {
|
|
|
8867 |
return nextTextNode(nodeStack);
|
|
|
8868 |
}
|
|
|
8869 |
if (node.childNodes) {
|
|
|
8870 |
pushNodes(nodeStack, node);
|
|
|
8871 |
return nextTextNode(nodeStack);
|
|
|
8872 |
}
|
|
|
8873 |
}
|
|
|
8874 |
pushNodes(nodeStack, cueDiv);
|
|
|
8875 |
while (text = nextTextNode(nodeStack)) {
|
|
|
8876 |
for (var i = 0; i < text.length; i++) {
|
|
|
8877 |
charCode = text.charCodeAt(i);
|
|
|
8878 |
if (isStrongRTLChar(charCode)) {
|
|
|
8879 |
return "rtl";
|
|
|
8880 |
}
|
|
|
8881 |
}
|
|
|
8882 |
}
|
|
|
8883 |
return "ltr";
|
|
|
8884 |
}
|
|
|
8885 |
function computeLinePos(cue) {
|
|
|
8886 |
if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) {
|
|
|
8887 |
return cue.line;
|
|
|
8888 |
}
|
|
|
8889 |
if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) {
|
|
|
8890 |
return -1;
|
|
|
8891 |
}
|
|
|
8892 |
var track = cue.track,
|
|
|
8893 |
trackList = track.textTrackList,
|
|
|
8894 |
count = 0;
|
|
|
8895 |
for (var i = 0; i < trackList.length && trackList[i] !== track; i++) {
|
|
|
8896 |
if (trackList[i].mode === "showing") {
|
|
|
8897 |
count++;
|
|
|
8898 |
}
|
|
|
8899 |
}
|
|
|
8900 |
return ++count * -1;
|
|
|
8901 |
}
|
|
|
8902 |
function StyleBox() {}
|
|
|
8903 |
|
|
|
8904 |
// Apply styles to a div. If there is no div passed then it defaults to the
|
|
|
8905 |
// div on 'this'.
|
|
|
8906 |
StyleBox.prototype.applyStyles = function (styles, div) {
|
|
|
8907 |
div = div || this.div;
|
|
|
8908 |
for (var prop in styles) {
|
|
|
8909 |
if (styles.hasOwnProperty(prop)) {
|
|
|
8910 |
div.style[prop] = styles[prop];
|
|
|
8911 |
}
|
|
|
8912 |
}
|
|
|
8913 |
};
|
|
|
8914 |
StyleBox.prototype.formatStyle = function (val, unit) {
|
|
|
8915 |
return val === 0 ? 0 : val + unit;
|
|
|
8916 |
};
|
|
|
8917 |
|
|
|
8918 |
// Constructs the computed display state of the cue (a div). Places the div
|
|
|
8919 |
// into the overlay which should be a block level element (usually a div).
|
|
|
8920 |
function CueStyleBox(window, cue, styleOptions) {
|
|
|
8921 |
StyleBox.call(this);
|
|
|
8922 |
this.cue = cue;
|
|
|
8923 |
|
|
|
8924 |
// Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
|
|
|
8925 |
// have inline positioning and will function as the cue background box.
|
|
|
8926 |
this.cueDiv = parseContent(window, cue.text);
|
|
|
8927 |
var styles = {
|
|
|
8928 |
color: "rgba(255, 255, 255, 1)",
|
|
|
8929 |
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
|
8930 |
position: "relative",
|
|
|
8931 |
left: 0,
|
|
|
8932 |
right: 0,
|
|
|
8933 |
top: 0,
|
|
|
8934 |
bottom: 0,
|
|
|
8935 |
display: "inline",
|
|
|
8936 |
writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
|
|
|
8937 |
unicodeBidi: "plaintext"
|
|
|
8938 |
};
|
|
|
8939 |
this.applyStyles(styles, this.cueDiv);
|
|
|
8940 |
|
|
|
8941 |
// Create an absolutely positioned div that will be used to position the cue
|
|
|
8942 |
// div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
|
|
|
8943 |
// mirrors of them except middle instead of center on Safari.
|
|
|
8944 |
this.div = window.document.createElement("div");
|
|
|
8945 |
styles = {
|
|
|
8946 |
direction: determineBidi(this.cueDiv),
|
|
|
8947 |
writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
|
|
|
8948 |
unicodeBidi: "plaintext",
|
|
|
8949 |
textAlign: cue.align === "middle" ? "center" : cue.align,
|
|
|
8950 |
font: styleOptions.font,
|
|
|
8951 |
whiteSpace: "pre-line",
|
|
|
8952 |
position: "absolute"
|
|
|
8953 |
};
|
|
|
8954 |
this.applyStyles(styles);
|
|
|
8955 |
this.div.appendChild(this.cueDiv);
|
|
|
8956 |
|
|
|
8957 |
// Calculate the distance from the reference edge of the viewport to the text
|
|
|
8958 |
// position of the cue box. The reference edge will be resolved later when
|
|
|
8959 |
// the box orientation styles are applied.
|
|
|
8960 |
var textPos = 0;
|
|
|
8961 |
switch (cue.positionAlign) {
|
|
|
8962 |
case "start":
|
|
|
8963 |
case "line-left":
|
|
|
8964 |
textPos = cue.position;
|
|
|
8965 |
break;
|
|
|
8966 |
case "center":
|
|
|
8967 |
textPos = cue.position - cue.size / 2;
|
|
|
8968 |
break;
|
|
|
8969 |
case "end":
|
|
|
8970 |
case "line-right":
|
|
|
8971 |
textPos = cue.position - cue.size;
|
|
|
8972 |
break;
|
|
|
8973 |
}
|
|
|
8974 |
|
|
|
8975 |
// Horizontal box orientation; textPos is the distance from the left edge of the
|
|
|
8976 |
// area to the left edge of the box and cue.size is the distance extending to
|
|
|
8977 |
// the right from there.
|
|
|
8978 |
if (cue.vertical === "") {
|
|
|
8979 |
this.applyStyles({
|
|
|
8980 |
left: this.formatStyle(textPos, "%"),
|
|
|
8981 |
width: this.formatStyle(cue.size, "%")
|
|
|
8982 |
});
|
|
|
8983 |
// Vertical box orientation; textPos is the distance from the top edge of the
|
|
|
8984 |
// area to the top edge of the box and cue.size is the height extending
|
|
|
8985 |
// downwards from there.
|
|
|
8986 |
} else {
|
|
|
8987 |
this.applyStyles({
|
|
|
8988 |
top: this.formatStyle(textPos, "%"),
|
|
|
8989 |
height: this.formatStyle(cue.size, "%")
|
|
|
8990 |
});
|
|
|
8991 |
}
|
|
|
8992 |
this.move = function (box) {
|
|
|
8993 |
this.applyStyles({
|
|
|
8994 |
top: this.formatStyle(box.top, "px"),
|
|
|
8995 |
bottom: this.formatStyle(box.bottom, "px"),
|
|
|
8996 |
left: this.formatStyle(box.left, "px"),
|
|
|
8997 |
right: this.formatStyle(box.right, "px"),
|
|
|
8998 |
height: this.formatStyle(box.height, "px"),
|
|
|
8999 |
width: this.formatStyle(box.width, "px")
|
|
|
9000 |
});
|
|
|
9001 |
};
|
|
|
9002 |
}
|
|
|
9003 |
CueStyleBox.prototype = _objCreate(StyleBox.prototype);
|
|
|
9004 |
CueStyleBox.prototype.constructor = CueStyleBox;
|
|
|
9005 |
|
|
|
9006 |
// Represents the co-ordinates of an Element in a way that we can easily
|
|
|
9007 |
// compute things with such as if it overlaps or intersects with another Element.
|
|
|
9008 |
// Can initialize it with either a StyleBox or another BoxPosition.
|
|
|
9009 |
function BoxPosition(obj) {
|
|
|
9010 |
// Either a BoxPosition was passed in and we need to copy it, or a StyleBox
|
|
|
9011 |
// was passed in and we need to copy the results of 'getBoundingClientRect'
|
|
|
9012 |
// as the object returned is readonly. All co-ordinate values are in reference
|
|
|
9013 |
// to the viewport origin (top left).
|
|
|
9014 |
var lh, height, width, top;
|
|
|
9015 |
if (obj.div) {
|
|
|
9016 |
height = obj.div.offsetHeight;
|
|
|
9017 |
width = obj.div.offsetWidth;
|
|
|
9018 |
top = obj.div.offsetTop;
|
|
|
9019 |
var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects();
|
|
|
9020 |
obj = obj.div.getBoundingClientRect();
|
|
|
9021 |
// In certain cases the outter div will be slightly larger then the sum of
|
|
|
9022 |
// the inner div's lines. This could be due to bold text, etc, on some platforms.
|
|
|
9023 |
// In this case we should get the average line height and use that. This will
|
|
|
9024 |
// result in the desired behaviour.
|
|
|
9025 |
lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0;
|
|
|
9026 |
}
|
|
|
9027 |
this.left = obj.left;
|
|
|
9028 |
this.right = obj.right;
|
|
|
9029 |
this.top = obj.top || top;
|
|
|
9030 |
this.height = obj.height || height;
|
|
|
9031 |
this.bottom = obj.bottom || top + (obj.height || height);
|
|
|
9032 |
this.width = obj.width || width;
|
|
|
9033 |
this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
|
|
|
9034 |
}
|
|
|
9035 |
|
|
|
9036 |
// Move the box along a particular axis. Optionally pass in an amount to move
|
|
|
9037 |
// the box. If no amount is passed then the default is the line height of the
|
|
|
9038 |
// box.
|
|
|
9039 |
BoxPosition.prototype.move = function (axis, toMove) {
|
|
|
9040 |
toMove = toMove !== undefined ? toMove : this.lineHeight;
|
|
|
9041 |
switch (axis) {
|
|
|
9042 |
case "+x":
|
|
|
9043 |
this.left += toMove;
|
|
|
9044 |
this.right += toMove;
|
|
|
9045 |
break;
|
|
|
9046 |
case "-x":
|
|
|
9047 |
this.left -= toMove;
|
|
|
9048 |
this.right -= toMove;
|
|
|
9049 |
break;
|
|
|
9050 |
case "+y":
|
|
|
9051 |
this.top += toMove;
|
|
|
9052 |
this.bottom += toMove;
|
|
|
9053 |
break;
|
|
|
9054 |
case "-y":
|
|
|
9055 |
this.top -= toMove;
|
|
|
9056 |
this.bottom -= toMove;
|
|
|
9057 |
break;
|
|
|
9058 |
}
|
|
|
9059 |
};
|
|
|
9060 |
|
|
|
9061 |
// Check if this box overlaps another box, b2.
|
|
|
9062 |
BoxPosition.prototype.overlaps = function (b2) {
|
|
|
9063 |
return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top;
|
|
|
9064 |
};
|
|
|
9065 |
|
|
|
9066 |
// Check if this box overlaps any other boxes in boxes.
|
|
|
9067 |
BoxPosition.prototype.overlapsAny = function (boxes) {
|
|
|
9068 |
for (var i = 0; i < boxes.length; i++) {
|
|
|
9069 |
if (this.overlaps(boxes[i])) {
|
|
|
9070 |
return true;
|
|
|
9071 |
}
|
|
|
9072 |
}
|
|
|
9073 |
return false;
|
|
|
9074 |
};
|
|
|
9075 |
|
|
|
9076 |
// Check if this box is within another box.
|
|
|
9077 |
BoxPosition.prototype.within = function (container) {
|
|
|
9078 |
return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right;
|
|
|
9079 |
};
|
|
|
9080 |
|
|
|
9081 |
// Check if this box is entirely within the container or it is overlapping
|
|
|
9082 |
// on the edge opposite of the axis direction passed. For example, if "+x" is
|
|
|
9083 |
// passed and the box is overlapping on the left edge of the container, then
|
|
|
9084 |
// return true.
|
|
|
9085 |
BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) {
|
|
|
9086 |
switch (axis) {
|
|
|
9087 |
case "+x":
|
|
|
9088 |
return this.left < container.left;
|
|
|
9089 |
case "-x":
|
|
|
9090 |
return this.right > container.right;
|
|
|
9091 |
case "+y":
|
|
|
9092 |
return this.top < container.top;
|
|
|
9093 |
case "-y":
|
|
|
9094 |
return this.bottom > container.bottom;
|
|
|
9095 |
}
|
|
|
9096 |
};
|
|
|
9097 |
|
|
|
9098 |
// Find the percentage of the area that this box is overlapping with another
|
|
|
9099 |
// box.
|
|
|
9100 |
BoxPosition.prototype.intersectPercentage = function (b2) {
|
|
|
9101 |
var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
|
|
|
9102 |
y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
|
|
|
9103 |
intersectArea = x * y;
|
|
|
9104 |
return intersectArea / (this.height * this.width);
|
|
|
9105 |
};
|
|
|
9106 |
|
|
|
9107 |
// Convert the positions from this box to CSS compatible positions using
|
|
|
9108 |
// the reference container's positions. This has to be done because this
|
|
|
9109 |
// box's positions are in reference to the viewport origin, whereas, CSS
|
|
|
9110 |
// values are in referecne to their respective edges.
|
|
|
9111 |
BoxPosition.prototype.toCSSCompatValues = function (reference) {
|
|
|
9112 |
return {
|
|
|
9113 |
top: this.top - reference.top,
|
|
|
9114 |
bottom: reference.bottom - this.bottom,
|
|
|
9115 |
left: this.left - reference.left,
|
|
|
9116 |
right: reference.right - this.right,
|
|
|
9117 |
height: this.height,
|
|
|
9118 |
width: this.width
|
|
|
9119 |
};
|
|
|
9120 |
};
|
|
|
9121 |
|
|
|
9122 |
// Get an object that represents the box's position without anything extra.
|
|
|
9123 |
// Can pass a StyleBox, HTMLElement, or another BoxPositon.
|
|
|
9124 |
BoxPosition.getSimpleBoxPosition = function (obj) {
|
|
|
9125 |
var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0;
|
|
|
9126 |
var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0;
|
|
|
9127 |
var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0;
|
|
|
9128 |
obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj;
|
|
|
9129 |
var ret = {
|
|
|
9130 |
left: obj.left,
|
|
|
9131 |
right: obj.right,
|
|
|
9132 |
top: obj.top || top,
|
|
|
9133 |
height: obj.height || height,
|
|
|
9134 |
bottom: obj.bottom || top + (obj.height || height),
|
|
|
9135 |
width: obj.width || width
|
|
|
9136 |
};
|
|
|
9137 |
return ret;
|
|
|
9138 |
};
|
|
|
9139 |
|
|
|
9140 |
// Move a StyleBox to its specified, or next best, position. The containerBox
|
|
|
9141 |
// is the box that contains the StyleBox, such as a div. boxPositions are
|
|
|
9142 |
// a list of other boxes that the styleBox can't overlap with.
|
|
|
9143 |
function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
|
|
|
9144 |
// Find the best position for a cue box, b, on the video. The axis parameter
|
|
|
9145 |
// is a list of axis, the order of which, it will move the box along. For example:
|
|
|
9146 |
// Passing ["+x", "-x"] will move the box first along the x axis in the positive
|
|
|
9147 |
// direction. If it doesn't find a good position for it there it will then move
|
|
|
9148 |
// it along the x axis in the negative direction.
|
|
|
9149 |
function findBestPosition(b, axis) {
|
|
|
9150 |
var bestPosition,
|
|
|
9151 |
specifiedPosition = new BoxPosition(b),
|
|
|
9152 |
percentage = 1; // Highest possible so the first thing we get is better.
|
|
|
9153 |
|
|
|
9154 |
for (var i = 0; i < axis.length; i++) {
|
|
|
9155 |
while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) {
|
|
|
9156 |
b.move(axis[i]);
|
|
|
9157 |
}
|
|
|
9158 |
// We found a spot where we aren't overlapping anything. This is our
|
|
|
9159 |
// best position.
|
|
|
9160 |
if (b.within(containerBox)) {
|
|
|
9161 |
return b;
|
|
|
9162 |
}
|
|
|
9163 |
var p = b.intersectPercentage(containerBox);
|
|
|
9164 |
// If we're outside the container box less then we were on our last try
|
|
|
9165 |
// then remember this position as the best position.
|
|
|
9166 |
if (percentage > p) {
|
|
|
9167 |
bestPosition = new BoxPosition(b);
|
|
|
9168 |
percentage = p;
|
|
|
9169 |
}
|
|
|
9170 |
// Reset the box position to the specified position.
|
|
|
9171 |
b = new BoxPosition(specifiedPosition);
|
|
|
9172 |
}
|
|
|
9173 |
return bestPosition || specifiedPosition;
|
|
|
9174 |
}
|
|
|
9175 |
var boxPosition = new BoxPosition(styleBox),
|
|
|
9176 |
cue = styleBox.cue,
|
|
|
9177 |
linePos = computeLinePos(cue),
|
|
|
9178 |
axis = [];
|
|
|
9179 |
|
|
|
9180 |
// If we have a line number to align the cue to.
|
|
|
9181 |
if (cue.snapToLines) {
|
|
|
9182 |
var size;
|
|
|
9183 |
switch (cue.vertical) {
|
|
|
9184 |
case "":
|
|
|
9185 |
axis = ["+y", "-y"];
|
|
|
9186 |
size = "height";
|
|
|
9187 |
break;
|
|
|
9188 |
case "rl":
|
|
|
9189 |
axis = ["+x", "-x"];
|
|
|
9190 |
size = "width";
|
|
|
9191 |
break;
|
|
|
9192 |
case "lr":
|
|
|
9193 |
axis = ["-x", "+x"];
|
|
|
9194 |
size = "width";
|
|
|
9195 |
break;
|
|
|
9196 |
}
|
|
|
9197 |
var step = boxPosition.lineHeight,
|
|
|
9198 |
position = step * Math.round(linePos),
|
|
|
9199 |
maxPosition = containerBox[size] + step,
|
|
|
9200 |
initialAxis = axis[0];
|
|
|
9201 |
|
|
|
9202 |
// If the specified intial position is greater then the max position then
|
|
|
9203 |
// clamp the box to the amount of steps it would take for the box to
|
|
|
9204 |
// reach the max position.
|
|
|
9205 |
if (Math.abs(position) > maxPosition) {
|
|
|
9206 |
position = position < 0 ? -1 : 1;
|
|
|
9207 |
position *= Math.ceil(maxPosition / step) * step;
|
|
|
9208 |
}
|
|
|
9209 |
|
|
|
9210 |
// If computed line position returns negative then line numbers are
|
|
|
9211 |
// relative to the bottom of the video instead of the top. Therefore, we
|
|
|
9212 |
// need to increase our initial position by the length or width of the
|
|
|
9213 |
// video, depending on the writing direction, and reverse our axis directions.
|
|
|
9214 |
if (linePos < 0) {
|
|
|
9215 |
position += cue.vertical === "" ? containerBox.height : containerBox.width;
|
|
|
9216 |
axis = axis.reverse();
|
|
|
9217 |
}
|
|
|
9218 |
|
|
|
9219 |
// Move the box to the specified position. This may not be its best
|
|
|
9220 |
// position.
|
|
|
9221 |
boxPosition.move(initialAxis, position);
|
|
|
9222 |
} else {
|
|
|
9223 |
// If we have a percentage line value for the cue.
|
|
|
9224 |
var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100;
|
|
|
9225 |
switch (cue.lineAlign) {
|
|
|
9226 |
case "center":
|
|
|
9227 |
linePos -= calculatedPercentage / 2;
|
|
|
9228 |
break;
|
|
|
9229 |
case "end":
|
|
|
9230 |
linePos -= calculatedPercentage;
|
|
|
9231 |
break;
|
|
|
9232 |
}
|
|
|
9233 |
|
|
|
9234 |
// Apply initial line position to the cue box.
|
|
|
9235 |
switch (cue.vertical) {
|
|
|
9236 |
case "":
|
|
|
9237 |
styleBox.applyStyles({
|
|
|
9238 |
top: styleBox.formatStyle(linePos, "%")
|
|
|
9239 |
});
|
|
|
9240 |
break;
|
|
|
9241 |
case "rl":
|
|
|
9242 |
styleBox.applyStyles({
|
|
|
9243 |
left: styleBox.formatStyle(linePos, "%")
|
|
|
9244 |
});
|
|
|
9245 |
break;
|
|
|
9246 |
case "lr":
|
|
|
9247 |
styleBox.applyStyles({
|
|
|
9248 |
right: styleBox.formatStyle(linePos, "%")
|
|
|
9249 |
});
|
|
|
9250 |
break;
|
|
|
9251 |
}
|
|
|
9252 |
axis = ["+y", "-x", "+x", "-y"];
|
|
|
9253 |
|
|
|
9254 |
// Get the box position again after we've applied the specified positioning
|
|
|
9255 |
// to it.
|
|
|
9256 |
boxPosition = new BoxPosition(styleBox);
|
|
|
9257 |
}
|
|
|
9258 |
var bestPosition = findBestPosition(boxPosition, axis);
|
|
|
9259 |
styleBox.move(bestPosition.toCSSCompatValues(containerBox));
|
|
|
9260 |
}
|
|
|
9261 |
function WebVTT$1() {
|
|
|
9262 |
// Nothing
|
|
|
9263 |
}
|
|
|
9264 |
|
|
|
9265 |
// Helper to allow strings to be decoded instead of the default binary utf8 data.
|
|
|
9266 |
WebVTT$1.StringDecoder = function () {
|
|
|
9267 |
return {
|
|
|
9268 |
decode: function (data) {
|
|
|
9269 |
if (!data) {
|
|
|
9270 |
return "";
|
|
|
9271 |
}
|
|
|
9272 |
if (typeof data !== "string") {
|
|
|
9273 |
throw new Error("Error - expected string data.");
|
|
|
9274 |
}
|
|
|
9275 |
return decodeURIComponent(encodeURIComponent(data));
|
|
|
9276 |
}
|
|
|
9277 |
};
|
|
|
9278 |
};
|
|
|
9279 |
WebVTT$1.convertCueToDOMTree = function (window, cuetext) {
|
|
|
9280 |
if (!window || !cuetext) {
|
|
|
9281 |
return null;
|
|
|
9282 |
}
|
|
|
9283 |
return parseContent(window, cuetext);
|
|
|
9284 |
};
|
|
|
9285 |
var FONT_SIZE_PERCENT = 0.05;
|
|
|
9286 |
var FONT_STYLE = "sans-serif";
|
|
|
9287 |
var CUE_BACKGROUND_PADDING = "1.5%";
|
|
|
9288 |
|
|
|
9289 |
// Runs the processing model over the cues and regions passed to it.
|
|
|
9290 |
// @param overlay A block level element (usually a div) that the computed cues
|
|
|
9291 |
// and regions will be placed into.
|
|
|
9292 |
WebVTT$1.processCues = function (window, cues, overlay) {
|
|
|
9293 |
if (!window || !cues || !overlay) {
|
|
|
9294 |
return null;
|
|
|
9295 |
}
|
|
|
9296 |
|
|
|
9297 |
// Remove all previous children.
|
|
|
9298 |
while (overlay.firstChild) {
|
|
|
9299 |
overlay.removeChild(overlay.firstChild);
|
|
|
9300 |
}
|
|
|
9301 |
var paddedOverlay = window.document.createElement("div");
|
|
|
9302 |
paddedOverlay.style.position = "absolute";
|
|
|
9303 |
paddedOverlay.style.left = "0";
|
|
|
9304 |
paddedOverlay.style.right = "0";
|
|
|
9305 |
paddedOverlay.style.top = "0";
|
|
|
9306 |
paddedOverlay.style.bottom = "0";
|
|
|
9307 |
paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
|
|
|
9308 |
overlay.appendChild(paddedOverlay);
|
|
|
9309 |
|
|
|
9310 |
// Determine if we need to compute the display states of the cues. This could
|
|
|
9311 |
// be the case if a cue's state has been changed since the last computation or
|
|
|
9312 |
// if it has not been computed yet.
|
|
|
9313 |
function shouldCompute(cues) {
|
|
|
9314 |
for (var i = 0; i < cues.length; i++) {
|
|
|
9315 |
if (cues[i].hasBeenReset || !cues[i].displayState) {
|
|
|
9316 |
return true;
|
|
|
9317 |
}
|
|
|
9318 |
}
|
|
|
9319 |
return false;
|
|
|
9320 |
}
|
|
|
9321 |
|
|
|
9322 |
// We don't need to recompute the cues' display states. Just reuse them.
|
|
|
9323 |
if (!shouldCompute(cues)) {
|
|
|
9324 |
for (var i = 0; i < cues.length; i++) {
|
|
|
9325 |
paddedOverlay.appendChild(cues[i].displayState);
|
|
|
9326 |
}
|
|
|
9327 |
return;
|
|
|
9328 |
}
|
|
|
9329 |
var boxPositions = [],
|
|
|
9330 |
containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
|
|
|
9331 |
fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
|
|
|
9332 |
var styleOptions = {
|
|
|
9333 |
font: fontSize + "px " + FONT_STYLE
|
|
|
9334 |
};
|
|
|
9335 |
(function () {
|
|
|
9336 |
var styleBox, cue;
|
|
|
9337 |
for (var i = 0; i < cues.length; i++) {
|
|
|
9338 |
cue = cues[i];
|
|
|
9339 |
|
|
|
9340 |
// Compute the intial position and styles of the cue div.
|
|
|
9341 |
styleBox = new CueStyleBox(window, cue, styleOptions);
|
|
|
9342 |
paddedOverlay.appendChild(styleBox.div);
|
|
|
9343 |
|
|
|
9344 |
// Move the cue div to it's correct line position.
|
|
|
9345 |
moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
|
|
|
9346 |
|
|
|
9347 |
// Remember the computed div so that we don't have to recompute it later
|
|
|
9348 |
// if we don't have too.
|
|
|
9349 |
cue.displayState = styleBox.div;
|
|
|
9350 |
boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
|
|
|
9351 |
}
|
|
|
9352 |
})();
|
|
|
9353 |
};
|
|
|
9354 |
WebVTT$1.Parser = function (window, vttjs, decoder) {
|
|
|
9355 |
if (!decoder) {
|
|
|
9356 |
decoder = vttjs;
|
|
|
9357 |
vttjs = {};
|
|
|
9358 |
}
|
|
|
9359 |
if (!vttjs) {
|
|
|
9360 |
vttjs = {};
|
|
|
9361 |
}
|
|
|
9362 |
this.window = window;
|
|
|
9363 |
this.vttjs = vttjs;
|
|
|
9364 |
this.state = "INITIAL";
|
|
|
9365 |
this.buffer = "";
|
|
|
9366 |
this.decoder = decoder || new TextDecoder("utf8");
|
|
|
9367 |
this.regionList = [];
|
|
|
9368 |
};
|
|
|
9369 |
WebVTT$1.Parser.prototype = {
|
|
|
9370 |
// If the error is a ParsingError then report it to the consumer if
|
|
|
9371 |
// possible. If it's not a ParsingError then throw it like normal.
|
|
|
9372 |
reportOrThrowError: function (e) {
|
|
|
9373 |
if (e instanceof ParsingError) {
|
|
|
9374 |
this.onparsingerror && this.onparsingerror(e);
|
|
|
9375 |
} else {
|
|
|
9376 |
throw e;
|
|
|
9377 |
}
|
|
|
9378 |
},
|
|
|
9379 |
parse: function (data) {
|
|
|
9380 |
var self = this;
|
|
|
9381 |
|
|
|
9382 |
// If there is no data then we won't decode it, but will just try to parse
|
|
|
9383 |
// whatever is in buffer already. This may occur in circumstances, for
|
|
|
9384 |
// example when flush() is called.
|
|
|
9385 |
if (data) {
|
|
|
9386 |
// Try to decode the data that we received.
|
|
|
9387 |
self.buffer += self.decoder.decode(data, {
|
|
|
9388 |
stream: true
|
|
|
9389 |
});
|
|
|
9390 |
}
|
|
|
9391 |
function collectNextLine() {
|
|
|
9392 |
var buffer = self.buffer;
|
|
|
9393 |
var pos = 0;
|
|
|
9394 |
while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
|
|
|
9395 |
++pos;
|
|
|
9396 |
}
|
|
|
9397 |
var line = buffer.substr(0, pos);
|
|
|
9398 |
// Advance the buffer early in case we fail below.
|
|
|
9399 |
if (buffer[pos] === '\r') {
|
|
|
9400 |
++pos;
|
|
|
9401 |
}
|
|
|
9402 |
if (buffer[pos] === '\n') {
|
|
|
9403 |
++pos;
|
|
|
9404 |
}
|
|
|
9405 |
self.buffer = buffer.substr(pos);
|
|
|
9406 |
return line;
|
|
|
9407 |
}
|
|
|
9408 |
|
|
|
9409 |
// 3.4 WebVTT region and WebVTT region settings syntax
|
|
|
9410 |
function parseRegion(input) {
|
|
|
9411 |
var settings = new Settings();
|
|
|
9412 |
parseOptions(input, function (k, v) {
|
|
|
9413 |
switch (k) {
|
|
|
9414 |
case "id":
|
|
|
9415 |
settings.set(k, v);
|
|
|
9416 |
break;
|
|
|
9417 |
case "width":
|
|
|
9418 |
settings.percent(k, v);
|
|
|
9419 |
break;
|
|
|
9420 |
case "lines":
|
|
|
9421 |
settings.integer(k, v);
|
|
|
9422 |
break;
|
|
|
9423 |
case "regionanchor":
|
|
|
9424 |
case "viewportanchor":
|
|
|
9425 |
var xy = v.split(',');
|
|
|
9426 |
if (xy.length !== 2) {
|
|
|
9427 |
break;
|
|
|
9428 |
}
|
|
|
9429 |
// We have to make sure both x and y parse, so use a temporary
|
|
|
9430 |
// settings object here.
|
|
|
9431 |
var anchor = new Settings();
|
|
|
9432 |
anchor.percent("x", xy[0]);
|
|
|
9433 |
anchor.percent("y", xy[1]);
|
|
|
9434 |
if (!anchor.has("x") || !anchor.has("y")) {
|
|
|
9435 |
break;
|
|
|
9436 |
}
|
|
|
9437 |
settings.set(k + "X", anchor.get("x"));
|
|
|
9438 |
settings.set(k + "Y", anchor.get("y"));
|
|
|
9439 |
break;
|
|
|
9440 |
case "scroll":
|
|
|
9441 |
settings.alt(k, v, ["up"]);
|
|
|
9442 |
break;
|
|
|
9443 |
}
|
|
|
9444 |
}, /=/, /\s/);
|
|
|
9445 |
|
|
|
9446 |
// Create the region, using default values for any values that were not
|
|
|
9447 |
// specified.
|
|
|
9448 |
if (settings.has("id")) {
|
|
|
9449 |
var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)();
|
|
|
9450 |
region.width = settings.get("width", 100);
|
|
|
9451 |
region.lines = settings.get("lines", 3);
|
|
|
9452 |
region.regionAnchorX = settings.get("regionanchorX", 0);
|
|
|
9453 |
region.regionAnchorY = settings.get("regionanchorY", 100);
|
|
|
9454 |
region.viewportAnchorX = settings.get("viewportanchorX", 0);
|
|
|
9455 |
region.viewportAnchorY = settings.get("viewportanchorY", 100);
|
|
|
9456 |
region.scroll = settings.get("scroll", "");
|
|
|
9457 |
// Register the region.
|
|
|
9458 |
self.onregion && self.onregion(region);
|
|
|
9459 |
// Remember the VTTRegion for later in case we parse any VTTCues that
|
|
|
9460 |
// reference it.
|
|
|
9461 |
self.regionList.push({
|
|
|
9462 |
id: settings.get("id"),
|
|
|
9463 |
region: region
|
|
|
9464 |
});
|
|
|
9465 |
}
|
|
|
9466 |
}
|
|
|
9467 |
|
|
|
9468 |
// draft-pantos-http-live-streaming-20
|
|
|
9469 |
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
|
|
|
9470 |
// 3.5 WebVTT
|
|
|
9471 |
function parseTimestampMap(input) {
|
|
|
9472 |
var settings = new Settings();
|
|
|
9473 |
parseOptions(input, function (k, v) {
|
|
|
9474 |
switch (k) {
|
|
|
9475 |
case "MPEGT":
|
|
|
9476 |
settings.integer(k + 'S', v);
|
|
|
9477 |
break;
|
|
|
9478 |
case "LOCA":
|
|
|
9479 |
settings.set(k + 'L', parseTimeStamp(v));
|
|
|
9480 |
break;
|
|
|
9481 |
}
|
|
|
9482 |
}, /[^\d]:/, /,/);
|
|
|
9483 |
self.ontimestampmap && self.ontimestampmap({
|
|
|
9484 |
"MPEGTS": settings.get("MPEGTS"),
|
|
|
9485 |
"LOCAL": settings.get("LOCAL")
|
|
|
9486 |
});
|
|
|
9487 |
}
|
|
|
9488 |
|
|
|
9489 |
// 3.2 WebVTT metadata header syntax
|
|
|
9490 |
function parseHeader(input) {
|
|
|
9491 |
if (input.match(/X-TIMESTAMP-MAP/)) {
|
|
|
9492 |
// This line contains HLS X-TIMESTAMP-MAP metadata
|
|
|
9493 |
parseOptions(input, function (k, v) {
|
|
|
9494 |
switch (k) {
|
|
|
9495 |
case "X-TIMESTAMP-MAP":
|
|
|
9496 |
parseTimestampMap(v);
|
|
|
9497 |
break;
|
|
|
9498 |
}
|
|
|
9499 |
}, /=/);
|
|
|
9500 |
} else {
|
|
|
9501 |
parseOptions(input, function (k, v) {
|
|
|
9502 |
switch (k) {
|
|
|
9503 |
case "Region":
|
|
|
9504 |
// 3.3 WebVTT region metadata header syntax
|
|
|
9505 |
parseRegion(v);
|
|
|
9506 |
break;
|
|
|
9507 |
}
|
|
|
9508 |
}, /:/);
|
|
|
9509 |
}
|
|
|
9510 |
}
|
|
|
9511 |
|
|
|
9512 |
// 5.1 WebVTT file parsing.
|
|
|
9513 |
try {
|
|
|
9514 |
var line;
|
|
|
9515 |
if (self.state === "INITIAL") {
|
|
|
9516 |
// We can't start parsing until we have the first line.
|
|
|
9517 |
if (!/\r\n|\n/.test(self.buffer)) {
|
|
|
9518 |
return this;
|
|
|
9519 |
}
|
|
|
9520 |
line = collectNextLine();
|
|
|
9521 |
var m = line.match(/^WEBVTT([ \t].*)?$/);
|
|
|
9522 |
if (!m || !m[0]) {
|
|
|
9523 |
throw new ParsingError(ParsingError.Errors.BadSignature);
|
|
|
9524 |
}
|
|
|
9525 |
self.state = "HEADER";
|
|
|
9526 |
}
|
|
|
9527 |
var alreadyCollectedLine = false;
|
|
|
9528 |
while (self.buffer) {
|
|
|
9529 |
// We can't parse a line until we have the full line.
|
|
|
9530 |
if (!/\r\n|\n/.test(self.buffer)) {
|
|
|
9531 |
return this;
|
|
|
9532 |
}
|
|
|
9533 |
if (!alreadyCollectedLine) {
|
|
|
9534 |
line = collectNextLine();
|
|
|
9535 |
} else {
|
|
|
9536 |
alreadyCollectedLine = false;
|
|
|
9537 |
}
|
|
|
9538 |
switch (self.state) {
|
|
|
9539 |
case "HEADER":
|
|
|
9540 |
// 13-18 - Allow a header (metadata) under the WEBVTT line.
|
|
|
9541 |
if (/:/.test(line)) {
|
|
|
9542 |
parseHeader(line);
|
|
|
9543 |
} else if (!line) {
|
|
|
9544 |
// An empty line terminates the header and starts the body (cues).
|
|
|
9545 |
self.state = "ID";
|
|
|
9546 |
}
|
|
|
9547 |
continue;
|
|
|
9548 |
case "NOTE":
|
|
|
9549 |
// Ignore NOTE blocks.
|
|
|
9550 |
if (!line) {
|
|
|
9551 |
self.state = "ID";
|
|
|
9552 |
}
|
|
|
9553 |
continue;
|
|
|
9554 |
case "ID":
|
|
|
9555 |
// Check for the start of NOTE blocks.
|
|
|
9556 |
if (/^NOTE($|[ \t])/.test(line)) {
|
|
|
9557 |
self.state = "NOTE";
|
|
|
9558 |
break;
|
|
|
9559 |
}
|
|
|
9560 |
// 19-29 - Allow any number of line terminators, then initialize new cue values.
|
|
|
9561 |
if (!line) {
|
|
|
9562 |
continue;
|
|
|
9563 |
}
|
|
|
9564 |
self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, "");
|
|
|
9565 |
// Safari still uses the old middle value and won't accept center
|
|
|
9566 |
try {
|
|
|
9567 |
self.cue.align = "center";
|
|
|
9568 |
} catch (e) {
|
|
|
9569 |
self.cue.align = "middle";
|
|
|
9570 |
}
|
|
|
9571 |
self.state = "CUE";
|
|
|
9572 |
// 30-39 - Check if self line contains an optional identifier or timing data.
|
|
|
9573 |
if (line.indexOf("-->") === -1) {
|
|
|
9574 |
self.cue.id = line;
|
|
|
9575 |
continue;
|
|
|
9576 |
}
|
|
|
9577 |
// Process line as start of a cue.
|
|
|
9578 |
/*falls through*/
|
|
|
9579 |
case "CUE":
|
|
|
9580 |
// 40 - Collect cue timings and settings.
|
|
|
9581 |
try {
|
|
|
9582 |
parseCue(line, self.cue, self.regionList);
|
|
|
9583 |
} catch (e) {
|
|
|
9584 |
self.reportOrThrowError(e);
|
|
|
9585 |
// In case of an error ignore rest of the cue.
|
|
|
9586 |
self.cue = null;
|
|
|
9587 |
self.state = "BADCUE";
|
|
|
9588 |
continue;
|
|
|
9589 |
}
|
|
|
9590 |
self.state = "CUETEXT";
|
|
|
9591 |
continue;
|
|
|
9592 |
case "CUETEXT":
|
|
|
9593 |
var hasSubstring = line.indexOf("-->") !== -1;
|
|
|
9594 |
// 34 - If we have an empty line then report the cue.
|
|
|
9595 |
// 35 - If we have the special substring '-->' then report the cue,
|
|
|
9596 |
// but do not collect the line as we need to process the current
|
|
|
9597 |
// one as a new cue.
|
|
|
9598 |
if (!line || hasSubstring && (alreadyCollectedLine = true)) {
|
|
|
9599 |
// We are done parsing self cue.
|
|
|
9600 |
self.oncue && self.oncue(self.cue);
|
|
|
9601 |
self.cue = null;
|
|
|
9602 |
self.state = "ID";
|
|
|
9603 |
continue;
|
|
|
9604 |
}
|
|
|
9605 |
if (self.cue.text) {
|
|
|
9606 |
self.cue.text += "\n";
|
|
|
9607 |
}
|
|
|
9608 |
self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n');
|
|
|
9609 |
continue;
|
|
|
9610 |
case "BADCUE":
|
|
|
9611 |
// BADCUE
|
|
|
9612 |
// 54-62 - Collect and discard the remaining cue.
|
|
|
9613 |
if (!line) {
|
|
|
9614 |
self.state = "ID";
|
|
|
9615 |
}
|
|
|
9616 |
continue;
|
|
|
9617 |
}
|
|
|
9618 |
}
|
|
|
9619 |
} catch (e) {
|
|
|
9620 |
self.reportOrThrowError(e);
|
|
|
9621 |
|
|
|
9622 |
// If we are currently parsing a cue, report what we have.
|
|
|
9623 |
if (self.state === "CUETEXT" && self.cue && self.oncue) {
|
|
|
9624 |
self.oncue(self.cue);
|
|
|
9625 |
}
|
|
|
9626 |
self.cue = null;
|
|
|
9627 |
// Enter BADWEBVTT state if header was not parsed correctly otherwise
|
|
|
9628 |
// another exception occurred so enter BADCUE state.
|
|
|
9629 |
self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
|
|
|
9630 |
}
|
|
|
9631 |
return this;
|
|
|
9632 |
},
|
|
|
9633 |
flush: function () {
|
|
|
9634 |
var self = this;
|
|
|
9635 |
try {
|
|
|
9636 |
// Finish decoding the stream.
|
|
|
9637 |
self.buffer += self.decoder.decode();
|
|
|
9638 |
// Synthesize the end of the current cue or region.
|
|
|
9639 |
if (self.cue || self.state === "HEADER") {
|
|
|
9640 |
self.buffer += "\n\n";
|
|
|
9641 |
self.parse();
|
|
|
9642 |
}
|
|
|
9643 |
// If we've flushed, parsed, and we're still on the INITIAL state then
|
|
|
9644 |
// that means we don't have enough of the stream to parse the first
|
|
|
9645 |
// line.
|
|
|
9646 |
if (self.state === "INITIAL") {
|
|
|
9647 |
throw new ParsingError(ParsingError.Errors.BadSignature);
|
|
|
9648 |
}
|
|
|
9649 |
} catch (e) {
|
|
|
9650 |
self.reportOrThrowError(e);
|
|
|
9651 |
}
|
|
|
9652 |
self.onflush && self.onflush();
|
|
|
9653 |
return this;
|
|
|
9654 |
}
|
|
|
9655 |
};
|
|
|
9656 |
var vtt = WebVTT$1;
|
|
|
9657 |
|
|
|
9658 |
/**
|
|
|
9659 |
* Copyright 2013 vtt.js Contributors
|
|
|
9660 |
*
|
|
|
9661 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
9662 |
* you may not use this file except in compliance with the License.
|
|
|
9663 |
* You may obtain a copy of the License at
|
|
|
9664 |
*
|
|
|
9665 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
9666 |
*
|
|
|
9667 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
9668 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
9669 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
9670 |
* See the License for the specific language governing permissions and
|
|
|
9671 |
* limitations under the License.
|
|
|
9672 |
*/
|
|
|
9673 |
|
|
|
9674 |
var autoKeyword = "auto";
|
|
|
9675 |
var directionSetting = {
|
|
|
9676 |
"": 1,
|
|
|
9677 |
"lr": 1,
|
|
|
9678 |
"rl": 1
|
|
|
9679 |
};
|
|
|
9680 |
var alignSetting = {
|
|
|
9681 |
"start": 1,
|
|
|
9682 |
"center": 1,
|
|
|
9683 |
"end": 1,
|
|
|
9684 |
"left": 1,
|
|
|
9685 |
"right": 1,
|
|
|
9686 |
"auto": 1,
|
|
|
9687 |
"line-left": 1,
|
|
|
9688 |
"line-right": 1
|
|
|
9689 |
};
|
|
|
9690 |
function findDirectionSetting(value) {
|
|
|
9691 |
if (typeof value !== "string") {
|
|
|
9692 |
return false;
|
|
|
9693 |
}
|
|
|
9694 |
var dir = directionSetting[value.toLowerCase()];
|
|
|
9695 |
return dir ? value.toLowerCase() : false;
|
|
|
9696 |
}
|
|
|
9697 |
function findAlignSetting(value) {
|
|
|
9698 |
if (typeof value !== "string") {
|
|
|
9699 |
return false;
|
|
|
9700 |
}
|
|
|
9701 |
var align = alignSetting[value.toLowerCase()];
|
|
|
9702 |
return align ? value.toLowerCase() : false;
|
|
|
9703 |
}
|
|
|
9704 |
function VTTCue(startTime, endTime, text) {
|
|
|
9705 |
/**
|
|
|
9706 |
* Shim implementation specific properties. These properties are not in
|
|
|
9707 |
* the spec.
|
|
|
9708 |
*/
|
|
|
9709 |
|
|
|
9710 |
// Lets us know when the VTTCue's data has changed in such a way that we need
|
|
|
9711 |
// to recompute its display state. This lets us compute its display state
|
|
|
9712 |
// lazily.
|
|
|
9713 |
this.hasBeenReset = false;
|
|
|
9714 |
|
|
|
9715 |
/**
|
|
|
9716 |
* VTTCue and TextTrackCue properties
|
|
|
9717 |
* http://dev.w3.org/html5/webvtt/#vttcue-interface
|
|
|
9718 |
*/
|
|
|
9719 |
|
|
|
9720 |
var _id = "";
|
|
|
9721 |
var _pauseOnExit = false;
|
|
|
9722 |
var _startTime = startTime;
|
|
|
9723 |
var _endTime = endTime;
|
|
|
9724 |
var _text = text;
|
|
|
9725 |
var _region = null;
|
|
|
9726 |
var _vertical = "";
|
|
|
9727 |
var _snapToLines = true;
|
|
|
9728 |
var _line = "auto";
|
|
|
9729 |
var _lineAlign = "start";
|
|
|
9730 |
var _position = "auto";
|
|
|
9731 |
var _positionAlign = "auto";
|
|
|
9732 |
var _size = 100;
|
|
|
9733 |
var _align = "center";
|
|
|
9734 |
Object.defineProperties(this, {
|
|
|
9735 |
"id": {
|
|
|
9736 |
enumerable: true,
|
|
|
9737 |
get: function () {
|
|
|
9738 |
return _id;
|
|
|
9739 |
},
|
|
|
9740 |
set: function (value) {
|
|
|
9741 |
_id = "" + value;
|
|
|
9742 |
}
|
|
|
9743 |
},
|
|
|
9744 |
"pauseOnExit": {
|
|
|
9745 |
enumerable: true,
|
|
|
9746 |
get: function () {
|
|
|
9747 |
return _pauseOnExit;
|
|
|
9748 |
},
|
|
|
9749 |
set: function (value) {
|
|
|
9750 |
_pauseOnExit = !!value;
|
|
|
9751 |
}
|
|
|
9752 |
},
|
|
|
9753 |
"startTime": {
|
|
|
9754 |
enumerable: true,
|
|
|
9755 |
get: function () {
|
|
|
9756 |
return _startTime;
|
|
|
9757 |
},
|
|
|
9758 |
set: function (value) {
|
|
|
9759 |
if (typeof value !== "number") {
|
|
|
9760 |
throw new TypeError("Start time must be set to a number.");
|
|
|
9761 |
}
|
|
|
9762 |
_startTime = value;
|
|
|
9763 |
this.hasBeenReset = true;
|
|
|
9764 |
}
|
|
|
9765 |
},
|
|
|
9766 |
"endTime": {
|
|
|
9767 |
enumerable: true,
|
|
|
9768 |
get: function () {
|
|
|
9769 |
return _endTime;
|
|
|
9770 |
},
|
|
|
9771 |
set: function (value) {
|
|
|
9772 |
if (typeof value !== "number") {
|
|
|
9773 |
throw new TypeError("End time must be set to a number.");
|
|
|
9774 |
}
|
|
|
9775 |
_endTime = value;
|
|
|
9776 |
this.hasBeenReset = true;
|
|
|
9777 |
}
|
|
|
9778 |
},
|
|
|
9779 |
"text": {
|
|
|
9780 |
enumerable: true,
|
|
|
9781 |
get: function () {
|
|
|
9782 |
return _text;
|
|
|
9783 |
},
|
|
|
9784 |
set: function (value) {
|
|
|
9785 |
_text = "" + value;
|
|
|
9786 |
this.hasBeenReset = true;
|
|
|
9787 |
}
|
|
|
9788 |
},
|
|
|
9789 |
"region": {
|
|
|
9790 |
enumerable: true,
|
|
|
9791 |
get: function () {
|
|
|
9792 |
return _region;
|
|
|
9793 |
},
|
|
|
9794 |
set: function (value) {
|
|
|
9795 |
_region = value;
|
|
|
9796 |
this.hasBeenReset = true;
|
|
|
9797 |
}
|
|
|
9798 |
},
|
|
|
9799 |
"vertical": {
|
|
|
9800 |
enumerable: true,
|
|
|
9801 |
get: function () {
|
|
|
9802 |
return _vertical;
|
|
|
9803 |
},
|
|
|
9804 |
set: function (value) {
|
|
|
9805 |
var setting = findDirectionSetting(value);
|
|
|
9806 |
// Have to check for false because the setting an be an empty string.
|
|
|
9807 |
if (setting === false) {
|
|
|
9808 |
throw new SyntaxError("Vertical: an invalid or illegal direction string was specified.");
|
|
|
9809 |
}
|
|
|
9810 |
_vertical = setting;
|
|
|
9811 |
this.hasBeenReset = true;
|
|
|
9812 |
}
|
|
|
9813 |
},
|
|
|
9814 |
"snapToLines": {
|
|
|
9815 |
enumerable: true,
|
|
|
9816 |
get: function () {
|
|
|
9817 |
return _snapToLines;
|
|
|
9818 |
},
|
|
|
9819 |
set: function (value) {
|
|
|
9820 |
_snapToLines = !!value;
|
|
|
9821 |
this.hasBeenReset = true;
|
|
|
9822 |
}
|
|
|
9823 |
},
|
|
|
9824 |
"line": {
|
|
|
9825 |
enumerable: true,
|
|
|
9826 |
get: function () {
|
|
|
9827 |
return _line;
|
|
|
9828 |
},
|
|
|
9829 |
set: function (value) {
|
|
|
9830 |
if (typeof value !== "number" && value !== autoKeyword) {
|
|
|
9831 |
throw new SyntaxError("Line: an invalid number or illegal string was specified.");
|
|
|
9832 |
}
|
|
|
9833 |
_line = value;
|
|
|
9834 |
this.hasBeenReset = true;
|
|
|
9835 |
}
|
|
|
9836 |
},
|
|
|
9837 |
"lineAlign": {
|
|
|
9838 |
enumerable: true,
|
|
|
9839 |
get: function () {
|
|
|
9840 |
return _lineAlign;
|
|
|
9841 |
},
|
|
|
9842 |
set: function (value) {
|
|
|
9843 |
var setting = findAlignSetting(value);
|
|
|
9844 |
if (!setting) {
|
|
|
9845 |
console.warn("lineAlign: an invalid or illegal string was specified.");
|
|
|
9846 |
} else {
|
|
|
9847 |
_lineAlign = setting;
|
|
|
9848 |
this.hasBeenReset = true;
|
|
|
9849 |
}
|
|
|
9850 |
}
|
|
|
9851 |
},
|
|
|
9852 |
"position": {
|
|
|
9853 |
enumerable: true,
|
|
|
9854 |
get: function () {
|
|
|
9855 |
return _position;
|
|
|
9856 |
},
|
|
|
9857 |
set: function (value) {
|
|
|
9858 |
if (value < 0 || value > 100) {
|
|
|
9859 |
throw new Error("Position must be between 0 and 100.");
|
|
|
9860 |
}
|
|
|
9861 |
_position = value;
|
|
|
9862 |
this.hasBeenReset = true;
|
|
|
9863 |
}
|
|
|
9864 |
},
|
|
|
9865 |
"positionAlign": {
|
|
|
9866 |
enumerable: true,
|
|
|
9867 |
get: function () {
|
|
|
9868 |
return _positionAlign;
|
|
|
9869 |
},
|
|
|
9870 |
set: function (value) {
|
|
|
9871 |
var setting = findAlignSetting(value);
|
|
|
9872 |
if (!setting) {
|
|
|
9873 |
console.warn("positionAlign: an invalid or illegal string was specified.");
|
|
|
9874 |
} else {
|
|
|
9875 |
_positionAlign = setting;
|
|
|
9876 |
this.hasBeenReset = true;
|
|
|
9877 |
}
|
|
|
9878 |
}
|
|
|
9879 |
},
|
|
|
9880 |
"size": {
|
|
|
9881 |
enumerable: true,
|
|
|
9882 |
get: function () {
|
|
|
9883 |
return _size;
|
|
|
9884 |
},
|
|
|
9885 |
set: function (value) {
|
|
|
9886 |
if (value < 0 || value > 100) {
|
|
|
9887 |
throw new Error("Size must be between 0 and 100.");
|
|
|
9888 |
}
|
|
|
9889 |
_size = value;
|
|
|
9890 |
this.hasBeenReset = true;
|
|
|
9891 |
}
|
|
|
9892 |
},
|
|
|
9893 |
"align": {
|
|
|
9894 |
enumerable: true,
|
|
|
9895 |
get: function () {
|
|
|
9896 |
return _align;
|
|
|
9897 |
},
|
|
|
9898 |
set: function (value) {
|
|
|
9899 |
var setting = findAlignSetting(value);
|
|
|
9900 |
if (!setting) {
|
|
|
9901 |
throw new SyntaxError("align: an invalid or illegal alignment string was specified.");
|
|
|
9902 |
}
|
|
|
9903 |
_align = setting;
|
|
|
9904 |
this.hasBeenReset = true;
|
|
|
9905 |
}
|
|
|
9906 |
}
|
|
|
9907 |
});
|
|
|
9908 |
|
|
|
9909 |
/**
|
|
|
9910 |
* Other <track> spec defined properties
|
|
|
9911 |
*/
|
|
|
9912 |
|
|
|
9913 |
// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state
|
|
|
9914 |
this.displayState = undefined;
|
|
|
9915 |
}
|
|
|
9916 |
|
|
|
9917 |
/**
|
|
|
9918 |
* VTTCue methods
|
|
|
9919 |
*/
|
|
|
9920 |
|
|
|
9921 |
VTTCue.prototype.getCueAsHTML = function () {
|
|
|
9922 |
// Assume WebVTT.convertCueToDOMTree is on the global.
|
|
|
9923 |
return WebVTT.convertCueToDOMTree(window, this.text);
|
|
|
9924 |
};
|
|
|
9925 |
var vttcue = VTTCue;
|
|
|
9926 |
|
|
|
9927 |
/**
|
|
|
9928 |
* Copyright 2013 vtt.js Contributors
|
|
|
9929 |
*
|
|
|
9930 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
9931 |
* you may not use this file except in compliance with the License.
|
|
|
9932 |
* You may obtain a copy of the License at
|
|
|
9933 |
*
|
|
|
9934 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
9935 |
*
|
|
|
9936 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
9937 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
9938 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
9939 |
* See the License for the specific language governing permissions and
|
|
|
9940 |
* limitations under the License.
|
|
|
9941 |
*/
|
|
|
9942 |
|
|
|
9943 |
var scrollSetting = {
|
|
|
9944 |
"": true,
|
|
|
9945 |
"up": true
|
|
|
9946 |
};
|
|
|
9947 |
function findScrollSetting(value) {
|
|
|
9948 |
if (typeof value !== "string") {
|
|
|
9949 |
return false;
|
|
|
9950 |
}
|
|
|
9951 |
var scroll = scrollSetting[value.toLowerCase()];
|
|
|
9952 |
return scroll ? value.toLowerCase() : false;
|
|
|
9953 |
}
|
|
|
9954 |
function isValidPercentValue(value) {
|
|
|
9955 |
return typeof value === "number" && value >= 0 && value <= 100;
|
|
|
9956 |
}
|
|
|
9957 |
|
|
|
9958 |
// VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface
|
|
|
9959 |
function VTTRegion() {
|
|
|
9960 |
var _width = 100;
|
|
|
9961 |
var _lines = 3;
|
|
|
9962 |
var _regionAnchorX = 0;
|
|
|
9963 |
var _regionAnchorY = 100;
|
|
|
9964 |
var _viewportAnchorX = 0;
|
|
|
9965 |
var _viewportAnchorY = 100;
|
|
|
9966 |
var _scroll = "";
|
|
|
9967 |
Object.defineProperties(this, {
|
|
|
9968 |
"width": {
|
|
|
9969 |
enumerable: true,
|
|
|
9970 |
get: function () {
|
|
|
9971 |
return _width;
|
|
|
9972 |
},
|
|
|
9973 |
set: function (value) {
|
|
|
9974 |
if (!isValidPercentValue(value)) {
|
|
|
9975 |
throw new Error("Width must be between 0 and 100.");
|
|
|
9976 |
}
|
|
|
9977 |
_width = value;
|
|
|
9978 |
}
|
|
|
9979 |
},
|
|
|
9980 |
"lines": {
|
|
|
9981 |
enumerable: true,
|
|
|
9982 |
get: function () {
|
|
|
9983 |
return _lines;
|
|
|
9984 |
},
|
|
|
9985 |
set: function (value) {
|
|
|
9986 |
if (typeof value !== "number") {
|
|
|
9987 |
throw new TypeError("Lines must be set to a number.");
|
|
|
9988 |
}
|
|
|
9989 |
_lines = value;
|
|
|
9990 |
}
|
|
|
9991 |
},
|
|
|
9992 |
"regionAnchorY": {
|
|
|
9993 |
enumerable: true,
|
|
|
9994 |
get: function () {
|
|
|
9995 |
return _regionAnchorY;
|
|
|
9996 |
},
|
|
|
9997 |
set: function (value) {
|
|
|
9998 |
if (!isValidPercentValue(value)) {
|
|
|
9999 |
throw new Error("RegionAnchorX must be between 0 and 100.");
|
|
|
10000 |
}
|
|
|
10001 |
_regionAnchorY = value;
|
|
|
10002 |
}
|
|
|
10003 |
},
|
|
|
10004 |
"regionAnchorX": {
|
|
|
10005 |
enumerable: true,
|
|
|
10006 |
get: function () {
|
|
|
10007 |
return _regionAnchorX;
|
|
|
10008 |
},
|
|
|
10009 |
set: function (value) {
|
|
|
10010 |
if (!isValidPercentValue(value)) {
|
|
|
10011 |
throw new Error("RegionAnchorY must be between 0 and 100.");
|
|
|
10012 |
}
|
|
|
10013 |
_regionAnchorX = value;
|
|
|
10014 |
}
|
|
|
10015 |
},
|
|
|
10016 |
"viewportAnchorY": {
|
|
|
10017 |
enumerable: true,
|
|
|
10018 |
get: function () {
|
|
|
10019 |
return _viewportAnchorY;
|
|
|
10020 |
},
|
|
|
10021 |
set: function (value) {
|
|
|
10022 |
if (!isValidPercentValue(value)) {
|
|
|
10023 |
throw new Error("ViewportAnchorY must be between 0 and 100.");
|
|
|
10024 |
}
|
|
|
10025 |
_viewportAnchorY = value;
|
|
|
10026 |
}
|
|
|
10027 |
},
|
|
|
10028 |
"viewportAnchorX": {
|
|
|
10029 |
enumerable: true,
|
|
|
10030 |
get: function () {
|
|
|
10031 |
return _viewportAnchorX;
|
|
|
10032 |
},
|
|
|
10033 |
set: function (value) {
|
|
|
10034 |
if (!isValidPercentValue(value)) {
|
|
|
10035 |
throw new Error("ViewportAnchorX must be between 0 and 100.");
|
|
|
10036 |
}
|
|
|
10037 |
_viewportAnchorX = value;
|
|
|
10038 |
}
|
|
|
10039 |
},
|
|
|
10040 |
"scroll": {
|
|
|
10041 |
enumerable: true,
|
|
|
10042 |
get: function () {
|
|
|
10043 |
return _scroll;
|
|
|
10044 |
},
|
|
|
10045 |
set: function (value) {
|
|
|
10046 |
var setting = findScrollSetting(value);
|
|
|
10047 |
// Have to check for false as an empty string is a legal value.
|
|
|
10048 |
if (setting === false) {
|
|
|
10049 |
console.warn("Scroll: an invalid or illegal string was specified.");
|
|
|
10050 |
} else {
|
|
|
10051 |
_scroll = setting;
|
|
|
10052 |
}
|
|
|
10053 |
}
|
|
|
10054 |
}
|
|
|
10055 |
});
|
|
|
10056 |
}
|
|
|
10057 |
var vttregion = VTTRegion;
|
|
|
10058 |
|
|
|
10059 |
var browserIndex = createCommonjsModule(function (module) {
|
|
|
10060 |
/**
|
|
|
10061 |
* Copyright 2013 vtt.js Contributors
|
|
|
10062 |
*
|
|
|
10063 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
10064 |
* you may not use this file except in compliance with the License.
|
|
|
10065 |
* You may obtain a copy of the License at
|
|
|
10066 |
*
|
|
|
10067 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
10068 |
*
|
|
|
10069 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
10070 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
10071 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
10072 |
* See the License for the specific language governing permissions and
|
|
|
10073 |
* limitations under the License.
|
|
|
10074 |
*/
|
|
|
10075 |
|
|
|
10076 |
// Default exports for Node. Export the extended versions of VTTCue and
|
|
|
10077 |
// VTTRegion in Node since we likely want the capability to convert back and
|
|
|
10078 |
// forth between JSON. If we don't then it's not that big of a deal since we're
|
|
|
10079 |
// off browser.
|
|
|
10080 |
|
|
|
10081 |
var vttjs = module.exports = {
|
|
|
10082 |
WebVTT: vtt,
|
|
|
10083 |
VTTCue: vttcue,
|
|
|
10084 |
VTTRegion: vttregion
|
|
|
10085 |
};
|
|
|
10086 |
window_1.vttjs = vttjs;
|
|
|
10087 |
window_1.WebVTT = vttjs.WebVTT;
|
|
|
10088 |
var cueShim = vttjs.VTTCue;
|
|
|
10089 |
var regionShim = vttjs.VTTRegion;
|
|
|
10090 |
var nativeVTTCue = window_1.VTTCue;
|
|
|
10091 |
var nativeVTTRegion = window_1.VTTRegion;
|
|
|
10092 |
vttjs.shim = function () {
|
|
|
10093 |
window_1.VTTCue = cueShim;
|
|
|
10094 |
window_1.VTTRegion = regionShim;
|
|
|
10095 |
};
|
|
|
10096 |
vttjs.restore = function () {
|
|
|
10097 |
window_1.VTTCue = nativeVTTCue;
|
|
|
10098 |
window_1.VTTRegion = nativeVTTRegion;
|
|
|
10099 |
};
|
|
|
10100 |
if (!window_1.VTTCue) {
|
|
|
10101 |
vttjs.shim();
|
|
|
10102 |
}
|
|
|
10103 |
});
|
|
|
10104 |
browserIndex.WebVTT;
|
|
|
10105 |
browserIndex.VTTCue;
|
|
|
10106 |
browserIndex.VTTRegion;
|
|
|
10107 |
|
|
|
10108 |
/**
|
|
|
10109 |
* @file tech.js
|
|
|
10110 |
*/
|
|
|
10111 |
|
|
|
10112 |
/**
|
|
|
10113 |
* An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
|
|
|
10114 |
* that just contains the src url alone.
|
|
|
10115 |
* * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
|
|
|
10116 |
* `var SourceString = 'http://example.com/some-video.mp4';`
|
|
|
10117 |
*
|
|
|
10118 |
* @typedef {Object|string} SourceObject
|
|
|
10119 |
*
|
|
|
10120 |
* @property {string} src
|
|
|
10121 |
* The url to the source
|
|
|
10122 |
*
|
|
|
10123 |
* @property {string} type
|
|
|
10124 |
* The mime type of the source
|
|
|
10125 |
*/
|
|
|
10126 |
|
|
|
10127 |
/**
|
|
|
10128 |
* A function used by {@link Tech} to create a new {@link TextTrack}.
|
|
|
10129 |
*
|
|
|
10130 |
* @private
|
|
|
10131 |
*
|
|
|
10132 |
* @param {Tech} self
|
|
|
10133 |
* An instance of the Tech class.
|
|
|
10134 |
*
|
|
|
10135 |
* @param {string} kind
|
|
|
10136 |
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
|
|
|
10137 |
*
|
|
|
10138 |
* @param {string} [label]
|
|
|
10139 |
* Label to identify the text track
|
|
|
10140 |
*
|
|
|
10141 |
* @param {string} [language]
|
|
|
10142 |
* Two letter language abbreviation
|
|
|
10143 |
*
|
|
|
10144 |
* @param {Object} [options={}]
|
|
|
10145 |
* An object with additional text track options
|
|
|
10146 |
*
|
|
|
10147 |
* @return {TextTrack}
|
|
|
10148 |
* The text track that was created.
|
|
|
10149 |
*/
|
|
|
10150 |
function createTrackHelper(self, kind, label, language, options = {}) {
|
|
|
10151 |
const tracks = self.textTracks();
|
|
|
10152 |
options.kind = kind;
|
|
|
10153 |
if (label) {
|
|
|
10154 |
options.label = label;
|
|
|
10155 |
}
|
|
|
10156 |
if (language) {
|
|
|
10157 |
options.language = language;
|
|
|
10158 |
}
|
|
|
10159 |
options.tech = self;
|
|
|
10160 |
const track = new ALL.text.TrackClass(options);
|
|
|
10161 |
tracks.addTrack(track);
|
|
|
10162 |
return track;
|
|
|
10163 |
}
|
|
|
10164 |
|
|
|
10165 |
/**
|
|
|
10166 |
* This is the base class for media playback technology controllers, such as
|
|
|
10167 |
* {@link HTML5}
|
|
|
10168 |
*
|
|
|
10169 |
* @extends Component
|
|
|
10170 |
*/
|
|
|
10171 |
class Tech extends Component$1 {
|
|
|
10172 |
/**
|
|
|
10173 |
* Create an instance of this Tech.
|
|
|
10174 |
*
|
|
|
10175 |
* @param {Object} [options]
|
|
|
10176 |
* The key/value store of player options.
|
|
|
10177 |
*
|
|
|
10178 |
* @param {Function} [ready]
|
|
|
10179 |
* Callback function to call when the `HTML5` Tech is ready.
|
|
|
10180 |
*/
|
|
|
10181 |
constructor(options = {}, ready = function () {}) {
|
|
|
10182 |
// we don't want the tech to report user activity automatically.
|
|
|
10183 |
// This is done manually in addControlsListeners
|
|
|
10184 |
options.reportTouchActivity = false;
|
|
|
10185 |
super(null, options, ready);
|
|
|
10186 |
this.onDurationChange_ = e => this.onDurationChange(e);
|
|
|
10187 |
this.trackProgress_ = e => this.trackProgress(e);
|
|
|
10188 |
this.trackCurrentTime_ = e => this.trackCurrentTime(e);
|
|
|
10189 |
this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
|
|
|
10190 |
this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
|
|
|
10191 |
this.queuedHanders_ = new Set();
|
|
|
10192 |
|
|
|
10193 |
// keep track of whether the current source has played at all to
|
|
|
10194 |
// implement a very limited played()
|
|
|
10195 |
this.hasStarted_ = false;
|
|
|
10196 |
this.on('playing', function () {
|
|
|
10197 |
this.hasStarted_ = true;
|
|
|
10198 |
});
|
|
|
10199 |
this.on('loadstart', function () {
|
|
|
10200 |
this.hasStarted_ = false;
|
|
|
10201 |
});
|
|
|
10202 |
ALL.names.forEach(name => {
|
|
|
10203 |
const props = ALL[name];
|
|
|
10204 |
if (options && options[props.getterName]) {
|
|
|
10205 |
this[props.privateName] = options[props.getterName];
|
|
|
10206 |
}
|
|
|
10207 |
});
|
|
|
10208 |
|
|
|
10209 |
// Manually track progress in cases where the browser/tech doesn't report it.
|
|
|
10210 |
if (!this.featuresProgressEvents) {
|
|
|
10211 |
this.manualProgressOn();
|
|
|
10212 |
}
|
|
|
10213 |
|
|
|
10214 |
// Manually track timeupdates in cases where the browser/tech doesn't report it.
|
|
|
10215 |
if (!this.featuresTimeupdateEvents) {
|
|
|
10216 |
this.manualTimeUpdatesOn();
|
|
|
10217 |
}
|
|
|
10218 |
['Text', 'Audio', 'Video'].forEach(track => {
|
|
|
10219 |
if (options[`native${track}Tracks`] === false) {
|
|
|
10220 |
this[`featuresNative${track}Tracks`] = false;
|
|
|
10221 |
}
|
|
|
10222 |
});
|
|
|
10223 |
if (options.nativeCaptions === false || options.nativeTextTracks === false) {
|
|
|
10224 |
this.featuresNativeTextTracks = false;
|
|
|
10225 |
} else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
|
|
|
10226 |
this.featuresNativeTextTracks = true;
|
|
|
10227 |
}
|
|
|
10228 |
if (!this.featuresNativeTextTracks) {
|
|
|
10229 |
this.emulateTextTracks();
|
|
|
10230 |
}
|
|
|
10231 |
this.preloadTextTracks = options.preloadTextTracks !== false;
|
|
|
10232 |
this.autoRemoteTextTracks_ = new ALL.text.ListClass();
|
|
|
10233 |
this.initTrackListeners();
|
|
|
10234 |
|
|
|
10235 |
// Turn on component tap events only if not using native controls
|
|
|
10236 |
if (!options.nativeControlsForTouch) {
|
|
|
10237 |
this.emitTapEvents();
|
|
|
10238 |
}
|
|
|
10239 |
if (this.constructor) {
|
|
|
10240 |
this.name_ = this.constructor.name || 'Unknown Tech';
|
|
|
10241 |
}
|
|
|
10242 |
}
|
|
|
10243 |
|
|
|
10244 |
/**
|
|
|
10245 |
* A special function to trigger source set in a way that will allow player
|
|
|
10246 |
* to re-trigger if the player or tech are not ready yet.
|
|
|
10247 |
*
|
|
|
10248 |
* @fires Tech#sourceset
|
|
|
10249 |
* @param {string} src The source string at the time of the source changing.
|
|
|
10250 |
*/
|
|
|
10251 |
triggerSourceset(src) {
|
|
|
10252 |
if (!this.isReady_) {
|
|
|
10253 |
// on initial ready we have to trigger source set
|
|
|
10254 |
// 1ms after ready so that player can watch for it.
|
|
|
10255 |
this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
|
|
|
10256 |
}
|
|
|
10257 |
|
|
|
10258 |
/**
|
|
|
10259 |
* Fired when the source is set on the tech causing the media element
|
|
|
10260 |
* to reload.
|
|
|
10261 |
*
|
|
|
10262 |
* @see {@link Player#event:sourceset}
|
|
|
10263 |
* @event Tech#sourceset
|
|
|
10264 |
* @type {Event}
|
|
|
10265 |
*/
|
|
|
10266 |
this.trigger({
|
|
|
10267 |
src,
|
|
|
10268 |
type: 'sourceset'
|
|
|
10269 |
});
|
|
|
10270 |
}
|
|
|
10271 |
|
|
|
10272 |
/* Fallbacks for unsupported event types
|
|
|
10273 |
================================================================================ */
|
|
|
10274 |
|
|
|
10275 |
/**
|
|
|
10276 |
* Polyfill the `progress` event for browsers that don't support it natively.
|
|
|
10277 |
*
|
|
|
10278 |
* @see {@link Tech#trackProgress}
|
|
|
10279 |
*/
|
|
|
10280 |
manualProgressOn() {
|
|
|
10281 |
this.on('durationchange', this.onDurationChange_);
|
|
|
10282 |
this.manualProgress = true;
|
|
|
10283 |
|
|
|
10284 |
// Trigger progress watching when a source begins loading
|
|
|
10285 |
this.one('ready', this.trackProgress_);
|
|
|
10286 |
}
|
|
|
10287 |
|
|
|
10288 |
/**
|
|
|
10289 |
* Turn off the polyfill for `progress` events that was created in
|
|
|
10290 |
* {@link Tech#manualProgressOn}
|
|
|
10291 |
*/
|
|
|
10292 |
manualProgressOff() {
|
|
|
10293 |
this.manualProgress = false;
|
|
|
10294 |
this.stopTrackingProgress();
|
|
|
10295 |
this.off('durationchange', this.onDurationChange_);
|
|
|
10296 |
}
|
|
|
10297 |
|
|
|
10298 |
/**
|
|
|
10299 |
* This is used to trigger a `progress` event when the buffered percent changes. It
|
|
|
10300 |
* sets an interval function that will be called every 500 milliseconds to check if the
|
|
|
10301 |
* buffer end percent has changed.
|
|
|
10302 |
*
|
|
|
10303 |
* > This function is called by {@link Tech#manualProgressOn}
|
|
|
10304 |
*
|
|
|
10305 |
* @param {Event} event
|
|
|
10306 |
* The `ready` event that caused this to run.
|
|
|
10307 |
*
|
|
|
10308 |
* @listens Tech#ready
|
|
|
10309 |
* @fires Tech#progress
|
|
|
10310 |
*/
|
|
|
10311 |
trackProgress(event) {
|
|
|
10312 |
this.stopTrackingProgress();
|
|
|
10313 |
this.progressInterval = this.setInterval(bind_(this, function () {
|
|
|
10314 |
// Don't trigger unless buffered amount is greater than last time
|
|
|
10315 |
|
|
|
10316 |
const numBufferedPercent = this.bufferedPercent();
|
|
|
10317 |
if (this.bufferedPercent_ !== numBufferedPercent) {
|
|
|
10318 |
/**
|
|
|
10319 |
* See {@link Player#progress}
|
|
|
10320 |
*
|
|
|
10321 |
* @event Tech#progress
|
|
|
10322 |
* @type {Event}
|
|
|
10323 |
*/
|
|
|
10324 |
this.trigger('progress');
|
|
|
10325 |
}
|
|
|
10326 |
this.bufferedPercent_ = numBufferedPercent;
|
|
|
10327 |
if (numBufferedPercent === 1) {
|
|
|
10328 |
this.stopTrackingProgress();
|
|
|
10329 |
}
|
|
|
10330 |
}), 500);
|
|
|
10331 |
}
|
|
|
10332 |
|
|
|
10333 |
/**
|
|
|
10334 |
* Update our internal duration on a `durationchange` event by calling
|
|
|
10335 |
* {@link Tech#duration}.
|
|
|
10336 |
*
|
|
|
10337 |
* @param {Event} event
|
|
|
10338 |
* The `durationchange` event that caused this to run.
|
|
|
10339 |
*
|
|
|
10340 |
* @listens Tech#durationchange
|
|
|
10341 |
*/
|
|
|
10342 |
onDurationChange(event) {
|
|
|
10343 |
this.duration_ = this.duration();
|
|
|
10344 |
}
|
|
|
10345 |
|
|
|
10346 |
/**
|
|
|
10347 |
* Get and create a `TimeRange` object for buffering.
|
|
|
10348 |
*
|
|
|
10349 |
* @return { import('../utils/time').TimeRange }
|
|
|
10350 |
* The time range object that was created.
|
|
|
10351 |
*/
|
|
|
10352 |
buffered() {
|
|
|
10353 |
return createTimeRanges$1(0, 0);
|
|
|
10354 |
}
|
|
|
10355 |
|
|
|
10356 |
/**
|
|
|
10357 |
* Get the percentage of the current video that is currently buffered.
|
|
|
10358 |
*
|
|
|
10359 |
* @return {number}
|
|
|
10360 |
* A number from 0 to 1 that represents the decimal percentage of the
|
|
|
10361 |
* video that is buffered.
|
|
|
10362 |
*
|
|
|
10363 |
*/
|
|
|
10364 |
bufferedPercent() {
|
|
|
10365 |
return bufferedPercent(this.buffered(), this.duration_);
|
|
|
10366 |
}
|
|
|
10367 |
|
|
|
10368 |
/**
|
|
|
10369 |
* Turn off the polyfill for `progress` events that was created in
|
|
|
10370 |
* {@link Tech#manualProgressOn}
|
|
|
10371 |
* Stop manually tracking progress events by clearing the interval that was set in
|
|
|
10372 |
* {@link Tech#trackProgress}.
|
|
|
10373 |
*/
|
|
|
10374 |
stopTrackingProgress() {
|
|
|
10375 |
this.clearInterval(this.progressInterval);
|
|
|
10376 |
}
|
|
|
10377 |
|
|
|
10378 |
/**
|
|
|
10379 |
* Polyfill the `timeupdate` event for browsers that don't support it.
|
|
|
10380 |
*
|
|
|
10381 |
* @see {@link Tech#trackCurrentTime}
|
|
|
10382 |
*/
|
|
|
10383 |
manualTimeUpdatesOn() {
|
|
|
10384 |
this.manualTimeUpdates = true;
|
|
|
10385 |
this.on('play', this.trackCurrentTime_);
|
|
|
10386 |
this.on('pause', this.stopTrackingCurrentTime_);
|
|
|
10387 |
}
|
|
|
10388 |
|
|
|
10389 |
/**
|
|
|
10390 |
* Turn off the polyfill for `timeupdate` events that was created in
|
|
|
10391 |
* {@link Tech#manualTimeUpdatesOn}
|
|
|
10392 |
*/
|
|
|
10393 |
manualTimeUpdatesOff() {
|
|
|
10394 |
this.manualTimeUpdates = false;
|
|
|
10395 |
this.stopTrackingCurrentTime();
|
|
|
10396 |
this.off('play', this.trackCurrentTime_);
|
|
|
10397 |
this.off('pause', this.stopTrackingCurrentTime_);
|
|
|
10398 |
}
|
|
|
10399 |
|
|
|
10400 |
/**
|
|
|
10401 |
* Sets up an interval function to track current time and trigger `timeupdate` every
|
|
|
10402 |
* 250 milliseconds.
|
|
|
10403 |
*
|
|
|
10404 |
* @listens Tech#play
|
|
|
10405 |
* @triggers Tech#timeupdate
|
|
|
10406 |
*/
|
|
|
10407 |
trackCurrentTime() {
|
|
|
10408 |
if (this.currentTimeInterval) {
|
|
|
10409 |
this.stopTrackingCurrentTime();
|
|
|
10410 |
}
|
|
|
10411 |
this.currentTimeInterval = this.setInterval(function () {
|
|
|
10412 |
/**
|
|
|
10413 |
* Triggered at an interval of 250ms to indicated that time is passing in the video.
|
|
|
10414 |
*
|
|
|
10415 |
* @event Tech#timeupdate
|
|
|
10416 |
* @type {Event}
|
|
|
10417 |
*/
|
|
|
10418 |
this.trigger({
|
|
|
10419 |
type: 'timeupdate',
|
|
|
10420 |
target: this,
|
|
|
10421 |
manuallyTriggered: true
|
|
|
10422 |
});
|
|
|
10423 |
|
|
|
10424 |
// 42 = 24 fps // 250 is what Webkit uses // FF uses 15
|
|
|
10425 |
}, 250);
|
|
|
10426 |
}
|
|
|
10427 |
|
|
|
10428 |
/**
|
|
|
10429 |
* Stop the interval function created in {@link Tech#trackCurrentTime} so that the
|
|
|
10430 |
* `timeupdate` event is no longer triggered.
|
|
|
10431 |
*
|
|
|
10432 |
* @listens {Tech#pause}
|
|
|
10433 |
*/
|
|
|
10434 |
stopTrackingCurrentTime() {
|
|
|
10435 |
this.clearInterval(this.currentTimeInterval);
|
|
|
10436 |
|
|
|
10437 |
// #1002 - if the video ends right before the next timeupdate would happen,
|
|
|
10438 |
// the progress bar won't make it all the way to the end
|
|
|
10439 |
this.trigger({
|
|
|
10440 |
type: 'timeupdate',
|
|
|
10441 |
target: this,
|
|
|
10442 |
manuallyTriggered: true
|
|
|
10443 |
});
|
|
|
10444 |
}
|
|
|
10445 |
|
|
|
10446 |
/**
|
|
|
10447 |
* Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
|
|
|
10448 |
* {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
|
|
|
10449 |
*
|
|
|
10450 |
* @fires Component#dispose
|
|
|
10451 |
*/
|
|
|
10452 |
dispose() {
|
|
|
10453 |
// clear out all tracks because we can't reuse them between techs
|
|
|
10454 |
this.clearTracks(NORMAL.names);
|
|
|
10455 |
|
|
|
10456 |
// Turn off any manual progress or timeupdate tracking
|
|
|
10457 |
if (this.manualProgress) {
|
|
|
10458 |
this.manualProgressOff();
|
|
|
10459 |
}
|
|
|
10460 |
if (this.manualTimeUpdates) {
|
|
|
10461 |
this.manualTimeUpdatesOff();
|
|
|
10462 |
}
|
|
|
10463 |
super.dispose();
|
|
|
10464 |
}
|
|
|
10465 |
|
|
|
10466 |
/**
|
|
|
10467 |
* Clear out a single `TrackList` or an array of `TrackLists` given their names.
|
|
|
10468 |
*
|
|
|
10469 |
* > Note: Techs without source handlers should call this between sources for `video`
|
|
|
10470 |
* & `audio` tracks. You don't want to use them between tracks!
|
|
|
10471 |
*
|
|
|
10472 |
* @param {string[]|string} types
|
|
|
10473 |
* TrackList names to clear, valid names are `video`, `audio`, and
|
|
|
10474 |
* `text`.
|
|
|
10475 |
*/
|
|
|
10476 |
clearTracks(types) {
|
|
|
10477 |
types = [].concat(types);
|
|
|
10478 |
// clear out all tracks because we can't reuse them between techs
|
|
|
10479 |
types.forEach(type => {
|
|
|
10480 |
const list = this[`${type}Tracks`]() || [];
|
|
|
10481 |
let i = list.length;
|
|
|
10482 |
while (i--) {
|
|
|
10483 |
const track = list[i];
|
|
|
10484 |
if (type === 'text') {
|
|
|
10485 |
this.removeRemoteTextTrack(track);
|
|
|
10486 |
}
|
|
|
10487 |
list.removeTrack(track);
|
|
|
10488 |
}
|
|
|
10489 |
});
|
|
|
10490 |
}
|
|
|
10491 |
|
|
|
10492 |
/**
|
|
|
10493 |
* Remove any TextTracks added via addRemoteTextTrack that are
|
|
|
10494 |
* flagged for automatic garbage collection
|
|
|
10495 |
*/
|
|
|
10496 |
cleanupAutoTextTracks() {
|
|
|
10497 |
const list = this.autoRemoteTextTracks_ || [];
|
|
|
10498 |
let i = list.length;
|
|
|
10499 |
while (i--) {
|
|
|
10500 |
const track = list[i];
|
|
|
10501 |
this.removeRemoteTextTrack(track);
|
|
|
10502 |
}
|
|
|
10503 |
}
|
|
|
10504 |
|
|
|
10505 |
/**
|
|
|
10506 |
* Reset the tech, which will removes all sources and reset the internal readyState.
|
|
|
10507 |
*
|
|
|
10508 |
* @abstract
|
|
|
10509 |
*/
|
|
|
10510 |
reset() {}
|
|
|
10511 |
|
|
|
10512 |
/**
|
|
|
10513 |
* Get the value of `crossOrigin` from the tech.
|
|
|
10514 |
*
|
|
|
10515 |
* @abstract
|
|
|
10516 |
*
|
|
|
10517 |
* @see {Html5#crossOrigin}
|
|
|
10518 |
*/
|
|
|
10519 |
crossOrigin() {}
|
|
|
10520 |
|
|
|
10521 |
/**
|
|
|
10522 |
* Set the value of `crossOrigin` on the tech.
|
|
|
10523 |
*
|
|
|
10524 |
* @abstract
|
|
|
10525 |
*
|
|
|
10526 |
* @param {string} crossOrigin the crossOrigin value
|
|
|
10527 |
* @see {Html5#setCrossOrigin}
|
|
|
10528 |
*/
|
|
|
10529 |
setCrossOrigin() {}
|
|
|
10530 |
|
|
|
10531 |
/**
|
|
|
10532 |
* Get or set an error on the Tech.
|
|
|
10533 |
*
|
|
|
10534 |
* @param {MediaError} [err]
|
|
|
10535 |
* Error to set on the Tech
|
|
|
10536 |
*
|
|
|
10537 |
* @return {MediaError|null}
|
|
|
10538 |
* The current error object on the tech, or null if there isn't one.
|
|
|
10539 |
*/
|
|
|
10540 |
error(err) {
|
|
|
10541 |
if (err !== undefined) {
|
|
|
10542 |
this.error_ = new MediaError(err);
|
|
|
10543 |
this.trigger('error');
|
|
|
10544 |
}
|
|
|
10545 |
return this.error_;
|
|
|
10546 |
}
|
|
|
10547 |
|
|
|
10548 |
/**
|
|
|
10549 |
* Returns the `TimeRange`s that have been played through for the current source.
|
|
|
10550 |
*
|
|
|
10551 |
* > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
|
|
|
10552 |
* It only checks whether the source has played at all or not.
|
|
|
10553 |
*
|
|
|
10554 |
* @return { import('../utils/time').TimeRange }
|
|
|
10555 |
* - A single time range if this video has played
|
|
|
10556 |
* - An empty set of ranges if not.
|
|
|
10557 |
*/
|
|
|
10558 |
played() {
|
|
|
10559 |
if (this.hasStarted_) {
|
|
|
10560 |
return createTimeRanges$1(0, 0);
|
|
|
10561 |
}
|
|
|
10562 |
return createTimeRanges$1();
|
|
|
10563 |
}
|
|
|
10564 |
|
|
|
10565 |
/**
|
|
|
10566 |
* Start playback
|
|
|
10567 |
*
|
|
|
10568 |
* @abstract
|
|
|
10569 |
*
|
|
|
10570 |
* @see {Html5#play}
|
|
|
10571 |
*/
|
|
|
10572 |
play() {}
|
|
|
10573 |
|
|
|
10574 |
/**
|
|
|
10575 |
* Set whether we are scrubbing or not
|
|
|
10576 |
*
|
|
|
10577 |
* @abstract
|
|
|
10578 |
* @param {boolean} _isScrubbing
|
|
|
10579 |
* - true for we are currently scrubbing
|
|
|
10580 |
* - false for we are no longer scrubbing
|
|
|
10581 |
*
|
|
|
10582 |
* @see {Html5#setScrubbing}
|
|
|
10583 |
*/
|
|
|
10584 |
setScrubbing(_isScrubbing) {}
|
|
|
10585 |
|
|
|
10586 |
/**
|
|
|
10587 |
* Get whether we are scrubbing or not
|
|
|
10588 |
*
|
|
|
10589 |
* @abstract
|
|
|
10590 |
*
|
|
|
10591 |
* @see {Html5#scrubbing}
|
|
|
10592 |
*/
|
|
|
10593 |
scrubbing() {}
|
|
|
10594 |
|
|
|
10595 |
/**
|
|
|
10596 |
* Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
|
|
|
10597 |
* previously called.
|
|
|
10598 |
*
|
|
|
10599 |
* @param {number} _seconds
|
|
|
10600 |
* Set the current time of the media to this.
|
|
|
10601 |
* @fires Tech#timeupdate
|
|
|
10602 |
*/
|
|
|
10603 |
setCurrentTime(_seconds) {
|
|
|
10604 |
// improve the accuracy of manual timeupdates
|
|
|
10605 |
if (this.manualTimeUpdates) {
|
|
|
10606 |
/**
|
|
|
10607 |
* A manual `timeupdate` event.
|
|
|
10608 |
*
|
|
|
10609 |
* @event Tech#timeupdate
|
|
|
10610 |
* @type {Event}
|
|
|
10611 |
*/
|
|
|
10612 |
this.trigger({
|
|
|
10613 |
type: 'timeupdate',
|
|
|
10614 |
target: this,
|
|
|
10615 |
manuallyTriggered: true
|
|
|
10616 |
});
|
|
|
10617 |
}
|
|
|
10618 |
}
|
|
|
10619 |
|
|
|
10620 |
/**
|
|
|
10621 |
* Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
|
|
|
10622 |
* {@link TextTrackList} events.
|
|
|
10623 |
*
|
|
|
10624 |
* This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
|
|
|
10625 |
*
|
|
|
10626 |
* @fires Tech#audiotrackchange
|
|
|
10627 |
* @fires Tech#videotrackchange
|
|
|
10628 |
* @fires Tech#texttrackchange
|
|
|
10629 |
*/
|
|
|
10630 |
initTrackListeners() {
|
|
|
10631 |
/**
|
|
|
10632 |
* Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
|
|
|
10633 |
*
|
|
|
10634 |
* @event Tech#audiotrackchange
|
|
|
10635 |
* @type {Event}
|
|
|
10636 |
*/
|
|
|
10637 |
|
|
|
10638 |
/**
|
|
|
10639 |
* Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
|
|
|
10640 |
*
|
|
|
10641 |
* @event Tech#videotrackchange
|
|
|
10642 |
* @type {Event}
|
|
|
10643 |
*/
|
|
|
10644 |
|
|
|
10645 |
/**
|
|
|
10646 |
* Triggered when tracks are added or removed on the Tech {@link TextTrackList}
|
|
|
10647 |
*
|
|
|
10648 |
* @event Tech#texttrackchange
|
|
|
10649 |
* @type {Event}
|
|
|
10650 |
*/
|
|
|
10651 |
NORMAL.names.forEach(name => {
|
|
|
10652 |
const props = NORMAL[name];
|
|
|
10653 |
const trackListChanges = () => {
|
|
|
10654 |
this.trigger(`${name}trackchange`);
|
|
|
10655 |
};
|
|
|
10656 |
const tracks = this[props.getterName]();
|
|
|
10657 |
tracks.addEventListener('removetrack', trackListChanges);
|
|
|
10658 |
tracks.addEventListener('addtrack', trackListChanges);
|
|
|
10659 |
this.on('dispose', () => {
|
|
|
10660 |
tracks.removeEventListener('removetrack', trackListChanges);
|
|
|
10661 |
tracks.removeEventListener('addtrack', trackListChanges);
|
|
|
10662 |
});
|
|
|
10663 |
});
|
|
|
10664 |
}
|
|
|
10665 |
|
|
|
10666 |
/**
|
|
|
10667 |
* Emulate TextTracks using vtt.js if necessary
|
|
|
10668 |
*
|
|
|
10669 |
* @fires Tech#vttjsloaded
|
|
|
10670 |
* @fires Tech#vttjserror
|
|
|
10671 |
*/
|
|
|
10672 |
addWebVttScript_() {
|
|
|
10673 |
if (window.WebVTT) {
|
|
|
10674 |
return;
|
|
|
10675 |
}
|
|
|
10676 |
|
|
|
10677 |
// Initially, Tech.el_ is a child of a dummy-div wait until the Component system
|
|
|
10678 |
// signals that the Tech is ready at which point Tech.el_ is part of the DOM
|
|
|
10679 |
// before inserting the WebVTT script
|
|
|
10680 |
if (document.body.contains(this.el())) {
|
|
|
10681 |
// load via require if available and vtt.js script location was not passed in
|
|
|
10682 |
// as an option. novtt builds will turn the above require call into an empty object
|
|
|
10683 |
// which will cause this if check to always fail.
|
|
|
10684 |
if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) {
|
|
|
10685 |
this.trigger('vttjsloaded');
|
|
|
10686 |
return;
|
|
|
10687 |
}
|
|
|
10688 |
|
|
|
10689 |
// load vtt.js via the script location option or the cdn of no location was
|
|
|
10690 |
// passed in
|
|
|
10691 |
const script = document.createElement('script');
|
|
|
10692 |
script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
|
|
|
10693 |
script.onload = () => {
|
|
|
10694 |
/**
|
|
|
10695 |
* Fired when vtt.js is loaded.
|
|
|
10696 |
*
|
|
|
10697 |
* @event Tech#vttjsloaded
|
|
|
10698 |
* @type {Event}
|
|
|
10699 |
*/
|
|
|
10700 |
this.trigger('vttjsloaded');
|
|
|
10701 |
};
|
|
|
10702 |
script.onerror = () => {
|
|
|
10703 |
/**
|
|
|
10704 |
* Fired when vtt.js was not loaded due to an error
|
|
|
10705 |
*
|
|
|
10706 |
* @event Tech#vttjsloaded
|
|
|
10707 |
* @type {Event}
|
|
|
10708 |
*/
|
|
|
10709 |
this.trigger('vttjserror');
|
|
|
10710 |
};
|
|
|
10711 |
this.on('dispose', () => {
|
|
|
10712 |
script.onload = null;
|
|
|
10713 |
script.onerror = null;
|
|
|
10714 |
});
|
|
|
10715 |
// but have not loaded yet and we set it to true before the inject so that
|
|
|
10716 |
// we don't overwrite the injected window.WebVTT if it loads right away
|
|
|
10717 |
window.WebVTT = true;
|
|
|
10718 |
this.el().parentNode.appendChild(script);
|
|
|
10719 |
} else {
|
|
|
10720 |
this.ready(this.addWebVttScript_);
|
|
|
10721 |
}
|
|
|
10722 |
}
|
|
|
10723 |
|
|
|
10724 |
/**
|
|
|
10725 |
* Emulate texttracks
|
|
|
10726 |
*
|
|
|
10727 |
*/
|
|
|
10728 |
emulateTextTracks() {
|
|
|
10729 |
const tracks = this.textTracks();
|
|
|
10730 |
const remoteTracks = this.remoteTextTracks();
|
|
|
10731 |
const handleAddTrack = e => tracks.addTrack(e.track);
|
|
|
10732 |
const handleRemoveTrack = e => tracks.removeTrack(e.track);
|
|
|
10733 |
remoteTracks.on('addtrack', handleAddTrack);
|
|
|
10734 |
remoteTracks.on('removetrack', handleRemoveTrack);
|
|
|
10735 |
this.addWebVttScript_();
|
|
|
10736 |
const updateDisplay = () => this.trigger('texttrackchange');
|
|
|
10737 |
const textTracksChanges = () => {
|
|
|
10738 |
updateDisplay();
|
|
|
10739 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
10740 |
const track = tracks[i];
|
|
|
10741 |
track.removeEventListener('cuechange', updateDisplay);
|
|
|
10742 |
if (track.mode === 'showing') {
|
|
|
10743 |
track.addEventListener('cuechange', updateDisplay);
|
|
|
10744 |
}
|
|
|
10745 |
}
|
|
|
10746 |
};
|
|
|
10747 |
textTracksChanges();
|
|
|
10748 |
tracks.addEventListener('change', textTracksChanges);
|
|
|
10749 |
tracks.addEventListener('addtrack', textTracksChanges);
|
|
|
10750 |
tracks.addEventListener('removetrack', textTracksChanges);
|
|
|
10751 |
this.on('dispose', function () {
|
|
|
10752 |
remoteTracks.off('addtrack', handleAddTrack);
|
|
|
10753 |
remoteTracks.off('removetrack', handleRemoveTrack);
|
|
|
10754 |
tracks.removeEventListener('change', textTracksChanges);
|
|
|
10755 |
tracks.removeEventListener('addtrack', textTracksChanges);
|
|
|
10756 |
tracks.removeEventListener('removetrack', textTracksChanges);
|
|
|
10757 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
10758 |
const track = tracks[i];
|
|
|
10759 |
track.removeEventListener('cuechange', updateDisplay);
|
|
|
10760 |
}
|
|
|
10761 |
});
|
|
|
10762 |
}
|
|
|
10763 |
|
|
|
10764 |
/**
|
|
|
10765 |
* Create and returns a remote {@link TextTrack} object.
|
|
|
10766 |
*
|
|
|
10767 |
* @param {string} kind
|
|
|
10768 |
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
|
|
|
10769 |
*
|
|
|
10770 |
* @param {string} [label]
|
|
|
10771 |
* Label to identify the text track
|
|
|
10772 |
*
|
|
|
10773 |
* @param {string} [language]
|
|
|
10774 |
* Two letter language abbreviation
|
|
|
10775 |
*
|
|
|
10776 |
* @return {TextTrack}
|
|
|
10777 |
* The TextTrack that gets created.
|
|
|
10778 |
*/
|
|
|
10779 |
addTextTrack(kind, label, language) {
|
|
|
10780 |
if (!kind) {
|
|
|
10781 |
throw new Error('TextTrack kind is required but was not provided');
|
|
|
10782 |
}
|
|
|
10783 |
return createTrackHelper(this, kind, label, language);
|
|
|
10784 |
}
|
|
|
10785 |
|
|
|
10786 |
/**
|
|
|
10787 |
* Create an emulated TextTrack for use by addRemoteTextTrack
|
|
|
10788 |
*
|
|
|
10789 |
* This is intended to be overridden by classes that inherit from
|
|
|
10790 |
* Tech in order to create native or custom TextTracks.
|
|
|
10791 |
*
|
|
|
10792 |
* @param {Object} options
|
|
|
10793 |
* The object should contain the options to initialize the TextTrack with.
|
|
|
10794 |
*
|
|
|
10795 |
* @param {string} [options.kind]
|
|
|
10796 |
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
|
|
|
10797 |
*
|
|
|
10798 |
* @param {string} [options.label].
|
|
|
10799 |
* Label to identify the text track
|
|
|
10800 |
*
|
|
|
10801 |
* @param {string} [options.language]
|
|
|
10802 |
* Two letter language abbreviation.
|
|
|
10803 |
*
|
|
|
10804 |
* @return {HTMLTrackElement}
|
|
|
10805 |
* The track element that gets created.
|
|
|
10806 |
*/
|
|
|
10807 |
createRemoteTextTrack(options) {
|
|
|
10808 |
const track = merge$2(options, {
|
|
|
10809 |
tech: this
|
|
|
10810 |
});
|
|
|
10811 |
return new REMOTE.remoteTextEl.TrackClass(track);
|
|
|
10812 |
}
|
|
|
10813 |
|
|
|
10814 |
/**
|
|
|
10815 |
* Creates a remote text track object and returns an html track element.
|
|
|
10816 |
*
|
|
|
10817 |
* > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
|
|
|
10818 |
*
|
|
|
10819 |
* @param {Object} options
|
|
|
10820 |
* See {@link Tech#createRemoteTextTrack} for more detailed properties.
|
|
|
10821 |
*
|
|
|
10822 |
* @param {boolean} [manualCleanup=false]
|
|
|
10823 |
* - When false: the TextTrack will be automatically removed from the video
|
|
|
10824 |
* element whenever the source changes
|
|
|
10825 |
* - When True: The TextTrack will have to be cleaned up manually
|
|
|
10826 |
*
|
|
|
10827 |
* @return {HTMLTrackElement}
|
|
|
10828 |
* An Html Track Element.
|
|
|
10829 |
*
|
|
|
10830 |
*/
|
|
|
10831 |
addRemoteTextTrack(options = {}, manualCleanup) {
|
|
|
10832 |
const htmlTrackElement = this.createRemoteTextTrack(options);
|
|
|
10833 |
if (typeof manualCleanup !== 'boolean') {
|
|
|
10834 |
manualCleanup = false;
|
|
|
10835 |
}
|
|
|
10836 |
|
|
|
10837 |
// store HTMLTrackElement and TextTrack to remote list
|
|
|
10838 |
this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
|
|
|
10839 |
this.remoteTextTracks().addTrack(htmlTrackElement.track);
|
|
|
10840 |
if (manualCleanup === false) {
|
|
|
10841 |
// create the TextTrackList if it doesn't exist
|
|
|
10842 |
this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
|
|
|
10843 |
}
|
|
|
10844 |
return htmlTrackElement;
|
|
|
10845 |
}
|
|
|
10846 |
|
|
|
10847 |
/**
|
|
|
10848 |
* Remove a remote text track from the remote `TextTrackList`.
|
|
|
10849 |
*
|
|
|
10850 |
* @param {TextTrack} track
|
|
|
10851 |
* `TextTrack` to remove from the `TextTrackList`
|
|
|
10852 |
*/
|
|
|
10853 |
removeRemoteTextTrack(track) {
|
|
|
10854 |
const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
|
|
|
10855 |
|
|
|
10856 |
// remove HTMLTrackElement and TextTrack from remote list
|
|
|
10857 |
this.remoteTextTrackEls().removeTrackElement_(trackElement);
|
|
|
10858 |
this.remoteTextTracks().removeTrack(track);
|
|
|
10859 |
this.autoRemoteTextTracks_.removeTrack(track);
|
|
|
10860 |
}
|
|
|
10861 |
|
|
|
10862 |
/**
|
|
|
10863 |
* Gets available media playback quality metrics as specified by the W3C's Media
|
|
|
10864 |
* Playback Quality API.
|
|
|
10865 |
*
|
|
|
10866 |
* @see [Spec]{@link https://wicg.github.io/media-playback-quality}
|
|
|
10867 |
*
|
|
|
10868 |
* @return {Object}
|
|
|
10869 |
* An object with supported media playback quality metrics
|
|
|
10870 |
*
|
|
|
10871 |
* @abstract
|
|
|
10872 |
*/
|
|
|
10873 |
getVideoPlaybackQuality() {
|
|
|
10874 |
return {};
|
|
|
10875 |
}
|
|
|
10876 |
|
|
|
10877 |
/**
|
|
|
10878 |
* Attempt to create a floating video window always on top of other windows
|
|
|
10879 |
* so that users may continue consuming media while they interact with other
|
|
|
10880 |
* content sites, or applications on their device.
|
|
|
10881 |
*
|
|
|
10882 |
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
|
|
|
10883 |
*
|
|
|
10884 |
* @return {Promise|undefined}
|
|
|
10885 |
* A promise with a Picture-in-Picture window if the browser supports
|
|
|
10886 |
* Promises (or one was passed in as an option). It returns undefined
|
|
|
10887 |
* otherwise.
|
|
|
10888 |
*
|
|
|
10889 |
* @abstract
|
|
|
10890 |
*/
|
|
|
10891 |
requestPictureInPicture() {
|
|
|
10892 |
return Promise.reject();
|
|
|
10893 |
}
|
|
|
10894 |
|
|
|
10895 |
/**
|
|
|
10896 |
* A method to check for the value of the 'disablePictureInPicture' <video> property.
|
|
|
10897 |
* Defaults to true, as it should be considered disabled if the tech does not support pip
|
|
|
10898 |
*
|
|
|
10899 |
* @abstract
|
|
|
10900 |
*/
|
|
|
10901 |
disablePictureInPicture() {
|
|
|
10902 |
return true;
|
|
|
10903 |
}
|
|
|
10904 |
|
|
|
10905 |
/**
|
|
|
10906 |
* A method to set or unset the 'disablePictureInPicture' <video> property.
|
|
|
10907 |
*
|
|
|
10908 |
* @abstract
|
|
|
10909 |
*/
|
|
|
10910 |
setDisablePictureInPicture() {}
|
|
|
10911 |
|
|
|
10912 |
/**
|
|
|
10913 |
* A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
|
|
|
10914 |
*
|
|
|
10915 |
* @param {function} cb
|
|
|
10916 |
* @return {number} request id
|
|
|
10917 |
*/
|
|
|
10918 |
requestVideoFrameCallback(cb) {
|
|
|
10919 |
const id = newGUID();
|
|
|
10920 |
if (!this.isReady_ || this.paused()) {
|
|
|
10921 |
this.queuedHanders_.add(id);
|
|
|
10922 |
this.one('playing', () => {
|
|
|
10923 |
if (this.queuedHanders_.has(id)) {
|
|
|
10924 |
this.queuedHanders_.delete(id);
|
|
|
10925 |
cb();
|
|
|
10926 |
}
|
|
|
10927 |
});
|
|
|
10928 |
} else {
|
|
|
10929 |
this.requestNamedAnimationFrame(id, cb);
|
|
|
10930 |
}
|
|
|
10931 |
return id;
|
|
|
10932 |
}
|
|
|
10933 |
|
|
|
10934 |
/**
|
|
|
10935 |
* A fallback implementation of cancelVideoFrameCallback
|
|
|
10936 |
*
|
|
|
10937 |
* @param {number} id id of callback to be cancelled
|
|
|
10938 |
*/
|
|
|
10939 |
cancelVideoFrameCallback(id) {
|
|
|
10940 |
if (this.queuedHanders_.has(id)) {
|
|
|
10941 |
this.queuedHanders_.delete(id);
|
|
|
10942 |
} else {
|
|
|
10943 |
this.cancelNamedAnimationFrame(id);
|
|
|
10944 |
}
|
|
|
10945 |
}
|
|
|
10946 |
|
|
|
10947 |
/**
|
|
|
10948 |
* A method to set a poster from a `Tech`.
|
|
|
10949 |
*
|
|
|
10950 |
* @abstract
|
|
|
10951 |
*/
|
|
|
10952 |
setPoster() {}
|
|
|
10953 |
|
|
|
10954 |
/**
|
|
|
10955 |
* A method to check for the presence of the 'playsinline' <video> attribute.
|
|
|
10956 |
*
|
|
|
10957 |
* @abstract
|
|
|
10958 |
*/
|
|
|
10959 |
playsinline() {}
|
|
|
10960 |
|
|
|
10961 |
/**
|
|
|
10962 |
* A method to set or unset the 'playsinline' <video> attribute.
|
|
|
10963 |
*
|
|
|
10964 |
* @abstract
|
|
|
10965 |
*/
|
|
|
10966 |
setPlaysinline() {}
|
|
|
10967 |
|
|
|
10968 |
/**
|
|
|
10969 |
* Attempt to force override of native audio tracks.
|
|
|
10970 |
*
|
|
|
10971 |
* @param {boolean} override - If set to true native audio will be overridden,
|
|
|
10972 |
* otherwise native audio will potentially be used.
|
|
|
10973 |
*
|
|
|
10974 |
* @abstract
|
|
|
10975 |
*/
|
|
|
10976 |
overrideNativeAudioTracks(override) {}
|
|
|
10977 |
|
|
|
10978 |
/**
|
|
|
10979 |
* Attempt to force override of native video tracks.
|
|
|
10980 |
*
|
|
|
10981 |
* @param {boolean} override - If set to true native video will be overridden,
|
|
|
10982 |
* otherwise native video will potentially be used.
|
|
|
10983 |
*
|
|
|
10984 |
* @abstract
|
|
|
10985 |
*/
|
|
|
10986 |
overrideNativeVideoTracks(override) {}
|
|
|
10987 |
|
|
|
10988 |
/**
|
|
|
10989 |
* Check if the tech can support the given mime-type.
|
|
|
10990 |
*
|
|
|
10991 |
* The base tech does not support any type, but source handlers might
|
|
|
10992 |
* overwrite this.
|
|
|
10993 |
*
|
|
|
10994 |
* @param {string} _type
|
|
|
10995 |
* The mimetype to check for support
|
|
|
10996 |
*
|
|
|
10997 |
* @return {string}
|
|
|
10998 |
* 'probably', 'maybe', or empty string
|
|
|
10999 |
*
|
|
|
11000 |
* @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
|
|
|
11001 |
*
|
|
|
11002 |
* @abstract
|
|
|
11003 |
*/
|
|
|
11004 |
canPlayType(_type) {
|
|
|
11005 |
return '';
|
|
|
11006 |
}
|
|
|
11007 |
|
|
|
11008 |
/**
|
|
|
11009 |
* Check if the type is supported by this tech.
|
|
|
11010 |
*
|
|
|
11011 |
* The base tech does not support any type, but source handlers might
|
|
|
11012 |
* overwrite this.
|
|
|
11013 |
*
|
|
|
11014 |
* @param {string} _type
|
|
|
11015 |
* The media type to check
|
|
|
11016 |
* @return {string} Returns the native video element's response
|
|
|
11017 |
*/
|
|
|
11018 |
static canPlayType(_type) {
|
|
|
11019 |
return '';
|
|
|
11020 |
}
|
|
|
11021 |
|
|
|
11022 |
/**
|
|
|
11023 |
* Check if the tech can support the given source
|
|
|
11024 |
*
|
|
|
11025 |
* @param {Object} srcObj
|
|
|
11026 |
* The source object
|
|
|
11027 |
* @param {Object} options
|
|
|
11028 |
* The options passed to the tech
|
|
|
11029 |
* @return {string} 'probably', 'maybe', or '' (empty string)
|
|
|
11030 |
*/
|
|
|
11031 |
static canPlaySource(srcObj, options) {
|
|
|
11032 |
return Tech.canPlayType(srcObj.type);
|
|
|
11033 |
}
|
|
|
11034 |
|
|
|
11035 |
/*
|
|
|
11036 |
* Return whether the argument is a Tech or not.
|
|
|
11037 |
* Can be passed either a Class like `Html5` or a instance like `player.tech_`
|
|
|
11038 |
*
|
|
|
11039 |
* @param {Object} component
|
|
|
11040 |
* The item to check
|
|
|
11041 |
*
|
|
|
11042 |
* @return {boolean}
|
|
|
11043 |
* Whether it is a tech or not
|
|
|
11044 |
* - True if it is a tech
|
|
|
11045 |
* - False if it is not
|
|
|
11046 |
*/
|
|
|
11047 |
static isTech(component) {
|
|
|
11048 |
return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
|
|
|
11049 |
}
|
|
|
11050 |
|
|
|
11051 |
/**
|
|
|
11052 |
* Registers a `Tech` into a shared list for videojs.
|
|
|
11053 |
*
|
|
|
11054 |
* @param {string} name
|
|
|
11055 |
* Name of the `Tech` to register.
|
|
|
11056 |
*
|
|
|
11057 |
* @param {Object} tech
|
|
|
11058 |
* The `Tech` class to register.
|
|
|
11059 |
*/
|
|
|
11060 |
static registerTech(name, tech) {
|
|
|
11061 |
if (!Tech.techs_) {
|
|
|
11062 |
Tech.techs_ = {};
|
|
|
11063 |
}
|
|
|
11064 |
if (!Tech.isTech(tech)) {
|
|
|
11065 |
throw new Error(`Tech ${name} must be a Tech`);
|
|
|
11066 |
}
|
|
|
11067 |
if (!Tech.canPlayType) {
|
|
|
11068 |
throw new Error('Techs must have a static canPlayType method on them');
|
|
|
11069 |
}
|
|
|
11070 |
if (!Tech.canPlaySource) {
|
|
|
11071 |
throw new Error('Techs must have a static canPlaySource method on them');
|
|
|
11072 |
}
|
|
|
11073 |
name = toTitleCase$1(name);
|
|
|
11074 |
Tech.techs_[name] = tech;
|
|
|
11075 |
Tech.techs_[toLowerCase(name)] = tech;
|
|
|
11076 |
if (name !== 'Tech') {
|
|
|
11077 |
// camel case the techName for use in techOrder
|
|
|
11078 |
Tech.defaultTechOrder_.push(name);
|
|
|
11079 |
}
|
|
|
11080 |
return tech;
|
|
|
11081 |
}
|
|
|
11082 |
|
|
|
11083 |
/**
|
|
|
11084 |
* Get a `Tech` from the shared list by name.
|
|
|
11085 |
*
|
|
|
11086 |
* @param {string} name
|
|
|
11087 |
* `camelCase` or `TitleCase` name of the Tech to get
|
|
|
11088 |
*
|
|
|
11089 |
* @return {Tech|undefined}
|
|
|
11090 |
* The `Tech` or undefined if there was no tech with the name requested.
|
|
|
11091 |
*/
|
|
|
11092 |
static getTech(name) {
|
|
|
11093 |
if (!name) {
|
|
|
11094 |
return;
|
|
|
11095 |
}
|
|
|
11096 |
if (Tech.techs_ && Tech.techs_[name]) {
|
|
|
11097 |
return Tech.techs_[name];
|
|
|
11098 |
}
|
|
|
11099 |
name = toTitleCase$1(name);
|
|
|
11100 |
if (window && window.videojs && window.videojs[name]) {
|
|
|
11101 |
log$1.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
|
|
|
11102 |
return window.videojs[name];
|
|
|
11103 |
}
|
|
|
11104 |
}
|
|
|
11105 |
}
|
|
|
11106 |
|
|
|
11107 |
/**
|
|
|
11108 |
* Get the {@link VideoTrackList}
|
|
|
11109 |
*
|
|
|
11110 |
* @returns {VideoTrackList}
|
|
|
11111 |
* @method Tech.prototype.videoTracks
|
|
|
11112 |
*/
|
|
|
11113 |
|
|
|
11114 |
/**
|
|
|
11115 |
* Get the {@link AudioTrackList}
|
|
|
11116 |
*
|
|
|
11117 |
* @returns {AudioTrackList}
|
|
|
11118 |
* @method Tech.prototype.audioTracks
|
|
|
11119 |
*/
|
|
|
11120 |
|
|
|
11121 |
/**
|
|
|
11122 |
* Get the {@link TextTrackList}
|
|
|
11123 |
*
|
|
|
11124 |
* @returns {TextTrackList}
|
|
|
11125 |
* @method Tech.prototype.textTracks
|
|
|
11126 |
*/
|
|
|
11127 |
|
|
|
11128 |
/**
|
|
|
11129 |
* Get the remote element {@link TextTrackList}
|
|
|
11130 |
*
|
|
|
11131 |
* @returns {TextTrackList}
|
|
|
11132 |
* @method Tech.prototype.remoteTextTracks
|
|
|
11133 |
*/
|
|
|
11134 |
|
|
|
11135 |
/**
|
|
|
11136 |
* Get the remote element {@link HtmlTrackElementList}
|
|
|
11137 |
*
|
|
|
11138 |
* @returns {HtmlTrackElementList}
|
|
|
11139 |
* @method Tech.prototype.remoteTextTrackEls
|
|
|
11140 |
*/
|
|
|
11141 |
|
|
|
11142 |
ALL.names.forEach(function (name) {
|
|
|
11143 |
const props = ALL[name];
|
|
|
11144 |
Tech.prototype[props.getterName] = function () {
|
|
|
11145 |
this[props.privateName] = this[props.privateName] || new props.ListClass();
|
|
|
11146 |
return this[props.privateName];
|
|
|
11147 |
};
|
|
|
11148 |
});
|
|
|
11149 |
|
|
|
11150 |
/**
|
|
|
11151 |
* List of associated text tracks
|
|
|
11152 |
*
|
|
|
11153 |
* @type {TextTrackList}
|
|
|
11154 |
* @private
|
|
|
11155 |
* @property Tech#textTracks_
|
|
|
11156 |
*/
|
|
|
11157 |
|
|
|
11158 |
/**
|
|
|
11159 |
* List of associated audio tracks.
|
|
|
11160 |
*
|
|
|
11161 |
* @type {AudioTrackList}
|
|
|
11162 |
* @private
|
|
|
11163 |
* @property Tech#audioTracks_
|
|
|
11164 |
*/
|
|
|
11165 |
|
|
|
11166 |
/**
|
|
|
11167 |
* List of associated video tracks.
|
|
|
11168 |
*
|
|
|
11169 |
* @type {VideoTrackList}
|
|
|
11170 |
* @private
|
|
|
11171 |
* @property Tech#videoTracks_
|
|
|
11172 |
*/
|
|
|
11173 |
|
|
|
11174 |
/**
|
|
|
11175 |
* Boolean indicating whether the `Tech` supports volume control.
|
|
|
11176 |
*
|
|
|
11177 |
* @type {boolean}
|
|
|
11178 |
* @default
|
|
|
11179 |
*/
|
|
|
11180 |
Tech.prototype.featuresVolumeControl = true;
|
|
|
11181 |
|
|
|
11182 |
/**
|
|
|
11183 |
* Boolean indicating whether the `Tech` supports muting volume.
|
|
|
11184 |
*
|
|
|
11185 |
* @type {boolean}
|
|
|
11186 |
* @default
|
|
|
11187 |
*/
|
|
|
11188 |
Tech.prototype.featuresMuteControl = true;
|
|
|
11189 |
|
|
|
11190 |
/**
|
|
|
11191 |
* Boolean indicating whether the `Tech` supports fullscreen resize control.
|
|
|
11192 |
* Resizing plugins using request fullscreen reloads the plugin
|
|
|
11193 |
*
|
|
|
11194 |
* @type {boolean}
|
|
|
11195 |
* @default
|
|
|
11196 |
*/
|
|
|
11197 |
Tech.prototype.featuresFullscreenResize = false;
|
|
|
11198 |
|
|
|
11199 |
/**
|
|
|
11200 |
* Boolean indicating whether the `Tech` supports changing the speed at which the video
|
|
|
11201 |
* plays. Examples:
|
|
|
11202 |
* - Set player to play 2x (twice) as fast
|
|
|
11203 |
* - Set player to play 0.5x (half) as fast
|
|
|
11204 |
*
|
|
|
11205 |
* @type {boolean}
|
|
|
11206 |
* @default
|
|
|
11207 |
*/
|
|
|
11208 |
Tech.prototype.featuresPlaybackRate = false;
|
|
|
11209 |
|
|
|
11210 |
/**
|
|
|
11211 |
* Boolean indicating whether the `Tech` supports the `progress` event.
|
|
|
11212 |
* This will be used to determine if {@link Tech#manualProgressOn} should be called.
|
|
|
11213 |
*
|
|
|
11214 |
* @type {boolean}
|
|
|
11215 |
* @default
|
|
|
11216 |
*/
|
|
|
11217 |
Tech.prototype.featuresProgressEvents = false;
|
|
|
11218 |
|
|
|
11219 |
/**
|
|
|
11220 |
* Boolean indicating whether the `Tech` supports the `sourceset` event.
|
|
|
11221 |
*
|
|
|
11222 |
* A tech should set this to `true` and then use {@link Tech#triggerSourceset}
|
|
|
11223 |
* to trigger a {@link Tech#event:sourceset} at the earliest time after getting
|
|
|
11224 |
* a new source.
|
|
|
11225 |
*
|
|
|
11226 |
* @type {boolean}
|
|
|
11227 |
* @default
|
|
|
11228 |
*/
|
|
|
11229 |
Tech.prototype.featuresSourceset = false;
|
|
|
11230 |
|
|
|
11231 |
/**
|
|
|
11232 |
* Boolean indicating whether the `Tech` supports the `timeupdate` event.
|
|
|
11233 |
* This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
|
|
|
11234 |
*
|
|
|
11235 |
* @type {boolean}
|
|
|
11236 |
* @default
|
|
|
11237 |
*/
|
|
|
11238 |
Tech.prototype.featuresTimeupdateEvents = false;
|
|
|
11239 |
|
|
|
11240 |
/**
|
|
|
11241 |
* Boolean indicating whether the `Tech` supports the native `TextTrack`s.
|
|
|
11242 |
* This will help us integrate with native `TextTrack`s if the browser supports them.
|
|
|
11243 |
*
|
|
|
11244 |
* @type {boolean}
|
|
|
11245 |
* @default
|
|
|
11246 |
*/
|
|
|
11247 |
Tech.prototype.featuresNativeTextTracks = false;
|
|
|
11248 |
|
|
|
11249 |
/**
|
|
|
11250 |
* Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
|
|
|
11251 |
*
|
|
|
11252 |
* @type {boolean}
|
|
|
11253 |
* @default
|
|
|
11254 |
*/
|
|
|
11255 |
Tech.prototype.featuresVideoFrameCallback = false;
|
|
|
11256 |
|
|
|
11257 |
/**
|
|
|
11258 |
* A functional mixin for techs that want to use the Source Handler pattern.
|
|
|
11259 |
* Source handlers are scripts for handling specific formats.
|
|
|
11260 |
* The source handler pattern is used for adaptive formats (HLS, DASH) that
|
|
|
11261 |
* manually load video data and feed it into a Source Buffer (Media Source Extensions)
|
|
|
11262 |
* Example: `Tech.withSourceHandlers.call(MyTech);`
|
|
|
11263 |
*
|
|
|
11264 |
* @param {Tech} _Tech
|
|
|
11265 |
* The tech to add source handler functions to.
|
|
|
11266 |
*
|
|
|
11267 |
* @mixes Tech~SourceHandlerAdditions
|
|
|
11268 |
*/
|
|
|
11269 |
Tech.withSourceHandlers = function (_Tech) {
|
|
|
11270 |
/**
|
|
|
11271 |
* Register a source handler
|
|
|
11272 |
*
|
|
|
11273 |
* @param {Function} handler
|
|
|
11274 |
* The source handler class
|
|
|
11275 |
*
|
|
|
11276 |
* @param {number} [index]
|
|
|
11277 |
* Register it at the following index
|
|
|
11278 |
*/
|
|
|
11279 |
_Tech.registerSourceHandler = function (handler, index) {
|
|
|
11280 |
let handlers = _Tech.sourceHandlers;
|
|
|
11281 |
if (!handlers) {
|
|
|
11282 |
handlers = _Tech.sourceHandlers = [];
|
|
|
11283 |
}
|
|
|
11284 |
if (index === undefined) {
|
|
|
11285 |
// add to the end of the list
|
|
|
11286 |
index = handlers.length;
|
|
|
11287 |
}
|
|
|
11288 |
handlers.splice(index, 0, handler);
|
|
|
11289 |
};
|
|
|
11290 |
|
|
|
11291 |
/**
|
|
|
11292 |
* Check if the tech can support the given type. Also checks the
|
|
|
11293 |
* Techs sourceHandlers.
|
|
|
11294 |
*
|
|
|
11295 |
* @param {string} type
|
|
|
11296 |
* The mimetype to check.
|
|
|
11297 |
*
|
|
|
11298 |
* @return {string}
|
|
|
11299 |
* 'probably', 'maybe', or '' (empty string)
|
|
|
11300 |
*/
|
|
|
11301 |
_Tech.canPlayType = function (type) {
|
|
|
11302 |
const handlers = _Tech.sourceHandlers || [];
|
|
|
11303 |
let can;
|
|
|
11304 |
for (let i = 0; i < handlers.length; i++) {
|
|
|
11305 |
can = handlers[i].canPlayType(type);
|
|
|
11306 |
if (can) {
|
|
|
11307 |
return can;
|
|
|
11308 |
}
|
|
|
11309 |
}
|
|
|
11310 |
return '';
|
|
|
11311 |
};
|
|
|
11312 |
|
|
|
11313 |
/**
|
|
|
11314 |
* Returns the first source handler that supports the source.
|
|
|
11315 |
*
|
|
|
11316 |
* TODO: Answer question: should 'probably' be prioritized over 'maybe'
|
|
|
11317 |
*
|
|
|
11318 |
* @param {SourceObject} source
|
|
|
11319 |
* The source object
|
|
|
11320 |
*
|
|
|
11321 |
* @param {Object} options
|
|
|
11322 |
* The options passed to the tech
|
|
|
11323 |
*
|
|
|
11324 |
* @return {SourceHandler|null}
|
|
|
11325 |
* The first source handler that supports the source or null if
|
|
|
11326 |
* no SourceHandler supports the source
|
|
|
11327 |
*/
|
|
|
11328 |
_Tech.selectSourceHandler = function (source, options) {
|
|
|
11329 |
const handlers = _Tech.sourceHandlers || [];
|
|
|
11330 |
let can;
|
|
|
11331 |
for (let i = 0; i < handlers.length; i++) {
|
|
|
11332 |
can = handlers[i].canHandleSource(source, options);
|
|
|
11333 |
if (can) {
|
|
|
11334 |
return handlers[i];
|
|
|
11335 |
}
|
|
|
11336 |
}
|
|
|
11337 |
return null;
|
|
|
11338 |
};
|
|
|
11339 |
|
|
|
11340 |
/**
|
|
|
11341 |
* Check if the tech can support the given source.
|
|
|
11342 |
*
|
|
|
11343 |
* @param {SourceObject} srcObj
|
|
|
11344 |
* The source object
|
|
|
11345 |
*
|
|
|
11346 |
* @param {Object} options
|
|
|
11347 |
* The options passed to the tech
|
|
|
11348 |
*
|
|
|
11349 |
* @return {string}
|
|
|
11350 |
* 'probably', 'maybe', or '' (empty string)
|
|
|
11351 |
*/
|
|
|
11352 |
_Tech.canPlaySource = function (srcObj, options) {
|
|
|
11353 |
const sh = _Tech.selectSourceHandler(srcObj, options);
|
|
|
11354 |
if (sh) {
|
|
|
11355 |
return sh.canHandleSource(srcObj, options);
|
|
|
11356 |
}
|
|
|
11357 |
return '';
|
|
|
11358 |
};
|
|
|
11359 |
|
|
|
11360 |
/**
|
|
|
11361 |
* When using a source handler, prefer its implementation of
|
|
|
11362 |
* any function normally provided by the tech.
|
|
|
11363 |
*/
|
|
|
11364 |
const deferrable = ['seekable', 'seeking', 'duration'];
|
|
|
11365 |
|
|
|
11366 |
/**
|
|
|
11367 |
* A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
|
|
|
11368 |
* function if it exists, with a fallback to the Techs seekable function.
|
|
|
11369 |
*
|
|
|
11370 |
* @method _Tech.seekable
|
|
|
11371 |
*/
|
|
|
11372 |
|
|
|
11373 |
/**
|
|
|
11374 |
* A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
|
|
|
11375 |
* function if it exists, otherwise it will fallback to the techs duration function.
|
|
|
11376 |
*
|
|
|
11377 |
* @method _Tech.duration
|
|
|
11378 |
*/
|
|
|
11379 |
|
|
|
11380 |
deferrable.forEach(function (fnName) {
|
|
|
11381 |
const originalFn = this[fnName];
|
|
|
11382 |
if (typeof originalFn !== 'function') {
|
|
|
11383 |
return;
|
|
|
11384 |
}
|
|
|
11385 |
this[fnName] = function () {
|
|
|
11386 |
if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
|
|
|
11387 |
return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
|
|
|
11388 |
}
|
|
|
11389 |
return originalFn.apply(this, arguments);
|
|
|
11390 |
};
|
|
|
11391 |
}, _Tech.prototype);
|
|
|
11392 |
|
|
|
11393 |
/**
|
|
|
11394 |
* Create a function for setting the source using a source object
|
|
|
11395 |
* and source handlers.
|
|
|
11396 |
* Should never be called unless a source handler was found.
|
|
|
11397 |
*
|
|
|
11398 |
* @param {SourceObject} source
|
|
|
11399 |
* A source object with src and type keys
|
|
|
11400 |
*/
|
|
|
11401 |
_Tech.prototype.setSource = function (source) {
|
|
|
11402 |
let sh = _Tech.selectSourceHandler(source, this.options_);
|
|
|
11403 |
if (!sh) {
|
|
|
11404 |
// Fall back to a native source handler when unsupported sources are
|
|
|
11405 |
// deliberately set
|
|
|
11406 |
if (_Tech.nativeSourceHandler) {
|
|
|
11407 |
sh = _Tech.nativeSourceHandler;
|
|
|
11408 |
} else {
|
|
|
11409 |
log$1.error('No source handler found for the current source.');
|
|
|
11410 |
}
|
|
|
11411 |
}
|
|
|
11412 |
|
|
|
11413 |
// Dispose any existing source handler
|
|
|
11414 |
this.disposeSourceHandler();
|
|
|
11415 |
this.off('dispose', this.disposeSourceHandler_);
|
|
|
11416 |
if (sh !== _Tech.nativeSourceHandler) {
|
|
|
11417 |
this.currentSource_ = source;
|
|
|
11418 |
}
|
|
|
11419 |
this.sourceHandler_ = sh.handleSource(source, this, this.options_);
|
|
|
11420 |
this.one('dispose', this.disposeSourceHandler_);
|
|
|
11421 |
};
|
|
|
11422 |
|
|
|
11423 |
/**
|
|
|
11424 |
* Clean up any existing SourceHandlers and listeners when the Tech is disposed.
|
|
|
11425 |
*
|
|
|
11426 |
* @listens Tech#dispose
|
|
|
11427 |
*/
|
|
|
11428 |
_Tech.prototype.disposeSourceHandler = function () {
|
|
|
11429 |
// if we have a source and get another one
|
|
|
11430 |
// then we are loading something new
|
|
|
11431 |
// than clear all of our current tracks
|
|
|
11432 |
if (this.currentSource_) {
|
|
|
11433 |
this.clearTracks(['audio', 'video']);
|
|
|
11434 |
this.currentSource_ = null;
|
|
|
11435 |
}
|
|
|
11436 |
|
|
|
11437 |
// always clean up auto-text tracks
|
|
|
11438 |
this.cleanupAutoTextTracks();
|
|
|
11439 |
if (this.sourceHandler_) {
|
|
|
11440 |
if (this.sourceHandler_.dispose) {
|
|
|
11441 |
this.sourceHandler_.dispose();
|
|
|
11442 |
}
|
|
|
11443 |
this.sourceHandler_ = null;
|
|
|
11444 |
}
|
|
|
11445 |
};
|
|
|
11446 |
};
|
|
|
11447 |
|
|
|
11448 |
// The base Tech class needs to be registered as a Component. It is the only
|
|
|
11449 |
// Tech that can be registered as a Component.
|
|
|
11450 |
Component$1.registerComponent('Tech', Tech);
|
|
|
11451 |
Tech.registerTech('Tech', Tech);
|
|
|
11452 |
|
|
|
11453 |
/**
|
|
|
11454 |
* A list of techs that should be added to techOrder on Players
|
|
|
11455 |
*
|
|
|
11456 |
* @private
|
|
|
11457 |
*/
|
|
|
11458 |
Tech.defaultTechOrder_ = [];
|
|
|
11459 |
|
|
|
11460 |
/**
|
|
|
11461 |
* @file middleware.js
|
|
|
11462 |
* @module middleware
|
|
|
11463 |
*/
|
|
|
11464 |
const middlewares = {};
|
|
|
11465 |
const middlewareInstances = {};
|
|
|
11466 |
const TERMINATOR = {};
|
|
|
11467 |
|
|
|
11468 |
/**
|
|
|
11469 |
* A middleware object is a plain JavaScript object that has methods that
|
|
|
11470 |
* match the {@link Tech} methods found in the lists of allowed
|
|
|
11471 |
* {@link module:middleware.allowedGetters|getters},
|
|
|
11472 |
* {@link module:middleware.allowedSetters|setters}, and
|
|
|
11473 |
* {@link module:middleware.allowedMediators|mediators}.
|
|
|
11474 |
*
|
|
|
11475 |
* @typedef {Object} MiddlewareObject
|
|
|
11476 |
*/
|
|
|
11477 |
|
|
|
11478 |
/**
|
|
|
11479 |
* A middleware factory function that should return a
|
|
|
11480 |
* {@link module:middleware~MiddlewareObject|MiddlewareObject}.
|
|
|
11481 |
*
|
|
|
11482 |
* This factory will be called for each player when needed, with the player
|
|
|
11483 |
* passed in as an argument.
|
|
|
11484 |
*
|
|
|
11485 |
* @callback MiddlewareFactory
|
|
|
11486 |
* @param { import('../player').default } player
|
|
|
11487 |
* A Video.js player.
|
|
|
11488 |
*/
|
|
|
11489 |
|
|
|
11490 |
/**
|
|
|
11491 |
* Define a middleware that the player should use by way of a factory function
|
|
|
11492 |
* that returns a middleware object.
|
|
|
11493 |
*
|
|
|
11494 |
* @param {string} type
|
|
|
11495 |
* The MIME type to match or `"*"` for all MIME types.
|
|
|
11496 |
*
|
|
|
11497 |
* @param {MiddlewareFactory} middleware
|
|
|
11498 |
* A middleware factory function that will be executed for
|
|
|
11499 |
* matching types.
|
|
|
11500 |
*/
|
|
|
11501 |
function use(type, middleware) {
|
|
|
11502 |
middlewares[type] = middlewares[type] || [];
|
|
|
11503 |
middlewares[type].push(middleware);
|
|
|
11504 |
}
|
|
|
11505 |
|
|
|
11506 |
/**
|
|
|
11507 |
* Asynchronously sets a source using middleware by recursing through any
|
|
|
11508 |
* matching middlewares and calling `setSource` on each, passing along the
|
|
|
11509 |
* previous returned value each time.
|
|
|
11510 |
*
|
|
|
11511 |
* @param { import('../player').default } player
|
|
|
11512 |
* A {@link Player} instance.
|
|
|
11513 |
*
|
|
|
11514 |
* @param {Tech~SourceObject} src
|
|
|
11515 |
* A source object.
|
|
|
11516 |
*
|
|
|
11517 |
* @param {Function}
|
|
|
11518 |
* The next middleware to run.
|
|
|
11519 |
*/
|
|
|
11520 |
function setSource(player, src, next) {
|
|
|
11521 |
player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
|
|
|
11522 |
}
|
|
|
11523 |
|
|
|
11524 |
/**
|
|
|
11525 |
* When the tech is set, passes the tech to each middleware's `setTech` method.
|
|
|
11526 |
*
|
|
|
11527 |
* @param {Object[]} middleware
|
|
|
11528 |
* An array of middleware instances.
|
|
|
11529 |
*
|
|
|
11530 |
* @param { import('../tech/tech').default } tech
|
|
|
11531 |
* A Video.js tech.
|
|
|
11532 |
*/
|
|
|
11533 |
function setTech(middleware, tech) {
|
|
|
11534 |
middleware.forEach(mw => mw.setTech && mw.setTech(tech));
|
|
|
11535 |
}
|
|
|
11536 |
|
|
|
11537 |
/**
|
|
|
11538 |
* Calls a getter on the tech first, through each middleware
|
|
|
11539 |
* from right to left to the player.
|
|
|
11540 |
*
|
|
|
11541 |
* @param {Object[]} middleware
|
|
|
11542 |
* An array of middleware instances.
|
|
|
11543 |
*
|
|
|
11544 |
* @param { import('../tech/tech').default } tech
|
|
|
11545 |
* The current tech.
|
|
|
11546 |
*
|
|
|
11547 |
* @param {string} method
|
|
|
11548 |
* A method name.
|
|
|
11549 |
*
|
|
|
11550 |
* @return {*}
|
|
|
11551 |
* The final value from the tech after middleware has intercepted it.
|
|
|
11552 |
*/
|
|
|
11553 |
function get(middleware, tech, method) {
|
|
|
11554 |
return middleware.reduceRight(middlewareIterator(method), tech[method]());
|
|
|
11555 |
}
|
|
|
11556 |
|
|
|
11557 |
/**
|
|
|
11558 |
* Takes the argument given to the player and calls the setter method on each
|
|
|
11559 |
* middleware from left to right to the tech.
|
|
|
11560 |
*
|
|
|
11561 |
* @param {Object[]} middleware
|
|
|
11562 |
* An array of middleware instances.
|
|
|
11563 |
*
|
|
|
11564 |
* @param { import('../tech/tech').default } tech
|
|
|
11565 |
* The current tech.
|
|
|
11566 |
*
|
|
|
11567 |
* @param {string} method
|
|
|
11568 |
* A method name.
|
|
|
11569 |
*
|
|
|
11570 |
* @param {*} arg
|
|
|
11571 |
* The value to set on the tech.
|
|
|
11572 |
*
|
|
|
11573 |
* @return {*}
|
|
|
11574 |
* The return value of the `method` of the `tech`.
|
|
|
11575 |
*/
|
|
|
11576 |
function set(middleware, tech, method, arg) {
|
|
|
11577 |
return tech[method](middleware.reduce(middlewareIterator(method), arg));
|
|
|
11578 |
}
|
|
|
11579 |
|
|
|
11580 |
/**
|
|
|
11581 |
* Takes the argument given to the player and calls the `call` version of the
|
|
|
11582 |
* method on each middleware from left to right.
|
|
|
11583 |
*
|
|
|
11584 |
* Then, call the passed in method on the tech and return the result unchanged
|
|
|
11585 |
* back to the player, through middleware, this time from right to left.
|
|
|
11586 |
*
|
|
|
11587 |
* @param {Object[]} middleware
|
|
|
11588 |
* An array of middleware instances.
|
|
|
11589 |
*
|
|
|
11590 |
* @param { import('../tech/tech').default } tech
|
|
|
11591 |
* The current tech.
|
|
|
11592 |
*
|
|
|
11593 |
* @param {string} method
|
|
|
11594 |
* A method name.
|
|
|
11595 |
*
|
|
|
11596 |
* @param {*} arg
|
|
|
11597 |
* The value to set on the tech.
|
|
|
11598 |
*
|
|
|
11599 |
* @return {*}
|
|
|
11600 |
* The return value of the `method` of the `tech`, regardless of the
|
|
|
11601 |
* return values of middlewares.
|
|
|
11602 |
*/
|
|
|
11603 |
function mediate(middleware, tech, method, arg = null) {
|
|
|
11604 |
const callMethod = 'call' + toTitleCase$1(method);
|
|
|
11605 |
const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
|
|
|
11606 |
const terminated = middlewareValue === TERMINATOR;
|
|
|
11607 |
// deprecated. The `null` return value should instead return TERMINATOR to
|
|
|
11608 |
// prevent confusion if a techs method actually returns null.
|
|
|
11609 |
const returnValue = terminated ? null : tech[method](middlewareValue);
|
|
|
11610 |
executeRight(middleware, method, returnValue, terminated);
|
|
|
11611 |
return returnValue;
|
|
|
11612 |
}
|
|
|
11613 |
|
|
|
11614 |
/**
|
|
|
11615 |
* Enumeration of allowed getters where the keys are method names.
|
|
|
11616 |
*
|
|
|
11617 |
* @type {Object}
|
|
|
11618 |
*/
|
|
|
11619 |
const allowedGetters = {
|
|
|
11620 |
buffered: 1,
|
|
|
11621 |
currentTime: 1,
|
|
|
11622 |
duration: 1,
|
|
|
11623 |
muted: 1,
|
|
|
11624 |
played: 1,
|
|
|
11625 |
paused: 1,
|
|
|
11626 |
seekable: 1,
|
|
|
11627 |
volume: 1,
|
|
|
11628 |
ended: 1
|
|
|
11629 |
};
|
|
|
11630 |
|
|
|
11631 |
/**
|
|
|
11632 |
* Enumeration of allowed setters where the keys are method names.
|
|
|
11633 |
*
|
|
|
11634 |
* @type {Object}
|
|
|
11635 |
*/
|
|
|
11636 |
const allowedSetters = {
|
|
|
11637 |
setCurrentTime: 1,
|
|
|
11638 |
setMuted: 1,
|
|
|
11639 |
setVolume: 1
|
|
|
11640 |
};
|
|
|
11641 |
|
|
|
11642 |
/**
|
|
|
11643 |
* Enumeration of allowed mediators where the keys are method names.
|
|
|
11644 |
*
|
|
|
11645 |
* @type {Object}
|
|
|
11646 |
*/
|
|
|
11647 |
const allowedMediators = {
|
|
|
11648 |
play: 1,
|
|
|
11649 |
pause: 1
|
|
|
11650 |
};
|
|
|
11651 |
function middlewareIterator(method) {
|
|
|
11652 |
return (value, mw) => {
|
|
|
11653 |
// if the previous middleware terminated, pass along the termination
|
|
|
11654 |
if (value === TERMINATOR) {
|
|
|
11655 |
return TERMINATOR;
|
|
|
11656 |
}
|
|
|
11657 |
if (mw[method]) {
|
|
|
11658 |
return mw[method](value);
|
|
|
11659 |
}
|
|
|
11660 |
return value;
|
|
|
11661 |
};
|
|
|
11662 |
}
|
|
|
11663 |
function executeRight(mws, method, value, terminated) {
|
|
|
11664 |
for (let i = mws.length - 1; i >= 0; i--) {
|
|
|
11665 |
const mw = mws[i];
|
|
|
11666 |
if (mw[method]) {
|
|
|
11667 |
mw[method](terminated, value);
|
|
|
11668 |
}
|
|
|
11669 |
}
|
|
|
11670 |
}
|
|
|
11671 |
|
|
|
11672 |
/**
|
|
|
11673 |
* Clear the middleware cache for a player.
|
|
|
11674 |
*
|
|
|
11675 |
* @param { import('../player').default } player
|
|
|
11676 |
* A {@link Player} instance.
|
|
|
11677 |
*/
|
|
|
11678 |
function clearCacheForPlayer(player) {
|
|
|
11679 |
middlewareInstances[player.id()] = null;
|
|
|
11680 |
}
|
|
|
11681 |
|
|
|
11682 |
/**
|
|
|
11683 |
* {
|
|
|
11684 |
* [playerId]: [[mwFactory, mwInstance], ...]
|
|
|
11685 |
* }
|
|
|
11686 |
*
|
|
|
11687 |
* @private
|
|
|
11688 |
*/
|
|
|
11689 |
function getOrCreateFactory(player, mwFactory) {
|
|
|
11690 |
const mws = middlewareInstances[player.id()];
|
|
|
11691 |
let mw = null;
|
|
|
11692 |
if (mws === undefined || mws === null) {
|
|
|
11693 |
mw = mwFactory(player);
|
|
|
11694 |
middlewareInstances[player.id()] = [[mwFactory, mw]];
|
|
|
11695 |
return mw;
|
|
|
11696 |
}
|
|
|
11697 |
for (let i = 0; i < mws.length; i++) {
|
|
|
11698 |
const [mwf, mwi] = mws[i];
|
|
|
11699 |
if (mwf !== mwFactory) {
|
|
|
11700 |
continue;
|
|
|
11701 |
}
|
|
|
11702 |
mw = mwi;
|
|
|
11703 |
}
|
|
|
11704 |
if (mw === null) {
|
|
|
11705 |
mw = mwFactory(player);
|
|
|
11706 |
mws.push([mwFactory, mw]);
|
|
|
11707 |
}
|
|
|
11708 |
return mw;
|
|
|
11709 |
}
|
|
|
11710 |
function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
|
|
|
11711 |
const [mwFactory, ...mwrest] = middleware;
|
|
|
11712 |
|
|
|
11713 |
// if mwFactory is a string, then we're at a fork in the road
|
|
|
11714 |
if (typeof mwFactory === 'string') {
|
|
|
11715 |
setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
|
|
|
11716 |
|
|
|
11717 |
// if we have an mwFactory, call it with the player to get the mw,
|
|
|
11718 |
// then call the mw's setSource method
|
|
|
11719 |
} else if (mwFactory) {
|
|
|
11720 |
const mw = getOrCreateFactory(player, mwFactory);
|
|
|
11721 |
|
|
|
11722 |
// if setSource isn't present, implicitly select this middleware
|
|
|
11723 |
if (!mw.setSource) {
|
|
|
11724 |
acc.push(mw);
|
|
|
11725 |
return setSourceHelper(src, mwrest, next, player, acc, lastRun);
|
|
|
11726 |
}
|
|
|
11727 |
mw.setSource(Object.assign({}, src), function (err, _src) {
|
|
|
11728 |
// something happened, try the next middleware on the current level
|
|
|
11729 |
// make sure to use the old src
|
|
|
11730 |
if (err) {
|
|
|
11731 |
return setSourceHelper(src, mwrest, next, player, acc, lastRun);
|
|
|
11732 |
}
|
|
|
11733 |
|
|
|
11734 |
// we've succeeded, now we need to go deeper
|
|
|
11735 |
acc.push(mw);
|
|
|
11736 |
|
|
|
11737 |
// if it's the same type, continue down the current chain
|
|
|
11738 |
// otherwise, we want to go down the new chain
|
|
|
11739 |
setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
|
|
|
11740 |
});
|
|
|
11741 |
} else if (mwrest.length) {
|
|
|
11742 |
setSourceHelper(src, mwrest, next, player, acc, lastRun);
|
|
|
11743 |
} else if (lastRun) {
|
|
|
11744 |
next(src, acc);
|
|
|
11745 |
} else {
|
|
|
11746 |
setSourceHelper(src, middlewares['*'], next, player, acc, true);
|
|
|
11747 |
}
|
|
|
11748 |
}
|
|
|
11749 |
|
|
|
11750 |
/**
|
|
|
11751 |
* Mimetypes
|
|
|
11752 |
*
|
|
|
11753 |
* @see https://www.iana.org/assignments/media-types/media-types.xhtml
|
|
|
11754 |
* @typedef Mimetypes~Kind
|
|
|
11755 |
* @enum
|
|
|
11756 |
*/
|
|
|
11757 |
const MimetypesKind = {
|
|
|
11758 |
opus: 'video/ogg',
|
|
|
11759 |
ogv: 'video/ogg',
|
|
|
11760 |
mp4: 'video/mp4',
|
|
|
11761 |
mov: 'video/mp4',
|
|
|
11762 |
m4v: 'video/mp4',
|
|
|
11763 |
mkv: 'video/x-matroska',
|
|
|
11764 |
m4a: 'audio/mp4',
|
|
|
11765 |
mp3: 'audio/mpeg',
|
|
|
11766 |
aac: 'audio/aac',
|
|
|
11767 |
caf: 'audio/x-caf',
|
|
|
11768 |
flac: 'audio/flac',
|
|
|
11769 |
oga: 'audio/ogg',
|
|
|
11770 |
wav: 'audio/wav',
|
|
|
11771 |
m3u8: 'application/x-mpegURL',
|
|
|
11772 |
mpd: 'application/dash+xml',
|
|
|
11773 |
jpg: 'image/jpeg',
|
|
|
11774 |
jpeg: 'image/jpeg',
|
|
|
11775 |
gif: 'image/gif',
|
|
|
11776 |
png: 'image/png',
|
|
|
11777 |
svg: 'image/svg+xml',
|
|
|
11778 |
webp: 'image/webp'
|
|
|
11779 |
};
|
|
|
11780 |
|
|
|
11781 |
/**
|
|
|
11782 |
* Get the mimetype of a given src url if possible
|
|
|
11783 |
*
|
|
|
11784 |
* @param {string} src
|
|
|
11785 |
* The url to the src
|
|
|
11786 |
*
|
|
|
11787 |
* @return {string}
|
|
|
11788 |
* return the mimetype if it was known or empty string otherwise
|
|
|
11789 |
*/
|
|
|
11790 |
const getMimetype = function (src = '') {
|
|
|
11791 |
const ext = getFileExtension(src);
|
|
|
11792 |
const mimetype = MimetypesKind[ext.toLowerCase()];
|
|
|
11793 |
return mimetype || '';
|
|
|
11794 |
};
|
|
|
11795 |
|
|
|
11796 |
/**
|
|
|
11797 |
* Find the mime type of a given source string if possible. Uses the player
|
|
|
11798 |
* source cache.
|
|
|
11799 |
*
|
|
|
11800 |
* @param { import('../player').default } player
|
|
|
11801 |
* The player object
|
|
|
11802 |
*
|
|
|
11803 |
* @param {string} src
|
|
|
11804 |
* The source string
|
|
|
11805 |
*
|
|
|
11806 |
* @return {string}
|
|
|
11807 |
* The type that was found
|
|
|
11808 |
*/
|
|
|
11809 |
const findMimetype = (player, src) => {
|
|
|
11810 |
if (!src) {
|
|
|
11811 |
return '';
|
|
|
11812 |
}
|
|
|
11813 |
|
|
|
11814 |
// 1. check for the type in the `source` cache
|
|
|
11815 |
if (player.cache_.source.src === src && player.cache_.source.type) {
|
|
|
11816 |
return player.cache_.source.type;
|
|
|
11817 |
}
|
|
|
11818 |
|
|
|
11819 |
// 2. see if we have this source in our `currentSources` cache
|
|
|
11820 |
const matchingSources = player.cache_.sources.filter(s => s.src === src);
|
|
|
11821 |
if (matchingSources.length) {
|
|
|
11822 |
return matchingSources[0].type;
|
|
|
11823 |
}
|
|
|
11824 |
|
|
|
11825 |
// 3. look for the src url in source elements and use the type there
|
|
|
11826 |
const sources = player.$$('source');
|
|
|
11827 |
for (let i = 0; i < sources.length; i++) {
|
|
|
11828 |
const s = sources[i];
|
|
|
11829 |
if (s.type && s.src && s.src === src) {
|
|
|
11830 |
return s.type;
|
|
|
11831 |
}
|
|
|
11832 |
}
|
|
|
11833 |
|
|
|
11834 |
// 4. finally fallback to our list of mime types based on src url extension
|
|
|
11835 |
return getMimetype(src);
|
|
|
11836 |
};
|
|
|
11837 |
|
|
|
11838 |
/**
|
|
|
11839 |
* @module filter-source
|
|
|
11840 |
*/
|
|
|
11841 |
|
|
|
11842 |
/**
|
|
|
11843 |
* Filter out single bad source objects or multiple source objects in an
|
|
|
11844 |
* array. Also flattens nested source object arrays into a 1 dimensional
|
|
|
11845 |
* array of source objects.
|
|
|
11846 |
*
|
|
|
11847 |
* @param {Tech~SourceObject|Tech~SourceObject[]} src
|
|
|
11848 |
* The src object to filter
|
|
|
11849 |
*
|
|
|
11850 |
* @return {Tech~SourceObject[]}
|
|
|
11851 |
* An array of sourceobjects containing only valid sources
|
|
|
11852 |
*
|
|
|
11853 |
* @private
|
|
|
11854 |
*/
|
|
|
11855 |
const filterSource = function (src) {
|
|
|
11856 |
// traverse array
|
|
|
11857 |
if (Array.isArray(src)) {
|
|
|
11858 |
let newsrc = [];
|
|
|
11859 |
src.forEach(function (srcobj) {
|
|
|
11860 |
srcobj = filterSource(srcobj);
|
|
|
11861 |
if (Array.isArray(srcobj)) {
|
|
|
11862 |
newsrc = newsrc.concat(srcobj);
|
|
|
11863 |
} else if (isObject$1(srcobj)) {
|
|
|
11864 |
newsrc.push(srcobj);
|
|
|
11865 |
}
|
|
|
11866 |
});
|
|
|
11867 |
src = newsrc;
|
|
|
11868 |
} else if (typeof src === 'string' && src.trim()) {
|
|
|
11869 |
// convert string into object
|
|
|
11870 |
src = [fixSource({
|
|
|
11871 |
src
|
|
|
11872 |
})];
|
|
|
11873 |
} else if (isObject$1(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
|
|
|
11874 |
// src is already valid
|
|
|
11875 |
src = [fixSource(src)];
|
|
|
11876 |
} else {
|
|
|
11877 |
// invalid source, turn it into an empty array
|
|
|
11878 |
src = [];
|
|
|
11879 |
}
|
|
|
11880 |
return src;
|
|
|
11881 |
};
|
|
|
11882 |
|
|
|
11883 |
/**
|
|
|
11884 |
* Checks src mimetype, adding it when possible
|
|
|
11885 |
*
|
|
|
11886 |
* @param {Tech~SourceObject} src
|
|
|
11887 |
* The src object to check
|
|
|
11888 |
* @return {Tech~SourceObject}
|
|
|
11889 |
* src Object with known type
|
|
|
11890 |
*/
|
|
|
11891 |
function fixSource(src) {
|
|
|
11892 |
if (!src.type) {
|
|
|
11893 |
const mimetype = getMimetype(src.src);
|
|
|
11894 |
if (mimetype) {
|
|
|
11895 |
src.type = mimetype;
|
|
|
11896 |
}
|
|
|
11897 |
}
|
|
|
11898 |
return src;
|
|
|
11899 |
}
|
|
|
11900 |
|
|
|
11901 |
var icons = "<svg xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play\">\n <path d=\"M16 10v28l22-14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-pause\">\n <path d=\"M12 38h8V10h-8v28zm16-28v28h8V10h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-audio\">\n <path d=\"M24 2C14.06 2 6 10.06 6 20v14c0 3.31 2.69 6 6 6h6V24h-8v-4c0-7.73 6.27-14 14-14s14 6.27 14 14v4h-8v16h6c3.31 0 6-2.69 6-6V20c0-9.94-8.06-18-18-18z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-captions\">\n <path d=\"M38 8H10c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM22 22h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2zm14 0h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-subtitles\">\n <path d=\"M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM8 24h8v4H8v-4zm20 12H8v-4h20v4zm12 0h-8v-4h8v4zm0-8H20v-4h20v4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-enter\">\n <path d=\"M14 28h-4v10h10v-4h-6v-6zm-4-8h4v-6h6v-4H10v10zm24 14h-6v4h10V28h-4v6zm-6-24v4h6v6h4V10H28z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-exit\">\n <path d=\"M10 32h6v6h4V28H10v4zm6-16h-6v4h10V10h-4v6zm12 22h4v-6h6v-4H28v10zm4-22v-6h-4v10h10v-4h-6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play-circle\">\n <path d=\"M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-mute\">\n <path d=\"M33 24c0-3.53-2.04-6.58-5-8.05v4.42l4.91 4.91c.06-.42.09-.85.09-1.28zm5 0c0 1.88-.41 3.65-1.08 5.28l3.03 3.03C41.25 29.82 42 27 42 24c0-8.56-5.99-15.72-14-17.54v4.13c5.78 1.72 10 7.07 10 13.41zM8.55 6L6 8.55 15.45 18H6v12h8l10 10V26.55l8.51 8.51c-1.34 1.03-2.85 1.86-4.51 2.36v4.13a17.94 17.94 0 0 0 7.37-3.62L39.45 42 42 39.45l-18-18L8.55 6zM24 8l-4.18 4.18L24 16.36V8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-low\">\n <path d=\"M14 18v12h8l10 10V8L22 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-medium\">\n <path d=\"M37 24c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zm-27-6v12h8l10 10V8L18 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-high\">\n <path d=\"M6 18v12h8l10 10V8L14 18H6zm27 6c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zM28 6.46v4.13c5.78 1.72 10 7.07 10 13.41s-4.22 11.69-10 13.41v4.13c8.01-1.82 14-8.97 14-17.54S36.01 8.28 28 6.46z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-spinner\">\n <path d=\"M18.8 21l9.53-16.51C26.94 4.18 25.49 4 24 4c-4.8 0-9.19 1.69-12.64 4.51l7.33 12.69.11-.2zm24.28-3c-1.84-5.85-6.3-10.52-11.99-12.68L23.77 18h19.31zm.52 2H28.62l.58 1 9.53 16.5C41.99 33.94 44 29.21 44 24c0-1.37-.14-2.71-.4-4zm-26.53 4l-7.8-13.5C6.01 14.06 4 18.79 4 24c0 1.37.14 2.71.4 4h14.98l-2.31-4zM4.92 30c1.84 5.85 6.3 10.52 11.99 12.68L24.23 30H4.92zm22.54 0l-7.8 13.51c1.4.31 2.85.49 4.34.49 4.8 0 9.19-1.69 12.64-4.51L29.31 26.8 27.46 30z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 24 24\" id=\"vjs-icon-hd\">\n <path d=\"M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm2-6h4c.55 0 1 .45 1 1v4c0 .55-.45 1-1 1h-4V9zm1.5 4.5h2v-3h-2v3z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-chapters\">\n <path d=\"M6 26h4v-4H6v4zm0 8h4v-4H6v4zm0-16h4v-4H6v4zm8 8h28v-4H14v4zm0 8h28v-4H14v4zm0-20v4h28v-4H14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 40 40\" id=\"vjs-icon-downloading\">\n <path d=\"M18.208 36.875q-3.208-.292-5.979-1.729-2.771-1.438-4.812-3.729-2.042-2.292-3.188-5.229-1.146-2.938-1.146-6.23 0-6.583 4.334-11.416 4.333-4.834 10.833-5.5v3.166q-5.167.75-8.583 4.646Q6.25 14.75 6.25 19.958q0 5.209 3.396 9.104 3.396 3.896 8.562 4.646zM20 28.417L11.542 20l2.083-2.083 4.917 4.916v-11.25h2.916v11.25l4.875-4.916L28.417 20zm1.792 8.458v-3.167q1.833-.25 3.541-.958 1.709-.708 3.167-1.875l2.333 2.292q-1.958 1.583-4.25 2.541-2.291.959-4.791 1.167zm6.791-27.792q-1.541-1.125-3.25-1.854-1.708-.729-3.541-1.021V3.042q2.5.25 4.77 1.208 2.271.958 4.271 2.5zm4.584 21.584l-2.25-2.25q1.166-1.5 1.854-3.209.687-1.708.937-3.541h3.209q-.292 2.5-1.229 4.791-.938 2.292-2.521 4.209zm.541-12.417q-.291-1.833-.958-3.562-.667-1.73-1.833-3.188l2.375-2.208q1.541 1.916 2.458 4.208.917 2.292 1.167 4.75z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download\">\n <path d=\"M10.8 40.55q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h26.35v-7.7h3.4v7.7q0 1.4-1 2.4t-2.4 1zM24 32.1L13.9 22.05l2.45-2.45 5.95 5.95V7.15h3.4v18.4l5.95-5.95 2.45 2.45z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-done\">\n <path d=\"M9.8 40.5v-3.45h28.4v3.45zm9.2-9.05L7.4 19.85l2.45-2.35L19 26.65l19.2-19.2 2.4 2.4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-off\">\n <path d=\"M4.9 4.75L43.25 43.1 41 45.3l-4.75-4.75q-.05.05-.075.025-.025-.025-.075-.025H10.8q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h22.05l-7-7-1.85 1.8L13.9 21.9l1.85-1.85L2.7 7zm26.75 14.7l2.45 2.45-3.75 3.8-2.45-2.5zM25.7 7.15V21.1l-3.4-3.45V7.15z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-share\">\n <path d=\"M36 32.17c-1.52 0-2.89.59-3.93 1.54L17.82 25.4c.11-.45.18-.92.18-1.4s-.07-.95-.18-1.4l14.1-8.23c1.07 1 2.5 1.62 4.08 1.62 3.31 0 6-2.69 6-6s-2.69-6-6-6-6 2.69-6 6c0 .48.07.95.18 1.4l-14.1 8.23c-1.07-1-2.5-1.62-4.08-1.62-3.31 0-6 2.69-6 6s2.69 6 6 6c1.58 0 3.01-.62 4.08-1.62l14.25 8.31c-.1.42-.16.86-.16 1.31A5.83 5.83 0 1 0 36 32.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cog\">\n <path d=\"M38.86 25.95c.08-.64.14-1.29.14-1.95s-.06-1.31-.14-1.95l4.23-3.31c.38-.3.49-.84.24-1.28l-4-6.93c-.25-.43-.77-.61-1.22-.43l-4.98 2.01c-1.03-.79-2.16-1.46-3.38-1.97L29 4.84c-.09-.47-.5-.84-1-.84h-8c-.5 0-.91.37-.99.84l-.75 5.3a14.8 14.8 0 0 0-3.38 1.97L9.9 10.1a1 1 0 0 0-1.22.43l-4 6.93c-.25.43-.14.97.24 1.28l4.22 3.31C9.06 22.69 9 23.34 9 24s.06 1.31.14 1.95l-4.22 3.31c-.38.3-.49.84-.24 1.28l4 6.93c.25.43.77.61 1.22.43l4.98-2.01c1.03.79 2.16 1.46 3.38 1.97l.75 5.3c.08.47.49.84.99.84h8c.5 0 .91-.37.99-.84l.75-5.3a14.8 14.8 0 0 0 3.38-1.97l4.98 2.01a1 1 0 0 0 1.22-.43l4-6.93c.25-.43.14-.97-.24-1.28l-4.22-3.31zM24 31c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-square\">\n <path d=\"M36 8H12c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm0 28H12V12h24v24z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle\">\n <circle cx=\"24\" cy=\"24\" r=\"20\"></circle>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-outline\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-inner-circle\">\n <path d=\"M24 4C12.97 4 4 12.97 4 24s8.97 20 20 20 20-8.97 20-20S35.03 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16zm6-16c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6 6 2.69 6 6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cancel\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm10 27.17L31.17 34 24 26.83 16.83 34 14 31.17 21.17 24 14 16.83 16.83 14 24 21.17 31.17 14 34 16.83 26.83 24 34 31.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-replay\">\n <path d=\"M24 10V2L14 12l10 10v-8c6.63 0 12 5.37 12 12s-5.37 12-12 12-12-5.37-12-12H8c0 8.84 7.16 16 16 16s16-7.16 16-16-7.16-16-16-16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-repeat\">\n <path d=\"M14 14h20v6l8-8-8-8v6H10v12h4v-8zm20 20H14v-6l-8 8 8 8v-6h24V26h-4v8z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-5\">\n <path d=\"M17.689 98l-8.697 8.696 8.697 8.697 2.486-2.485-4.32-4.319h1.302c4.93 0 9.071 1.722 12.424 5.165 3.352 3.443 5.029 7.638 5.029 12.584h3.55c0-2.958-.553-5.73-1.658-8.313-1.104-2.583-2.622-4.841-4.555-6.774-1.932-1.932-4.19-3.45-6.773-4.555-2.584-1.104-5.355-1.657-8.313-1.657H15.5l4.615-4.615zm-8.08 21.659v13.861h11.357v5.008H9.609V143h12.7c.834 0 1.55-.298 2.146-.894.596-.597.895-1.31.895-2.145v-7.781c0-.835-.299-1.55-.895-2.147a2.929 2.929 0 0 0-2.147-.894h-8.227v-5.096H25.35v-4.384z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-10\">\n <path d=\"M42.315 125.63c0-4.997-1.694-9.235-5.08-12.713-3.388-3.479-7.571-5.218-12.552-5.218h-1.315l4.363 4.363-2.51 2.51-8.787-8.786L25.221 97l2.45 2.45-4.662 4.663h1.375c2.988 0 5.788.557 8.397 1.673 2.61 1.116 4.892 2.65 6.844 4.602 1.953 1.953 3.487 4.234 4.602 6.844 1.116 2.61 1.674 5.41 1.674 8.398zM8.183 142v-19.657H3.176V117.8h9.643V142zm13.63 0c-1.156 0-2.127-.393-2.912-1.178-.778-.778-1.168-1.746-1.168-2.902v-16.04c0-1.156.393-2.127 1.178-2.912.779-.779 1.746-1.168 2.902-1.168h7.696c1.156 0 2.126.392 2.911 1.177.779.78 1.168 1.747 1.168 2.903v16.04c0 1.156-.392 2.127-1.177 2.912-.779.779-1.746 1.168-2.902 1.168zm.556-4.636h6.583v-15.02H22.37z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-30\">\n <path d=\"M26.047 97l-8.733 8.732 8.733 8.733 2.496-2.494-4.336-4.338h1.307c4.95 0 9.108 1.73 12.474 5.187 3.367 3.458 5.051 7.668 5.051 12.635h3.565c0-2.97-.556-5.751-1.665-8.346-1.109-2.594-2.633-4.862-4.574-6.802-1.94-1.941-4.208-3.466-6.803-4.575-2.594-1.109-5.375-1.664-8.345-1.664H23.85l4.634-4.634zM2.555 117.531v4.688h10.297v5.25H5.873v4.687h6.979v5.156H2.555V142H13.36c1.061 0 1.95-.395 2.668-1.186.718-.79 1.076-1.772 1.076-2.94v-16.218c0-1.168-.358-2.149-1.076-2.94-.717-.79-1.607-1.185-2.668-1.185zm22.482.14c-1.149 0-2.11.39-2.885 1.165-.78.78-1.172 1.744-1.172 2.893v15.943c0 1.149.388 2.11 1.163 2.885.78.78 1.745 1.172 2.894 1.172h7.649c1.148 0 2.11-.388 2.884-1.163.78-.78 1.17-1.745 1.17-2.894v-15.943c0-1.15-.386-2.111-1.16-2.885-.78-.78-1.746-1.172-2.894-1.172zm.553 4.518h6.545v14.93H25.59z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-5\">\n <path d=\"M29.508 97l-2.431 2.43 4.625 4.625h-1.364c-2.965 0-5.742.554-8.332 1.66-2.589 1.107-4.851 2.629-6.788 4.566-1.937 1.937-3.458 4.2-4.565 6.788-1.107 2.59-1.66 5.367-1.66 8.331h3.557c0-4.957 1.68-9.16 5.04-12.611 3.36-3.45 7.51-5.177 12.451-5.177h1.304l-4.326 4.33 2.49 2.49 8.715-8.716zm-9.783 21.61v13.89h11.382v5.018H19.725V142h12.727a2.93 2.93 0 0 0 2.15-.896 2.93 2.93 0 0 0 .896-2.15v-7.798c0-.837-.299-1.554-.896-2.152a2.93 2.93 0 0 0-2.15-.896h-8.245V123h11.29v-4.392z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-10\">\n <path d=\"M23.119 97l-2.386 2.383 4.538 4.538h-1.339c-2.908 0-5.633.543-8.173 1.63-2.54 1.085-4.76 2.577-6.66 4.478-1.9 1.9-3.392 4.12-4.478 6.66-1.085 2.54-1.629 5.264-1.629 8.172h3.49c0-4.863 1.648-8.986 4.944-12.372 3.297-3.385 7.368-5.078 12.216-5.078h1.279l-4.245 4.247 2.443 2.442 8.55-8.55zm-9.52 21.45v4.42h4.871V142h4.513v-23.55zm18.136 0c-1.125 0-2.066.377-2.824 1.135-.764.764-1.148 1.709-1.148 2.834v15.612c0 1.124.38 2.066 1.139 2.824.764.764 1.708 1.145 2.833 1.145h7.489c1.125 0 2.066-.378 2.824-1.136.764-.764 1.145-1.709 1.145-2.833v-15.612c0-1.125-.378-2.067-1.136-2.825-.764-.764-1.708-1.145-2.833-1.145zm.54 4.42h6.408v14.617h-6.407z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-30\">\n <path d=\"M25.549 97l-2.437 2.434 4.634 4.635H26.38c-2.97 0-5.753.555-8.347 1.664-2.594 1.109-4.861 2.633-6.802 4.574-1.94 1.94-3.465 4.207-4.574 6.802-1.109 2.594-1.664 5.377-1.664 8.347h3.565c0-4.967 1.683-9.178 5.05-12.636 3.366-3.458 7.525-5.187 12.475-5.187h1.307l-4.335 4.338 2.495 2.494 8.732-8.732zm-11.553 20.53v4.689h10.297v5.249h-6.978v4.688h6.978v5.156H13.996V142h10.808c1.06 0 1.948-.395 2.666-1.186.718-.79 1.077-1.771 1.077-2.94v-16.217c0-1.169-.36-2.15-1.077-2.94-.718-.79-1.605-1.186-2.666-1.186zm21.174.168c-1.149 0-2.11.389-2.884 1.163-.78.78-1.172 1.745-1.172 2.894v15.942c0 1.15.388 2.11 1.162 2.885.78.78 1.745 1.17 2.894 1.17h7.649c1.149 0 2.11-.386 2.885-1.16.78-.78 1.17-1.746 1.17-2.895v-15.942c0-1.15-.387-2.11-1.161-2.885-.78-.78-1.745-1.172-2.894-1.172zm.552 4.516h6.542v14.931h-6.542z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 512 512\" id=\"vjs-icon-audio-description\">\n <g fill-rule=\"evenodd\"><path d=\"M227.29 381.351V162.993c50.38-1.017 89.108-3.028 117.631 17.126 27.374 19.342 48.734 56.965 44.89 105.325-4.067 51.155-41.335 94.139-89.776 98.475-24.085 2.155-71.972 0-71.972 0s-.84-1.352-.773-2.568m48.755-54.804c31.43 1.26 53.208-16.633 56.495-45.386 4.403-38.51-21.188-63.552-58.041-60.796v103.612c-.036 1.466.575 2.22 1.546 2.57\"></path><path d=\"M383.78 381.328c13.336 3.71 17.387-11.06 23.215-21.408 12.722-22.571 22.294-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.226 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M425.154 381.328c13.336 3.71 17.384-11.061 23.215-21.408 12.721-22.571 22.291-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.511c-.586 3.874 2.226 7.315 3.866 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M466.26 381.328c13.337 3.71 17.385-11.061 23.216-21.408 12.722-22.571 22.292-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.225 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894M4.477 383.005H72.58l18.573-28.484 64.169-.135s.065 19.413.065 28.62h48.756V160.307h-58.816c-5.653 9.537-140.85 222.697-140.85 222.697zm152.667-145.282v71.158l-40.453-.27 40.453-70.888z\"></path></g>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-next-item\">\n <path d=\"M12 36l17-12-17-12v24zm20-24v24h4V12h-4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-previous-item\">\n <path d=\"M12 12h4v24h-4zm7 12l17 12V12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-shuffle\">\n <path d=\"M21.17 18.34L10.83 8 8 10.83l10.34 10.34 2.83-2.83zM29 8l4.09 4.09L8 37.17 10.83 40l25.09-25.09L40 19V8H29zm.66 18.83l-2.83 2.83 6.26 6.26L29 40h11V29l-4.09 4.09-6.25-6.26z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cast\">\n <path d=\"M42 6H6c-2.21 0-4 1.79-4 4v6h4v-6h36v28H28v4h14c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zM2 36v6h6c0-3.31-2.69-6-6-6zm0-8v4c5.52 0 10 4.48 10 10h4c0-7.73-6.27-14-14-14zm0-8v4c9.94 0 18 8.06 18 18h4c0-12.15-9.85-22-22-22z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-picture-in-picture-enter\">\n <path d=\"M38 22H22v11.99h16V22zm8 16V9.96C46 7.76 44.2 6 42 6H6C3.8 6 2 7.76 2 9.96V38c0 2.2 1.8 4 4 4h36c2.2 0 4-1.8 4-4zm-4 .04H6V9.94h36v28.1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 22 18\" id=\"vjs-icon-picture-in-picture-exit\">\n <path d=\"M18 4H4v10h14V4zm4 12V1.98C22 .88 21.1 0 20 0H2C.9 0 0 .88 0 1.98V16c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 .02H2V1.97h18v14.05z\"></path>\n <path fill=\"none\" d=\"M-1-3h24v24H-1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-facebook\">\n <path d=\"M1343 12v264h-157q-86 0-116 36t-30 108v189h293l-39 296h-254v759H734V905H479V609h255V391q0-186 104-288.5T1115 0q147 0 228 12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-linkedin\">\n <path d=\"M477 625v991H147V625h330zm21-306q1 73-50.5 122T312 490h-2q-82 0-132-49t-50-122q0-74 51.5-122.5T314 148t133 48.5T498 319zm1166 729v568h-329v-530q0-105-40.5-164.5T1168 862q-63 0-105.5 34.5T999 982q-11 30-11 81v553H659q2-399 2-647t-1-296l-1-48h329v144h-2q20-32 41-56t56.5-52 87-43.5T1285 602q171 0 275 113.5t104 332.5z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-twitter\">\n <path d=\"M1684 408q-67 98-162 167 1 14 1 42 0 130-38 259.5T1369.5 1125 1185 1335.5t-258 146-323 54.5q-271 0-496-145 35 4 78 4 225 0 401-138-105-2-188-64.5T285 1033q33 5 61 5 43 0 85-11-112-23-185.5-111.5T172 710v-4q68 38 146 41-66-44-105-115t-39-154q0-88 44-163 121 149 294.5 238.5T884 653q-8-38-8-74 0-134 94.5-228.5T1199 256q140 0 236 102 109-21 205-78-37 115-142 178 93-10 186-50z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-tumblr\">\n <path d=\"M1328 1329l80 237q-23 35-111 66t-177 32q-104 2-190.5-26T787 1564t-95-106-55.5-120-16.5-118V676H452V461q72-26 129-69.5t91-90 58-102 34-99T779 12q1-5 4.5-8.5T791 0h244v424h333v252h-334v518q0 30 6.5 56t22.5 52.5 49.5 41.5 81.5 14q78-2 134-29z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-pinterest\">\n <path d=\"M1664 896q0 209-103 385.5T1281.5 1561 896 1664q-111 0-218-32 59-93 78-164 9-34 54-211 20 39 73 67.5t114 28.5q121 0 216-68.5t147-188.5 52-270q0-114-59.5-214T1180 449t-255-63q-105 0-196 29t-154.5 77-109 110.5-67 129.5T377 866q0 104 40 183t117 111q30 12 38-20 2-7 8-31t8-30q6-23-11-43-51-61-51-151 0-151 104.5-259.5T904 517q151 0 235.5 82t84.5 213q0 170-68.5 289T980 1220q-61 0-98-43.5T859 1072q8-35 26.5-93.5t30-103T927 800q0-50-27-83t-77-33q-62 0-105 57t-43 142q0 73 25 122l-99 418q-17 70-13 177-206-91-333-281T128 896q0-209 103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z\"></path>\n </symbol>\n </defs>\n</svg>";
|
|
|
11902 |
|
|
|
11903 |
/**
|
|
|
11904 |
* @file loader.js
|
|
|
11905 |
*/
|
|
|
11906 |
|
|
|
11907 |
/**
|
|
|
11908 |
* The `MediaLoader` is the `Component` that decides which playback technology to load
|
|
|
11909 |
* when a player is initialized.
|
|
|
11910 |
*
|
|
|
11911 |
* @extends Component
|
|
|
11912 |
*/
|
|
|
11913 |
class MediaLoader extends Component$1 {
|
|
|
11914 |
/**
|
|
|
11915 |
* Create an instance of this class.
|
|
|
11916 |
*
|
|
|
11917 |
* @param { import('../player').default } player
|
|
|
11918 |
* The `Player` that this class should attach to.
|
|
|
11919 |
*
|
|
|
11920 |
* @param {Object} [options]
|
|
|
11921 |
* The key/value store of player options.
|
|
|
11922 |
*
|
|
|
11923 |
* @param {Function} [ready]
|
|
|
11924 |
* The function that is run when this component is ready.
|
|
|
11925 |
*/
|
|
|
11926 |
constructor(player, options, ready) {
|
|
|
11927 |
// MediaLoader has no element
|
|
|
11928 |
const options_ = merge$2({
|
|
|
11929 |
createEl: false
|
|
|
11930 |
}, options);
|
|
|
11931 |
super(player, options_, ready);
|
|
|
11932 |
|
|
|
11933 |
// If there are no sources when the player is initialized,
|
|
|
11934 |
// load the first supported playback technology.
|
|
|
11935 |
|
|
|
11936 |
if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
|
|
|
11937 |
for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
|
|
|
11938 |
const techName = toTitleCase$1(j[i]);
|
|
|
11939 |
let tech = Tech.getTech(techName);
|
|
|
11940 |
|
|
|
11941 |
// Support old behavior of techs being registered as components.
|
|
|
11942 |
// Remove once that deprecated behavior is removed.
|
|
|
11943 |
if (!techName) {
|
|
|
11944 |
tech = Component$1.getComponent(techName);
|
|
|
11945 |
}
|
|
|
11946 |
|
|
|
11947 |
// Check if the browser supports this technology
|
|
|
11948 |
if (tech && tech.isSupported()) {
|
|
|
11949 |
player.loadTech_(techName);
|
|
|
11950 |
break;
|
|
|
11951 |
}
|
|
|
11952 |
}
|
|
|
11953 |
} else {
|
|
|
11954 |
// Loop through playback technologies (e.g. HTML5) and check for support.
|
|
|
11955 |
// Then load the best source.
|
|
|
11956 |
// A few assumptions here:
|
|
|
11957 |
// All playback technologies respect preload false.
|
|
|
11958 |
player.src(options.playerOptions.sources);
|
|
|
11959 |
}
|
|
|
11960 |
}
|
|
|
11961 |
}
|
|
|
11962 |
Component$1.registerComponent('MediaLoader', MediaLoader);
|
|
|
11963 |
|
|
|
11964 |
/**
|
|
|
11965 |
* @file clickable-component.js
|
|
|
11966 |
*/
|
|
|
11967 |
|
|
|
11968 |
/**
|
|
|
11969 |
* Component which is clickable or keyboard actionable, but is not a
|
|
|
11970 |
* native HTML button.
|
|
|
11971 |
*
|
|
|
11972 |
* @extends Component
|
|
|
11973 |
*/
|
|
|
11974 |
class ClickableComponent extends Component$1 {
|
|
|
11975 |
/**
|
|
|
11976 |
* Creates an instance of this class.
|
|
|
11977 |
*
|
|
|
11978 |
* @param { import('./player').default } player
|
|
|
11979 |
* The `Player` that this class should be attached to.
|
|
|
11980 |
*
|
|
|
11981 |
* @param {Object} [options]
|
|
|
11982 |
* The key/value store of component options.
|
|
|
11983 |
*
|
|
|
11984 |
* @param {function} [options.clickHandler]
|
|
|
11985 |
* The function to call when the button is clicked / activated
|
|
|
11986 |
*
|
|
|
11987 |
* @param {string} [options.controlText]
|
|
|
11988 |
* The text to set on the button
|
|
|
11989 |
*
|
|
|
11990 |
* @param {string} [options.className]
|
|
|
11991 |
* A class or space separated list of classes to add the component
|
|
|
11992 |
*
|
|
|
11993 |
*/
|
|
|
11994 |
constructor(player, options) {
|
|
|
11995 |
super(player, options);
|
|
|
11996 |
if (this.options_.controlText) {
|
|
|
11997 |
this.controlText(this.options_.controlText);
|
|
|
11998 |
}
|
|
|
11999 |
this.handleMouseOver_ = e => this.handleMouseOver(e);
|
|
|
12000 |
this.handleMouseOut_ = e => this.handleMouseOut(e);
|
|
|
12001 |
this.handleClick_ = e => this.handleClick(e);
|
|
|
12002 |
this.handleKeyDown_ = e => this.handleKeyDown(e);
|
|
|
12003 |
this.emitTapEvents();
|
|
|
12004 |
this.enable();
|
|
|
12005 |
}
|
|
|
12006 |
|
|
|
12007 |
/**
|
|
|
12008 |
* Create the `ClickableComponent`s DOM element.
|
|
|
12009 |
*
|
|
|
12010 |
* @param {string} [tag=div]
|
|
|
12011 |
* The element's node type.
|
|
|
12012 |
*
|
|
|
12013 |
* @param {Object} [props={}]
|
|
|
12014 |
* An object of properties that should be set on the element.
|
|
|
12015 |
*
|
|
|
12016 |
* @param {Object} [attributes={}]
|
|
|
12017 |
* An object of attributes that should be set on the element.
|
|
|
12018 |
*
|
|
|
12019 |
* @return {Element}
|
|
|
12020 |
* The element that gets created.
|
|
|
12021 |
*/
|
|
|
12022 |
createEl(tag = 'div', props = {}, attributes = {}) {
|
|
|
12023 |
props = Object.assign({
|
|
|
12024 |
className: this.buildCSSClass(),
|
|
|
12025 |
tabIndex: 0
|
|
|
12026 |
}, props);
|
|
|
12027 |
if (tag === 'button') {
|
|
|
12028 |
log$1.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
|
|
|
12029 |
}
|
|
|
12030 |
|
|
|
12031 |
// Add ARIA attributes for clickable element which is not a native HTML button
|
|
|
12032 |
attributes = Object.assign({
|
|
|
12033 |
role: 'button'
|
|
|
12034 |
}, attributes);
|
|
|
12035 |
this.tabIndex_ = props.tabIndex;
|
|
|
12036 |
const el = createEl(tag, props, attributes);
|
|
|
12037 |
if (!this.player_.options_.experimentalSvgIcons) {
|
|
|
12038 |
el.appendChild(createEl('span', {
|
|
|
12039 |
className: 'vjs-icon-placeholder'
|
|
|
12040 |
}, {
|
|
|
12041 |
'aria-hidden': true
|
|
|
12042 |
}));
|
|
|
12043 |
}
|
|
|
12044 |
this.createControlTextEl(el);
|
|
|
12045 |
return el;
|
|
|
12046 |
}
|
|
|
12047 |
dispose() {
|
|
|
12048 |
// remove controlTextEl_ on dispose
|
|
|
12049 |
this.controlTextEl_ = null;
|
|
|
12050 |
super.dispose();
|
|
|
12051 |
}
|
|
|
12052 |
|
|
|
12053 |
/**
|
|
|
12054 |
* Create a control text element on this `ClickableComponent`
|
|
|
12055 |
*
|
|
|
12056 |
* @param {Element} [el]
|
|
|
12057 |
* Parent element for the control text.
|
|
|
12058 |
*
|
|
|
12059 |
* @return {Element}
|
|
|
12060 |
* The control text element that gets created.
|
|
|
12061 |
*/
|
|
|
12062 |
createControlTextEl(el) {
|
|
|
12063 |
this.controlTextEl_ = createEl('span', {
|
|
|
12064 |
className: 'vjs-control-text'
|
|
|
12065 |
}, {
|
|
|
12066 |
// let the screen reader user know that the text of the element may change
|
|
|
12067 |
'aria-live': 'polite'
|
|
|
12068 |
});
|
|
|
12069 |
if (el) {
|
|
|
12070 |
el.appendChild(this.controlTextEl_);
|
|
|
12071 |
}
|
|
|
12072 |
this.controlText(this.controlText_, el);
|
|
|
12073 |
return this.controlTextEl_;
|
|
|
12074 |
}
|
|
|
12075 |
|
|
|
12076 |
/**
|
|
|
12077 |
* Get or set the localize text to use for the controls on the `ClickableComponent`.
|
|
|
12078 |
*
|
|
|
12079 |
* @param {string} [text]
|
|
|
12080 |
* Control text for element.
|
|
|
12081 |
*
|
|
|
12082 |
* @param {Element} [el=this.el()]
|
|
|
12083 |
* Element to set the title on.
|
|
|
12084 |
*
|
|
|
12085 |
* @return {string}
|
|
|
12086 |
* - The control text when getting
|
|
|
12087 |
*/
|
|
|
12088 |
controlText(text, el = this.el()) {
|
|
|
12089 |
if (text === undefined) {
|
|
|
12090 |
return this.controlText_ || 'Need Text';
|
|
|
12091 |
}
|
|
|
12092 |
const localizedText = this.localize(text);
|
|
|
12093 |
|
|
|
12094 |
/** @protected */
|
|
|
12095 |
this.controlText_ = text;
|
|
|
12096 |
textContent(this.controlTextEl_, localizedText);
|
|
|
12097 |
if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
|
|
|
12098 |
// Set title attribute if only an icon is shown
|
|
|
12099 |
el.setAttribute('title', localizedText);
|
|
|
12100 |
}
|
|
|
12101 |
}
|
|
|
12102 |
|
|
|
12103 |
/**
|
|
|
12104 |
* Builds the default DOM `className`.
|
|
|
12105 |
*
|
|
|
12106 |
* @return {string}
|
|
|
12107 |
* The DOM `className` for this object.
|
|
|
12108 |
*/
|
|
|
12109 |
buildCSSClass() {
|
|
|
12110 |
return `vjs-control vjs-button ${super.buildCSSClass()}`;
|
|
|
12111 |
}
|
|
|
12112 |
|
|
|
12113 |
/**
|
|
|
12114 |
* Enable this `ClickableComponent`
|
|
|
12115 |
*/
|
|
|
12116 |
enable() {
|
|
|
12117 |
if (!this.enabled_) {
|
|
|
12118 |
this.enabled_ = true;
|
|
|
12119 |
this.removeClass('vjs-disabled');
|
|
|
12120 |
this.el_.setAttribute('aria-disabled', 'false');
|
|
|
12121 |
if (typeof this.tabIndex_ !== 'undefined') {
|
|
|
12122 |
this.el_.setAttribute('tabIndex', this.tabIndex_);
|
|
|
12123 |
}
|
|
|
12124 |
this.on(['tap', 'click'], this.handleClick_);
|
|
|
12125 |
this.on('keydown', this.handleKeyDown_);
|
|
|
12126 |
}
|
|
|
12127 |
}
|
|
|
12128 |
|
|
|
12129 |
/**
|
|
|
12130 |
* Disable this `ClickableComponent`
|
|
|
12131 |
*/
|
|
|
12132 |
disable() {
|
|
|
12133 |
this.enabled_ = false;
|
|
|
12134 |
this.addClass('vjs-disabled');
|
|
|
12135 |
this.el_.setAttribute('aria-disabled', 'true');
|
|
|
12136 |
if (typeof this.tabIndex_ !== 'undefined') {
|
|
|
12137 |
this.el_.removeAttribute('tabIndex');
|
|
|
12138 |
}
|
|
|
12139 |
this.off('mouseover', this.handleMouseOver_);
|
|
|
12140 |
this.off('mouseout', this.handleMouseOut_);
|
|
|
12141 |
this.off(['tap', 'click'], this.handleClick_);
|
|
|
12142 |
this.off('keydown', this.handleKeyDown_);
|
|
|
12143 |
}
|
|
|
12144 |
|
|
|
12145 |
/**
|
|
|
12146 |
* Handles language change in ClickableComponent for the player in components
|
|
|
12147 |
*
|
|
|
12148 |
*
|
|
|
12149 |
*/
|
|
|
12150 |
handleLanguagechange() {
|
|
|
12151 |
this.controlText(this.controlText_);
|
|
|
12152 |
}
|
|
|
12153 |
|
|
|
12154 |
/**
|
|
|
12155 |
* Event handler that is called when a `ClickableComponent` receives a
|
|
|
12156 |
* `click` or `tap` event.
|
|
|
12157 |
*
|
|
|
12158 |
* @param {Event} event
|
|
|
12159 |
* The `tap` or `click` event that caused this function to be called.
|
|
|
12160 |
*
|
|
|
12161 |
* @listens tap
|
|
|
12162 |
* @listens click
|
|
|
12163 |
* @abstract
|
|
|
12164 |
*/
|
|
|
12165 |
handleClick(event) {
|
|
|
12166 |
if (this.options_.clickHandler) {
|
|
|
12167 |
this.options_.clickHandler.call(this, arguments);
|
|
|
12168 |
}
|
|
|
12169 |
}
|
|
|
12170 |
|
|
|
12171 |
/**
|
|
|
12172 |
* Event handler that is called when a `ClickableComponent` receives a
|
|
|
12173 |
* `keydown` event.
|
|
|
12174 |
*
|
|
|
12175 |
* By default, if the key is Space or Enter, it will trigger a `click` event.
|
|
|
12176 |
*
|
|
|
12177 |
* @param {KeyboardEvent} event
|
|
|
12178 |
* The `keydown` event that caused this function to be called.
|
|
|
12179 |
*
|
|
|
12180 |
* @listens keydown
|
|
|
12181 |
*/
|
|
|
12182 |
handleKeyDown(event) {
|
|
|
12183 |
// Support Space or Enter key operation to fire a click event. Also,
|
|
|
12184 |
// prevent the event from propagating through the DOM and triggering
|
|
|
12185 |
// Player hotkeys.
|
|
|
12186 |
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
|
|
|
12187 |
event.preventDefault();
|
|
|
12188 |
event.stopPropagation();
|
|
|
12189 |
this.trigger('click');
|
|
|
12190 |
} else {
|
|
|
12191 |
// Pass keypress handling up for unsupported keys
|
|
|
12192 |
super.handleKeyDown(event);
|
|
|
12193 |
}
|
|
|
12194 |
}
|
|
|
12195 |
}
|
|
|
12196 |
Component$1.registerComponent('ClickableComponent', ClickableComponent);
|
|
|
12197 |
|
|
|
12198 |
/**
|
|
|
12199 |
* @file poster-image.js
|
|
|
12200 |
*/
|
|
|
12201 |
|
|
|
12202 |
/**
|
|
|
12203 |
* A `ClickableComponent` that handles showing the poster image for the player.
|
|
|
12204 |
*
|
|
|
12205 |
* @extends ClickableComponent
|
|
|
12206 |
*/
|
|
|
12207 |
class PosterImage extends ClickableComponent {
|
|
|
12208 |
/**
|
|
|
12209 |
* Create an instance of this class.
|
|
|
12210 |
*
|
|
|
12211 |
* @param { import('./player').default } player
|
|
|
12212 |
* The `Player` that this class should attach to.
|
|
|
12213 |
*
|
|
|
12214 |
* @param {Object} [options]
|
|
|
12215 |
* The key/value store of player options.
|
|
|
12216 |
*/
|
|
|
12217 |
constructor(player, options) {
|
|
|
12218 |
super(player, options);
|
|
|
12219 |
this.update();
|
|
|
12220 |
this.update_ = e => this.update(e);
|
|
|
12221 |
player.on('posterchange', this.update_);
|
|
|
12222 |
}
|
|
|
12223 |
|
|
|
12224 |
/**
|
|
|
12225 |
* Clean up and dispose of the `PosterImage`.
|
|
|
12226 |
*/
|
|
|
12227 |
dispose() {
|
|
|
12228 |
this.player().off('posterchange', this.update_);
|
|
|
12229 |
super.dispose();
|
|
|
12230 |
}
|
|
|
12231 |
|
|
|
12232 |
/**
|
|
|
12233 |
* Create the `PosterImage`s DOM element.
|
|
|
12234 |
*
|
|
|
12235 |
* @return {Element}
|
|
|
12236 |
* The element that gets created.
|
|
|
12237 |
*/
|
|
|
12238 |
createEl() {
|
|
|
12239 |
// The el is an empty div to keep position in the DOM
|
|
|
12240 |
// A picture and img el will be inserted when a source is set
|
|
|
12241 |
return createEl('div', {
|
|
|
12242 |
className: 'vjs-poster'
|
|
|
12243 |
});
|
|
|
12244 |
}
|
|
|
12245 |
|
|
|
12246 |
/**
|
|
|
12247 |
* Get or set the `PosterImage`'s crossOrigin option.
|
|
|
12248 |
*
|
|
|
12249 |
* @param {string|null} [value]
|
|
|
12250 |
* The value to set the crossOrigin to. If an argument is
|
|
|
12251 |
* given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
|
|
|
12252 |
*
|
|
|
12253 |
* @return {string|null}
|
|
|
12254 |
* - The current crossOrigin value of the `Player` when getting.
|
|
|
12255 |
* - undefined when setting
|
|
|
12256 |
*/
|
|
|
12257 |
crossOrigin(value) {
|
|
|
12258 |
// `null` can be set to unset a value
|
|
|
12259 |
if (typeof value === 'undefined') {
|
|
|
12260 |
if (this.$('img')) {
|
|
|
12261 |
// If the poster's element exists, give its value
|
|
|
12262 |
return this.$('img').crossOrigin;
|
|
|
12263 |
} else if (this.player_.tech_ && this.player_.tech_.isReady_) {
|
|
|
12264 |
// If not but the tech is ready, query the tech
|
|
|
12265 |
return this.player_.crossOrigin();
|
|
|
12266 |
}
|
|
|
12267 |
// Otherwise check options as the poster is usually set before the state of crossorigin
|
|
|
12268 |
// can be retrieved by the getter
|
|
|
12269 |
return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
|
|
|
12270 |
}
|
|
|
12271 |
if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
|
|
|
12272 |
this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
|
|
|
12273 |
return;
|
|
|
12274 |
}
|
|
|
12275 |
if (this.$('img')) {
|
|
|
12276 |
this.$('img').crossOrigin = value;
|
|
|
12277 |
}
|
|
|
12278 |
return;
|
|
|
12279 |
}
|
|
|
12280 |
|
|
|
12281 |
/**
|
|
|
12282 |
* An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
|
|
|
12283 |
*
|
|
|
12284 |
* @listens Player#posterchange
|
|
|
12285 |
*
|
|
|
12286 |
* @param {Event} [event]
|
|
|
12287 |
* The `Player#posterchange` event that triggered this function.
|
|
|
12288 |
*/
|
|
|
12289 |
update(event) {
|
|
|
12290 |
const url = this.player().poster();
|
|
|
12291 |
this.setSrc(url);
|
|
|
12292 |
|
|
|
12293 |
// If there's no poster source we should display:none on this component
|
|
|
12294 |
// so it's not still clickable or right-clickable
|
|
|
12295 |
if (url) {
|
|
|
12296 |
this.show();
|
|
|
12297 |
} else {
|
|
|
12298 |
this.hide();
|
|
|
12299 |
}
|
|
|
12300 |
}
|
|
|
12301 |
|
|
|
12302 |
/**
|
|
|
12303 |
* Set the source of the `PosterImage` depending on the display method. (Re)creates
|
|
|
12304 |
* the inner picture and img elementss when needed.
|
|
|
12305 |
*
|
|
|
12306 |
* @param {string} [url]
|
|
|
12307 |
* The URL to the source for the `PosterImage`. If not specified or falsy,
|
|
|
12308 |
* any source and ant inner picture/img are removed.
|
|
|
12309 |
*/
|
|
|
12310 |
setSrc(url) {
|
|
|
12311 |
if (!url) {
|
|
|
12312 |
this.el_.textContent = '';
|
|
|
12313 |
return;
|
|
|
12314 |
}
|
|
|
12315 |
if (!this.$('img')) {
|
|
|
12316 |
this.el_.appendChild(createEl('picture', {
|
|
|
12317 |
className: 'vjs-poster',
|
|
|
12318 |
// Don't want poster to be tabbable.
|
|
|
12319 |
tabIndex: -1
|
|
|
12320 |
}, {}, createEl('img', {
|
|
|
12321 |
loading: 'lazy',
|
|
|
12322 |
crossOrigin: this.crossOrigin()
|
|
|
12323 |
}, {
|
|
|
12324 |
alt: ''
|
|
|
12325 |
})));
|
|
|
12326 |
}
|
|
|
12327 |
this.$('img').src = url;
|
|
|
12328 |
}
|
|
|
12329 |
|
|
|
12330 |
/**
|
|
|
12331 |
* An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
|
|
|
12332 |
* {@link ClickableComponent#handleClick} for instances where this will be triggered.
|
|
|
12333 |
*
|
|
|
12334 |
* @listens tap
|
|
|
12335 |
* @listens click
|
|
|
12336 |
* @listens keydown
|
|
|
12337 |
*
|
|
|
12338 |
* @param {Event} event
|
|
|
12339 |
+ The `click`, `tap` or `keydown` event that caused this function to be called.
|
|
|
12340 |
*/
|
|
|
12341 |
handleClick(event) {
|
|
|
12342 |
// We don't want a click to trigger playback when controls are disabled
|
|
|
12343 |
if (!this.player_.controls()) {
|
|
|
12344 |
return;
|
|
|
12345 |
}
|
|
|
12346 |
if (this.player_.tech(true)) {
|
|
|
12347 |
this.player_.tech(true).focus();
|
|
|
12348 |
}
|
|
|
12349 |
if (this.player_.paused()) {
|
|
|
12350 |
silencePromise(this.player_.play());
|
|
|
12351 |
} else {
|
|
|
12352 |
this.player_.pause();
|
|
|
12353 |
}
|
|
|
12354 |
}
|
|
|
12355 |
}
|
|
|
12356 |
|
|
|
12357 |
/**
|
|
|
12358 |
* Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
|
|
|
12359 |
* sets the `crossOrigin` property on the `<img>` tag to control the CORS
|
|
|
12360 |
* behavior.
|
|
|
12361 |
*
|
|
|
12362 |
* @param {string|null} [value]
|
|
|
12363 |
* The value to set the `PosterImages`'s crossorigin to. If an argument is
|
|
|
12364 |
* given, must be one of `anonymous` or `use-credentials`.
|
|
|
12365 |
*
|
|
|
12366 |
* @return {string|null|undefined}
|
|
|
12367 |
* - The current crossorigin value of the `Player` when getting.
|
|
|
12368 |
* - undefined when setting
|
|
|
12369 |
*/
|
|
|
12370 |
PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
|
|
|
12371 |
Component$1.registerComponent('PosterImage', PosterImage);
|
|
|
12372 |
|
|
|
12373 |
/**
|
|
|
12374 |
* @file text-track-display.js
|
|
|
12375 |
*/
|
|
|
12376 |
const darkGray = '#222';
|
|
|
12377 |
const lightGray = '#ccc';
|
|
|
12378 |
const fontMap = {
|
|
|
12379 |
monospace: 'monospace',
|
|
|
12380 |
sansSerif: 'sans-serif',
|
|
|
12381 |
serif: 'serif',
|
|
|
12382 |
monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
|
|
|
12383 |
monospaceSerif: '"Courier New", monospace',
|
|
|
12384 |
proportionalSansSerif: 'sans-serif',
|
|
|
12385 |
proportionalSerif: 'serif',
|
|
|
12386 |
casual: '"Comic Sans MS", Impact, fantasy',
|
|
|
12387 |
script: '"Monotype Corsiva", cursive',
|
|
|
12388 |
smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
|
|
|
12389 |
};
|
|
|
12390 |
|
|
|
12391 |
/**
|
|
|
12392 |
* Construct an rgba color from a given hex color code.
|
|
|
12393 |
*
|
|
|
12394 |
* @param {number} color
|
|
|
12395 |
* Hex number for color, like #f0e or #f604e2.
|
|
|
12396 |
*
|
|
|
12397 |
* @param {number} opacity
|
|
|
12398 |
* Value for opacity, 0.0 - 1.0.
|
|
|
12399 |
*
|
|
|
12400 |
* @return {string}
|
|
|
12401 |
* The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
|
|
|
12402 |
*/
|
|
|
12403 |
function constructColor(color, opacity) {
|
|
|
12404 |
let hex;
|
|
|
12405 |
if (color.length === 4) {
|
|
|
12406 |
// color looks like "#f0e"
|
|
|
12407 |
hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
|
|
12408 |
} else if (color.length === 7) {
|
|
|
12409 |
// color looks like "#f604e2"
|
|
|
12410 |
hex = color.slice(1);
|
|
|
12411 |
} else {
|
|
|
12412 |
throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
|
|
|
12413 |
}
|
|
|
12414 |
return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
|
|
|
12415 |
}
|
|
|
12416 |
|
|
|
12417 |
/**
|
|
|
12418 |
* Try to update the style of a DOM element. Some style changes will throw an error,
|
|
|
12419 |
* particularly in IE8. Those should be noops.
|
|
|
12420 |
*
|
|
|
12421 |
* @param {Element} el
|
|
|
12422 |
* The DOM element to be styled.
|
|
|
12423 |
*
|
|
|
12424 |
* @param {string} style
|
|
|
12425 |
* The CSS property on the element that should be styled.
|
|
|
12426 |
*
|
|
|
12427 |
* @param {string} rule
|
|
|
12428 |
* The style rule that should be applied to the property.
|
|
|
12429 |
*
|
|
|
12430 |
* @private
|
|
|
12431 |
*/
|
|
|
12432 |
function tryUpdateStyle(el, style, rule) {
|
|
|
12433 |
try {
|
|
|
12434 |
el.style[style] = rule;
|
|
|
12435 |
} catch (e) {
|
|
|
12436 |
// Satisfies linter.
|
|
|
12437 |
return;
|
|
|
12438 |
}
|
|
|
12439 |
}
|
|
|
12440 |
|
|
|
12441 |
/**
|
|
|
12442 |
* Converts the CSS top/right/bottom/left property numeric value to string in pixels.
|
|
|
12443 |
*
|
|
|
12444 |
* @param {number} position
|
|
|
12445 |
* The CSS top/right/bottom/left property value.
|
|
|
12446 |
*
|
|
|
12447 |
* @return {string}
|
|
|
12448 |
* The CSS property value that was created, like '10px'.
|
|
|
12449 |
*
|
|
|
12450 |
* @private
|
|
|
12451 |
*/
|
|
|
12452 |
function getCSSPositionValue(position) {
|
|
|
12453 |
return position ? `${position}px` : '';
|
|
|
12454 |
}
|
|
|
12455 |
|
|
|
12456 |
/**
|
|
|
12457 |
* The component for displaying text track cues.
|
|
|
12458 |
*
|
|
|
12459 |
* @extends Component
|
|
|
12460 |
*/
|
|
|
12461 |
class TextTrackDisplay extends Component$1 {
|
|
|
12462 |
/**
|
|
|
12463 |
* Creates an instance of this class.
|
|
|
12464 |
*
|
|
|
12465 |
* @param { import('../player').default } player
|
|
|
12466 |
* The `Player` that this class should be attached to.
|
|
|
12467 |
*
|
|
|
12468 |
* @param {Object} [options]
|
|
|
12469 |
* The key/value store of player options.
|
|
|
12470 |
*
|
|
|
12471 |
* @param {Function} [ready]
|
|
|
12472 |
* The function to call when `TextTrackDisplay` is ready.
|
|
|
12473 |
*/
|
|
|
12474 |
constructor(player, options, ready) {
|
|
|
12475 |
super(player, options, ready);
|
|
|
12476 |
const updateDisplayTextHandler = e => this.updateDisplay(e);
|
|
|
12477 |
const updateDisplayHandler = e => {
|
|
|
12478 |
this.updateDisplayOverlay();
|
|
|
12479 |
this.updateDisplay(e);
|
|
|
12480 |
};
|
|
|
12481 |
player.on('loadstart', e => this.toggleDisplay(e));
|
|
|
12482 |
player.on('texttrackchange', updateDisplayTextHandler);
|
|
|
12483 |
player.on('loadedmetadata', e => {
|
|
|
12484 |
this.updateDisplayOverlay();
|
|
|
12485 |
this.preselectTrack(e);
|
|
|
12486 |
});
|
|
|
12487 |
|
|
|
12488 |
// This used to be called during player init, but was causing an error
|
|
|
12489 |
// if a track should show by default and the display hadn't loaded yet.
|
|
|
12490 |
// Should probably be moved to an external track loader when we support
|
|
|
12491 |
// tracks that don't need a display.
|
|
|
12492 |
player.ready(bind_(this, function () {
|
|
|
12493 |
if (player.tech_ && player.tech_.featuresNativeTextTracks) {
|
|
|
12494 |
this.hide();
|
|
|
12495 |
return;
|
|
|
12496 |
}
|
|
|
12497 |
player.on('fullscreenchange', updateDisplayHandler);
|
|
|
12498 |
player.on('playerresize', updateDisplayHandler);
|
|
|
12499 |
const screenOrientation = window.screen.orientation || window;
|
|
|
12500 |
const changeOrientationEvent = window.screen.orientation ? 'change' : 'orientationchange';
|
|
|
12501 |
screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
|
|
|
12502 |
player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
|
|
|
12503 |
const tracks = this.options_.playerOptions.tracks || [];
|
|
|
12504 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
12505 |
this.player_.addRemoteTextTrack(tracks[i], true);
|
|
|
12506 |
}
|
|
|
12507 |
this.preselectTrack();
|
|
|
12508 |
}));
|
|
|
12509 |
}
|
|
|
12510 |
|
|
|
12511 |
/**
|
|
|
12512 |
* Preselect a track following this precedence:
|
|
|
12513 |
* - matches the previously selected {@link TextTrack}'s language and kind
|
|
|
12514 |
* - matches the previously selected {@link TextTrack}'s language only
|
|
|
12515 |
* - is the first default captions track
|
|
|
12516 |
* - is the first default descriptions track
|
|
|
12517 |
*
|
|
|
12518 |
* @listens Player#loadstart
|
|
|
12519 |
*/
|
|
|
12520 |
preselectTrack() {
|
|
|
12521 |
const modes = {
|
|
|
12522 |
captions: 1,
|
|
|
12523 |
subtitles: 1
|
|
|
12524 |
};
|
|
|
12525 |
const trackList = this.player_.textTracks();
|
|
|
12526 |
const userPref = this.player_.cache_.selectedLanguage;
|
|
|
12527 |
let firstDesc;
|
|
|
12528 |
let firstCaptions;
|
|
|
12529 |
let preferredTrack;
|
|
|
12530 |
for (let i = 0; i < trackList.length; i++) {
|
|
|
12531 |
const track = trackList[i];
|
|
|
12532 |
if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
|
|
|
12533 |
// Always choose the track that matches both language and kind
|
|
|
12534 |
if (track.kind === userPref.kind) {
|
|
|
12535 |
preferredTrack = track;
|
|
|
12536 |
// or choose the first track that matches language
|
|
|
12537 |
} else if (!preferredTrack) {
|
|
|
12538 |
preferredTrack = track;
|
|
|
12539 |
}
|
|
|
12540 |
|
|
|
12541 |
// clear everything if offTextTrackMenuItem was clicked
|
|
|
12542 |
} else if (userPref && !userPref.enabled) {
|
|
|
12543 |
preferredTrack = null;
|
|
|
12544 |
firstDesc = null;
|
|
|
12545 |
firstCaptions = null;
|
|
|
12546 |
} else if (track.default) {
|
|
|
12547 |
if (track.kind === 'descriptions' && !firstDesc) {
|
|
|
12548 |
firstDesc = track;
|
|
|
12549 |
} else if (track.kind in modes && !firstCaptions) {
|
|
|
12550 |
firstCaptions = track;
|
|
|
12551 |
}
|
|
|
12552 |
}
|
|
|
12553 |
}
|
|
|
12554 |
|
|
|
12555 |
// The preferredTrack matches the user preference and takes
|
|
|
12556 |
// precedence over all the other tracks.
|
|
|
12557 |
// So, display the preferredTrack before the first default track
|
|
|
12558 |
// and the subtitles/captions track before the descriptions track
|
|
|
12559 |
if (preferredTrack) {
|
|
|
12560 |
preferredTrack.mode = 'showing';
|
|
|
12561 |
} else if (firstCaptions) {
|
|
|
12562 |
firstCaptions.mode = 'showing';
|
|
|
12563 |
} else if (firstDesc) {
|
|
|
12564 |
firstDesc.mode = 'showing';
|
|
|
12565 |
}
|
|
|
12566 |
}
|
|
|
12567 |
|
|
|
12568 |
/**
|
|
|
12569 |
* Turn display of {@link TextTrack}'s from the current state into the other state.
|
|
|
12570 |
* There are only two states:
|
|
|
12571 |
* - 'shown'
|
|
|
12572 |
* - 'hidden'
|
|
|
12573 |
*
|
|
|
12574 |
* @listens Player#loadstart
|
|
|
12575 |
*/
|
|
|
12576 |
toggleDisplay() {
|
|
|
12577 |
if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
|
|
|
12578 |
this.hide();
|
|
|
12579 |
} else {
|
|
|
12580 |
this.show();
|
|
|
12581 |
}
|
|
|
12582 |
}
|
|
|
12583 |
|
|
|
12584 |
/**
|
|
|
12585 |
* Create the {@link Component}'s DOM element.
|
|
|
12586 |
*
|
|
|
12587 |
* @return {Element}
|
|
|
12588 |
* The element that was created.
|
|
|
12589 |
*/
|
|
|
12590 |
createEl() {
|
|
|
12591 |
return super.createEl('div', {
|
|
|
12592 |
className: 'vjs-text-track-display'
|
|
|
12593 |
}, {
|
|
|
12594 |
'translate': 'yes',
|
|
|
12595 |
'aria-live': 'off',
|
|
|
12596 |
'aria-atomic': 'true'
|
|
|
12597 |
});
|
|
|
12598 |
}
|
|
|
12599 |
|
|
|
12600 |
/**
|
|
|
12601 |
* Clear all displayed {@link TextTrack}s.
|
|
|
12602 |
*/
|
|
|
12603 |
clearDisplay() {
|
|
|
12604 |
if (typeof window.WebVTT === 'function') {
|
|
|
12605 |
window.WebVTT.processCues(window, [], this.el_);
|
|
|
12606 |
}
|
|
|
12607 |
}
|
|
|
12608 |
|
|
|
12609 |
/**
|
|
|
12610 |
* Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
|
|
|
12611 |
* a {@link Player#fullscreenchange} is fired.
|
|
|
12612 |
*
|
|
|
12613 |
* @listens Player#texttrackchange
|
|
|
12614 |
* @listens Player#fullscreenchange
|
|
|
12615 |
*/
|
|
|
12616 |
updateDisplay() {
|
|
|
12617 |
const tracks = this.player_.textTracks();
|
|
|
12618 |
const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
|
|
|
12619 |
this.clearDisplay();
|
|
|
12620 |
if (allowMultipleShowingTracks) {
|
|
|
12621 |
const showingTracks = [];
|
|
|
12622 |
for (let i = 0; i < tracks.length; ++i) {
|
|
|
12623 |
const track = tracks[i];
|
|
|
12624 |
if (track.mode !== 'showing') {
|
|
|
12625 |
continue;
|
|
|
12626 |
}
|
|
|
12627 |
showingTracks.push(track);
|
|
|
12628 |
}
|
|
|
12629 |
this.updateForTrack(showingTracks);
|
|
|
12630 |
return;
|
|
|
12631 |
}
|
|
|
12632 |
|
|
|
12633 |
// Track display prioritization model: if multiple tracks are 'showing',
|
|
|
12634 |
// display the first 'subtitles' or 'captions' track which is 'showing',
|
|
|
12635 |
// otherwise display the first 'descriptions' track which is 'showing'
|
|
|
12636 |
|
|
|
12637 |
let descriptionsTrack = null;
|
|
|
12638 |
let captionsSubtitlesTrack = null;
|
|
|
12639 |
let i = tracks.length;
|
|
|
12640 |
while (i--) {
|
|
|
12641 |
const track = tracks[i];
|
|
|
12642 |
if (track.mode === 'showing') {
|
|
|
12643 |
if (track.kind === 'descriptions') {
|
|
|
12644 |
descriptionsTrack = track;
|
|
|
12645 |
} else {
|
|
|
12646 |
captionsSubtitlesTrack = track;
|
|
|
12647 |
}
|
|
|
12648 |
}
|
|
|
12649 |
}
|
|
|
12650 |
if (captionsSubtitlesTrack) {
|
|
|
12651 |
if (this.getAttribute('aria-live') !== 'off') {
|
|
|
12652 |
this.setAttribute('aria-live', 'off');
|
|
|
12653 |
}
|
|
|
12654 |
this.updateForTrack(captionsSubtitlesTrack);
|
|
|
12655 |
} else if (descriptionsTrack) {
|
|
|
12656 |
if (this.getAttribute('aria-live') !== 'assertive') {
|
|
|
12657 |
this.setAttribute('aria-live', 'assertive');
|
|
|
12658 |
}
|
|
|
12659 |
this.updateForTrack(descriptionsTrack);
|
|
|
12660 |
}
|
|
|
12661 |
}
|
|
|
12662 |
|
|
|
12663 |
/**
|
|
|
12664 |
* Updates the displayed TextTrack to be sure it overlays the video when a either
|
|
|
12665 |
* a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired.
|
|
|
12666 |
*/
|
|
|
12667 |
updateDisplayOverlay() {
|
|
|
12668 |
// inset-inline and inset-block are not supprted on old chrome, but these are
|
|
|
12669 |
// only likely to be used on TV devices
|
|
|
12670 |
if (!this.player_.videoHeight() || !window.CSS.supports('inset-inline: 10px')) {
|
|
|
12671 |
return;
|
|
|
12672 |
}
|
|
|
12673 |
const playerWidth = this.player_.currentWidth();
|
|
|
12674 |
const playerHeight = this.player_.currentHeight();
|
|
|
12675 |
const playerAspectRatio = playerWidth / playerHeight;
|
|
|
12676 |
const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight();
|
|
|
12677 |
let insetInlineMatch = 0;
|
|
|
12678 |
let insetBlockMatch = 0;
|
|
|
12679 |
if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) {
|
|
|
12680 |
if (playerAspectRatio > videoAspectRatio) {
|
|
|
12681 |
insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2);
|
|
|
12682 |
} else {
|
|
|
12683 |
insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2);
|
|
|
12684 |
}
|
|
|
12685 |
}
|
|
|
12686 |
tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch));
|
|
|
12687 |
tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch));
|
|
|
12688 |
}
|
|
|
12689 |
|
|
|
12690 |
/**
|
|
|
12691 |
* Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
|
|
|
12692 |
*
|
|
|
12693 |
* @param {TextTrack} track
|
|
|
12694 |
* Text track object containing active cues to style.
|
|
|
12695 |
*/
|
|
|
12696 |
updateDisplayState(track) {
|
|
|
12697 |
const overrides = this.player_.textTrackSettings.getValues();
|
|
|
12698 |
const cues = track.activeCues;
|
|
|
12699 |
let i = cues.length;
|
|
|
12700 |
while (i--) {
|
|
|
12701 |
const cue = cues[i];
|
|
|
12702 |
if (!cue) {
|
|
|
12703 |
continue;
|
|
|
12704 |
}
|
|
|
12705 |
const cueDiv = cue.displayState;
|
|
|
12706 |
if (overrides.color) {
|
|
|
12707 |
cueDiv.firstChild.style.color = overrides.color;
|
|
|
12708 |
}
|
|
|
12709 |
if (overrides.textOpacity) {
|
|
|
12710 |
tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
|
|
|
12711 |
}
|
|
|
12712 |
if (overrides.backgroundColor) {
|
|
|
12713 |
cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
|
|
|
12714 |
}
|
|
|
12715 |
if (overrides.backgroundOpacity) {
|
|
|
12716 |
tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
|
|
|
12717 |
}
|
|
|
12718 |
if (overrides.windowColor) {
|
|
|
12719 |
if (overrides.windowOpacity) {
|
|
|
12720 |
tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
|
|
|
12721 |
} else {
|
|
|
12722 |
cueDiv.style.backgroundColor = overrides.windowColor;
|
|
|
12723 |
}
|
|
|
12724 |
}
|
|
|
12725 |
if (overrides.edgeStyle) {
|
|
|
12726 |
if (overrides.edgeStyle === 'dropshadow') {
|
|
|
12727 |
cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
|
|
|
12728 |
} else if (overrides.edgeStyle === 'raised') {
|
|
|
12729 |
cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
|
|
|
12730 |
} else if (overrides.edgeStyle === 'depressed') {
|
|
|
12731 |
cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
|
|
|
12732 |
} else if (overrides.edgeStyle === 'uniform') {
|
|
|
12733 |
cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
|
|
|
12734 |
}
|
|
|
12735 |
}
|
|
|
12736 |
if (overrides.fontPercent && overrides.fontPercent !== 1) {
|
|
|
12737 |
const fontSize = window.parseFloat(cueDiv.style.fontSize);
|
|
|
12738 |
cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
|
|
|
12739 |
cueDiv.style.height = 'auto';
|
|
|
12740 |
cueDiv.style.top = 'auto';
|
|
|
12741 |
}
|
|
|
12742 |
if (overrides.fontFamily && overrides.fontFamily !== 'default') {
|
|
|
12743 |
if (overrides.fontFamily === 'small-caps') {
|
|
|
12744 |
cueDiv.firstChild.style.fontVariant = 'small-caps';
|
|
|
12745 |
} else {
|
|
|
12746 |
cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
|
|
|
12747 |
}
|
|
|
12748 |
}
|
|
|
12749 |
}
|
|
|
12750 |
}
|
|
|
12751 |
|
|
|
12752 |
/**
|
|
|
12753 |
* Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
|
|
|
12754 |
*
|
|
|
12755 |
* @param {TextTrack|TextTrack[]} tracks
|
|
|
12756 |
* Text track object or text track array to be added to the list.
|
|
|
12757 |
*/
|
|
|
12758 |
updateForTrack(tracks) {
|
|
|
12759 |
if (!Array.isArray(tracks)) {
|
|
|
12760 |
tracks = [tracks];
|
|
|
12761 |
}
|
|
|
12762 |
if (typeof window.WebVTT !== 'function' || tracks.every(track => {
|
|
|
12763 |
return !track.activeCues;
|
|
|
12764 |
})) {
|
|
|
12765 |
return;
|
|
|
12766 |
}
|
|
|
12767 |
const cues = [];
|
|
|
12768 |
|
|
|
12769 |
// push all active track cues
|
|
|
12770 |
for (let i = 0; i < tracks.length; ++i) {
|
|
|
12771 |
const track = tracks[i];
|
|
|
12772 |
for (let j = 0; j < track.activeCues.length; ++j) {
|
|
|
12773 |
cues.push(track.activeCues[j]);
|
|
|
12774 |
}
|
|
|
12775 |
}
|
|
|
12776 |
|
|
|
12777 |
// removes all cues before it processes new ones
|
|
|
12778 |
window.WebVTT.processCues(window, cues, this.el_);
|
|
|
12779 |
|
|
|
12780 |
// add unique class to each language text track & add settings styling if necessary
|
|
|
12781 |
for (let i = 0; i < tracks.length; ++i) {
|
|
|
12782 |
const track = tracks[i];
|
|
|
12783 |
for (let j = 0; j < track.activeCues.length; ++j) {
|
|
|
12784 |
const cueEl = track.activeCues[j].displayState;
|
|
|
12785 |
addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
|
|
|
12786 |
if (track.language) {
|
|
|
12787 |
setAttribute(cueEl, 'lang', track.language);
|
|
|
12788 |
}
|
|
|
12789 |
}
|
|
|
12790 |
if (this.player_.textTrackSettings) {
|
|
|
12791 |
this.updateDisplayState(track);
|
|
|
12792 |
}
|
|
|
12793 |
}
|
|
|
12794 |
}
|
|
|
12795 |
}
|
|
|
12796 |
Component$1.registerComponent('TextTrackDisplay', TextTrackDisplay);
|
|
|
12797 |
|
|
|
12798 |
/**
|
|
|
12799 |
* @file loading-spinner.js
|
|
|
12800 |
*/
|
|
|
12801 |
|
|
|
12802 |
/**
|
|
|
12803 |
* A loading spinner for use during waiting/loading events.
|
|
|
12804 |
*
|
|
|
12805 |
* @extends Component
|
|
|
12806 |
*/
|
|
|
12807 |
class LoadingSpinner extends Component$1 {
|
|
|
12808 |
/**
|
|
|
12809 |
* Create the `LoadingSpinner`s DOM element.
|
|
|
12810 |
*
|
|
|
12811 |
* @return {Element}
|
|
|
12812 |
* The dom element that gets created.
|
|
|
12813 |
*/
|
|
|
12814 |
createEl() {
|
|
|
12815 |
const isAudio = this.player_.isAudio();
|
|
|
12816 |
const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
|
|
|
12817 |
const controlText = createEl('span', {
|
|
|
12818 |
className: 'vjs-control-text',
|
|
|
12819 |
textContent: this.localize('{1} is loading.', [playerType])
|
|
|
12820 |
});
|
|
|
12821 |
const el = super.createEl('div', {
|
|
|
12822 |
className: 'vjs-loading-spinner',
|
|
|
12823 |
dir: 'ltr'
|
|
|
12824 |
});
|
|
|
12825 |
el.appendChild(controlText);
|
|
|
12826 |
return el;
|
|
|
12827 |
}
|
|
|
12828 |
|
|
|
12829 |
/**
|
|
|
12830 |
* Update control text on languagechange
|
|
|
12831 |
*/
|
|
|
12832 |
handleLanguagechange() {
|
|
|
12833 |
this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
|
|
|
12834 |
}
|
|
|
12835 |
}
|
|
|
12836 |
Component$1.registerComponent('LoadingSpinner', LoadingSpinner);
|
|
|
12837 |
|
|
|
12838 |
/**
|
|
|
12839 |
* @file button.js
|
|
|
12840 |
*/
|
|
|
12841 |
|
|
|
12842 |
/**
|
|
|
12843 |
* Base class for all buttons.
|
|
|
12844 |
*
|
|
|
12845 |
* @extends ClickableComponent
|
|
|
12846 |
*/
|
|
|
12847 |
class Button extends ClickableComponent {
|
|
|
12848 |
/**
|
|
|
12849 |
* Create the `Button`s DOM element.
|
|
|
12850 |
*
|
|
|
12851 |
* @param {string} [tag="button"]
|
|
|
12852 |
* The element's node type. This argument is IGNORED: no matter what
|
|
|
12853 |
* is passed, it will always create a `button` element.
|
|
|
12854 |
*
|
|
|
12855 |
* @param {Object} [props={}]
|
|
|
12856 |
* An object of properties that should be set on the element.
|
|
|
12857 |
*
|
|
|
12858 |
* @param {Object} [attributes={}]
|
|
|
12859 |
* An object of attributes that should be set on the element.
|
|
|
12860 |
*
|
|
|
12861 |
* @return {Element}
|
|
|
12862 |
* The element that gets created.
|
|
|
12863 |
*/
|
|
|
12864 |
createEl(tag, props = {}, attributes = {}) {
|
|
|
12865 |
tag = 'button';
|
|
|
12866 |
props = Object.assign({
|
|
|
12867 |
className: this.buildCSSClass()
|
|
|
12868 |
}, props);
|
|
|
12869 |
|
|
|
12870 |
// Add attributes for button element
|
|
|
12871 |
attributes = Object.assign({
|
|
|
12872 |
// Necessary since the default button type is "submit"
|
|
|
12873 |
type: 'button'
|
|
|
12874 |
}, attributes);
|
|
|
12875 |
const el = createEl(tag, props, attributes);
|
|
|
12876 |
if (!this.player_.options_.experimentalSvgIcons) {
|
|
|
12877 |
el.appendChild(createEl('span', {
|
|
|
12878 |
className: 'vjs-icon-placeholder'
|
|
|
12879 |
}, {
|
|
|
12880 |
'aria-hidden': true
|
|
|
12881 |
}));
|
|
|
12882 |
}
|
|
|
12883 |
this.createControlTextEl(el);
|
|
|
12884 |
return el;
|
|
|
12885 |
}
|
|
|
12886 |
|
|
|
12887 |
/**
|
|
|
12888 |
* Add a child `Component` inside of this `Button`.
|
|
|
12889 |
*
|
|
|
12890 |
* @param {string|Component} child
|
|
|
12891 |
* The name or instance of a child to add.
|
|
|
12892 |
*
|
|
|
12893 |
* @param {Object} [options={}]
|
|
|
12894 |
* The key/value store of options that will get passed to children of
|
|
|
12895 |
* the child.
|
|
|
12896 |
*
|
|
|
12897 |
* @return {Component}
|
|
|
12898 |
* The `Component` that gets added as a child. When using a string the
|
|
|
12899 |
* `Component` will get created by this process.
|
|
|
12900 |
*
|
|
|
12901 |
* @deprecated since version 5
|
|
|
12902 |
*/
|
|
|
12903 |
addChild(child, options = {}) {
|
|
|
12904 |
const className = this.constructor.name;
|
|
|
12905 |
log$1.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
|
|
|
12906 |
|
|
|
12907 |
// Avoid the error message generated by ClickableComponent's addChild method
|
|
|
12908 |
return Component$1.prototype.addChild.call(this, child, options);
|
|
|
12909 |
}
|
|
|
12910 |
|
|
|
12911 |
/**
|
|
|
12912 |
* Enable the `Button` element so that it can be activated or clicked. Use this with
|
|
|
12913 |
* {@link Button#disable}.
|
|
|
12914 |
*/
|
|
|
12915 |
enable() {
|
|
|
12916 |
super.enable();
|
|
|
12917 |
this.el_.removeAttribute('disabled');
|
|
|
12918 |
}
|
|
|
12919 |
|
|
|
12920 |
/**
|
|
|
12921 |
* Disable the `Button` element so that it cannot be activated or clicked. Use this with
|
|
|
12922 |
* {@link Button#enable}.
|
|
|
12923 |
*/
|
|
|
12924 |
disable() {
|
|
|
12925 |
super.disable();
|
|
|
12926 |
this.el_.setAttribute('disabled', 'disabled');
|
|
|
12927 |
}
|
|
|
12928 |
|
|
|
12929 |
/**
|
|
|
12930 |
* This gets called when a `Button` has focus and `keydown` is triggered via a key
|
|
|
12931 |
* press.
|
|
|
12932 |
*
|
|
|
12933 |
* @param {KeyboardEvent} event
|
|
|
12934 |
* The event that caused this function to get called.
|
|
|
12935 |
*
|
|
|
12936 |
* @listens keydown
|
|
|
12937 |
*/
|
|
|
12938 |
handleKeyDown(event) {
|
|
|
12939 |
// Ignore Space or Enter key operation, which is handled by the browser for
|
|
|
12940 |
// a button - though not for its super class, ClickableComponent. Also,
|
|
|
12941 |
// prevent the event from propagating through the DOM and triggering Player
|
|
|
12942 |
// hotkeys. We do not preventDefault here because we _want_ the browser to
|
|
|
12943 |
// handle it.
|
|
|
12944 |
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
|
|
|
12945 |
event.stopPropagation();
|
|
|
12946 |
return;
|
|
|
12947 |
}
|
|
|
12948 |
|
|
|
12949 |
// Pass keypress handling up for unsupported keys
|
|
|
12950 |
super.handleKeyDown(event);
|
|
|
12951 |
}
|
|
|
12952 |
}
|
|
|
12953 |
Component$1.registerComponent('Button', Button);
|
|
|
12954 |
|
|
|
12955 |
/**
|
|
|
12956 |
* @file big-play-button.js
|
|
|
12957 |
*/
|
|
|
12958 |
|
|
|
12959 |
/**
|
|
|
12960 |
* The initial play button that shows before the video has played. The hiding of the
|
|
|
12961 |
* `BigPlayButton` get done via CSS and `Player` states.
|
|
|
12962 |
*
|
|
|
12963 |
* @extends Button
|
|
|
12964 |
*/
|
|
|
12965 |
class BigPlayButton extends Button {
|
|
|
12966 |
constructor(player, options) {
|
|
|
12967 |
super(player, options);
|
|
|
12968 |
this.mouseused_ = false;
|
|
|
12969 |
this.setIcon('play');
|
|
|
12970 |
this.on('mousedown', e => this.handleMouseDown(e));
|
|
|
12971 |
}
|
|
|
12972 |
|
|
|
12973 |
/**
|
|
|
12974 |
* Builds the default DOM `className`.
|
|
|
12975 |
*
|
|
|
12976 |
* @return {string}
|
|
|
12977 |
* The DOM `className` for this object. Always returns 'vjs-big-play-button'.
|
|
|
12978 |
*/
|
|
|
12979 |
buildCSSClass() {
|
|
|
12980 |
return 'vjs-big-play-button';
|
|
|
12981 |
}
|
|
|
12982 |
|
|
|
12983 |
/**
|
|
|
12984 |
* This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
|
|
|
12985 |
* for more detailed information on what a click can be.
|
|
|
12986 |
*
|
|
|
12987 |
* @param {KeyboardEvent|MouseEvent|TouchEvent} event
|
|
|
12988 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
12989 |
* called.
|
|
|
12990 |
*
|
|
|
12991 |
* @listens tap
|
|
|
12992 |
* @listens click
|
|
|
12993 |
*/
|
|
|
12994 |
handleClick(event) {
|
|
|
12995 |
const playPromise = this.player_.play();
|
|
|
12996 |
|
|
|
12997 |
// exit early if clicked via the mouse
|
|
|
12998 |
if (this.mouseused_ && 'clientX' in event && 'clientY' in event) {
|
|
|
12999 |
silencePromise(playPromise);
|
|
|
13000 |
if (this.player_.tech(true)) {
|
|
|
13001 |
this.player_.tech(true).focus();
|
|
|
13002 |
}
|
|
|
13003 |
return;
|
|
|
13004 |
}
|
|
|
13005 |
const cb = this.player_.getChild('controlBar');
|
|
|
13006 |
const playToggle = cb && cb.getChild('playToggle');
|
|
|
13007 |
if (!playToggle) {
|
|
|
13008 |
this.player_.tech(true).focus();
|
|
|
13009 |
return;
|
|
|
13010 |
}
|
|
|
13011 |
const playFocus = () => playToggle.focus();
|
|
|
13012 |
if (isPromise(playPromise)) {
|
|
|
13013 |
playPromise.then(playFocus, () => {});
|
|
|
13014 |
} else {
|
|
|
13015 |
this.setTimeout(playFocus, 1);
|
|
|
13016 |
}
|
|
|
13017 |
}
|
|
|
13018 |
|
|
|
13019 |
/**
|
|
|
13020 |
* Event handler that is called when a `BigPlayButton` receives a
|
|
|
13021 |
* `keydown` event.
|
|
|
13022 |
*
|
|
|
13023 |
* @param {KeyboardEvent} event
|
|
|
13024 |
* The `keydown` event that caused this function to be called.
|
|
|
13025 |
*
|
|
|
13026 |
* @listens keydown
|
|
|
13027 |
*/
|
|
|
13028 |
handleKeyDown(event) {
|
|
|
13029 |
this.mouseused_ = false;
|
|
|
13030 |
super.handleKeyDown(event);
|
|
|
13031 |
}
|
|
|
13032 |
|
|
|
13033 |
/**
|
|
|
13034 |
* Handle `mousedown` events on the `BigPlayButton`.
|
|
|
13035 |
*
|
|
|
13036 |
* @param {MouseEvent} event
|
|
|
13037 |
* `mousedown` or `touchstart` event that triggered this function
|
|
|
13038 |
*
|
|
|
13039 |
* @listens mousedown
|
|
|
13040 |
*/
|
|
|
13041 |
handleMouseDown(event) {
|
|
|
13042 |
this.mouseused_ = true;
|
|
|
13043 |
}
|
|
|
13044 |
}
|
|
|
13045 |
|
|
|
13046 |
/**
|
|
|
13047 |
* The text that should display over the `BigPlayButton`s controls. Added to for localization.
|
|
|
13048 |
*
|
|
|
13049 |
* @type {string}
|
|
|
13050 |
* @protected
|
|
|
13051 |
*/
|
|
|
13052 |
BigPlayButton.prototype.controlText_ = 'Play Video';
|
|
|
13053 |
Component$1.registerComponent('BigPlayButton', BigPlayButton);
|
|
|
13054 |
|
|
|
13055 |
/**
|
|
|
13056 |
* @file close-button.js
|
|
|
13057 |
*/
|
|
|
13058 |
|
|
|
13059 |
/**
|
|
|
13060 |
* The `CloseButton` is a `{@link Button}` that fires a `close` event when
|
|
|
13061 |
* it gets clicked.
|
|
|
13062 |
*
|
|
|
13063 |
* @extends Button
|
|
|
13064 |
*/
|
|
|
13065 |
class CloseButton extends Button {
|
|
|
13066 |
/**
|
|
|
13067 |
* Creates an instance of the this class.
|
|
|
13068 |
*
|
|
|
13069 |
* @param { import('./player').default } player
|
|
|
13070 |
* The `Player` that this class should be attached to.
|
|
|
13071 |
*
|
|
|
13072 |
* @param {Object} [options]
|
|
|
13073 |
* The key/value store of player options.
|
|
|
13074 |
*/
|
|
|
13075 |
constructor(player, options) {
|
|
|
13076 |
super(player, options);
|
|
|
13077 |
this.setIcon('cancel');
|
|
|
13078 |
this.controlText(options && options.controlText || this.localize('Close'));
|
|
|
13079 |
}
|
|
|
13080 |
|
|
|
13081 |
/**
|
|
|
13082 |
* Builds the default DOM `className`.
|
|
|
13083 |
*
|
|
|
13084 |
* @return {string}
|
|
|
13085 |
* The DOM `className` for this object.
|
|
|
13086 |
*/
|
|
|
13087 |
buildCSSClass() {
|
|
|
13088 |
return `vjs-close-button ${super.buildCSSClass()}`;
|
|
|
13089 |
}
|
|
|
13090 |
|
|
|
13091 |
/**
|
|
|
13092 |
* This gets called when a `CloseButton` gets clicked. See
|
|
|
13093 |
* {@link ClickableComponent#handleClick} for more information on when
|
|
|
13094 |
* this will be triggered
|
|
|
13095 |
*
|
|
|
13096 |
* @param {Event} event
|
|
|
13097 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
13098 |
* called.
|
|
|
13099 |
*
|
|
|
13100 |
* @listens tap
|
|
|
13101 |
* @listens click
|
|
|
13102 |
* @fires CloseButton#close
|
|
|
13103 |
*/
|
|
|
13104 |
handleClick(event) {
|
|
|
13105 |
/**
|
|
|
13106 |
* Triggered when the a `CloseButton` is clicked.
|
|
|
13107 |
*
|
|
|
13108 |
* @event CloseButton#close
|
|
|
13109 |
* @type {Event}
|
|
|
13110 |
*
|
|
|
13111 |
* @property {boolean} [bubbles=false]
|
|
|
13112 |
* set to false so that the close event does not
|
|
|
13113 |
* bubble up to parents if there is no listener
|
|
|
13114 |
*/
|
|
|
13115 |
this.trigger({
|
|
|
13116 |
type: 'close',
|
|
|
13117 |
bubbles: false
|
|
|
13118 |
});
|
|
|
13119 |
}
|
|
|
13120 |
/**
|
|
|
13121 |
* Event handler that is called when a `CloseButton` receives a
|
|
|
13122 |
* `keydown` event.
|
|
|
13123 |
*
|
|
|
13124 |
* By default, if the key is Esc, it will trigger a `click` event.
|
|
|
13125 |
*
|
|
|
13126 |
* @param {KeyboardEvent} event
|
|
|
13127 |
* The `keydown` event that caused this function to be called.
|
|
|
13128 |
*
|
|
|
13129 |
* @listens keydown
|
|
|
13130 |
*/
|
|
|
13131 |
handleKeyDown(event) {
|
|
|
13132 |
// Esc button will trigger `click` event
|
|
|
13133 |
if (keycode.isEventKey(event, 'Esc')) {
|
|
|
13134 |
event.preventDefault();
|
|
|
13135 |
event.stopPropagation();
|
|
|
13136 |
this.trigger('click');
|
|
|
13137 |
} else {
|
|
|
13138 |
// Pass keypress handling up for unsupported keys
|
|
|
13139 |
super.handleKeyDown(event);
|
|
|
13140 |
}
|
|
|
13141 |
}
|
|
|
13142 |
}
|
|
|
13143 |
Component$1.registerComponent('CloseButton', CloseButton);
|
|
|
13144 |
|
|
|
13145 |
/**
|
|
|
13146 |
* @file play-toggle.js
|
|
|
13147 |
*/
|
|
|
13148 |
|
|
|
13149 |
/**
|
|
|
13150 |
* Button to toggle between play and pause.
|
|
|
13151 |
*
|
|
|
13152 |
* @extends Button
|
|
|
13153 |
*/
|
|
|
13154 |
class PlayToggle extends Button {
|
|
|
13155 |
/**
|
|
|
13156 |
* Creates an instance of this class.
|
|
|
13157 |
*
|
|
|
13158 |
* @param { import('./player').default } player
|
|
|
13159 |
* The `Player` that this class should be attached to.
|
|
|
13160 |
*
|
|
|
13161 |
* @param {Object} [options={}]
|
|
|
13162 |
* The key/value store of player options.
|
|
|
13163 |
*/
|
|
|
13164 |
constructor(player, options = {}) {
|
|
|
13165 |
super(player, options);
|
|
|
13166 |
|
|
|
13167 |
// show or hide replay icon
|
|
|
13168 |
options.replay = options.replay === undefined || options.replay;
|
|
|
13169 |
this.setIcon('play');
|
|
|
13170 |
this.on(player, 'play', e => this.handlePlay(e));
|
|
|
13171 |
this.on(player, 'pause', e => this.handlePause(e));
|
|
|
13172 |
if (options.replay) {
|
|
|
13173 |
this.on(player, 'ended', e => this.handleEnded(e));
|
|
|
13174 |
}
|
|
|
13175 |
}
|
|
|
13176 |
|
|
|
13177 |
/**
|
|
|
13178 |
* Builds the default DOM `className`.
|
|
|
13179 |
*
|
|
|
13180 |
* @return {string}
|
|
|
13181 |
* The DOM `className` for this object.
|
|
|
13182 |
*/
|
|
|
13183 |
buildCSSClass() {
|
|
|
13184 |
return `vjs-play-control ${super.buildCSSClass()}`;
|
|
|
13185 |
}
|
|
|
13186 |
|
|
|
13187 |
/**
|
|
|
13188 |
* This gets called when an `PlayToggle` is "clicked". See
|
|
|
13189 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
13190 |
*
|
|
|
13191 |
* @param {Event} [event]
|
|
|
13192 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
13193 |
* called.
|
|
|
13194 |
*
|
|
|
13195 |
* @listens tap
|
|
|
13196 |
* @listens click
|
|
|
13197 |
*/
|
|
|
13198 |
handleClick(event) {
|
|
|
13199 |
if (this.player_.paused()) {
|
|
|
13200 |
silencePromise(this.player_.play());
|
|
|
13201 |
} else {
|
|
|
13202 |
this.player_.pause();
|
|
|
13203 |
}
|
|
|
13204 |
}
|
|
|
13205 |
|
|
|
13206 |
/**
|
|
|
13207 |
* This gets called once after the video has ended and the user seeks so that
|
|
|
13208 |
* we can change the replay button back to a play button.
|
|
|
13209 |
*
|
|
|
13210 |
* @param {Event} [event]
|
|
|
13211 |
* The event that caused this function to run.
|
|
|
13212 |
*
|
|
|
13213 |
* @listens Player#seeked
|
|
|
13214 |
*/
|
|
|
13215 |
handleSeeked(event) {
|
|
|
13216 |
this.removeClass('vjs-ended');
|
|
|
13217 |
if (this.player_.paused()) {
|
|
|
13218 |
this.handlePause(event);
|
|
|
13219 |
} else {
|
|
|
13220 |
this.handlePlay(event);
|
|
|
13221 |
}
|
|
|
13222 |
}
|
|
|
13223 |
|
|
|
13224 |
/**
|
|
|
13225 |
* Add the vjs-playing class to the element so it can change appearance.
|
|
|
13226 |
*
|
|
|
13227 |
* @param {Event} [event]
|
|
|
13228 |
* The event that caused this function to run.
|
|
|
13229 |
*
|
|
|
13230 |
* @listens Player#play
|
|
|
13231 |
*/
|
|
|
13232 |
handlePlay(event) {
|
|
|
13233 |
this.removeClass('vjs-ended', 'vjs-paused');
|
|
|
13234 |
this.addClass('vjs-playing');
|
|
|
13235 |
// change the button text to "Pause"
|
|
|
13236 |
this.setIcon('pause');
|
|
|
13237 |
this.controlText('Pause');
|
|
|
13238 |
}
|
|
|
13239 |
|
|
|
13240 |
/**
|
|
|
13241 |
* Add the vjs-paused class to the element so it can change appearance.
|
|
|
13242 |
*
|
|
|
13243 |
* @param {Event} [event]
|
|
|
13244 |
* The event that caused this function to run.
|
|
|
13245 |
*
|
|
|
13246 |
* @listens Player#pause
|
|
|
13247 |
*/
|
|
|
13248 |
handlePause(event) {
|
|
|
13249 |
this.removeClass('vjs-playing');
|
|
|
13250 |
this.addClass('vjs-paused');
|
|
|
13251 |
// change the button text to "Play"
|
|
|
13252 |
this.setIcon('play');
|
|
|
13253 |
this.controlText('Play');
|
|
|
13254 |
}
|
|
|
13255 |
|
|
|
13256 |
/**
|
|
|
13257 |
* Add the vjs-ended class to the element so it can change appearance
|
|
|
13258 |
*
|
|
|
13259 |
* @param {Event} [event]
|
|
|
13260 |
* The event that caused this function to run.
|
|
|
13261 |
*
|
|
|
13262 |
* @listens Player#ended
|
|
|
13263 |
*/
|
|
|
13264 |
handleEnded(event) {
|
|
|
13265 |
this.removeClass('vjs-playing');
|
|
|
13266 |
this.addClass('vjs-ended');
|
|
|
13267 |
// change the button text to "Replay"
|
|
|
13268 |
this.setIcon('replay');
|
|
|
13269 |
this.controlText('Replay');
|
|
|
13270 |
|
|
|
13271 |
// on the next seek remove the replay button
|
|
|
13272 |
this.one(this.player_, 'seeked', e => this.handleSeeked(e));
|
|
|
13273 |
}
|
|
|
13274 |
}
|
|
|
13275 |
|
|
|
13276 |
/**
|
|
|
13277 |
* The text that should display over the `PlayToggle`s controls. Added for localization.
|
|
|
13278 |
*
|
|
|
13279 |
* @type {string}
|
|
|
13280 |
* @protected
|
|
|
13281 |
*/
|
|
|
13282 |
PlayToggle.prototype.controlText_ = 'Play';
|
|
|
13283 |
Component$1.registerComponent('PlayToggle', PlayToggle);
|
|
|
13284 |
|
|
|
13285 |
/**
|
|
|
13286 |
* @file time-display.js
|
|
|
13287 |
*/
|
|
|
13288 |
|
|
|
13289 |
/**
|
|
|
13290 |
* Displays time information about the video
|
|
|
13291 |
*
|
|
|
13292 |
* @extends Component
|
|
|
13293 |
*/
|
|
|
13294 |
class TimeDisplay extends Component$1 {
|
|
|
13295 |
/**
|
|
|
13296 |
* Creates an instance of this class.
|
|
|
13297 |
*
|
|
|
13298 |
* @param { import('../../player').default } player
|
|
|
13299 |
* The `Player` that this class should be attached to.
|
|
|
13300 |
*
|
|
|
13301 |
* @param {Object} [options]
|
|
|
13302 |
* The key/value store of player options.
|
|
|
13303 |
*/
|
|
|
13304 |
constructor(player, options) {
|
|
|
13305 |
super(player, options);
|
|
|
13306 |
this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e));
|
|
|
13307 |
this.updateTextNode_();
|
|
|
13308 |
}
|
|
|
13309 |
|
|
|
13310 |
/**
|
|
|
13311 |
* Create the `Component`'s DOM element
|
|
|
13312 |
*
|
|
|
13313 |
* @return {Element}
|
|
|
13314 |
* The element that was created.
|
|
|
13315 |
*/
|
|
|
13316 |
createEl() {
|
|
|
13317 |
const className = this.buildCSSClass();
|
|
|
13318 |
const el = super.createEl('div', {
|
|
|
13319 |
className: `${className} vjs-time-control vjs-control`
|
|
|
13320 |
});
|
|
|
13321 |
const span = createEl('span', {
|
|
|
13322 |
className: 'vjs-control-text',
|
|
|
13323 |
textContent: `${this.localize(this.labelText_)}\u00a0`
|
|
|
13324 |
}, {
|
|
|
13325 |
role: 'presentation'
|
|
|
13326 |
});
|
|
|
13327 |
el.appendChild(span);
|
|
|
13328 |
this.contentEl_ = createEl('span', {
|
|
|
13329 |
className: `${className}-display`
|
|
|
13330 |
}, {
|
|
|
13331 |
// span elements have no implicit role, but some screen readers (notably VoiceOver)
|
|
|
13332 |
// treat them as a break between items in the DOM when using arrow keys
|
|
|
13333 |
// (or left-to-right swipes on iOS) to read contents of a page. Using
|
|
|
13334 |
// role='presentation' causes VoiceOver to NOT treat this span as a break.
|
|
|
13335 |
role: 'presentation'
|
|
|
13336 |
});
|
|
|
13337 |
el.appendChild(this.contentEl_);
|
|
|
13338 |
return el;
|
|
|
13339 |
}
|
|
|
13340 |
dispose() {
|
|
|
13341 |
this.contentEl_ = null;
|
|
|
13342 |
this.textNode_ = null;
|
|
|
13343 |
super.dispose();
|
|
|
13344 |
}
|
|
|
13345 |
|
|
|
13346 |
/**
|
|
|
13347 |
* Updates the displayed time according to the `updateContent` function which is defined in the child class.
|
|
|
13348 |
*
|
|
|
13349 |
* @param {Event} [event]
|
|
|
13350 |
* The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called.
|
|
|
13351 |
*/
|
|
|
13352 |
update(event) {
|
|
|
13353 |
if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') {
|
|
|
13354 |
return;
|
|
|
13355 |
}
|
|
|
13356 |
this.updateContent(event);
|
|
|
13357 |
}
|
|
|
13358 |
|
|
|
13359 |
/**
|
|
|
13360 |
* Updates the time display text node with a new time
|
|
|
13361 |
*
|
|
|
13362 |
* @param {number} [time=0] the time to update to
|
|
|
13363 |
*
|
|
|
13364 |
* @private
|
|
|
13365 |
*/
|
|
|
13366 |
updateTextNode_(time = 0) {
|
|
|
13367 |
time = formatTime(time);
|
|
|
13368 |
if (this.formattedTime_ === time) {
|
|
|
13369 |
return;
|
|
|
13370 |
}
|
|
|
13371 |
this.formattedTime_ = time;
|
|
|
13372 |
this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
|
|
|
13373 |
if (!this.contentEl_) {
|
|
|
13374 |
return;
|
|
|
13375 |
}
|
|
|
13376 |
let oldNode = this.textNode_;
|
|
|
13377 |
if (oldNode && this.contentEl_.firstChild !== oldNode) {
|
|
|
13378 |
oldNode = null;
|
|
|
13379 |
log$1.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.');
|
|
|
13380 |
}
|
|
|
13381 |
this.textNode_ = document.createTextNode(this.formattedTime_);
|
|
|
13382 |
if (!this.textNode_) {
|
|
|
13383 |
return;
|
|
|
13384 |
}
|
|
|
13385 |
if (oldNode) {
|
|
|
13386 |
this.contentEl_.replaceChild(this.textNode_, oldNode);
|
|
|
13387 |
} else {
|
|
|
13388 |
this.contentEl_.appendChild(this.textNode_);
|
|
|
13389 |
}
|
|
|
13390 |
});
|
|
|
13391 |
}
|
|
|
13392 |
|
|
|
13393 |
/**
|
|
|
13394 |
* To be filled out in the child class, should update the displayed time
|
|
|
13395 |
* in accordance with the fact that the current time has changed.
|
|
|
13396 |
*
|
|
|
13397 |
* @param {Event} [event]
|
|
|
13398 |
* The `timeupdate` event that caused this to run.
|
|
|
13399 |
*
|
|
|
13400 |
* @listens Player#timeupdate
|
|
|
13401 |
*/
|
|
|
13402 |
updateContent(event) {}
|
|
|
13403 |
}
|
|
|
13404 |
|
|
|
13405 |
/**
|
|
|
13406 |
* The text that is added to the `TimeDisplay` for screen reader users.
|
|
|
13407 |
*
|
|
|
13408 |
* @type {string}
|
|
|
13409 |
* @private
|
|
|
13410 |
*/
|
|
|
13411 |
TimeDisplay.prototype.labelText_ = 'Time';
|
|
|
13412 |
|
|
|
13413 |
/**
|
|
|
13414 |
* The text that should display over the `TimeDisplay`s controls. Added to for localization.
|
|
|
13415 |
*
|
|
|
13416 |
* @type {string}
|
|
|
13417 |
* @protected
|
|
|
13418 |
*
|
|
|
13419 |
* @deprecated in v7; controlText_ is not used in non-active display Components
|
|
|
13420 |
*/
|
|
|
13421 |
TimeDisplay.prototype.controlText_ = 'Time';
|
|
|
13422 |
Component$1.registerComponent('TimeDisplay', TimeDisplay);
|
|
|
13423 |
|
|
|
13424 |
/**
|
|
|
13425 |
* @file current-time-display.js
|
|
|
13426 |
*/
|
|
|
13427 |
|
|
|
13428 |
/**
|
|
|
13429 |
* Displays the current time
|
|
|
13430 |
*
|
|
|
13431 |
* @extends Component
|
|
|
13432 |
*/
|
|
|
13433 |
class CurrentTimeDisplay extends TimeDisplay {
|
|
|
13434 |
/**
|
|
|
13435 |
* Builds the default DOM `className`.
|
|
|
13436 |
*
|
|
|
13437 |
* @return {string}
|
|
|
13438 |
* The DOM `className` for this object.
|
|
|
13439 |
*/
|
|
|
13440 |
buildCSSClass() {
|
|
|
13441 |
return 'vjs-current-time';
|
|
|
13442 |
}
|
|
|
13443 |
|
|
|
13444 |
/**
|
|
|
13445 |
* Update current time display
|
|
|
13446 |
*
|
|
|
13447 |
* @param {Event} [event]
|
|
|
13448 |
* The `timeupdate` event that caused this function to run.
|
|
|
13449 |
*
|
|
|
13450 |
* @listens Player#timeupdate
|
|
|
13451 |
*/
|
|
|
13452 |
updateContent(event) {
|
|
|
13453 |
// Allows for smooth scrubbing, when player can't keep up.
|
|
|
13454 |
let time;
|
|
|
13455 |
if (this.player_.ended()) {
|
|
|
13456 |
time = this.player_.duration();
|
|
|
13457 |
} else {
|
|
|
13458 |
time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
|
|
|
13459 |
}
|
|
|
13460 |
this.updateTextNode_(time);
|
|
|
13461 |
}
|
|
|
13462 |
}
|
|
|
13463 |
|
|
|
13464 |
/**
|
|
|
13465 |
* The text that is added to the `CurrentTimeDisplay` for screen reader users.
|
|
|
13466 |
*
|
|
|
13467 |
* @type {string}
|
|
|
13468 |
* @private
|
|
|
13469 |
*/
|
|
|
13470 |
CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
|
|
|
13471 |
|
|
|
13472 |
/**
|
|
|
13473 |
* The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
|
|
|
13474 |
*
|
|
|
13475 |
* @type {string}
|
|
|
13476 |
* @protected
|
|
|
13477 |
*
|
|
|
13478 |
* @deprecated in v7; controlText_ is not used in non-active display Components
|
|
|
13479 |
*/
|
|
|
13480 |
CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
|
|
|
13481 |
Component$1.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
|
|
|
13482 |
|
|
|
13483 |
/**
|
|
|
13484 |
* @file duration-display.js
|
|
|
13485 |
*/
|
|
|
13486 |
|
|
|
13487 |
/**
|
|
|
13488 |
* Displays the duration
|
|
|
13489 |
*
|
|
|
13490 |
* @extends Component
|
|
|
13491 |
*/
|
|
|
13492 |
class DurationDisplay extends TimeDisplay {
|
|
|
13493 |
/**
|
|
|
13494 |
* Creates an instance of this class.
|
|
|
13495 |
*
|
|
|
13496 |
* @param { import('../../player').default } player
|
|
|
13497 |
* The `Player` that this class should be attached to.
|
|
|
13498 |
*
|
|
|
13499 |
* @param {Object} [options]
|
|
|
13500 |
* The key/value store of player options.
|
|
|
13501 |
*/
|
|
|
13502 |
constructor(player, options) {
|
|
|
13503 |
super(player, options);
|
|
|
13504 |
const updateContent = e => this.updateContent(e);
|
|
|
13505 |
|
|
|
13506 |
// we do not want to/need to throttle duration changes,
|
|
|
13507 |
// as they should always display the changed duration as
|
|
|
13508 |
// it has changed
|
|
|
13509 |
this.on(player, 'durationchange', updateContent);
|
|
|
13510 |
|
|
|
13511 |
// Listen to loadstart because the player duration is reset when a new media element is loaded,
|
|
|
13512 |
// but the durationchange on the user agent will not fire.
|
|
|
13513 |
// @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
|
|
|
13514 |
this.on(player, 'loadstart', updateContent);
|
|
|
13515 |
|
|
|
13516 |
// Also listen for timeupdate (in the parent) and loadedmetadata because removing those
|
|
|
13517 |
// listeners could have broken dependent applications/libraries. These
|
|
|
13518 |
// can likely be removed for 7.0.
|
|
|
13519 |
this.on(player, 'loadedmetadata', updateContent);
|
|
|
13520 |
}
|
|
|
13521 |
|
|
|
13522 |
/**
|
|
|
13523 |
* Builds the default DOM `className`.
|
|
|
13524 |
*
|
|
|
13525 |
* @return {string}
|
|
|
13526 |
* The DOM `className` for this object.
|
|
|
13527 |
*/
|
|
|
13528 |
buildCSSClass() {
|
|
|
13529 |
return 'vjs-duration';
|
|
|
13530 |
}
|
|
|
13531 |
|
|
|
13532 |
/**
|
|
|
13533 |
* Update duration time display.
|
|
|
13534 |
*
|
|
|
13535 |
* @param {Event} [event]
|
|
|
13536 |
* The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
|
|
|
13537 |
* this function to be called.
|
|
|
13538 |
*
|
|
|
13539 |
* @listens Player#durationchange
|
|
|
13540 |
* @listens Player#timeupdate
|
|
|
13541 |
* @listens Player#loadedmetadata
|
|
|
13542 |
*/
|
|
|
13543 |
updateContent(event) {
|
|
|
13544 |
const duration = this.player_.duration();
|
|
|
13545 |
this.updateTextNode_(duration);
|
|
|
13546 |
}
|
|
|
13547 |
}
|
|
|
13548 |
|
|
|
13549 |
/**
|
|
|
13550 |
* The text that is added to the `DurationDisplay` for screen reader users.
|
|
|
13551 |
*
|
|
|
13552 |
* @type {string}
|
|
|
13553 |
* @private
|
|
|
13554 |
*/
|
|
|
13555 |
DurationDisplay.prototype.labelText_ = 'Duration';
|
|
|
13556 |
|
|
|
13557 |
/**
|
|
|
13558 |
* The text that should display over the `DurationDisplay`s controls. Added to for localization.
|
|
|
13559 |
*
|
|
|
13560 |
* @type {string}
|
|
|
13561 |
* @protected
|
|
|
13562 |
*
|
|
|
13563 |
* @deprecated in v7; controlText_ is not used in non-active display Components
|
|
|
13564 |
*/
|
|
|
13565 |
DurationDisplay.prototype.controlText_ = 'Duration';
|
|
|
13566 |
Component$1.registerComponent('DurationDisplay', DurationDisplay);
|
|
|
13567 |
|
|
|
13568 |
/**
|
|
|
13569 |
* @file time-divider.js
|
|
|
13570 |
*/
|
|
|
13571 |
|
|
|
13572 |
/**
|
|
|
13573 |
* The separator between the current time and duration.
|
|
|
13574 |
* Can be hidden if it's not needed in the design.
|
|
|
13575 |
*
|
|
|
13576 |
* @extends Component
|
|
|
13577 |
*/
|
|
|
13578 |
class TimeDivider extends Component$1 {
|
|
|
13579 |
/**
|
|
|
13580 |
* Create the component's DOM element
|
|
|
13581 |
*
|
|
|
13582 |
* @return {Element}
|
|
|
13583 |
* The element that was created.
|
|
|
13584 |
*/
|
|
|
13585 |
createEl() {
|
|
|
13586 |
const el = super.createEl('div', {
|
|
|
13587 |
className: 'vjs-time-control vjs-time-divider'
|
|
|
13588 |
}, {
|
|
|
13589 |
// this element and its contents can be hidden from assistive techs since
|
|
|
13590 |
// it is made extraneous by the announcement of the control text
|
|
|
13591 |
// for the current time and duration displays
|
|
|
13592 |
'aria-hidden': true
|
|
|
13593 |
});
|
|
|
13594 |
const div = super.createEl('div');
|
|
|
13595 |
const span = super.createEl('span', {
|
|
|
13596 |
textContent: '/'
|
|
|
13597 |
});
|
|
|
13598 |
div.appendChild(span);
|
|
|
13599 |
el.appendChild(div);
|
|
|
13600 |
return el;
|
|
|
13601 |
}
|
|
|
13602 |
}
|
|
|
13603 |
Component$1.registerComponent('TimeDivider', TimeDivider);
|
|
|
13604 |
|
|
|
13605 |
/**
|
|
|
13606 |
* @file remaining-time-display.js
|
|
|
13607 |
*/
|
|
|
13608 |
|
|
|
13609 |
/**
|
|
|
13610 |
* Displays the time left in the video
|
|
|
13611 |
*
|
|
|
13612 |
* @extends Component
|
|
|
13613 |
*/
|
|
|
13614 |
class RemainingTimeDisplay extends TimeDisplay {
|
|
|
13615 |
/**
|
|
|
13616 |
* Creates an instance of this class.
|
|
|
13617 |
*
|
|
|
13618 |
* @param { import('../../player').default } player
|
|
|
13619 |
* The `Player` that this class should be attached to.
|
|
|
13620 |
*
|
|
|
13621 |
* @param {Object} [options]
|
|
|
13622 |
* The key/value store of player options.
|
|
|
13623 |
*/
|
|
|
13624 |
constructor(player, options) {
|
|
|
13625 |
super(player, options);
|
|
|
13626 |
this.on(player, 'durationchange', e => this.updateContent(e));
|
|
|
13627 |
}
|
|
|
13628 |
|
|
|
13629 |
/**
|
|
|
13630 |
* Builds the default DOM `className`.
|
|
|
13631 |
*
|
|
|
13632 |
* @return {string}
|
|
|
13633 |
* The DOM `className` for this object.
|
|
|
13634 |
*/
|
|
|
13635 |
buildCSSClass() {
|
|
|
13636 |
return 'vjs-remaining-time';
|
|
|
13637 |
}
|
|
|
13638 |
|
|
|
13639 |
/**
|
|
|
13640 |
* Create the `Component`'s DOM element with the "minus" character prepend to the time
|
|
|
13641 |
*
|
|
|
13642 |
* @return {Element}
|
|
|
13643 |
* The element that was created.
|
|
|
13644 |
*/
|
|
|
13645 |
createEl() {
|
|
|
13646 |
const el = super.createEl();
|
|
|
13647 |
if (this.options_.displayNegative !== false) {
|
|
|
13648 |
el.insertBefore(createEl('span', {}, {
|
|
|
13649 |
'aria-hidden': true
|
|
|
13650 |
}, '-'), this.contentEl_);
|
|
|
13651 |
}
|
|
|
13652 |
return el;
|
|
|
13653 |
}
|
|
|
13654 |
|
|
|
13655 |
/**
|
|
|
13656 |
* Update remaining time display.
|
|
|
13657 |
*
|
|
|
13658 |
* @param {Event} [event]
|
|
|
13659 |
* The `timeupdate` or `durationchange` event that caused this to run.
|
|
|
13660 |
*
|
|
|
13661 |
* @listens Player#timeupdate
|
|
|
13662 |
* @listens Player#durationchange
|
|
|
13663 |
*/
|
|
|
13664 |
updateContent(event) {
|
|
|
13665 |
if (typeof this.player_.duration() !== 'number') {
|
|
|
13666 |
return;
|
|
|
13667 |
}
|
|
|
13668 |
let time;
|
|
|
13669 |
|
|
|
13670 |
// @deprecated We should only use remainingTimeDisplay
|
|
|
13671 |
// as of video.js 7
|
|
|
13672 |
if (this.player_.ended()) {
|
|
|
13673 |
time = 0;
|
|
|
13674 |
} else if (this.player_.remainingTimeDisplay) {
|
|
|
13675 |
time = this.player_.remainingTimeDisplay();
|
|
|
13676 |
} else {
|
|
|
13677 |
time = this.player_.remainingTime();
|
|
|
13678 |
}
|
|
|
13679 |
this.updateTextNode_(time);
|
|
|
13680 |
}
|
|
|
13681 |
}
|
|
|
13682 |
|
|
|
13683 |
/**
|
|
|
13684 |
* The text that is added to the `RemainingTimeDisplay` for screen reader users.
|
|
|
13685 |
*
|
|
|
13686 |
* @type {string}
|
|
|
13687 |
* @private
|
|
|
13688 |
*/
|
|
|
13689 |
RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
|
|
|
13690 |
|
|
|
13691 |
/**
|
|
|
13692 |
* The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
|
|
|
13693 |
*
|
|
|
13694 |
* @type {string}
|
|
|
13695 |
* @protected
|
|
|
13696 |
*
|
|
|
13697 |
* @deprecated in v7; controlText_ is not used in non-active display Components
|
|
|
13698 |
*/
|
|
|
13699 |
RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
|
|
|
13700 |
Component$1.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
|
|
|
13701 |
|
|
|
13702 |
/**
|
|
|
13703 |
* @file live-display.js
|
|
|
13704 |
*/
|
|
|
13705 |
|
|
|
13706 |
// TODO - Future make it click to snap to live
|
|
|
13707 |
|
|
|
13708 |
/**
|
|
|
13709 |
* Displays the live indicator when duration is Infinity.
|
|
|
13710 |
*
|
|
|
13711 |
* @extends Component
|
|
|
13712 |
*/
|
|
|
13713 |
class LiveDisplay extends Component$1 {
|
|
|
13714 |
/**
|
|
|
13715 |
* Creates an instance of this class.
|
|
|
13716 |
*
|
|
|
13717 |
* @param { import('./player').default } player
|
|
|
13718 |
* The `Player` that this class should be attached to.
|
|
|
13719 |
*
|
|
|
13720 |
* @param {Object} [options]
|
|
|
13721 |
* The key/value store of player options.
|
|
|
13722 |
*/
|
|
|
13723 |
constructor(player, options) {
|
|
|
13724 |
super(player, options);
|
|
|
13725 |
this.updateShowing();
|
|
|
13726 |
this.on(this.player(), 'durationchange', e => this.updateShowing(e));
|
|
|
13727 |
}
|
|
|
13728 |
|
|
|
13729 |
/**
|
|
|
13730 |
* Create the `Component`'s DOM element
|
|
|
13731 |
*
|
|
|
13732 |
* @return {Element}
|
|
|
13733 |
* The element that was created.
|
|
|
13734 |
*/
|
|
|
13735 |
createEl() {
|
|
|
13736 |
const el = super.createEl('div', {
|
|
|
13737 |
className: 'vjs-live-control vjs-control'
|
|
|
13738 |
});
|
|
|
13739 |
this.contentEl_ = createEl('div', {
|
|
|
13740 |
className: 'vjs-live-display'
|
|
|
13741 |
}, {
|
|
|
13742 |
'aria-live': 'off'
|
|
|
13743 |
});
|
|
|
13744 |
this.contentEl_.appendChild(createEl('span', {
|
|
|
13745 |
className: 'vjs-control-text',
|
|
|
13746 |
textContent: `${this.localize('Stream Type')}\u00a0`
|
|
|
13747 |
}));
|
|
|
13748 |
this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE')));
|
|
|
13749 |
el.appendChild(this.contentEl_);
|
|
|
13750 |
return el;
|
|
|
13751 |
}
|
|
|
13752 |
dispose() {
|
|
|
13753 |
this.contentEl_ = null;
|
|
|
13754 |
super.dispose();
|
|
|
13755 |
}
|
|
|
13756 |
|
|
|
13757 |
/**
|
|
|
13758 |
* Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
|
|
|
13759 |
* it accordingly
|
|
|
13760 |
*
|
|
|
13761 |
* @param {Event} [event]
|
|
|
13762 |
* The {@link Player#durationchange} event that caused this function to run.
|
|
|
13763 |
*
|
|
|
13764 |
* @listens Player#durationchange
|
|
|
13765 |
*/
|
|
|
13766 |
updateShowing(event) {
|
|
|
13767 |
if (this.player().duration() === Infinity) {
|
|
|
13768 |
this.show();
|
|
|
13769 |
} else {
|
|
|
13770 |
this.hide();
|
|
|
13771 |
}
|
|
|
13772 |
}
|
|
|
13773 |
}
|
|
|
13774 |
Component$1.registerComponent('LiveDisplay', LiveDisplay);
|
|
|
13775 |
|
|
|
13776 |
/**
|
|
|
13777 |
* @file seek-to-live.js
|
|
|
13778 |
*/
|
|
|
13779 |
|
|
|
13780 |
/**
|
|
|
13781 |
* Displays the live indicator when duration is Infinity.
|
|
|
13782 |
*
|
|
|
13783 |
* @extends Component
|
|
|
13784 |
*/
|
|
|
13785 |
class SeekToLive extends Button {
|
|
|
13786 |
/**
|
|
|
13787 |
* Creates an instance of this class.
|
|
|
13788 |
*
|
|
|
13789 |
* @param { import('./player').default } player
|
|
|
13790 |
* The `Player` that this class should be attached to.
|
|
|
13791 |
*
|
|
|
13792 |
* @param {Object} [options]
|
|
|
13793 |
* The key/value store of player options.
|
|
|
13794 |
*/
|
|
|
13795 |
constructor(player, options) {
|
|
|
13796 |
super(player, options);
|
|
|
13797 |
this.updateLiveEdgeStatus();
|
|
|
13798 |
if (this.player_.liveTracker) {
|
|
|
13799 |
this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
|
|
|
13800 |
this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
|
|
|
13801 |
}
|
|
|
13802 |
}
|
|
|
13803 |
|
|
|
13804 |
/**
|
|
|
13805 |
* Create the `Component`'s DOM element
|
|
|
13806 |
*
|
|
|
13807 |
* @return {Element}
|
|
|
13808 |
* The element that was created.
|
|
|
13809 |
*/
|
|
|
13810 |
createEl() {
|
|
|
13811 |
const el = super.createEl('button', {
|
|
|
13812 |
className: 'vjs-seek-to-live-control vjs-control'
|
|
|
13813 |
});
|
|
|
13814 |
this.setIcon('circle', el);
|
|
|
13815 |
this.textEl_ = createEl('span', {
|
|
|
13816 |
className: 'vjs-seek-to-live-text',
|
|
|
13817 |
textContent: this.localize('LIVE')
|
|
|
13818 |
}, {
|
|
|
13819 |
'aria-hidden': 'true'
|
|
|
13820 |
});
|
|
|
13821 |
el.appendChild(this.textEl_);
|
|
|
13822 |
return el;
|
|
|
13823 |
}
|
|
|
13824 |
|
|
|
13825 |
/**
|
|
|
13826 |
* Update the state of this button if we are at the live edge
|
|
|
13827 |
* or not
|
|
|
13828 |
*/
|
|
|
13829 |
updateLiveEdgeStatus() {
|
|
|
13830 |
// default to live edge
|
|
|
13831 |
if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
|
|
|
13832 |
this.setAttribute('aria-disabled', true);
|
|
|
13833 |
this.addClass('vjs-at-live-edge');
|
|
|
13834 |
this.controlText('Seek to live, currently playing live');
|
|
|
13835 |
} else {
|
|
|
13836 |
this.setAttribute('aria-disabled', false);
|
|
|
13837 |
this.removeClass('vjs-at-live-edge');
|
|
|
13838 |
this.controlText('Seek to live, currently behind live');
|
|
|
13839 |
}
|
|
|
13840 |
}
|
|
|
13841 |
|
|
|
13842 |
/**
|
|
|
13843 |
* On click bring us as near to the live point as possible.
|
|
|
13844 |
* This requires that we wait for the next `live-seekable-change`
|
|
|
13845 |
* event which will happen every segment length seconds.
|
|
|
13846 |
*/
|
|
|
13847 |
handleClick() {
|
|
|
13848 |
this.player_.liveTracker.seekToLiveEdge();
|
|
|
13849 |
}
|
|
|
13850 |
|
|
|
13851 |
/**
|
|
|
13852 |
* Dispose of the element and stop tracking
|
|
|
13853 |
*/
|
|
|
13854 |
dispose() {
|
|
|
13855 |
if (this.player_.liveTracker) {
|
|
|
13856 |
this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
|
|
|
13857 |
}
|
|
|
13858 |
this.textEl_ = null;
|
|
|
13859 |
super.dispose();
|
|
|
13860 |
}
|
|
|
13861 |
}
|
|
|
13862 |
/**
|
|
|
13863 |
* The text that should display over the `SeekToLive`s control. Added for localization.
|
|
|
13864 |
*
|
|
|
13865 |
* @type {string}
|
|
|
13866 |
* @protected
|
|
|
13867 |
*/
|
|
|
13868 |
SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
|
|
|
13869 |
Component$1.registerComponent('SeekToLive', SeekToLive);
|
|
|
13870 |
|
|
|
13871 |
/**
|
|
|
13872 |
* @file num.js
|
|
|
13873 |
* @module num
|
|
|
13874 |
*/
|
|
|
13875 |
|
|
|
13876 |
/**
|
|
|
13877 |
* Keep a number between a min and a max value
|
|
|
13878 |
*
|
|
|
13879 |
* @param {number} number
|
|
|
13880 |
* The number to clamp
|
|
|
13881 |
*
|
|
|
13882 |
* @param {number} min
|
|
|
13883 |
* The minimum value
|
|
|
13884 |
* @param {number} max
|
|
|
13885 |
* The maximum value
|
|
|
13886 |
*
|
|
|
13887 |
* @return {number}
|
|
|
13888 |
* the clamped number
|
|
|
13889 |
*/
|
|
|
13890 |
function clamp(number, min, max) {
|
|
|
13891 |
number = Number(number);
|
|
|
13892 |
return Math.min(max, Math.max(min, isNaN(number) ? min : number));
|
|
|
13893 |
}
|
|
|
13894 |
|
|
|
13895 |
var Num = /*#__PURE__*/Object.freeze({
|
|
|
13896 |
__proto__: null,
|
|
|
13897 |
clamp: clamp
|
|
|
13898 |
});
|
|
|
13899 |
|
|
|
13900 |
/**
|
|
|
13901 |
* @file slider.js
|
|
|
13902 |
*/
|
|
|
13903 |
|
|
|
13904 |
/**
|
|
|
13905 |
* The base functionality for a slider. Can be vertical or horizontal.
|
|
|
13906 |
* For instance the volume bar or the seek bar on a video is a slider.
|
|
|
13907 |
*
|
|
|
13908 |
* @extends Component
|
|
|
13909 |
*/
|
|
|
13910 |
class Slider extends Component$1 {
|
|
|
13911 |
/**
|
|
|
13912 |
* Create an instance of this class
|
|
|
13913 |
*
|
|
|
13914 |
* @param { import('../player').default } player
|
|
|
13915 |
* The `Player` that this class should be attached to.
|
|
|
13916 |
*
|
|
|
13917 |
* @param {Object} [options]
|
|
|
13918 |
* The key/value store of player options.
|
|
|
13919 |
*/
|
|
|
13920 |
constructor(player, options) {
|
|
|
13921 |
super(player, options);
|
|
|
13922 |
this.handleMouseDown_ = e => this.handleMouseDown(e);
|
|
|
13923 |
this.handleMouseUp_ = e => this.handleMouseUp(e);
|
|
|
13924 |
this.handleKeyDown_ = e => this.handleKeyDown(e);
|
|
|
13925 |
this.handleClick_ = e => this.handleClick(e);
|
|
|
13926 |
this.handleMouseMove_ = e => this.handleMouseMove(e);
|
|
|
13927 |
this.update_ = e => this.update(e);
|
|
|
13928 |
|
|
|
13929 |
// Set property names to bar to match with the child Slider class is looking for
|
|
|
13930 |
this.bar = this.getChild(this.options_.barName);
|
|
|
13931 |
|
|
|
13932 |
// Set a horizontal or vertical class on the slider depending on the slider type
|
|
|
13933 |
this.vertical(!!this.options_.vertical);
|
|
|
13934 |
this.enable();
|
|
|
13935 |
}
|
|
|
13936 |
|
|
|
13937 |
/**
|
|
|
13938 |
* Are controls are currently enabled for this slider or not.
|
|
|
13939 |
*
|
|
|
13940 |
* @return {boolean}
|
|
|
13941 |
* true if controls are enabled, false otherwise
|
|
|
13942 |
*/
|
|
|
13943 |
enabled() {
|
|
|
13944 |
return this.enabled_;
|
|
|
13945 |
}
|
|
|
13946 |
|
|
|
13947 |
/**
|
|
|
13948 |
* Enable controls for this slider if they are disabled
|
|
|
13949 |
*/
|
|
|
13950 |
enable() {
|
|
|
13951 |
if (this.enabled()) {
|
|
|
13952 |
return;
|
|
|
13953 |
}
|
|
|
13954 |
this.on('mousedown', this.handleMouseDown_);
|
|
|
13955 |
this.on('touchstart', this.handleMouseDown_);
|
|
|
13956 |
this.on('keydown', this.handleKeyDown_);
|
|
|
13957 |
this.on('click', this.handleClick_);
|
|
|
13958 |
|
|
|
13959 |
// TODO: deprecated, controlsvisible does not seem to be fired
|
|
|
13960 |
this.on(this.player_, 'controlsvisible', this.update);
|
|
|
13961 |
if (this.playerEvent) {
|
|
|
13962 |
this.on(this.player_, this.playerEvent, this.update);
|
|
|
13963 |
}
|
|
|
13964 |
this.removeClass('disabled');
|
|
|
13965 |
this.setAttribute('tabindex', 0);
|
|
|
13966 |
this.enabled_ = true;
|
|
|
13967 |
}
|
|
|
13968 |
|
|
|
13969 |
/**
|
|
|
13970 |
* Disable controls for this slider if they are enabled
|
|
|
13971 |
*/
|
|
|
13972 |
disable() {
|
|
|
13973 |
if (!this.enabled()) {
|
|
|
13974 |
return;
|
|
|
13975 |
}
|
|
|
13976 |
const doc = this.bar.el_.ownerDocument;
|
|
|
13977 |
this.off('mousedown', this.handleMouseDown_);
|
|
|
13978 |
this.off('touchstart', this.handleMouseDown_);
|
|
|
13979 |
this.off('keydown', this.handleKeyDown_);
|
|
|
13980 |
this.off('click', this.handleClick_);
|
|
|
13981 |
this.off(this.player_, 'controlsvisible', this.update_);
|
|
|
13982 |
this.off(doc, 'mousemove', this.handleMouseMove_);
|
|
|
13983 |
this.off(doc, 'mouseup', this.handleMouseUp_);
|
|
|
13984 |
this.off(doc, 'touchmove', this.handleMouseMove_);
|
|
|
13985 |
this.off(doc, 'touchend', this.handleMouseUp_);
|
|
|
13986 |
this.removeAttribute('tabindex');
|
|
|
13987 |
this.addClass('disabled');
|
|
|
13988 |
if (this.playerEvent) {
|
|
|
13989 |
this.off(this.player_, this.playerEvent, this.update);
|
|
|
13990 |
}
|
|
|
13991 |
this.enabled_ = false;
|
|
|
13992 |
}
|
|
|
13993 |
|
|
|
13994 |
/**
|
|
|
13995 |
* Create the `Slider`s DOM element.
|
|
|
13996 |
*
|
|
|
13997 |
* @param {string} type
|
|
|
13998 |
* Type of element to create.
|
|
|
13999 |
*
|
|
|
14000 |
* @param {Object} [props={}]
|
|
|
14001 |
* List of properties in Object form.
|
|
|
14002 |
*
|
|
|
14003 |
* @param {Object} [attributes={}]
|
|
|
14004 |
* list of attributes in Object form.
|
|
|
14005 |
*
|
|
|
14006 |
* @return {Element}
|
|
|
14007 |
* The element that gets created.
|
|
|
14008 |
*/
|
|
|
14009 |
createEl(type, props = {}, attributes = {}) {
|
|
|
14010 |
// Add the slider element class to all sub classes
|
|
|
14011 |
props.className = props.className + ' vjs-slider';
|
|
|
14012 |
props = Object.assign({
|
|
|
14013 |
tabIndex: 0
|
|
|
14014 |
}, props);
|
|
|
14015 |
attributes = Object.assign({
|
|
|
14016 |
'role': 'slider',
|
|
|
14017 |
'aria-valuenow': 0,
|
|
|
14018 |
'aria-valuemin': 0,
|
|
|
14019 |
'aria-valuemax': 100
|
|
|
14020 |
}, attributes);
|
|
|
14021 |
return super.createEl(type, props, attributes);
|
|
|
14022 |
}
|
|
|
14023 |
|
|
|
14024 |
/**
|
|
|
14025 |
* Handle `mousedown` or `touchstart` events on the `Slider`.
|
|
|
14026 |
*
|
|
|
14027 |
* @param {MouseEvent} event
|
|
|
14028 |
* `mousedown` or `touchstart` event that triggered this function
|
|
|
14029 |
*
|
|
|
14030 |
* @listens mousedown
|
|
|
14031 |
* @listens touchstart
|
|
|
14032 |
* @fires Slider#slideractive
|
|
|
14033 |
*/
|
|
|
14034 |
handleMouseDown(event) {
|
|
|
14035 |
const doc = this.bar.el_.ownerDocument;
|
|
|
14036 |
if (event.type === 'mousedown') {
|
|
|
14037 |
event.preventDefault();
|
|
|
14038 |
}
|
|
|
14039 |
// Do not call preventDefault() on touchstart in Chrome
|
|
|
14040 |
// to avoid console warnings. Use a 'touch-action: none' style
|
|
|
14041 |
// instead to prevent unintended scrolling.
|
|
|
14042 |
// https://developers.google.com/web/updates/2017/01/scrolling-intervention
|
|
|
14043 |
if (event.type === 'touchstart' && !IS_CHROME) {
|
|
|
14044 |
event.preventDefault();
|
|
|
14045 |
}
|
|
|
14046 |
blockTextSelection();
|
|
|
14047 |
this.addClass('vjs-sliding');
|
|
|
14048 |
/**
|
|
|
14049 |
* Triggered when the slider is in an active state
|
|
|
14050 |
*
|
|
|
14051 |
* @event Slider#slideractive
|
|
|
14052 |
* @type {MouseEvent}
|
|
|
14053 |
*/
|
|
|
14054 |
this.trigger('slideractive');
|
|
|
14055 |
this.on(doc, 'mousemove', this.handleMouseMove_);
|
|
|
14056 |
this.on(doc, 'mouseup', this.handleMouseUp_);
|
|
|
14057 |
this.on(doc, 'touchmove', this.handleMouseMove_);
|
|
|
14058 |
this.on(doc, 'touchend', this.handleMouseUp_);
|
|
|
14059 |
this.handleMouseMove(event, true);
|
|
|
14060 |
}
|
|
|
14061 |
|
|
|
14062 |
/**
|
|
|
14063 |
* Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
|
|
|
14064 |
* The `mousemove` and `touchmove` events will only only trigger this function during
|
|
|
14065 |
* `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
|
|
|
14066 |
* {@link Slider#handleMouseUp}.
|
|
|
14067 |
*
|
|
|
14068 |
* @param {MouseEvent} event
|
|
|
14069 |
* `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
|
|
|
14070 |
* this function
|
|
|
14071 |
* @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
|
|
|
14072 |
*
|
|
|
14073 |
* @listens mousemove
|
|
|
14074 |
* @listens touchmove
|
|
|
14075 |
*/
|
|
|
14076 |
handleMouseMove(event) {}
|
|
|
14077 |
|
|
|
14078 |
/**
|
|
|
14079 |
* Handle `mouseup` or `touchend` events on the `Slider`.
|
|
|
14080 |
*
|
|
|
14081 |
* @param {MouseEvent} event
|
|
|
14082 |
* `mouseup` or `touchend` event that triggered this function.
|
|
|
14083 |
*
|
|
|
14084 |
* @listens touchend
|
|
|
14085 |
* @listens mouseup
|
|
|
14086 |
* @fires Slider#sliderinactive
|
|
|
14087 |
*/
|
|
|
14088 |
handleMouseUp(event) {
|
|
|
14089 |
const doc = this.bar.el_.ownerDocument;
|
|
|
14090 |
unblockTextSelection();
|
|
|
14091 |
this.removeClass('vjs-sliding');
|
|
|
14092 |
/**
|
|
|
14093 |
* Triggered when the slider is no longer in an active state.
|
|
|
14094 |
*
|
|
|
14095 |
* @event Slider#sliderinactive
|
|
|
14096 |
* @type {Event}
|
|
|
14097 |
*/
|
|
|
14098 |
this.trigger('sliderinactive');
|
|
|
14099 |
this.off(doc, 'mousemove', this.handleMouseMove_);
|
|
|
14100 |
this.off(doc, 'mouseup', this.handleMouseUp_);
|
|
|
14101 |
this.off(doc, 'touchmove', this.handleMouseMove_);
|
|
|
14102 |
this.off(doc, 'touchend', this.handleMouseUp_);
|
|
|
14103 |
this.update();
|
|
|
14104 |
}
|
|
|
14105 |
|
|
|
14106 |
/**
|
|
|
14107 |
* Update the progress bar of the `Slider`.
|
|
|
14108 |
*
|
|
|
14109 |
* @return {number}
|
|
|
14110 |
* The percentage of progress the progress bar represents as a
|
|
|
14111 |
* number from 0 to 1.
|
|
|
14112 |
*/
|
|
|
14113 |
update() {
|
|
|
14114 |
// In VolumeBar init we have a setTimeout for update that pops and update
|
|
|
14115 |
// to the end of the execution stack. The player is destroyed before then
|
|
|
14116 |
// update will cause an error
|
|
|
14117 |
// If there's no bar...
|
|
|
14118 |
if (!this.el_ || !this.bar) {
|
|
|
14119 |
return;
|
|
|
14120 |
}
|
|
|
14121 |
|
|
|
14122 |
// clamp progress between 0 and 1
|
|
|
14123 |
// and only round to four decimal places, as we round to two below
|
|
|
14124 |
const progress = this.getProgress();
|
|
|
14125 |
if (progress === this.progress_) {
|
|
|
14126 |
return progress;
|
|
|
14127 |
}
|
|
|
14128 |
this.progress_ = progress;
|
|
|
14129 |
this.requestNamedAnimationFrame('Slider#update', () => {
|
|
|
14130 |
// Set the new bar width or height
|
|
|
14131 |
const sizeKey = this.vertical() ? 'height' : 'width';
|
|
|
14132 |
|
|
|
14133 |
// Convert to a percentage for css value
|
|
|
14134 |
this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
|
|
|
14135 |
});
|
|
|
14136 |
return progress;
|
|
|
14137 |
}
|
|
|
14138 |
|
|
|
14139 |
/**
|
|
|
14140 |
* Get the percentage of the bar that should be filled
|
|
|
14141 |
* but clamped and rounded.
|
|
|
14142 |
*
|
|
|
14143 |
* @return {number}
|
|
|
14144 |
* percentage filled that the slider is
|
|
|
14145 |
*/
|
|
|
14146 |
getProgress() {
|
|
|
14147 |
return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
|
|
|
14148 |
}
|
|
|
14149 |
|
|
|
14150 |
/**
|
|
|
14151 |
* Calculate distance for slider
|
|
|
14152 |
*
|
|
|
14153 |
* @param {Event} event
|
|
|
14154 |
* The event that caused this function to run.
|
|
|
14155 |
*
|
|
|
14156 |
* @return {number}
|
|
|
14157 |
* The current position of the Slider.
|
|
|
14158 |
* - position.x for vertical `Slider`s
|
|
|
14159 |
* - position.y for horizontal `Slider`s
|
|
|
14160 |
*/
|
|
|
14161 |
calculateDistance(event) {
|
|
|
14162 |
const position = getPointerPosition(this.el_, event);
|
|
|
14163 |
if (this.vertical()) {
|
|
|
14164 |
return position.y;
|
|
|
14165 |
}
|
|
|
14166 |
return position.x;
|
|
|
14167 |
}
|
|
|
14168 |
|
|
|
14169 |
/**
|
|
|
14170 |
* Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
|
|
|
14171 |
* arrow keys. This function will only be called when the slider has focus. See
|
|
|
14172 |
* {@link Slider#handleFocus} and {@link Slider#handleBlur}.
|
|
|
14173 |
*
|
|
|
14174 |
* @param {KeyboardEvent} event
|
|
|
14175 |
* the `keydown` event that caused this function to run.
|
|
|
14176 |
*
|
|
|
14177 |
* @listens keydown
|
|
|
14178 |
*/
|
|
|
14179 |
handleKeyDown(event) {
|
|
|
14180 |
// Left and Down Arrows
|
|
|
14181 |
if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
|
|
|
14182 |
event.preventDefault();
|
|
|
14183 |
event.stopPropagation();
|
|
|
14184 |
this.stepBack();
|
|
|
14185 |
|
|
|
14186 |
// Up and Right Arrows
|
|
|
14187 |
} else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
|
|
|
14188 |
event.preventDefault();
|
|
|
14189 |
event.stopPropagation();
|
|
|
14190 |
this.stepForward();
|
|
|
14191 |
} else {
|
|
|
14192 |
// Pass keydown handling up for unsupported keys
|
|
|
14193 |
super.handleKeyDown(event);
|
|
|
14194 |
}
|
|
|
14195 |
}
|
|
|
14196 |
|
|
|
14197 |
/**
|
|
|
14198 |
* Listener for click events on slider, used to prevent clicks
|
|
|
14199 |
* from bubbling up to parent elements like button menus.
|
|
|
14200 |
*
|
|
|
14201 |
* @param {Object} event
|
|
|
14202 |
* Event that caused this object to run
|
|
|
14203 |
*/
|
|
|
14204 |
handleClick(event) {
|
|
|
14205 |
event.stopPropagation();
|
|
|
14206 |
event.preventDefault();
|
|
|
14207 |
}
|
|
|
14208 |
|
|
|
14209 |
/**
|
|
|
14210 |
* Get/set if slider is horizontal for vertical
|
|
|
14211 |
*
|
|
|
14212 |
* @param {boolean} [bool]
|
|
|
14213 |
* - true if slider is vertical,
|
|
|
14214 |
* - false is horizontal
|
|
|
14215 |
*
|
|
|
14216 |
* @return {boolean}
|
|
|
14217 |
* - true if slider is vertical, and getting
|
|
|
14218 |
* - false if the slider is horizontal, and getting
|
|
|
14219 |
*/
|
|
|
14220 |
vertical(bool) {
|
|
|
14221 |
if (bool === undefined) {
|
|
|
14222 |
return this.vertical_ || false;
|
|
|
14223 |
}
|
|
|
14224 |
this.vertical_ = !!bool;
|
|
|
14225 |
if (this.vertical_) {
|
|
|
14226 |
this.addClass('vjs-slider-vertical');
|
|
|
14227 |
} else {
|
|
|
14228 |
this.addClass('vjs-slider-horizontal');
|
|
|
14229 |
}
|
|
|
14230 |
}
|
|
|
14231 |
}
|
|
|
14232 |
Component$1.registerComponent('Slider', Slider);
|
|
|
14233 |
|
|
|
14234 |
/**
|
|
|
14235 |
* @file load-progress-bar.js
|
|
|
14236 |
*/
|
|
|
14237 |
|
|
|
14238 |
// get the percent width of a time compared to the total end
|
|
|
14239 |
const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
|
|
|
14240 |
|
|
|
14241 |
/**
|
|
|
14242 |
* Shows loading progress
|
|
|
14243 |
*
|
|
|
14244 |
* @extends Component
|
|
|
14245 |
*/
|
|
|
14246 |
class LoadProgressBar extends Component$1 {
|
|
|
14247 |
/**
|
|
|
14248 |
* Creates an instance of this class.
|
|
|
14249 |
*
|
|
|
14250 |
* @param { import('../../player').default } player
|
|
|
14251 |
* The `Player` that this class should be attached to.
|
|
|
14252 |
*
|
|
|
14253 |
* @param {Object} [options]
|
|
|
14254 |
* The key/value store of player options.
|
|
|
14255 |
*/
|
|
|
14256 |
constructor(player, options) {
|
|
|
14257 |
super(player, options);
|
|
|
14258 |
this.partEls_ = [];
|
|
|
14259 |
this.on(player, 'progress', e => this.update(e));
|
|
|
14260 |
}
|
|
|
14261 |
|
|
|
14262 |
/**
|
|
|
14263 |
* Create the `Component`'s DOM element
|
|
|
14264 |
*
|
|
|
14265 |
* @return {Element}
|
|
|
14266 |
* The element that was created.
|
|
|
14267 |
*/
|
|
|
14268 |
createEl() {
|
|
|
14269 |
const el = super.createEl('div', {
|
|
|
14270 |
className: 'vjs-load-progress'
|
|
|
14271 |
});
|
|
|
14272 |
const wrapper = createEl('span', {
|
|
|
14273 |
className: 'vjs-control-text'
|
|
|
14274 |
});
|
|
|
14275 |
const loadedText = createEl('span', {
|
|
|
14276 |
textContent: this.localize('Loaded')
|
|
|
14277 |
});
|
|
|
14278 |
const separator = document.createTextNode(': ');
|
|
|
14279 |
this.percentageEl_ = createEl('span', {
|
|
|
14280 |
className: 'vjs-control-text-loaded-percentage',
|
|
|
14281 |
textContent: '0%'
|
|
|
14282 |
});
|
|
|
14283 |
el.appendChild(wrapper);
|
|
|
14284 |
wrapper.appendChild(loadedText);
|
|
|
14285 |
wrapper.appendChild(separator);
|
|
|
14286 |
wrapper.appendChild(this.percentageEl_);
|
|
|
14287 |
return el;
|
|
|
14288 |
}
|
|
|
14289 |
dispose() {
|
|
|
14290 |
this.partEls_ = null;
|
|
|
14291 |
this.percentageEl_ = null;
|
|
|
14292 |
super.dispose();
|
|
|
14293 |
}
|
|
|
14294 |
|
|
|
14295 |
/**
|
|
|
14296 |
* Update progress bar
|
|
|
14297 |
*
|
|
|
14298 |
* @param {Event} [event]
|
|
|
14299 |
* The `progress` event that caused this function to run.
|
|
|
14300 |
*
|
|
|
14301 |
* @listens Player#progress
|
|
|
14302 |
*/
|
|
|
14303 |
update(event) {
|
|
|
14304 |
this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
|
|
|
14305 |
const liveTracker = this.player_.liveTracker;
|
|
|
14306 |
const buffered = this.player_.buffered();
|
|
|
14307 |
const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
|
|
|
14308 |
const bufferedEnd = this.player_.bufferedEnd();
|
|
|
14309 |
const children = this.partEls_;
|
|
|
14310 |
const percent = percentify(bufferedEnd, duration);
|
|
|
14311 |
if (this.percent_ !== percent) {
|
|
|
14312 |
// update the width of the progress bar
|
|
|
14313 |
this.el_.style.width = percent;
|
|
|
14314 |
// update the control-text
|
|
|
14315 |
textContent(this.percentageEl_, percent);
|
|
|
14316 |
this.percent_ = percent;
|
|
|
14317 |
}
|
|
|
14318 |
|
|
|
14319 |
// add child elements to represent the individual buffered time ranges
|
|
|
14320 |
for (let i = 0; i < buffered.length; i++) {
|
|
|
14321 |
const start = buffered.start(i);
|
|
|
14322 |
const end = buffered.end(i);
|
|
|
14323 |
let part = children[i];
|
|
|
14324 |
if (!part) {
|
|
|
14325 |
part = this.el_.appendChild(createEl());
|
|
|
14326 |
children[i] = part;
|
|
|
14327 |
}
|
|
|
14328 |
|
|
|
14329 |
// only update if changed
|
|
|
14330 |
if (part.dataset.start === start && part.dataset.end === end) {
|
|
|
14331 |
continue;
|
|
|
14332 |
}
|
|
|
14333 |
part.dataset.start = start;
|
|
|
14334 |
part.dataset.end = end;
|
|
|
14335 |
|
|
|
14336 |
// set the percent based on the width of the progress bar (bufferedEnd)
|
|
|
14337 |
part.style.left = percentify(start, bufferedEnd);
|
|
|
14338 |
part.style.width = percentify(end - start, bufferedEnd);
|
|
|
14339 |
}
|
|
|
14340 |
|
|
|
14341 |
// remove unused buffered range elements
|
|
|
14342 |
for (let i = children.length; i > buffered.length; i--) {
|
|
|
14343 |
this.el_.removeChild(children[i - 1]);
|
|
|
14344 |
}
|
|
|
14345 |
children.length = buffered.length;
|
|
|
14346 |
});
|
|
|
14347 |
}
|
|
|
14348 |
}
|
|
|
14349 |
Component$1.registerComponent('LoadProgressBar', LoadProgressBar);
|
|
|
14350 |
|
|
|
14351 |
/**
|
|
|
14352 |
* @file time-tooltip.js
|
|
|
14353 |
*/
|
|
|
14354 |
|
|
|
14355 |
/**
|
|
|
14356 |
* Time tooltips display a time above the progress bar.
|
|
|
14357 |
*
|
|
|
14358 |
* @extends Component
|
|
|
14359 |
*/
|
|
|
14360 |
class TimeTooltip extends Component$1 {
|
|
|
14361 |
/**
|
|
|
14362 |
* Creates an instance of this class.
|
|
|
14363 |
*
|
|
|
14364 |
* @param { import('../../player').default } player
|
|
|
14365 |
* The {@link Player} that this class should be attached to.
|
|
|
14366 |
*
|
|
|
14367 |
* @param {Object} [options]
|
|
|
14368 |
* The key/value store of player options.
|
|
|
14369 |
*/
|
|
|
14370 |
constructor(player, options) {
|
|
|
14371 |
super(player, options);
|
|
|
14372 |
this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
|
|
|
14373 |
}
|
|
|
14374 |
|
|
|
14375 |
/**
|
|
|
14376 |
* Create the time tooltip DOM element
|
|
|
14377 |
*
|
|
|
14378 |
* @return {Element}
|
|
|
14379 |
* The element that was created.
|
|
|
14380 |
*/
|
|
|
14381 |
createEl() {
|
|
|
14382 |
return super.createEl('div', {
|
|
|
14383 |
className: 'vjs-time-tooltip'
|
|
|
14384 |
}, {
|
|
|
14385 |
'aria-hidden': 'true'
|
|
|
14386 |
});
|
|
|
14387 |
}
|
|
|
14388 |
|
|
|
14389 |
/**
|
|
|
14390 |
* Updates the position of the time tooltip relative to the `SeekBar`.
|
|
|
14391 |
*
|
|
|
14392 |
* @param {Object} seekBarRect
|
|
|
14393 |
* The `ClientRect` for the {@link SeekBar} element.
|
|
|
14394 |
*
|
|
|
14395 |
* @param {number} seekBarPoint
|
|
|
14396 |
* A number from 0 to 1, representing a horizontal reference point
|
|
|
14397 |
* from the left edge of the {@link SeekBar}
|
|
|
14398 |
*/
|
|
|
14399 |
update(seekBarRect, seekBarPoint, content) {
|
|
|
14400 |
const tooltipRect = findPosition(this.el_);
|
|
|
14401 |
const playerRect = getBoundingClientRect(this.player_.el());
|
|
|
14402 |
const seekBarPointPx = seekBarRect.width * seekBarPoint;
|
|
|
14403 |
|
|
|
14404 |
// do nothing if either rect isn't available
|
|
|
14405 |
// for example, if the player isn't in the DOM for testing
|
|
|
14406 |
if (!playerRect || !tooltipRect) {
|
|
|
14407 |
return;
|
|
|
14408 |
}
|
|
|
14409 |
|
|
|
14410 |
// This is the space left of the `seekBarPoint` available within the bounds
|
|
|
14411 |
// of the player. We calculate any gap between the left edge of the player
|
|
|
14412 |
// and the left edge of the `SeekBar` and add the number of pixels in the
|
|
|
14413 |
// `SeekBar` before hitting the `seekBarPoint`
|
|
|
14414 |
const spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
|
|
|
14415 |
|
|
|
14416 |
// This is the space right of the `seekBarPoint` available within the bounds
|
|
|
14417 |
// of the player. We calculate the number of pixels from the `seekBarPoint`
|
|
|
14418 |
// to the right edge of the `SeekBar` and add to that any gap between the
|
|
|
14419 |
// right edge of the `SeekBar` and the player.
|
|
|
14420 |
const spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
|
|
|
14421 |
|
|
|
14422 |
// This is the number of pixels by which the tooltip will need to be pulled
|
|
|
14423 |
// further to the right to center it over the `seekBarPoint`.
|
|
|
14424 |
let pullTooltipBy = tooltipRect.width / 2;
|
|
|
14425 |
|
|
|
14426 |
// Adjust the `pullTooltipBy` distance to the left or right depending on
|
|
|
14427 |
// the results of the space calculations above.
|
|
|
14428 |
if (spaceLeftOfPoint < pullTooltipBy) {
|
|
|
14429 |
pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
|
|
|
14430 |
} else if (spaceRightOfPoint < pullTooltipBy) {
|
|
|
14431 |
pullTooltipBy = spaceRightOfPoint;
|
|
|
14432 |
}
|
|
|
14433 |
|
|
|
14434 |
// Due to the imprecision of decimal/ratio based calculations and varying
|
|
|
14435 |
// rounding behaviors, there are cases where the spacing adjustment is off
|
|
|
14436 |
// by a pixel or two. This adds insurance to these calculations.
|
|
|
14437 |
if (pullTooltipBy < 0) {
|
|
|
14438 |
pullTooltipBy = 0;
|
|
|
14439 |
} else if (pullTooltipBy > tooltipRect.width) {
|
|
|
14440 |
pullTooltipBy = tooltipRect.width;
|
|
|
14441 |
}
|
|
|
14442 |
|
|
|
14443 |
// prevent small width fluctuations within 0.4px from
|
|
|
14444 |
// changing the value below.
|
|
|
14445 |
// This really helps for live to prevent the play
|
|
|
14446 |
// progress time tooltip from jittering
|
|
|
14447 |
pullTooltipBy = Math.round(pullTooltipBy);
|
|
|
14448 |
this.el_.style.right = `-${pullTooltipBy}px`;
|
|
|
14449 |
this.write(content);
|
|
|
14450 |
}
|
|
|
14451 |
|
|
|
14452 |
/**
|
|
|
14453 |
* Write the time to the tooltip DOM element.
|
|
|
14454 |
*
|
|
|
14455 |
* @param {string} content
|
|
|
14456 |
* The formatted time for the tooltip.
|
|
|
14457 |
*/
|
|
|
14458 |
write(content) {
|
|
|
14459 |
textContent(this.el_, content);
|
|
|
14460 |
}
|
|
|
14461 |
|
|
|
14462 |
/**
|
|
|
14463 |
* Updates the position of the time tooltip relative to the `SeekBar`.
|
|
|
14464 |
*
|
|
|
14465 |
* @param {Object} seekBarRect
|
|
|
14466 |
* The `ClientRect` for the {@link SeekBar} element.
|
|
|
14467 |
*
|
|
|
14468 |
* @param {number} seekBarPoint
|
|
|
14469 |
* A number from 0 to 1, representing a horizontal reference point
|
|
|
14470 |
* from the left edge of the {@link SeekBar}
|
|
|
14471 |
*
|
|
|
14472 |
* @param {number} time
|
|
|
14473 |
* The time to update the tooltip to, not used during live playback
|
|
|
14474 |
*
|
|
|
14475 |
* @param {Function} cb
|
|
|
14476 |
* A function that will be called during the request animation frame
|
|
|
14477 |
* for tooltips that need to do additional animations from the default
|
|
|
14478 |
*/
|
|
|
14479 |
updateTime(seekBarRect, seekBarPoint, time, cb) {
|
|
|
14480 |
this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
|
|
|
14481 |
let content;
|
|
|
14482 |
const duration = this.player_.duration();
|
|
|
14483 |
if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
|
|
|
14484 |
const liveWindow = this.player_.liveTracker.liveWindow();
|
|
|
14485 |
const secondsBehind = liveWindow - seekBarPoint * liveWindow;
|
|
|
14486 |
content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
|
|
|
14487 |
} else {
|
|
|
14488 |
content = formatTime(time, duration);
|
|
|
14489 |
}
|
|
|
14490 |
this.update(seekBarRect, seekBarPoint, content);
|
|
|
14491 |
if (cb) {
|
|
|
14492 |
cb();
|
|
|
14493 |
}
|
|
|
14494 |
});
|
|
|
14495 |
}
|
|
|
14496 |
}
|
|
|
14497 |
Component$1.registerComponent('TimeTooltip', TimeTooltip);
|
|
|
14498 |
|
|
|
14499 |
/**
|
|
|
14500 |
* @file play-progress-bar.js
|
|
|
14501 |
*/
|
|
|
14502 |
|
|
|
14503 |
/**
|
|
|
14504 |
* Used by {@link SeekBar} to display media playback progress as part of the
|
|
|
14505 |
* {@link ProgressControl}.
|
|
|
14506 |
*
|
|
|
14507 |
* @extends Component
|
|
|
14508 |
*/
|
|
|
14509 |
class PlayProgressBar extends Component$1 {
|
|
|
14510 |
/**
|
|
|
14511 |
* Creates an instance of this class.
|
|
|
14512 |
*
|
|
|
14513 |
* @param { import('../../player').default } player
|
|
|
14514 |
* The {@link Player} that this class should be attached to.
|
|
|
14515 |
*
|
|
|
14516 |
* @param {Object} [options]
|
|
|
14517 |
* The key/value store of player options.
|
|
|
14518 |
*/
|
|
|
14519 |
constructor(player, options) {
|
|
|
14520 |
super(player, options);
|
|
|
14521 |
this.setIcon('circle');
|
|
|
14522 |
this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
|
|
|
14523 |
}
|
|
|
14524 |
|
|
|
14525 |
/**
|
|
|
14526 |
* Create the the DOM element for this class.
|
|
|
14527 |
*
|
|
|
14528 |
* @return {Element}
|
|
|
14529 |
* The element that was created.
|
|
|
14530 |
*/
|
|
|
14531 |
createEl() {
|
|
|
14532 |
return super.createEl('div', {
|
|
|
14533 |
className: 'vjs-play-progress vjs-slider-bar'
|
|
|
14534 |
}, {
|
|
|
14535 |
'aria-hidden': 'true'
|
|
|
14536 |
});
|
|
|
14537 |
}
|
|
|
14538 |
|
|
|
14539 |
/**
|
|
|
14540 |
* Enqueues updates to its own DOM as well as the DOM of its
|
|
|
14541 |
* {@link TimeTooltip} child.
|
|
|
14542 |
*
|
|
|
14543 |
* @param {Object} seekBarRect
|
|
|
14544 |
* The `ClientRect` for the {@link SeekBar} element.
|
|
|
14545 |
*
|
|
|
14546 |
* @param {number} seekBarPoint
|
|
|
14547 |
* A number from 0 to 1, representing a horizontal reference point
|
|
|
14548 |
* from the left edge of the {@link SeekBar}
|
|
|
14549 |
*/
|
|
|
14550 |
update(seekBarRect, seekBarPoint) {
|
|
|
14551 |
const timeTooltip = this.getChild('timeTooltip');
|
|
|
14552 |
if (!timeTooltip) {
|
|
|
14553 |
return;
|
|
|
14554 |
}
|
|
|
14555 |
const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
|
|
|
14556 |
timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
|
|
|
14557 |
}
|
|
|
14558 |
}
|
|
|
14559 |
|
|
|
14560 |
/**
|
|
|
14561 |
* Default options for {@link PlayProgressBar}.
|
|
|
14562 |
*
|
|
|
14563 |
* @type {Object}
|
|
|
14564 |
* @private
|
|
|
14565 |
*/
|
|
|
14566 |
PlayProgressBar.prototype.options_ = {
|
|
|
14567 |
children: []
|
|
|
14568 |
};
|
|
|
14569 |
|
|
|
14570 |
// Time tooltips should not be added to a player on mobile devices
|
|
|
14571 |
if (!IS_IOS && !IS_ANDROID) {
|
|
|
14572 |
PlayProgressBar.prototype.options_.children.push('timeTooltip');
|
|
|
14573 |
}
|
|
|
14574 |
Component$1.registerComponent('PlayProgressBar', PlayProgressBar);
|
|
|
14575 |
|
|
|
14576 |
/**
|
|
|
14577 |
* @file mouse-time-display.js
|
|
|
14578 |
*/
|
|
|
14579 |
|
|
|
14580 |
/**
|
|
|
14581 |
* The {@link MouseTimeDisplay} component tracks mouse movement over the
|
|
|
14582 |
* {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
|
|
|
14583 |
* indicating the time which is represented by a given point in the
|
|
|
14584 |
* {@link ProgressControl}.
|
|
|
14585 |
*
|
|
|
14586 |
* @extends Component
|
|
|
14587 |
*/
|
|
|
14588 |
class MouseTimeDisplay extends Component$1 {
|
|
|
14589 |
/**
|
|
|
14590 |
* Creates an instance of this class.
|
|
|
14591 |
*
|
|
|
14592 |
* @param { import('../../player').default } player
|
|
|
14593 |
* The {@link Player} that this class should be attached to.
|
|
|
14594 |
*
|
|
|
14595 |
* @param {Object} [options]
|
|
|
14596 |
* The key/value store of player options.
|
|
|
14597 |
*/
|
|
|
14598 |
constructor(player, options) {
|
|
|
14599 |
super(player, options);
|
|
|
14600 |
this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
|
|
|
14601 |
}
|
|
|
14602 |
|
|
|
14603 |
/**
|
|
|
14604 |
* Create the DOM element for this class.
|
|
|
14605 |
*
|
|
|
14606 |
* @return {Element}
|
|
|
14607 |
* The element that was created.
|
|
|
14608 |
*/
|
|
|
14609 |
createEl() {
|
|
|
14610 |
return super.createEl('div', {
|
|
|
14611 |
className: 'vjs-mouse-display'
|
|
|
14612 |
});
|
|
|
14613 |
}
|
|
|
14614 |
|
|
|
14615 |
/**
|
|
|
14616 |
* Enqueues updates to its own DOM as well as the DOM of its
|
|
|
14617 |
* {@link TimeTooltip} child.
|
|
|
14618 |
*
|
|
|
14619 |
* @param {Object} seekBarRect
|
|
|
14620 |
* The `ClientRect` for the {@link SeekBar} element.
|
|
|
14621 |
*
|
|
|
14622 |
* @param {number} seekBarPoint
|
|
|
14623 |
* A number from 0 to 1, representing a horizontal reference point
|
|
|
14624 |
* from the left edge of the {@link SeekBar}
|
|
|
14625 |
*/
|
|
|
14626 |
update(seekBarRect, seekBarPoint) {
|
|
|
14627 |
const time = seekBarPoint * this.player_.duration();
|
|
|
14628 |
this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
|
|
|
14629 |
this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
|
|
|
14630 |
});
|
|
|
14631 |
}
|
|
|
14632 |
}
|
|
|
14633 |
|
|
|
14634 |
/**
|
|
|
14635 |
* Default options for `MouseTimeDisplay`
|
|
|
14636 |
*
|
|
|
14637 |
* @type {Object}
|
|
|
14638 |
* @private
|
|
|
14639 |
*/
|
|
|
14640 |
MouseTimeDisplay.prototype.options_ = {
|
|
|
14641 |
children: ['timeTooltip']
|
|
|
14642 |
};
|
|
|
14643 |
Component$1.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
|
|
|
14644 |
|
|
|
14645 |
/**
|
|
|
14646 |
* @file seek-bar.js
|
|
|
14647 |
*/
|
|
|
14648 |
|
|
|
14649 |
// The number of seconds the `step*` functions move the timeline.
|
|
|
14650 |
const STEP_SECONDS = 5;
|
|
|
14651 |
|
|
|
14652 |
// The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
|
|
|
14653 |
const PAGE_KEY_MULTIPLIER = 12;
|
|
|
14654 |
|
|
|
14655 |
/**
|
|
|
14656 |
* Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
|
|
|
14657 |
* as its `bar`.
|
|
|
14658 |
*
|
|
|
14659 |
* @extends Slider
|
|
|
14660 |
*/
|
|
|
14661 |
class SeekBar extends Slider {
|
|
|
14662 |
/**
|
|
|
14663 |
* Creates an instance of this class.
|
|
|
14664 |
*
|
|
|
14665 |
* @param { import('../../player').default } player
|
|
|
14666 |
* The `Player` that this class should be attached to.
|
|
|
14667 |
*
|
|
|
14668 |
* @param {Object} [options]
|
|
|
14669 |
* The key/value store of player options.
|
|
|
14670 |
*/
|
|
|
14671 |
constructor(player, options) {
|
|
|
14672 |
super(player, options);
|
|
|
14673 |
this.setEventHandlers_();
|
|
|
14674 |
}
|
|
|
14675 |
|
|
|
14676 |
/**
|
|
|
14677 |
* Sets the event handlers
|
|
|
14678 |
*
|
|
|
14679 |
* @private
|
|
|
14680 |
*/
|
|
|
14681 |
setEventHandlers_() {
|
|
|
14682 |
this.update_ = bind_(this, this.update);
|
|
|
14683 |
this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
|
|
|
14684 |
this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
|
|
|
14685 |
if (this.player_.liveTracker) {
|
|
|
14686 |
this.on(this.player_.liveTracker, 'liveedgechange', this.update);
|
|
|
14687 |
}
|
|
|
14688 |
|
|
|
14689 |
// when playing, let's ensure we smoothly update the play progress bar
|
|
|
14690 |
// via an interval
|
|
|
14691 |
this.updateInterval = null;
|
|
|
14692 |
this.enableIntervalHandler_ = e => this.enableInterval_(e);
|
|
|
14693 |
this.disableIntervalHandler_ = e => this.disableInterval_(e);
|
|
|
14694 |
this.on(this.player_, ['playing'], this.enableIntervalHandler_);
|
|
|
14695 |
this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
|
|
|
14696 |
|
|
|
14697 |
// we don't need to update the play progress if the document is hidden,
|
|
|
14698 |
// also, this causes the CPU to spike and eventually crash the page on IE11.
|
|
|
14699 |
if ('hidden' in document && 'visibilityState' in document) {
|
|
|
14700 |
this.on(document, 'visibilitychange', this.toggleVisibility_);
|
|
|
14701 |
}
|
|
|
14702 |
}
|
|
|
14703 |
toggleVisibility_(e) {
|
|
|
14704 |
if (document.visibilityState === 'hidden') {
|
|
|
14705 |
this.cancelNamedAnimationFrame('SeekBar#update');
|
|
|
14706 |
this.cancelNamedAnimationFrame('Slider#update');
|
|
|
14707 |
this.disableInterval_(e);
|
|
|
14708 |
} else {
|
|
|
14709 |
if (!this.player_.ended() && !this.player_.paused()) {
|
|
|
14710 |
this.enableInterval_();
|
|
|
14711 |
}
|
|
|
14712 |
|
|
|
14713 |
// we just switched back to the page and someone may be looking, so, update ASAP
|
|
|
14714 |
this.update();
|
|
|
14715 |
}
|
|
|
14716 |
}
|
|
|
14717 |
enableInterval_() {
|
|
|
14718 |
if (this.updateInterval) {
|
|
|
14719 |
return;
|
|
|
14720 |
}
|
|
|
14721 |
this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
|
|
|
14722 |
}
|
|
|
14723 |
disableInterval_(e) {
|
|
|
14724 |
if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
|
|
|
14725 |
return;
|
|
|
14726 |
}
|
|
|
14727 |
if (!this.updateInterval) {
|
|
|
14728 |
return;
|
|
|
14729 |
}
|
|
|
14730 |
this.clearInterval(this.updateInterval);
|
|
|
14731 |
this.updateInterval = null;
|
|
|
14732 |
}
|
|
|
14733 |
|
|
|
14734 |
/**
|
|
|
14735 |
* Create the `Component`'s DOM element
|
|
|
14736 |
*
|
|
|
14737 |
* @return {Element}
|
|
|
14738 |
* The element that was created.
|
|
|
14739 |
*/
|
|
|
14740 |
createEl() {
|
|
|
14741 |
return super.createEl('div', {
|
|
|
14742 |
className: 'vjs-progress-holder'
|
|
|
14743 |
}, {
|
|
|
14744 |
'aria-label': this.localize('Progress Bar')
|
|
|
14745 |
});
|
|
|
14746 |
}
|
|
|
14747 |
|
|
|
14748 |
/**
|
|
|
14749 |
* This function updates the play progress bar and accessibility
|
|
|
14750 |
* attributes to whatever is passed in.
|
|
|
14751 |
*
|
|
|
14752 |
* @param {Event} [event]
|
|
|
14753 |
* The `timeupdate` or `ended` event that caused this to run.
|
|
|
14754 |
*
|
|
|
14755 |
* @listens Player#timeupdate
|
|
|
14756 |
*
|
|
|
14757 |
* @return {number}
|
|
|
14758 |
* The current percent at a number from 0-1
|
|
|
14759 |
*/
|
|
|
14760 |
update(event) {
|
|
|
14761 |
// ignore updates while the tab is hidden
|
|
|
14762 |
if (document.visibilityState === 'hidden') {
|
|
|
14763 |
return;
|
|
|
14764 |
}
|
|
|
14765 |
const percent = super.update();
|
|
|
14766 |
this.requestNamedAnimationFrame('SeekBar#update', () => {
|
|
|
14767 |
const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
|
|
|
14768 |
const liveTracker = this.player_.liveTracker;
|
|
|
14769 |
let duration = this.player_.duration();
|
|
|
14770 |
if (liveTracker && liveTracker.isLive()) {
|
|
|
14771 |
duration = this.player_.liveTracker.liveCurrentTime();
|
|
|
14772 |
}
|
|
|
14773 |
if (this.percent_ !== percent) {
|
|
|
14774 |
// machine readable value of progress bar (percentage complete)
|
|
|
14775 |
this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
|
|
|
14776 |
this.percent_ = percent;
|
|
|
14777 |
}
|
|
|
14778 |
if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
|
|
|
14779 |
// human readable value of progress bar (time complete)
|
|
|
14780 |
this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
|
|
|
14781 |
this.currentTime_ = currentTime;
|
|
|
14782 |
this.duration_ = duration;
|
|
|
14783 |
}
|
|
|
14784 |
|
|
|
14785 |
// update the progress bar time tooltip with the current time
|
|
|
14786 |
if (this.bar) {
|
|
|
14787 |
this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
|
|
|
14788 |
}
|
|
|
14789 |
});
|
|
|
14790 |
return percent;
|
|
|
14791 |
}
|
|
|
14792 |
|
|
|
14793 |
/**
|
|
|
14794 |
* Prevent liveThreshold from causing seeks to seem like they
|
|
|
14795 |
* are not happening from a user perspective.
|
|
|
14796 |
*
|
|
|
14797 |
* @param {number} ct
|
|
|
14798 |
* current time to seek to
|
|
|
14799 |
*/
|
|
|
14800 |
userSeek_(ct) {
|
|
|
14801 |
if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
|
|
|
14802 |
this.player_.liveTracker.nextSeekedFromUser();
|
|
|
14803 |
}
|
|
|
14804 |
this.player_.currentTime(ct);
|
|
|
14805 |
}
|
|
|
14806 |
|
|
|
14807 |
/**
|
|
|
14808 |
* Get the value of current time but allows for smooth scrubbing,
|
|
|
14809 |
* when player can't keep up.
|
|
|
14810 |
*
|
|
|
14811 |
* @return {number}
|
|
|
14812 |
* The current time value to display
|
|
|
14813 |
*
|
|
|
14814 |
* @private
|
|
|
14815 |
*/
|
|
|
14816 |
getCurrentTime_() {
|
|
|
14817 |
return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
|
|
|
14818 |
}
|
|
|
14819 |
|
|
|
14820 |
/**
|
|
|
14821 |
* Get the percentage of media played so far.
|
|
|
14822 |
*
|
|
|
14823 |
* @return {number}
|
|
|
14824 |
* The percentage of media played so far (0 to 1).
|
|
|
14825 |
*/
|
|
|
14826 |
getPercent() {
|
|
|
14827 |
const currentTime = this.getCurrentTime_();
|
|
|
14828 |
let percent;
|
|
|
14829 |
const liveTracker = this.player_.liveTracker;
|
|
|
14830 |
if (liveTracker && liveTracker.isLive()) {
|
|
|
14831 |
percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
|
|
|
14832 |
|
|
|
14833 |
// prevent the percent from changing at the live edge
|
|
|
14834 |
if (liveTracker.atLiveEdge()) {
|
|
|
14835 |
percent = 1;
|
|
|
14836 |
}
|
|
|
14837 |
} else {
|
|
|
14838 |
percent = currentTime / this.player_.duration();
|
|
|
14839 |
}
|
|
|
14840 |
return percent;
|
|
|
14841 |
}
|
|
|
14842 |
|
|
|
14843 |
/**
|
|
|
14844 |
* Handle mouse down on seek bar
|
|
|
14845 |
*
|
|
|
14846 |
* @param {MouseEvent} event
|
|
|
14847 |
* The `mousedown` event that caused this to run.
|
|
|
14848 |
*
|
|
|
14849 |
* @listens mousedown
|
|
|
14850 |
*/
|
|
|
14851 |
handleMouseDown(event) {
|
|
|
14852 |
if (!isSingleLeftClick(event)) {
|
|
|
14853 |
return;
|
|
|
14854 |
}
|
|
|
14855 |
|
|
|
14856 |
// Stop event propagation to prevent double fire in progress-control.js
|
|
|
14857 |
event.stopPropagation();
|
|
|
14858 |
this.videoWasPlaying = !this.player_.paused();
|
|
|
14859 |
this.player_.pause();
|
|
|
14860 |
super.handleMouseDown(event);
|
|
|
14861 |
}
|
|
|
14862 |
|
|
|
14863 |
/**
|
|
|
14864 |
* Handle mouse move on seek bar
|
|
|
14865 |
*
|
|
|
14866 |
* @param {MouseEvent} event
|
|
|
14867 |
* The `mousemove` event that caused this to run.
|
|
|
14868 |
* @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
|
|
|
14869 |
*
|
|
|
14870 |
* @listens mousemove
|
|
|
14871 |
*/
|
|
|
14872 |
handleMouseMove(event, mouseDown = false) {
|
|
|
14873 |
if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) {
|
|
|
14874 |
return;
|
|
|
14875 |
}
|
|
|
14876 |
if (!mouseDown && !this.player_.scrubbing()) {
|
|
|
14877 |
this.player_.scrubbing(true);
|
|
|
14878 |
}
|
|
|
14879 |
let newTime;
|
|
|
14880 |
const distance = this.calculateDistance(event);
|
|
|
14881 |
const liveTracker = this.player_.liveTracker;
|
|
|
14882 |
if (!liveTracker || !liveTracker.isLive()) {
|
|
|
14883 |
newTime = distance * this.player_.duration();
|
|
|
14884 |
|
|
|
14885 |
// Don't let video end while scrubbing.
|
|
|
14886 |
if (newTime === this.player_.duration()) {
|
|
|
14887 |
newTime = newTime - 0.1;
|
|
|
14888 |
}
|
|
|
14889 |
} else {
|
|
|
14890 |
if (distance >= 0.99) {
|
|
|
14891 |
liveTracker.seekToLiveEdge();
|
|
|
14892 |
return;
|
|
|
14893 |
}
|
|
|
14894 |
const seekableStart = liveTracker.seekableStart();
|
|
|
14895 |
const seekableEnd = liveTracker.liveCurrentTime();
|
|
|
14896 |
newTime = seekableStart + distance * liveTracker.liveWindow();
|
|
|
14897 |
|
|
|
14898 |
// Don't let video end while scrubbing.
|
|
|
14899 |
if (newTime >= seekableEnd) {
|
|
|
14900 |
newTime = seekableEnd;
|
|
|
14901 |
}
|
|
|
14902 |
|
|
|
14903 |
// Compensate for precision differences so that currentTime is not less
|
|
|
14904 |
// than seekable start
|
|
|
14905 |
if (newTime <= seekableStart) {
|
|
|
14906 |
newTime = seekableStart + 0.1;
|
|
|
14907 |
}
|
|
|
14908 |
|
|
|
14909 |
// On android seekableEnd can be Infinity sometimes,
|
|
|
14910 |
// this will cause newTime to be Infinity, which is
|
|
|
14911 |
// not a valid currentTime.
|
|
|
14912 |
if (newTime === Infinity) {
|
|
|
14913 |
return;
|
|
|
14914 |
}
|
|
|
14915 |
}
|
|
|
14916 |
|
|
|
14917 |
// Set new time (tell player to seek to new time)
|
|
|
14918 |
this.userSeek_(newTime);
|
|
|
14919 |
if (this.player_.options_.enableSmoothSeeking) {
|
|
|
14920 |
this.update();
|
|
|
14921 |
}
|
|
|
14922 |
}
|
|
|
14923 |
enable() {
|
|
|
14924 |
super.enable();
|
|
|
14925 |
const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
|
|
|
14926 |
if (!mouseTimeDisplay) {
|
|
|
14927 |
return;
|
|
|
14928 |
}
|
|
|
14929 |
mouseTimeDisplay.show();
|
|
|
14930 |
}
|
|
|
14931 |
disable() {
|
|
|
14932 |
super.disable();
|
|
|
14933 |
const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
|
|
|
14934 |
if (!mouseTimeDisplay) {
|
|
|
14935 |
return;
|
|
|
14936 |
}
|
|
|
14937 |
mouseTimeDisplay.hide();
|
|
|
14938 |
}
|
|
|
14939 |
|
|
|
14940 |
/**
|
|
|
14941 |
* Handle mouse up on seek bar
|
|
|
14942 |
*
|
|
|
14943 |
* @param {MouseEvent} event
|
|
|
14944 |
* The `mouseup` event that caused this to run.
|
|
|
14945 |
*
|
|
|
14946 |
* @listens mouseup
|
|
|
14947 |
*/
|
|
|
14948 |
handleMouseUp(event) {
|
|
|
14949 |
super.handleMouseUp(event);
|
|
|
14950 |
|
|
|
14951 |
// Stop event propagation to prevent double fire in progress-control.js
|
|
|
14952 |
if (event) {
|
|
|
14953 |
event.stopPropagation();
|
|
|
14954 |
}
|
|
|
14955 |
this.player_.scrubbing(false);
|
|
|
14956 |
|
|
|
14957 |
/**
|
|
|
14958 |
* Trigger timeupdate because we're done seeking and the time has changed.
|
|
|
14959 |
* This is particularly useful for if the player is paused to time the time displays.
|
|
|
14960 |
*
|
|
|
14961 |
* @event Tech#timeupdate
|
|
|
14962 |
* @type {Event}
|
|
|
14963 |
*/
|
|
|
14964 |
this.player_.trigger({
|
|
|
14965 |
type: 'timeupdate',
|
|
|
14966 |
target: this,
|
|
|
14967 |
manuallyTriggered: true
|
|
|
14968 |
});
|
|
|
14969 |
if (this.videoWasPlaying) {
|
|
|
14970 |
silencePromise(this.player_.play());
|
|
|
14971 |
} else {
|
|
|
14972 |
// We're done seeking and the time has changed.
|
|
|
14973 |
// If the player is paused, make sure we display the correct time on the seek bar.
|
|
|
14974 |
this.update_();
|
|
|
14975 |
}
|
|
|
14976 |
}
|
|
|
14977 |
|
|
|
14978 |
/**
|
|
|
14979 |
* Move more quickly fast forward for keyboard-only users
|
|
|
14980 |
*/
|
|
|
14981 |
stepForward() {
|
|
|
14982 |
this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
|
|
|
14983 |
}
|
|
|
14984 |
|
|
|
14985 |
/**
|
|
|
14986 |
* Move more quickly rewind for keyboard-only users
|
|
|
14987 |
*/
|
|
|
14988 |
stepBack() {
|
|
|
14989 |
this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
|
|
|
14990 |
}
|
|
|
14991 |
|
|
|
14992 |
/**
|
|
|
14993 |
* Toggles the playback state of the player
|
|
|
14994 |
* This gets called when enter or space is used on the seekbar
|
|
|
14995 |
*
|
|
|
14996 |
* @param {KeyboardEvent} event
|
|
|
14997 |
* The `keydown` event that caused this function to be called
|
|
|
14998 |
*
|
|
|
14999 |
*/
|
|
|
15000 |
handleAction(event) {
|
|
|
15001 |
if (this.player_.paused()) {
|
|
|
15002 |
this.player_.play();
|
|
|
15003 |
} else {
|
|
|
15004 |
this.player_.pause();
|
|
|
15005 |
}
|
|
|
15006 |
}
|
|
|
15007 |
|
|
|
15008 |
/**
|
|
|
15009 |
* Called when this SeekBar has focus and a key gets pressed down.
|
|
|
15010 |
* Supports the following keys:
|
|
|
15011 |
*
|
|
|
15012 |
* Space or Enter key fire a click event
|
|
|
15013 |
* Home key moves to start of the timeline
|
|
|
15014 |
* End key moves to end of the timeline
|
|
|
15015 |
* Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
|
|
|
15016 |
* PageDown key moves back a larger step than ArrowDown
|
|
|
15017 |
* PageUp key moves forward a large step
|
|
|
15018 |
*
|
|
|
15019 |
* @param {KeyboardEvent} event
|
|
|
15020 |
* The `keydown` event that caused this function to be called.
|
|
|
15021 |
*
|
|
|
15022 |
* @listens keydown
|
|
|
15023 |
*/
|
|
|
15024 |
handleKeyDown(event) {
|
|
|
15025 |
const liveTracker = this.player_.liveTracker;
|
|
|
15026 |
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
|
|
|
15027 |
event.preventDefault();
|
|
|
15028 |
event.stopPropagation();
|
|
|
15029 |
this.handleAction(event);
|
|
|
15030 |
} else if (keycode.isEventKey(event, 'Home')) {
|
|
|
15031 |
event.preventDefault();
|
|
|
15032 |
event.stopPropagation();
|
|
|
15033 |
this.userSeek_(0);
|
|
|
15034 |
} else if (keycode.isEventKey(event, 'End')) {
|
|
|
15035 |
event.preventDefault();
|
|
|
15036 |
event.stopPropagation();
|
|
|
15037 |
if (liveTracker && liveTracker.isLive()) {
|
|
|
15038 |
this.userSeek_(liveTracker.liveCurrentTime());
|
|
|
15039 |
} else {
|
|
|
15040 |
this.userSeek_(this.player_.duration());
|
|
|
15041 |
}
|
|
|
15042 |
} else if (/^[0-9]$/.test(keycode(event))) {
|
|
|
15043 |
event.preventDefault();
|
|
|
15044 |
event.stopPropagation();
|
|
|
15045 |
const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;
|
|
|
15046 |
if (liveTracker && liveTracker.isLive()) {
|
|
|
15047 |
this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
|
|
|
15048 |
} else {
|
|
|
15049 |
this.userSeek_(this.player_.duration() * gotoFraction);
|
|
|
15050 |
}
|
|
|
15051 |
} else if (keycode.isEventKey(event, 'PgDn')) {
|
|
|
15052 |
event.preventDefault();
|
|
|
15053 |
event.stopPropagation();
|
|
|
15054 |
this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
|
|
|
15055 |
} else if (keycode.isEventKey(event, 'PgUp')) {
|
|
|
15056 |
event.preventDefault();
|
|
|
15057 |
event.stopPropagation();
|
|
|
15058 |
this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
|
|
|
15059 |
} else {
|
|
|
15060 |
// Pass keydown handling up for unsupported keys
|
|
|
15061 |
super.handleKeyDown(event);
|
|
|
15062 |
}
|
|
|
15063 |
}
|
|
|
15064 |
dispose() {
|
|
|
15065 |
this.disableInterval_();
|
|
|
15066 |
this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
|
|
|
15067 |
if (this.player_.liveTracker) {
|
|
|
15068 |
this.off(this.player_.liveTracker, 'liveedgechange', this.update);
|
|
|
15069 |
}
|
|
|
15070 |
this.off(this.player_, ['playing'], this.enableIntervalHandler_);
|
|
|
15071 |
this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
|
|
|
15072 |
|
|
|
15073 |
// we don't need to update the play progress if the document is hidden,
|
|
|
15074 |
// also, this causes the CPU to spike and eventually crash the page on IE11.
|
|
|
15075 |
if ('hidden' in document && 'visibilityState' in document) {
|
|
|
15076 |
this.off(document, 'visibilitychange', this.toggleVisibility_);
|
|
|
15077 |
}
|
|
|
15078 |
super.dispose();
|
|
|
15079 |
}
|
|
|
15080 |
}
|
|
|
15081 |
|
|
|
15082 |
/**
|
|
|
15083 |
* Default options for the `SeekBar`
|
|
|
15084 |
*
|
|
|
15085 |
* @type {Object}
|
|
|
15086 |
* @private
|
|
|
15087 |
*/
|
|
|
15088 |
SeekBar.prototype.options_ = {
|
|
|
15089 |
children: ['loadProgressBar', 'playProgressBar'],
|
|
|
15090 |
barName: 'playProgressBar'
|
|
|
15091 |
};
|
|
|
15092 |
|
|
|
15093 |
// MouseTimeDisplay tooltips should not be added to a player on mobile devices
|
|
|
15094 |
if (!IS_IOS && !IS_ANDROID) {
|
|
|
15095 |
SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
|
|
|
15096 |
}
|
|
|
15097 |
Component$1.registerComponent('SeekBar', SeekBar);
|
|
|
15098 |
|
|
|
15099 |
/**
|
|
|
15100 |
* @file progress-control.js
|
|
|
15101 |
*/
|
|
|
15102 |
|
|
|
15103 |
/**
|
|
|
15104 |
* The Progress Control component contains the seek bar, load progress,
|
|
|
15105 |
* and play progress.
|
|
|
15106 |
*
|
|
|
15107 |
* @extends Component
|
|
|
15108 |
*/
|
|
|
15109 |
class ProgressControl extends Component$1 {
|
|
|
15110 |
/**
|
|
|
15111 |
* Creates an instance of this class.
|
|
|
15112 |
*
|
|
|
15113 |
* @param { import('../../player').default } player
|
|
|
15114 |
* The `Player` that this class should be attached to.
|
|
|
15115 |
*
|
|
|
15116 |
* @param {Object} [options]
|
|
|
15117 |
* The key/value store of player options.
|
|
|
15118 |
*/
|
|
|
15119 |
constructor(player, options) {
|
|
|
15120 |
super(player, options);
|
|
|
15121 |
this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
|
|
|
15122 |
this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
|
|
|
15123 |
this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
|
|
|
15124 |
this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
|
|
|
15125 |
this.enable();
|
|
|
15126 |
}
|
|
|
15127 |
|
|
|
15128 |
/**
|
|
|
15129 |
* Create the `Component`'s DOM element
|
|
|
15130 |
*
|
|
|
15131 |
* @return {Element}
|
|
|
15132 |
* The element that was created.
|
|
|
15133 |
*/
|
|
|
15134 |
createEl() {
|
|
|
15135 |
return super.createEl('div', {
|
|
|
15136 |
className: 'vjs-progress-control vjs-control'
|
|
|
15137 |
});
|
|
|
15138 |
}
|
|
|
15139 |
|
|
|
15140 |
/**
|
|
|
15141 |
* When the mouse moves over the `ProgressControl`, the pointer position
|
|
|
15142 |
* gets passed down to the `MouseTimeDisplay` component.
|
|
|
15143 |
*
|
|
|
15144 |
* @param {Event} event
|
|
|
15145 |
* The `mousemove` event that caused this function to run.
|
|
|
15146 |
*
|
|
|
15147 |
* @listen mousemove
|
|
|
15148 |
*/
|
|
|
15149 |
handleMouseMove(event) {
|
|
|
15150 |
const seekBar = this.getChild('seekBar');
|
|
|
15151 |
if (!seekBar) {
|
|
|
15152 |
return;
|
|
|
15153 |
}
|
|
|
15154 |
const playProgressBar = seekBar.getChild('playProgressBar');
|
|
|
15155 |
const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
|
|
|
15156 |
if (!playProgressBar && !mouseTimeDisplay) {
|
|
|
15157 |
return;
|
|
|
15158 |
}
|
|
|
15159 |
const seekBarEl = seekBar.el();
|
|
|
15160 |
const seekBarRect = findPosition(seekBarEl);
|
|
|
15161 |
let seekBarPoint = getPointerPosition(seekBarEl, event).x;
|
|
|
15162 |
|
|
|
15163 |
// The default skin has a gap on either side of the `SeekBar`. This means
|
|
|
15164 |
// that it's possible to trigger this behavior outside the boundaries of
|
|
|
15165 |
// the `SeekBar`. This ensures we stay within it at all times.
|
|
|
15166 |
seekBarPoint = clamp(seekBarPoint, 0, 1);
|
|
|
15167 |
if (mouseTimeDisplay) {
|
|
|
15168 |
mouseTimeDisplay.update(seekBarRect, seekBarPoint);
|
|
|
15169 |
}
|
|
|
15170 |
if (playProgressBar) {
|
|
|
15171 |
playProgressBar.update(seekBarRect, seekBar.getProgress());
|
|
|
15172 |
}
|
|
|
15173 |
}
|
|
|
15174 |
|
|
|
15175 |
/**
|
|
|
15176 |
* A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
|
|
|
15177 |
*
|
|
|
15178 |
* @method ProgressControl#throttledHandleMouseSeek
|
|
|
15179 |
* @param {Event} event
|
|
|
15180 |
* The `mousemove` event that caused this function to run.
|
|
|
15181 |
*
|
|
|
15182 |
* @listen mousemove
|
|
|
15183 |
* @listen touchmove
|
|
|
15184 |
*/
|
|
|
15185 |
|
|
|
15186 |
/**
|
|
|
15187 |
* Handle `mousemove` or `touchmove` events on the `ProgressControl`.
|
|
|
15188 |
*
|
|
|
15189 |
* @param {Event} event
|
|
|
15190 |
* `mousedown` or `touchstart` event that triggered this function
|
|
|
15191 |
*
|
|
|
15192 |
* @listens mousemove
|
|
|
15193 |
* @listens touchmove
|
|
|
15194 |
*/
|
|
|
15195 |
handleMouseSeek(event) {
|
|
|
15196 |
const seekBar = this.getChild('seekBar');
|
|
|
15197 |
if (seekBar) {
|
|
|
15198 |
seekBar.handleMouseMove(event);
|
|
|
15199 |
}
|
|
|
15200 |
}
|
|
|
15201 |
|
|
|
15202 |
/**
|
|
|
15203 |
* Are controls are currently enabled for this progress control.
|
|
|
15204 |
*
|
|
|
15205 |
* @return {boolean}
|
|
|
15206 |
* true if controls are enabled, false otherwise
|
|
|
15207 |
*/
|
|
|
15208 |
enabled() {
|
|
|
15209 |
return this.enabled_;
|
|
|
15210 |
}
|
|
|
15211 |
|
|
|
15212 |
/**
|
|
|
15213 |
* Disable all controls on the progress control and its children
|
|
|
15214 |
*/
|
|
|
15215 |
disable() {
|
|
|
15216 |
this.children().forEach(child => child.disable && child.disable());
|
|
|
15217 |
if (!this.enabled()) {
|
|
|
15218 |
return;
|
|
|
15219 |
}
|
|
|
15220 |
this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
|
|
|
15221 |
this.off(this.el_, 'mousemove', this.handleMouseMove);
|
|
|
15222 |
this.removeListenersAddedOnMousedownAndTouchstart();
|
|
|
15223 |
this.addClass('disabled');
|
|
|
15224 |
this.enabled_ = false;
|
|
|
15225 |
|
|
|
15226 |
// Restore normal playback state if controls are disabled while scrubbing
|
|
|
15227 |
if (this.player_.scrubbing()) {
|
|
|
15228 |
const seekBar = this.getChild('seekBar');
|
|
|
15229 |
this.player_.scrubbing(false);
|
|
|
15230 |
if (seekBar.videoWasPlaying) {
|
|
|
15231 |
silencePromise(this.player_.play());
|
|
|
15232 |
}
|
|
|
15233 |
}
|
|
|
15234 |
}
|
|
|
15235 |
|
|
|
15236 |
/**
|
|
|
15237 |
* Enable all controls on the progress control and its children
|
|
|
15238 |
*/
|
|
|
15239 |
enable() {
|
|
|
15240 |
this.children().forEach(child => child.enable && child.enable());
|
|
|
15241 |
if (this.enabled()) {
|
|
|
15242 |
return;
|
|
|
15243 |
}
|
|
|
15244 |
this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
|
|
|
15245 |
this.on(this.el_, 'mousemove', this.handleMouseMove);
|
|
|
15246 |
this.removeClass('disabled');
|
|
|
15247 |
this.enabled_ = true;
|
|
|
15248 |
}
|
|
|
15249 |
|
|
|
15250 |
/**
|
|
|
15251 |
* Cleanup listeners after the user finishes interacting with the progress controls
|
|
|
15252 |
*/
|
|
|
15253 |
removeListenersAddedOnMousedownAndTouchstart() {
|
|
|
15254 |
const doc = this.el_.ownerDocument;
|
|
|
15255 |
this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
|
|
|
15256 |
this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
|
|
|
15257 |
this.off(doc, 'mouseup', this.handleMouseUpHandler_);
|
|
|
15258 |
this.off(doc, 'touchend', this.handleMouseUpHandler_);
|
|
|
15259 |
}
|
|
|
15260 |
|
|
|
15261 |
/**
|
|
|
15262 |
* Handle `mousedown` or `touchstart` events on the `ProgressControl`.
|
|
|
15263 |
*
|
|
|
15264 |
* @param {Event} event
|
|
|
15265 |
* `mousedown` or `touchstart` event that triggered this function
|
|
|
15266 |
*
|
|
|
15267 |
* @listens mousedown
|
|
|
15268 |
* @listens touchstart
|
|
|
15269 |
*/
|
|
|
15270 |
handleMouseDown(event) {
|
|
|
15271 |
const doc = this.el_.ownerDocument;
|
|
|
15272 |
const seekBar = this.getChild('seekBar');
|
|
|
15273 |
if (seekBar) {
|
|
|
15274 |
seekBar.handleMouseDown(event);
|
|
|
15275 |
}
|
|
|
15276 |
this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
|
|
|
15277 |
this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
|
|
|
15278 |
this.on(doc, 'mouseup', this.handleMouseUpHandler_);
|
|
|
15279 |
this.on(doc, 'touchend', this.handleMouseUpHandler_);
|
|
|
15280 |
}
|
|
|
15281 |
|
|
|
15282 |
/**
|
|
|
15283 |
* Handle `mouseup` or `touchend` events on the `ProgressControl`.
|
|
|
15284 |
*
|
|
|
15285 |
* @param {Event} event
|
|
|
15286 |
* `mouseup` or `touchend` event that triggered this function.
|
|
|
15287 |
*
|
|
|
15288 |
* @listens touchend
|
|
|
15289 |
* @listens mouseup
|
|
|
15290 |
*/
|
|
|
15291 |
handleMouseUp(event) {
|
|
|
15292 |
const seekBar = this.getChild('seekBar');
|
|
|
15293 |
if (seekBar) {
|
|
|
15294 |
seekBar.handleMouseUp(event);
|
|
|
15295 |
}
|
|
|
15296 |
this.removeListenersAddedOnMousedownAndTouchstart();
|
|
|
15297 |
}
|
|
|
15298 |
}
|
|
|
15299 |
|
|
|
15300 |
/**
|
|
|
15301 |
* Default options for `ProgressControl`
|
|
|
15302 |
*
|
|
|
15303 |
* @type {Object}
|
|
|
15304 |
* @private
|
|
|
15305 |
*/
|
|
|
15306 |
ProgressControl.prototype.options_ = {
|
|
|
15307 |
children: ['seekBar']
|
|
|
15308 |
};
|
|
|
15309 |
Component$1.registerComponent('ProgressControl', ProgressControl);
|
|
|
15310 |
|
|
|
15311 |
/**
|
|
|
15312 |
* @file picture-in-picture-toggle.js
|
|
|
15313 |
*/
|
|
|
15314 |
|
|
|
15315 |
/**
|
|
|
15316 |
* Toggle Picture-in-Picture mode
|
|
|
15317 |
*
|
|
|
15318 |
* @extends Button
|
|
|
15319 |
*/
|
|
|
15320 |
class PictureInPictureToggle extends Button {
|
|
|
15321 |
/**
|
|
|
15322 |
* Creates an instance of this class.
|
|
|
15323 |
*
|
|
|
15324 |
* @param { import('./player').default } player
|
|
|
15325 |
* The `Player` that this class should be attached to.
|
|
|
15326 |
*
|
|
|
15327 |
* @param {Object} [options]
|
|
|
15328 |
* The key/value store of player options.
|
|
|
15329 |
*
|
|
|
15330 |
* @listens Player#enterpictureinpicture
|
|
|
15331 |
* @listens Player#leavepictureinpicture
|
|
|
15332 |
*/
|
|
|
15333 |
constructor(player, options) {
|
|
|
15334 |
super(player, options);
|
|
|
15335 |
this.setIcon('picture-in-picture-enter');
|
|
|
15336 |
this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
|
|
|
15337 |
this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
|
|
|
15338 |
this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange());
|
|
|
15339 |
|
|
|
15340 |
// TODO: Deactivate button on player emptied event.
|
|
|
15341 |
this.disable();
|
|
|
15342 |
}
|
|
|
15343 |
|
|
|
15344 |
/**
|
|
|
15345 |
* Builds the default DOM `className`.
|
|
|
15346 |
*
|
|
|
15347 |
* @return {string}
|
|
|
15348 |
* The DOM `className` for this object.
|
|
|
15349 |
*/
|
|
|
15350 |
buildCSSClass() {
|
|
|
15351 |
return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`;
|
|
|
15352 |
}
|
|
|
15353 |
|
|
|
15354 |
/**
|
|
|
15355 |
* Displays or hides the button depending on the audio mode detection.
|
|
|
15356 |
* Exits picture-in-picture if it is enabled when switching to audio mode.
|
|
|
15357 |
*/
|
|
|
15358 |
handlePictureInPictureAudioModeChange() {
|
|
|
15359 |
// This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
|
|
|
15360 |
const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio';
|
|
|
15361 |
const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode();
|
|
|
15362 |
if (!isAudioMode) {
|
|
|
15363 |
this.show();
|
|
|
15364 |
return;
|
|
|
15365 |
}
|
|
|
15366 |
if (this.player_.isInPictureInPicture()) {
|
|
|
15367 |
this.player_.exitPictureInPicture();
|
|
|
15368 |
}
|
|
|
15369 |
this.hide();
|
|
|
15370 |
}
|
|
|
15371 |
|
|
|
15372 |
/**
|
|
|
15373 |
* Enables or disables button based on availability of a Picture-In-Picture mode.
|
|
|
15374 |
*
|
|
|
15375 |
* Enabled if
|
|
|
15376 |
* - `player.options().enableDocumentPictureInPicture` is true and
|
|
|
15377 |
* window.documentPictureInPicture is available; or
|
|
|
15378 |
* - `player.disablePictureInPicture()` is false and
|
|
|
15379 |
* element.requestPictureInPicture is available
|
|
|
15380 |
*/
|
|
|
15381 |
handlePictureInPictureEnabledChange() {
|
|
|
15382 |
if (document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window) {
|
|
|
15383 |
this.enable();
|
|
|
15384 |
} else {
|
|
|
15385 |
this.disable();
|
|
|
15386 |
}
|
|
|
15387 |
}
|
|
|
15388 |
|
|
|
15389 |
/**
|
|
|
15390 |
* Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
|
|
|
15391 |
*
|
|
|
15392 |
* @param {Event} [event]
|
|
|
15393 |
* The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
|
|
|
15394 |
* called.
|
|
|
15395 |
*
|
|
|
15396 |
* @listens Player#enterpictureinpicture
|
|
|
15397 |
* @listens Player#leavepictureinpicture
|
|
|
15398 |
*/
|
|
|
15399 |
handlePictureInPictureChange(event) {
|
|
|
15400 |
if (this.player_.isInPictureInPicture()) {
|
|
|
15401 |
this.setIcon('picture-in-picture-exit');
|
|
|
15402 |
this.controlText('Exit Picture-in-Picture');
|
|
|
15403 |
} else {
|
|
|
15404 |
this.setIcon('picture-in-picture-enter');
|
|
|
15405 |
this.controlText('Picture-in-Picture');
|
|
|
15406 |
}
|
|
|
15407 |
this.handlePictureInPictureEnabledChange();
|
|
|
15408 |
}
|
|
|
15409 |
|
|
|
15410 |
/**
|
|
|
15411 |
* This gets called when an `PictureInPictureToggle` is "clicked". See
|
|
|
15412 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
15413 |
*
|
|
|
15414 |
* @param {Event} [event]
|
|
|
15415 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
15416 |
* called.
|
|
|
15417 |
*
|
|
|
15418 |
* @listens tap
|
|
|
15419 |
* @listens click
|
|
|
15420 |
*/
|
|
|
15421 |
handleClick(event) {
|
|
|
15422 |
if (!this.player_.isInPictureInPicture()) {
|
|
|
15423 |
this.player_.requestPictureInPicture();
|
|
|
15424 |
} else {
|
|
|
15425 |
this.player_.exitPictureInPicture();
|
|
|
15426 |
}
|
|
|
15427 |
}
|
|
|
15428 |
|
|
|
15429 |
/**
|
|
|
15430 |
* Show the `Component`s element if it is hidden by removing the
|
|
|
15431 |
* 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API.
|
|
|
15432 |
*/
|
|
|
15433 |
show() {
|
|
|
15434 |
// Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox.
|
|
|
15435 |
if (typeof document.exitPictureInPicture !== 'function') {
|
|
|
15436 |
return;
|
|
|
15437 |
}
|
|
|
15438 |
super.show();
|
|
|
15439 |
}
|
|
|
15440 |
}
|
|
|
15441 |
|
|
|
15442 |
/**
|
|
|
15443 |
* The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
|
|
|
15444 |
*
|
|
|
15445 |
* @type {string}
|
|
|
15446 |
* @protected
|
|
|
15447 |
*/
|
|
|
15448 |
PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
|
|
|
15449 |
Component$1.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
|
|
|
15450 |
|
|
|
15451 |
/**
|
|
|
15452 |
* @file fullscreen-toggle.js
|
|
|
15453 |
*/
|
|
|
15454 |
|
|
|
15455 |
/**
|
|
|
15456 |
* Toggle fullscreen video
|
|
|
15457 |
*
|
|
|
15458 |
* @extends Button
|
|
|
15459 |
*/
|
|
|
15460 |
class FullscreenToggle extends Button {
|
|
|
15461 |
/**
|
|
|
15462 |
* Creates an instance of this class.
|
|
|
15463 |
*
|
|
|
15464 |
* @param { import('./player').default } player
|
|
|
15465 |
* The `Player` that this class should be attached to.
|
|
|
15466 |
*
|
|
|
15467 |
* @param {Object} [options]
|
|
|
15468 |
* The key/value store of player options.
|
|
|
15469 |
*/
|
|
|
15470 |
constructor(player, options) {
|
|
|
15471 |
super(player, options);
|
|
|
15472 |
this.setIcon('fullscreen-enter');
|
|
|
15473 |
this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
|
|
|
15474 |
if (document[player.fsApi_.fullscreenEnabled] === false) {
|
|
|
15475 |
this.disable();
|
|
|
15476 |
}
|
|
|
15477 |
}
|
|
|
15478 |
|
|
|
15479 |
/**
|
|
|
15480 |
* Builds the default DOM `className`.
|
|
|
15481 |
*
|
|
|
15482 |
* @return {string}
|
|
|
15483 |
* The DOM `className` for this object.
|
|
|
15484 |
*/
|
|
|
15485 |
buildCSSClass() {
|
|
|
15486 |
return `vjs-fullscreen-control ${super.buildCSSClass()}`;
|
|
|
15487 |
}
|
|
|
15488 |
|
|
|
15489 |
/**
|
|
|
15490 |
* Handles fullscreenchange on the player and change control text accordingly.
|
|
|
15491 |
*
|
|
|
15492 |
* @param {Event} [event]
|
|
|
15493 |
* The {@link Player#fullscreenchange} event that caused this function to be
|
|
|
15494 |
* called.
|
|
|
15495 |
*
|
|
|
15496 |
* @listens Player#fullscreenchange
|
|
|
15497 |
*/
|
|
|
15498 |
handleFullscreenChange(event) {
|
|
|
15499 |
if (this.player_.isFullscreen()) {
|
|
|
15500 |
this.controlText('Exit Fullscreen');
|
|
|
15501 |
this.setIcon('fullscreen-exit');
|
|
|
15502 |
} else {
|
|
|
15503 |
this.controlText('Fullscreen');
|
|
|
15504 |
this.setIcon('fullscreen-enter');
|
|
|
15505 |
}
|
|
|
15506 |
}
|
|
|
15507 |
|
|
|
15508 |
/**
|
|
|
15509 |
* This gets called when an `FullscreenToggle` is "clicked". See
|
|
|
15510 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
15511 |
*
|
|
|
15512 |
* @param {Event} [event]
|
|
|
15513 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
15514 |
* called.
|
|
|
15515 |
*
|
|
|
15516 |
* @listens tap
|
|
|
15517 |
* @listens click
|
|
|
15518 |
*/
|
|
|
15519 |
handleClick(event) {
|
|
|
15520 |
if (!this.player_.isFullscreen()) {
|
|
|
15521 |
this.player_.requestFullscreen();
|
|
|
15522 |
} else {
|
|
|
15523 |
this.player_.exitFullscreen();
|
|
|
15524 |
}
|
|
|
15525 |
}
|
|
|
15526 |
}
|
|
|
15527 |
|
|
|
15528 |
/**
|
|
|
15529 |
* The text that should display over the `FullscreenToggle`s controls. Added for localization.
|
|
|
15530 |
*
|
|
|
15531 |
* @type {string}
|
|
|
15532 |
* @protected
|
|
|
15533 |
*/
|
|
|
15534 |
FullscreenToggle.prototype.controlText_ = 'Fullscreen';
|
|
|
15535 |
Component$1.registerComponent('FullscreenToggle', FullscreenToggle);
|
|
|
15536 |
|
|
|
15537 |
/**
|
|
|
15538 |
* Check if volume control is supported and if it isn't hide the
|
|
|
15539 |
* `Component` that was passed using the `vjs-hidden` class.
|
|
|
15540 |
*
|
|
|
15541 |
* @param { import('../../component').default } self
|
|
|
15542 |
* The component that should be hidden if volume is unsupported
|
|
|
15543 |
*
|
|
|
15544 |
* @param { import('../../player').default } player
|
|
|
15545 |
* A reference to the player
|
|
|
15546 |
*
|
|
|
15547 |
* @private
|
|
|
15548 |
*/
|
|
|
15549 |
const checkVolumeSupport = function (self, player) {
|
|
|
15550 |
// hide volume controls when they're not supported by the current tech
|
|
|
15551 |
if (player.tech_ && !player.tech_.featuresVolumeControl) {
|
|
|
15552 |
self.addClass('vjs-hidden');
|
|
|
15553 |
}
|
|
|
15554 |
self.on(player, 'loadstart', function () {
|
|
|
15555 |
if (!player.tech_.featuresVolumeControl) {
|
|
|
15556 |
self.addClass('vjs-hidden');
|
|
|
15557 |
} else {
|
|
|
15558 |
self.removeClass('vjs-hidden');
|
|
|
15559 |
}
|
|
|
15560 |
});
|
|
|
15561 |
};
|
|
|
15562 |
|
|
|
15563 |
/**
|
|
|
15564 |
* @file volume-level.js
|
|
|
15565 |
*/
|
|
|
15566 |
|
|
|
15567 |
/**
|
|
|
15568 |
* Shows volume level
|
|
|
15569 |
*
|
|
|
15570 |
* @extends Component
|
|
|
15571 |
*/
|
|
|
15572 |
class VolumeLevel extends Component$1 {
|
|
|
15573 |
/**
|
|
|
15574 |
* Create the `Component`'s DOM element
|
|
|
15575 |
*
|
|
|
15576 |
* @return {Element}
|
|
|
15577 |
* The element that was created.
|
|
|
15578 |
*/
|
|
|
15579 |
createEl() {
|
|
|
15580 |
const el = super.createEl('div', {
|
|
|
15581 |
className: 'vjs-volume-level'
|
|
|
15582 |
});
|
|
|
15583 |
this.setIcon('circle', el);
|
|
|
15584 |
el.appendChild(super.createEl('span', {
|
|
|
15585 |
className: 'vjs-control-text'
|
|
|
15586 |
}));
|
|
|
15587 |
return el;
|
|
|
15588 |
}
|
|
|
15589 |
}
|
|
|
15590 |
Component$1.registerComponent('VolumeLevel', VolumeLevel);
|
|
|
15591 |
|
|
|
15592 |
/**
|
|
|
15593 |
* @file volume-level-tooltip.js
|
|
|
15594 |
*/
|
|
|
15595 |
|
|
|
15596 |
/**
|
|
|
15597 |
* Volume level tooltips display a volume above or side by side the volume bar.
|
|
|
15598 |
*
|
|
|
15599 |
* @extends Component
|
|
|
15600 |
*/
|
|
|
15601 |
class VolumeLevelTooltip extends Component$1 {
|
|
|
15602 |
/**
|
|
|
15603 |
* Creates an instance of this class.
|
|
|
15604 |
*
|
|
|
15605 |
* @param { import('../../player').default } player
|
|
|
15606 |
* The {@link Player} that this class should be attached to.
|
|
|
15607 |
*
|
|
|
15608 |
* @param {Object} [options]
|
|
|
15609 |
* The key/value store of player options.
|
|
|
15610 |
*/
|
|
|
15611 |
constructor(player, options) {
|
|
|
15612 |
super(player, options);
|
|
|
15613 |
this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
|
|
|
15614 |
}
|
|
|
15615 |
|
|
|
15616 |
/**
|
|
|
15617 |
* Create the volume tooltip DOM element
|
|
|
15618 |
*
|
|
|
15619 |
* @return {Element}
|
|
|
15620 |
* The element that was created.
|
|
|
15621 |
*/
|
|
|
15622 |
createEl() {
|
|
|
15623 |
return super.createEl('div', {
|
|
|
15624 |
className: 'vjs-volume-tooltip'
|
|
|
15625 |
}, {
|
|
|
15626 |
'aria-hidden': 'true'
|
|
|
15627 |
});
|
|
|
15628 |
}
|
|
|
15629 |
|
|
|
15630 |
/**
|
|
|
15631 |
* Updates the position of the tooltip relative to the `VolumeBar` and
|
|
|
15632 |
* its content text.
|
|
|
15633 |
*
|
|
|
15634 |
* @param {Object} rangeBarRect
|
|
|
15635 |
* The `ClientRect` for the {@link VolumeBar} element.
|
|
|
15636 |
*
|
|
|
15637 |
* @param {number} rangeBarPoint
|
|
|
15638 |
* A number from 0 to 1, representing a horizontal/vertical reference point
|
|
|
15639 |
* from the left edge of the {@link VolumeBar}
|
|
|
15640 |
*
|
|
|
15641 |
* @param {boolean} vertical
|
|
|
15642 |
* Referees to the Volume control position
|
|
|
15643 |
* in the control bar{@link VolumeControl}
|
|
|
15644 |
*
|
|
|
15645 |
*/
|
|
|
15646 |
update(rangeBarRect, rangeBarPoint, vertical, content) {
|
|
|
15647 |
if (!vertical) {
|
|
|
15648 |
const tooltipRect = getBoundingClientRect(this.el_);
|
|
|
15649 |
const playerRect = getBoundingClientRect(this.player_.el());
|
|
|
15650 |
const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
|
|
|
15651 |
if (!playerRect || !tooltipRect) {
|
|
|
15652 |
return;
|
|
|
15653 |
}
|
|
|
15654 |
const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
|
|
|
15655 |
const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
|
|
|
15656 |
let pullTooltipBy = tooltipRect.width / 2;
|
|
|
15657 |
if (spaceLeftOfPoint < pullTooltipBy) {
|
|
|
15658 |
pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
|
|
|
15659 |
} else if (spaceRightOfPoint < pullTooltipBy) {
|
|
|
15660 |
pullTooltipBy = spaceRightOfPoint;
|
|
|
15661 |
}
|
|
|
15662 |
if (pullTooltipBy < 0) {
|
|
|
15663 |
pullTooltipBy = 0;
|
|
|
15664 |
} else if (pullTooltipBy > tooltipRect.width) {
|
|
|
15665 |
pullTooltipBy = tooltipRect.width;
|
|
|
15666 |
}
|
|
|
15667 |
this.el_.style.right = `-${pullTooltipBy}px`;
|
|
|
15668 |
}
|
|
|
15669 |
this.write(`${content}%`);
|
|
|
15670 |
}
|
|
|
15671 |
|
|
|
15672 |
/**
|
|
|
15673 |
* Write the volume to the tooltip DOM element.
|
|
|
15674 |
*
|
|
|
15675 |
* @param {string} content
|
|
|
15676 |
* The formatted volume for the tooltip.
|
|
|
15677 |
*/
|
|
|
15678 |
write(content) {
|
|
|
15679 |
textContent(this.el_, content);
|
|
|
15680 |
}
|
|
|
15681 |
|
|
|
15682 |
/**
|
|
|
15683 |
* Updates the position of the volume tooltip relative to the `VolumeBar`.
|
|
|
15684 |
*
|
|
|
15685 |
* @param {Object} rangeBarRect
|
|
|
15686 |
* The `ClientRect` for the {@link VolumeBar} element.
|
|
|
15687 |
*
|
|
|
15688 |
* @param {number} rangeBarPoint
|
|
|
15689 |
* A number from 0 to 1, representing a horizontal/vertical reference point
|
|
|
15690 |
* from the left edge of the {@link VolumeBar}
|
|
|
15691 |
*
|
|
|
15692 |
* @param {boolean} vertical
|
|
|
15693 |
* Referees to the Volume control position
|
|
|
15694 |
* in the control bar{@link VolumeControl}
|
|
|
15695 |
*
|
|
|
15696 |
* @param {number} volume
|
|
|
15697 |
* The volume level to update the tooltip to
|
|
|
15698 |
*
|
|
|
15699 |
* @param {Function} cb
|
|
|
15700 |
* A function that will be called during the request animation frame
|
|
|
15701 |
* for tooltips that need to do additional animations from the default
|
|
|
15702 |
*/
|
|
|
15703 |
updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
|
|
|
15704 |
this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
|
|
|
15705 |
this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
|
|
|
15706 |
if (cb) {
|
|
|
15707 |
cb();
|
|
|
15708 |
}
|
|
|
15709 |
});
|
|
|
15710 |
}
|
|
|
15711 |
}
|
|
|
15712 |
Component$1.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
|
|
|
15713 |
|
|
|
15714 |
/**
|
|
|
15715 |
* @file mouse-volume-level-display.js
|
|
|
15716 |
*/
|
|
|
15717 |
|
|
|
15718 |
/**
|
|
|
15719 |
* The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
|
|
|
15720 |
* {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
|
|
|
15721 |
* indicating the volume level which is represented by a given point in the
|
|
|
15722 |
* {@link VolumeBar}.
|
|
|
15723 |
*
|
|
|
15724 |
* @extends Component
|
|
|
15725 |
*/
|
|
|
15726 |
class MouseVolumeLevelDisplay extends Component$1 {
|
|
|
15727 |
/**
|
|
|
15728 |
* Creates an instance of this class.
|
|
|
15729 |
*
|
|
|
15730 |
* @param { import('../../player').default } player
|
|
|
15731 |
* The {@link Player} that this class should be attached to.
|
|
|
15732 |
*
|
|
|
15733 |
* @param {Object} [options]
|
|
|
15734 |
* The key/value store of player options.
|
|
|
15735 |
*/
|
|
|
15736 |
constructor(player, options) {
|
|
|
15737 |
super(player, options);
|
|
|
15738 |
this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
|
|
|
15739 |
}
|
|
|
15740 |
|
|
|
15741 |
/**
|
|
|
15742 |
* Create the DOM element for this class.
|
|
|
15743 |
*
|
|
|
15744 |
* @return {Element}
|
|
|
15745 |
* The element that was created.
|
|
|
15746 |
*/
|
|
|
15747 |
createEl() {
|
|
|
15748 |
return super.createEl('div', {
|
|
|
15749 |
className: 'vjs-mouse-display'
|
|
|
15750 |
});
|
|
|
15751 |
}
|
|
|
15752 |
|
|
|
15753 |
/**
|
|
|
15754 |
* Enquires updates to its own DOM as well as the DOM of its
|
|
|
15755 |
* {@link VolumeLevelTooltip} child.
|
|
|
15756 |
*
|
|
|
15757 |
* @param {Object} rangeBarRect
|
|
|
15758 |
* The `ClientRect` for the {@link VolumeBar} element.
|
|
|
15759 |
*
|
|
|
15760 |
* @param {number} rangeBarPoint
|
|
|
15761 |
* A number from 0 to 1, representing a horizontal/vertical reference point
|
|
|
15762 |
* from the left edge of the {@link VolumeBar}
|
|
|
15763 |
*
|
|
|
15764 |
* @param {boolean} vertical
|
|
|
15765 |
* Referees to the Volume control position
|
|
|
15766 |
* in the control bar{@link VolumeControl}
|
|
|
15767 |
*
|
|
|
15768 |
*/
|
|
|
15769 |
update(rangeBarRect, rangeBarPoint, vertical) {
|
|
|
15770 |
const volume = 100 * rangeBarPoint;
|
|
|
15771 |
this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
|
|
|
15772 |
if (vertical) {
|
|
|
15773 |
this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
|
|
|
15774 |
} else {
|
|
|
15775 |
this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
|
|
|
15776 |
}
|
|
|
15777 |
});
|
|
|
15778 |
}
|
|
|
15779 |
}
|
|
|
15780 |
|
|
|
15781 |
/**
|
|
|
15782 |
* Default options for `MouseVolumeLevelDisplay`
|
|
|
15783 |
*
|
|
|
15784 |
* @type {Object}
|
|
|
15785 |
* @private
|
|
|
15786 |
*/
|
|
|
15787 |
MouseVolumeLevelDisplay.prototype.options_ = {
|
|
|
15788 |
children: ['volumeLevelTooltip']
|
|
|
15789 |
};
|
|
|
15790 |
Component$1.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
|
|
|
15791 |
|
|
|
15792 |
/**
|
|
|
15793 |
* @file volume-bar.js
|
|
|
15794 |
*/
|
|
|
15795 |
|
|
|
15796 |
/**
|
|
|
15797 |
* The bar that contains the volume level and can be clicked on to adjust the level
|
|
|
15798 |
*
|
|
|
15799 |
* @extends Slider
|
|
|
15800 |
*/
|
|
|
15801 |
class VolumeBar extends Slider {
|
|
|
15802 |
/**
|
|
|
15803 |
* Creates an instance of this class.
|
|
|
15804 |
*
|
|
|
15805 |
* @param { import('../../player').default } player
|
|
|
15806 |
* The `Player` that this class should be attached to.
|
|
|
15807 |
*
|
|
|
15808 |
* @param {Object} [options]
|
|
|
15809 |
* The key/value store of player options.
|
|
|
15810 |
*/
|
|
|
15811 |
constructor(player, options) {
|
|
|
15812 |
super(player, options);
|
|
|
15813 |
this.on('slideractive', e => this.updateLastVolume_(e));
|
|
|
15814 |
this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
|
|
|
15815 |
player.ready(() => this.updateARIAAttributes());
|
|
|
15816 |
}
|
|
|
15817 |
|
|
|
15818 |
/**
|
|
|
15819 |
* Create the `Component`'s DOM element
|
|
|
15820 |
*
|
|
|
15821 |
* @return {Element}
|
|
|
15822 |
* The element that was created.
|
|
|
15823 |
*/
|
|
|
15824 |
createEl() {
|
|
|
15825 |
return super.createEl('div', {
|
|
|
15826 |
className: 'vjs-volume-bar vjs-slider-bar'
|
|
|
15827 |
}, {
|
|
|
15828 |
'aria-label': this.localize('Volume Level'),
|
|
|
15829 |
'aria-live': 'polite'
|
|
|
15830 |
});
|
|
|
15831 |
}
|
|
|
15832 |
|
|
|
15833 |
/**
|
|
|
15834 |
* Handle mouse down on volume bar
|
|
|
15835 |
*
|
|
|
15836 |
* @param {Event} event
|
|
|
15837 |
* The `mousedown` event that caused this to run.
|
|
|
15838 |
*
|
|
|
15839 |
* @listens mousedown
|
|
|
15840 |
*/
|
|
|
15841 |
handleMouseDown(event) {
|
|
|
15842 |
if (!isSingleLeftClick(event)) {
|
|
|
15843 |
return;
|
|
|
15844 |
}
|
|
|
15845 |
super.handleMouseDown(event);
|
|
|
15846 |
}
|
|
|
15847 |
|
|
|
15848 |
/**
|
|
|
15849 |
* Handle movement events on the {@link VolumeMenuButton}.
|
|
|
15850 |
*
|
|
|
15851 |
* @param {Event} event
|
|
|
15852 |
* The event that caused this function to run.
|
|
|
15853 |
*
|
|
|
15854 |
* @listens mousemove
|
|
|
15855 |
*/
|
|
|
15856 |
handleMouseMove(event) {
|
|
|
15857 |
const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
|
|
|
15858 |
if (mouseVolumeLevelDisplay) {
|
|
|
15859 |
const volumeBarEl = this.el();
|
|
|
15860 |
const volumeBarRect = getBoundingClientRect(volumeBarEl);
|
|
|
15861 |
const vertical = this.vertical();
|
|
|
15862 |
let volumeBarPoint = getPointerPosition(volumeBarEl, event);
|
|
|
15863 |
volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
|
|
|
15864 |
// The default skin has a gap on either side of the `VolumeBar`. This means
|
|
|
15865 |
// that it's possible to trigger this behavior outside the boundaries of
|
|
|
15866 |
// the `VolumeBar`. This ensures we stay within it at all times.
|
|
|
15867 |
volumeBarPoint = clamp(volumeBarPoint, 0, 1);
|
|
|
15868 |
mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
|
|
|
15869 |
}
|
|
|
15870 |
if (!isSingleLeftClick(event)) {
|
|
|
15871 |
return;
|
|
|
15872 |
}
|
|
|
15873 |
this.checkMuted();
|
|
|
15874 |
this.player_.volume(this.calculateDistance(event));
|
|
|
15875 |
}
|
|
|
15876 |
|
|
|
15877 |
/**
|
|
|
15878 |
* If the player is muted unmute it.
|
|
|
15879 |
*/
|
|
|
15880 |
checkMuted() {
|
|
|
15881 |
if (this.player_.muted()) {
|
|
|
15882 |
this.player_.muted(false);
|
|
|
15883 |
}
|
|
|
15884 |
}
|
|
|
15885 |
|
|
|
15886 |
/**
|
|
|
15887 |
* Get percent of volume level
|
|
|
15888 |
*
|
|
|
15889 |
* @return {number}
|
|
|
15890 |
* Volume level percent as a decimal number.
|
|
|
15891 |
*/
|
|
|
15892 |
getPercent() {
|
|
|
15893 |
if (this.player_.muted()) {
|
|
|
15894 |
return 0;
|
|
|
15895 |
}
|
|
|
15896 |
return this.player_.volume();
|
|
|
15897 |
}
|
|
|
15898 |
|
|
|
15899 |
/**
|
|
|
15900 |
* Increase volume level for keyboard users
|
|
|
15901 |
*/
|
|
|
15902 |
stepForward() {
|
|
|
15903 |
this.checkMuted();
|
|
|
15904 |
this.player_.volume(this.player_.volume() + 0.1);
|
|
|
15905 |
}
|
|
|
15906 |
|
|
|
15907 |
/**
|
|
|
15908 |
* Decrease volume level for keyboard users
|
|
|
15909 |
*/
|
|
|
15910 |
stepBack() {
|
|
|
15911 |
this.checkMuted();
|
|
|
15912 |
this.player_.volume(this.player_.volume() - 0.1);
|
|
|
15913 |
}
|
|
|
15914 |
|
|
|
15915 |
/**
|
|
|
15916 |
* Update ARIA accessibility attributes
|
|
|
15917 |
*
|
|
|
15918 |
* @param {Event} [event]
|
|
|
15919 |
* The `volumechange` event that caused this function to run.
|
|
|
15920 |
*
|
|
|
15921 |
* @listens Player#volumechange
|
|
|
15922 |
*/
|
|
|
15923 |
updateARIAAttributes(event) {
|
|
|
15924 |
const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
|
|
|
15925 |
this.el_.setAttribute('aria-valuenow', ariaValue);
|
|
|
15926 |
this.el_.setAttribute('aria-valuetext', ariaValue + '%');
|
|
|
15927 |
}
|
|
|
15928 |
|
|
|
15929 |
/**
|
|
|
15930 |
* Returns the current value of the player volume as a percentage
|
|
|
15931 |
*
|
|
|
15932 |
* @private
|
|
|
15933 |
*/
|
|
|
15934 |
volumeAsPercentage_() {
|
|
|
15935 |
return Math.round(this.player_.volume() * 100);
|
|
|
15936 |
}
|
|
|
15937 |
|
|
|
15938 |
/**
|
|
|
15939 |
* When user starts dragging the VolumeBar, store the volume and listen for
|
|
|
15940 |
* the end of the drag. When the drag ends, if the volume was set to zero,
|
|
|
15941 |
* set lastVolume to the stored volume.
|
|
|
15942 |
*
|
|
|
15943 |
* @listens slideractive
|
|
|
15944 |
* @private
|
|
|
15945 |
*/
|
|
|
15946 |
updateLastVolume_() {
|
|
|
15947 |
const volumeBeforeDrag = this.player_.volume();
|
|
|
15948 |
this.one('sliderinactive', () => {
|
|
|
15949 |
if (this.player_.volume() === 0) {
|
|
|
15950 |
this.player_.lastVolume_(volumeBeforeDrag);
|
|
|
15951 |
}
|
|
|
15952 |
});
|
|
|
15953 |
}
|
|
|
15954 |
}
|
|
|
15955 |
|
|
|
15956 |
/**
|
|
|
15957 |
* Default options for the `VolumeBar`
|
|
|
15958 |
*
|
|
|
15959 |
* @type {Object}
|
|
|
15960 |
* @private
|
|
|
15961 |
*/
|
|
|
15962 |
VolumeBar.prototype.options_ = {
|
|
|
15963 |
children: ['volumeLevel'],
|
|
|
15964 |
barName: 'volumeLevel'
|
|
|
15965 |
};
|
|
|
15966 |
|
|
|
15967 |
// MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
|
|
|
15968 |
if (!IS_IOS && !IS_ANDROID) {
|
|
|
15969 |
VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
|
|
|
15970 |
}
|
|
|
15971 |
|
|
|
15972 |
/**
|
|
|
15973 |
* Call the update event for this Slider when this event happens on the player.
|
|
|
15974 |
*
|
|
|
15975 |
* @type {string}
|
|
|
15976 |
*/
|
|
|
15977 |
VolumeBar.prototype.playerEvent = 'volumechange';
|
|
|
15978 |
Component$1.registerComponent('VolumeBar', VolumeBar);
|
|
|
15979 |
|
|
|
15980 |
/**
|
|
|
15981 |
* @file volume-control.js
|
|
|
15982 |
*/
|
|
|
15983 |
|
|
|
15984 |
/**
|
|
|
15985 |
* The component for controlling the volume level
|
|
|
15986 |
*
|
|
|
15987 |
* @extends Component
|
|
|
15988 |
*/
|
|
|
15989 |
class VolumeControl extends Component$1 {
|
|
|
15990 |
/**
|
|
|
15991 |
* Creates an instance of this class.
|
|
|
15992 |
*
|
|
|
15993 |
* @param { import('../../player').default } player
|
|
|
15994 |
* The `Player` that this class should be attached to.
|
|
|
15995 |
*
|
|
|
15996 |
* @param {Object} [options={}]
|
|
|
15997 |
* The key/value store of player options.
|
|
|
15998 |
*/
|
|
|
15999 |
constructor(player, options = {}) {
|
|
|
16000 |
options.vertical = options.vertical || false;
|
|
|
16001 |
|
|
|
16002 |
// Pass the vertical option down to the VolumeBar if
|
|
|
16003 |
// the VolumeBar is turned on.
|
|
|
16004 |
if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
|
|
|
16005 |
options.volumeBar = options.volumeBar || {};
|
|
|
16006 |
options.volumeBar.vertical = options.vertical;
|
|
|
16007 |
}
|
|
|
16008 |
super(player, options);
|
|
|
16009 |
|
|
|
16010 |
// hide this control if volume support is missing
|
|
|
16011 |
checkVolumeSupport(this, player);
|
|
|
16012 |
this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
|
|
|
16013 |
this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
|
|
|
16014 |
this.on('mousedown', e => this.handleMouseDown(e));
|
|
|
16015 |
this.on('touchstart', e => this.handleMouseDown(e));
|
|
|
16016 |
this.on('mousemove', e => this.handleMouseMove(e));
|
|
|
16017 |
|
|
|
16018 |
// while the slider is active (the mouse has been pressed down and
|
|
|
16019 |
// is dragging) or in focus we do not want to hide the VolumeBar
|
|
|
16020 |
this.on(this.volumeBar, ['focus', 'slideractive'], () => {
|
|
|
16021 |
this.volumeBar.addClass('vjs-slider-active');
|
|
|
16022 |
this.addClass('vjs-slider-active');
|
|
|
16023 |
this.trigger('slideractive');
|
|
|
16024 |
});
|
|
|
16025 |
this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
|
|
|
16026 |
this.volumeBar.removeClass('vjs-slider-active');
|
|
|
16027 |
this.removeClass('vjs-slider-active');
|
|
|
16028 |
this.trigger('sliderinactive');
|
|
|
16029 |
});
|
|
|
16030 |
}
|
|
|
16031 |
|
|
|
16032 |
/**
|
|
|
16033 |
* Create the `Component`'s DOM element
|
|
|
16034 |
*
|
|
|
16035 |
* @return {Element}
|
|
|
16036 |
* The element that was created.
|
|
|
16037 |
*/
|
|
|
16038 |
createEl() {
|
|
|
16039 |
let orientationClass = 'vjs-volume-horizontal';
|
|
|
16040 |
if (this.options_.vertical) {
|
|
|
16041 |
orientationClass = 'vjs-volume-vertical';
|
|
|
16042 |
}
|
|
|
16043 |
return super.createEl('div', {
|
|
|
16044 |
className: `vjs-volume-control vjs-control ${orientationClass}`
|
|
|
16045 |
});
|
|
|
16046 |
}
|
|
|
16047 |
|
|
|
16048 |
/**
|
|
|
16049 |
* Handle `mousedown` or `touchstart` events on the `VolumeControl`.
|
|
|
16050 |
*
|
|
|
16051 |
* @param {Event} event
|
|
|
16052 |
* `mousedown` or `touchstart` event that triggered this function
|
|
|
16053 |
*
|
|
|
16054 |
* @listens mousedown
|
|
|
16055 |
* @listens touchstart
|
|
|
16056 |
*/
|
|
|
16057 |
handleMouseDown(event) {
|
|
|
16058 |
const doc = this.el_.ownerDocument;
|
|
|
16059 |
this.on(doc, 'mousemove', this.throttledHandleMouseMove);
|
|
|
16060 |
this.on(doc, 'touchmove', this.throttledHandleMouseMove);
|
|
|
16061 |
this.on(doc, 'mouseup', this.handleMouseUpHandler_);
|
|
|
16062 |
this.on(doc, 'touchend', this.handleMouseUpHandler_);
|
|
|
16063 |
}
|
|
|
16064 |
|
|
|
16065 |
/**
|
|
|
16066 |
* Handle `mouseup` or `touchend` events on the `VolumeControl`.
|
|
|
16067 |
*
|
|
|
16068 |
* @param {Event} event
|
|
|
16069 |
* `mouseup` or `touchend` event that triggered this function.
|
|
|
16070 |
*
|
|
|
16071 |
* @listens touchend
|
|
|
16072 |
* @listens mouseup
|
|
|
16073 |
*/
|
|
|
16074 |
handleMouseUp(event) {
|
|
|
16075 |
const doc = this.el_.ownerDocument;
|
|
|
16076 |
this.off(doc, 'mousemove', this.throttledHandleMouseMove);
|
|
|
16077 |
this.off(doc, 'touchmove', this.throttledHandleMouseMove);
|
|
|
16078 |
this.off(doc, 'mouseup', this.handleMouseUpHandler_);
|
|
|
16079 |
this.off(doc, 'touchend', this.handleMouseUpHandler_);
|
|
|
16080 |
}
|
|
|
16081 |
|
|
|
16082 |
/**
|
|
|
16083 |
* Handle `mousedown` or `touchstart` events on the `VolumeControl`.
|
|
|
16084 |
*
|
|
|
16085 |
* @param {Event} event
|
|
|
16086 |
* `mousedown` or `touchstart` event that triggered this function
|
|
|
16087 |
*
|
|
|
16088 |
* @listens mousedown
|
|
|
16089 |
* @listens touchstart
|
|
|
16090 |
*/
|
|
|
16091 |
handleMouseMove(event) {
|
|
|
16092 |
this.volumeBar.handleMouseMove(event);
|
|
|
16093 |
}
|
|
|
16094 |
}
|
|
|
16095 |
|
|
|
16096 |
/**
|
|
|
16097 |
* Default options for the `VolumeControl`
|
|
|
16098 |
*
|
|
|
16099 |
* @type {Object}
|
|
|
16100 |
* @private
|
|
|
16101 |
*/
|
|
|
16102 |
VolumeControl.prototype.options_ = {
|
|
|
16103 |
children: ['volumeBar']
|
|
|
16104 |
};
|
|
|
16105 |
Component$1.registerComponent('VolumeControl', VolumeControl);
|
|
|
16106 |
|
|
|
16107 |
/**
|
|
|
16108 |
* Check if muting volume is supported and if it isn't hide the mute toggle
|
|
|
16109 |
* button.
|
|
|
16110 |
*
|
|
|
16111 |
* @param { import('../../component').default } self
|
|
|
16112 |
* A reference to the mute toggle button
|
|
|
16113 |
*
|
|
|
16114 |
* @param { import('../../player').default } player
|
|
|
16115 |
* A reference to the player
|
|
|
16116 |
*
|
|
|
16117 |
* @private
|
|
|
16118 |
*/
|
|
|
16119 |
const checkMuteSupport = function (self, player) {
|
|
|
16120 |
// hide mute toggle button if it's not supported by the current tech
|
|
|
16121 |
if (player.tech_ && !player.tech_.featuresMuteControl) {
|
|
|
16122 |
self.addClass('vjs-hidden');
|
|
|
16123 |
}
|
|
|
16124 |
self.on(player, 'loadstart', function () {
|
|
|
16125 |
if (!player.tech_.featuresMuteControl) {
|
|
|
16126 |
self.addClass('vjs-hidden');
|
|
|
16127 |
} else {
|
|
|
16128 |
self.removeClass('vjs-hidden');
|
|
|
16129 |
}
|
|
|
16130 |
});
|
|
|
16131 |
};
|
|
|
16132 |
|
|
|
16133 |
/**
|
|
|
16134 |
* @file mute-toggle.js
|
|
|
16135 |
*/
|
|
|
16136 |
|
|
|
16137 |
/**
|
|
|
16138 |
* A button component for muting the audio.
|
|
|
16139 |
*
|
|
|
16140 |
* @extends Button
|
|
|
16141 |
*/
|
|
|
16142 |
class MuteToggle extends Button {
|
|
|
16143 |
/**
|
|
|
16144 |
* Creates an instance of this class.
|
|
|
16145 |
*
|
|
|
16146 |
* @param { import('./player').default } player
|
|
|
16147 |
* The `Player` that this class should be attached to.
|
|
|
16148 |
*
|
|
|
16149 |
* @param {Object} [options]
|
|
|
16150 |
* The key/value store of player options.
|
|
|
16151 |
*/
|
|
|
16152 |
constructor(player, options) {
|
|
|
16153 |
super(player, options);
|
|
|
16154 |
|
|
|
16155 |
// hide this control if volume support is missing
|
|
|
16156 |
checkMuteSupport(this, player);
|
|
|
16157 |
this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
|
|
|
16158 |
}
|
|
|
16159 |
|
|
|
16160 |
/**
|
|
|
16161 |
* Builds the default DOM `className`.
|
|
|
16162 |
*
|
|
|
16163 |
* @return {string}
|
|
|
16164 |
* The DOM `className` for this object.
|
|
|
16165 |
*/
|
|
|
16166 |
buildCSSClass() {
|
|
|
16167 |
return `vjs-mute-control ${super.buildCSSClass()}`;
|
|
|
16168 |
}
|
|
|
16169 |
|
|
|
16170 |
/**
|
|
|
16171 |
* This gets called when an `MuteToggle` is "clicked". See
|
|
|
16172 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
16173 |
*
|
|
|
16174 |
* @param {Event} [event]
|
|
|
16175 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
16176 |
* called.
|
|
|
16177 |
*
|
|
|
16178 |
* @listens tap
|
|
|
16179 |
* @listens click
|
|
|
16180 |
*/
|
|
|
16181 |
handleClick(event) {
|
|
|
16182 |
const vol = this.player_.volume();
|
|
|
16183 |
const lastVolume = this.player_.lastVolume_();
|
|
|
16184 |
if (vol === 0) {
|
|
|
16185 |
const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
|
|
|
16186 |
this.player_.volume(volumeToSet);
|
|
|
16187 |
this.player_.muted(false);
|
|
|
16188 |
} else {
|
|
|
16189 |
this.player_.muted(this.player_.muted() ? false : true);
|
|
|
16190 |
}
|
|
|
16191 |
}
|
|
|
16192 |
|
|
|
16193 |
/**
|
|
|
16194 |
* Update the `MuteToggle` button based on the state of `volume` and `muted`
|
|
|
16195 |
* on the player.
|
|
|
16196 |
*
|
|
|
16197 |
* @param {Event} [event]
|
|
|
16198 |
* The {@link Player#loadstart} event if this function was called
|
|
|
16199 |
* through an event.
|
|
|
16200 |
*
|
|
|
16201 |
* @listens Player#loadstart
|
|
|
16202 |
* @listens Player#volumechange
|
|
|
16203 |
*/
|
|
|
16204 |
update(event) {
|
|
|
16205 |
this.updateIcon_();
|
|
|
16206 |
this.updateControlText_();
|
|
|
16207 |
}
|
|
|
16208 |
|
|
|
16209 |
/**
|
|
|
16210 |
* Update the appearance of the `MuteToggle` icon.
|
|
|
16211 |
*
|
|
|
16212 |
* Possible states (given `level` variable below):
|
|
|
16213 |
* - 0: crossed out
|
|
|
16214 |
* - 1: zero bars of volume
|
|
|
16215 |
* - 2: one bar of volume
|
|
|
16216 |
* - 3: two bars of volume
|
|
|
16217 |
*
|
|
|
16218 |
* @private
|
|
|
16219 |
*/
|
|
|
16220 |
updateIcon_() {
|
|
|
16221 |
const vol = this.player_.volume();
|
|
|
16222 |
let level = 3;
|
|
|
16223 |
this.setIcon('volume-high');
|
|
|
16224 |
|
|
|
16225 |
// in iOS when a player is loaded with muted attribute
|
|
|
16226 |
// and volume is changed with a native mute button
|
|
|
16227 |
// we want to make sure muted state is updated
|
|
|
16228 |
if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
|
|
|
16229 |
this.player_.muted(this.player_.tech_.el_.muted);
|
|
|
16230 |
}
|
|
|
16231 |
if (vol === 0 || this.player_.muted()) {
|
|
|
16232 |
this.setIcon('volume-mute');
|
|
|
16233 |
level = 0;
|
|
|
16234 |
} else if (vol < 0.33) {
|
|
|
16235 |
this.setIcon('volume-low');
|
|
|
16236 |
level = 1;
|
|
|
16237 |
} else if (vol < 0.67) {
|
|
|
16238 |
this.setIcon('volume-medium');
|
|
|
16239 |
level = 2;
|
|
|
16240 |
}
|
|
|
16241 |
removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
|
|
|
16242 |
addClass(this.el_, `vjs-vol-${level}`);
|
|
|
16243 |
}
|
|
|
16244 |
|
|
|
16245 |
/**
|
|
|
16246 |
* If `muted` has changed on the player, update the control text
|
|
|
16247 |
* (`title` attribute on `vjs-mute-control` element and content of
|
|
|
16248 |
* `vjs-control-text` element).
|
|
|
16249 |
*
|
|
|
16250 |
* @private
|
|
|
16251 |
*/
|
|
|
16252 |
updateControlText_() {
|
|
|
16253 |
const soundOff = this.player_.muted() || this.player_.volume() === 0;
|
|
|
16254 |
const text = soundOff ? 'Unmute' : 'Mute';
|
|
|
16255 |
if (this.controlText() !== text) {
|
|
|
16256 |
this.controlText(text);
|
|
|
16257 |
}
|
|
|
16258 |
}
|
|
|
16259 |
}
|
|
|
16260 |
|
|
|
16261 |
/**
|
|
|
16262 |
* The text that should display over the `MuteToggle`s controls. Added for localization.
|
|
|
16263 |
*
|
|
|
16264 |
* @type {string}
|
|
|
16265 |
* @protected
|
|
|
16266 |
*/
|
|
|
16267 |
MuteToggle.prototype.controlText_ = 'Mute';
|
|
|
16268 |
Component$1.registerComponent('MuteToggle', MuteToggle);
|
|
|
16269 |
|
|
|
16270 |
/**
|
|
|
16271 |
* @file volume-control.js
|
|
|
16272 |
*/
|
|
|
16273 |
|
|
|
16274 |
/**
|
|
|
16275 |
* A Component to contain the MuteToggle and VolumeControl so that
|
|
|
16276 |
* they can work together.
|
|
|
16277 |
*
|
|
|
16278 |
* @extends Component
|
|
|
16279 |
*/
|
|
|
16280 |
class VolumePanel extends Component$1 {
|
|
|
16281 |
/**
|
|
|
16282 |
* Creates an instance of this class.
|
|
|
16283 |
*
|
|
|
16284 |
* @param { import('./player').default } player
|
|
|
16285 |
* The `Player` that this class should be attached to.
|
|
|
16286 |
*
|
|
|
16287 |
* @param {Object} [options={}]
|
|
|
16288 |
* The key/value store of player options.
|
|
|
16289 |
*/
|
|
|
16290 |
constructor(player, options = {}) {
|
|
|
16291 |
if (typeof options.inline !== 'undefined') {
|
|
|
16292 |
options.inline = options.inline;
|
|
|
16293 |
} else {
|
|
|
16294 |
options.inline = true;
|
|
|
16295 |
}
|
|
|
16296 |
|
|
|
16297 |
// pass the inline option down to the VolumeControl as vertical if
|
|
|
16298 |
// the VolumeControl is on.
|
|
|
16299 |
if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
|
|
|
16300 |
options.volumeControl = options.volumeControl || {};
|
|
|
16301 |
options.volumeControl.vertical = !options.inline;
|
|
|
16302 |
}
|
|
|
16303 |
super(player, options);
|
|
|
16304 |
|
|
|
16305 |
// this handler is used by mouse handler methods below
|
|
|
16306 |
this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
|
|
|
16307 |
this.on(player, ['loadstart'], e => this.volumePanelState_(e));
|
|
|
16308 |
this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
|
|
|
16309 |
this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
|
|
|
16310 |
this.on('keydown', e => this.handleKeyPress(e));
|
|
|
16311 |
this.on('mouseover', e => this.handleMouseOver(e));
|
|
|
16312 |
this.on('mouseout', e => this.handleMouseOut(e));
|
|
|
16313 |
|
|
|
16314 |
// while the slider is active (the mouse has been pressed down and
|
|
|
16315 |
// is dragging) we do not want to hide the VolumeBar
|
|
|
16316 |
this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
|
|
|
16317 |
this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
|
|
|
16318 |
}
|
|
|
16319 |
|
|
|
16320 |
/**
|
|
|
16321 |
* Add vjs-slider-active class to the VolumePanel
|
|
|
16322 |
*
|
|
|
16323 |
* @listens VolumeControl#slideractive
|
|
|
16324 |
* @private
|
|
|
16325 |
*/
|
|
|
16326 |
sliderActive_() {
|
|
|
16327 |
this.addClass('vjs-slider-active');
|
|
|
16328 |
}
|
|
|
16329 |
|
|
|
16330 |
/**
|
|
|
16331 |
* Removes vjs-slider-active class to the VolumePanel
|
|
|
16332 |
*
|
|
|
16333 |
* @listens VolumeControl#sliderinactive
|
|
|
16334 |
* @private
|
|
|
16335 |
*/
|
|
|
16336 |
sliderInactive_() {
|
|
|
16337 |
this.removeClass('vjs-slider-active');
|
|
|
16338 |
}
|
|
|
16339 |
|
|
|
16340 |
/**
|
|
|
16341 |
* Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
|
|
|
16342 |
* depending on MuteToggle and VolumeControl state
|
|
|
16343 |
*
|
|
|
16344 |
* @listens Player#loadstart
|
|
|
16345 |
* @private
|
|
|
16346 |
*/
|
|
|
16347 |
volumePanelState_() {
|
|
|
16348 |
// hide volume panel if neither volume control or mute toggle
|
|
|
16349 |
// are displayed
|
|
|
16350 |
if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
|
|
|
16351 |
this.addClass('vjs-hidden');
|
|
|
16352 |
}
|
|
|
16353 |
|
|
|
16354 |
// if only mute toggle is visible we don't want
|
|
|
16355 |
// volume panel expanding when hovered or active
|
|
|
16356 |
if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
|
|
|
16357 |
this.addClass('vjs-mute-toggle-only');
|
|
|
16358 |
}
|
|
|
16359 |
}
|
|
|
16360 |
|
|
|
16361 |
/**
|
|
|
16362 |
* Create the `Component`'s DOM element
|
|
|
16363 |
*
|
|
|
16364 |
* @return {Element}
|
|
|
16365 |
* The element that was created.
|
|
|
16366 |
*/
|
|
|
16367 |
createEl() {
|
|
|
16368 |
let orientationClass = 'vjs-volume-panel-horizontal';
|
|
|
16369 |
if (!this.options_.inline) {
|
|
|
16370 |
orientationClass = 'vjs-volume-panel-vertical';
|
|
|
16371 |
}
|
|
|
16372 |
return super.createEl('div', {
|
|
|
16373 |
className: `vjs-volume-panel vjs-control ${orientationClass}`
|
|
|
16374 |
});
|
|
|
16375 |
}
|
|
|
16376 |
|
|
|
16377 |
/**
|
|
|
16378 |
* Dispose of the `volume-panel` and all child components.
|
|
|
16379 |
*/
|
|
|
16380 |
dispose() {
|
|
|
16381 |
this.handleMouseOut();
|
|
|
16382 |
super.dispose();
|
|
|
16383 |
}
|
|
|
16384 |
|
|
|
16385 |
/**
|
|
|
16386 |
* Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
|
|
|
16387 |
* the volume panel and sets focus on `MuteToggle`.
|
|
|
16388 |
*
|
|
|
16389 |
* @param {Event} event
|
|
|
16390 |
* The `keyup` event that caused this function to be called.
|
|
|
16391 |
*
|
|
|
16392 |
* @listens keyup
|
|
|
16393 |
*/
|
|
|
16394 |
handleVolumeControlKeyUp(event) {
|
|
|
16395 |
if (keycode.isEventKey(event, 'Esc')) {
|
|
|
16396 |
this.muteToggle.focus();
|
|
|
16397 |
}
|
|
|
16398 |
}
|
|
|
16399 |
|
|
|
16400 |
/**
|
|
|
16401 |
* This gets called when a `VolumePanel` gains hover via a `mouseover` event.
|
|
|
16402 |
* Turns on listening for `mouseover` event. When they happen it
|
|
|
16403 |
* calls `this.handleMouseOver`.
|
|
|
16404 |
*
|
|
|
16405 |
* @param {Event} event
|
|
|
16406 |
* The `mouseover` event that caused this function to be called.
|
|
|
16407 |
*
|
|
|
16408 |
* @listens mouseover
|
|
|
16409 |
*/
|
|
|
16410 |
handleMouseOver(event) {
|
|
|
16411 |
this.addClass('vjs-hover');
|
|
|
16412 |
on(document, 'keyup', this.handleKeyPressHandler_);
|
|
|
16413 |
}
|
|
|
16414 |
|
|
|
16415 |
/**
|
|
|
16416 |
* This gets called when a `VolumePanel` gains hover via a `mouseout` event.
|
|
|
16417 |
* Turns on listening for `mouseout` event. When they happen it
|
|
|
16418 |
* calls `this.handleMouseOut`.
|
|
|
16419 |
*
|
|
|
16420 |
* @param {Event} event
|
|
|
16421 |
* The `mouseout` event that caused this function to be called.
|
|
|
16422 |
*
|
|
|
16423 |
* @listens mouseout
|
|
|
16424 |
*/
|
|
|
16425 |
handleMouseOut(event) {
|
|
|
16426 |
this.removeClass('vjs-hover');
|
|
|
16427 |
off(document, 'keyup', this.handleKeyPressHandler_);
|
|
|
16428 |
}
|
|
|
16429 |
|
|
|
16430 |
/**
|
|
|
16431 |
* Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
|
|
|
16432 |
* looking for ESC, which hides the `VolumeControl`.
|
|
|
16433 |
*
|
|
|
16434 |
* @param {Event} event
|
|
|
16435 |
* The keypress that triggered this event.
|
|
|
16436 |
*
|
|
|
16437 |
* @listens keydown | keyup
|
|
|
16438 |
*/
|
|
|
16439 |
handleKeyPress(event) {
|
|
|
16440 |
if (keycode.isEventKey(event, 'Esc')) {
|
|
|
16441 |
this.handleMouseOut();
|
|
|
16442 |
}
|
|
|
16443 |
}
|
|
|
16444 |
}
|
|
|
16445 |
|
|
|
16446 |
/**
|
|
|
16447 |
* Default options for the `VolumeControl`
|
|
|
16448 |
*
|
|
|
16449 |
* @type {Object}
|
|
|
16450 |
* @private
|
|
|
16451 |
*/
|
|
|
16452 |
VolumePanel.prototype.options_ = {
|
|
|
16453 |
children: ['muteToggle', 'volumeControl']
|
|
|
16454 |
};
|
|
|
16455 |
Component$1.registerComponent('VolumePanel', VolumePanel);
|
|
|
16456 |
|
|
|
16457 |
/**
|
|
|
16458 |
* Button to skip forward a configurable amount of time
|
|
|
16459 |
* through a video. Renders in the control bar.
|
|
|
16460 |
*
|
|
|
16461 |
* e.g. options: {controlBar: {skipButtons: forward: 5}}
|
|
|
16462 |
*
|
|
|
16463 |
* @extends Button
|
|
|
16464 |
*/
|
|
|
16465 |
class SkipForward extends Button {
|
|
|
16466 |
constructor(player, options) {
|
|
|
16467 |
super(player, options);
|
|
|
16468 |
this.validOptions = [5, 10, 30];
|
|
|
16469 |
this.skipTime = this.getSkipForwardTime();
|
|
|
16470 |
if (this.skipTime && this.validOptions.includes(this.skipTime)) {
|
|
|
16471 |
this.setIcon(`forward-${this.skipTime}`);
|
|
|
16472 |
this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
|
|
|
16473 |
this.show();
|
|
|
16474 |
} else {
|
|
|
16475 |
this.hide();
|
|
|
16476 |
}
|
|
|
16477 |
}
|
|
|
16478 |
getSkipForwardTime() {
|
|
|
16479 |
const playerOptions = this.options_.playerOptions;
|
|
|
16480 |
return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
|
|
|
16481 |
}
|
|
|
16482 |
buildCSSClass() {
|
|
|
16483 |
return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
|
|
|
16484 |
}
|
|
|
16485 |
|
|
|
16486 |
/**
|
|
|
16487 |
* On click, skips forward in the duration/seekable range by a configurable amount of seconds.
|
|
|
16488 |
* If the time left in the duration/seekable range is less than the configured 'skip forward' time,
|
|
|
16489 |
* skips to end of duration/seekable range.
|
|
|
16490 |
*
|
|
|
16491 |
* Handle a click on a `SkipForward` button
|
|
|
16492 |
*
|
|
|
16493 |
* @param {EventTarget~Event} event
|
|
|
16494 |
* The `click` event that caused this function
|
|
|
16495 |
* to be called
|
|
|
16496 |
*/
|
|
|
16497 |
handleClick(event) {
|
|
|
16498 |
if (isNaN(this.player_.duration())) {
|
|
|
16499 |
return;
|
|
|
16500 |
}
|
|
|
16501 |
const currentVideoTime = this.player_.currentTime();
|
|
|
16502 |
const liveTracker = this.player_.liveTracker;
|
|
|
16503 |
const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
|
|
|
16504 |
let newTime;
|
|
|
16505 |
if (currentVideoTime + this.skipTime <= duration) {
|
|
|
16506 |
newTime = currentVideoTime + this.skipTime;
|
|
|
16507 |
} else {
|
|
|
16508 |
newTime = duration;
|
|
|
16509 |
}
|
|
|
16510 |
this.player_.currentTime(newTime);
|
|
|
16511 |
}
|
|
|
16512 |
|
|
|
16513 |
/**
|
|
|
16514 |
* Update control text on languagechange
|
|
|
16515 |
*/
|
|
|
16516 |
handleLanguagechange() {
|
|
|
16517 |
this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
|
|
|
16518 |
}
|
|
|
16519 |
}
|
|
|
16520 |
SkipForward.prototype.controlText_ = 'Skip Forward';
|
|
|
16521 |
Component$1.registerComponent('SkipForward', SkipForward);
|
|
|
16522 |
|
|
|
16523 |
/**
|
|
|
16524 |
* Button to skip backward a configurable amount of time
|
|
|
16525 |
* through a video. Renders in the control bar.
|
|
|
16526 |
*
|
|
|
16527 |
* * e.g. options: {controlBar: {skipButtons: backward: 5}}
|
|
|
16528 |
*
|
|
|
16529 |
* @extends Button
|
|
|
16530 |
*/
|
|
|
16531 |
class SkipBackward extends Button {
|
|
|
16532 |
constructor(player, options) {
|
|
|
16533 |
super(player, options);
|
|
|
16534 |
this.validOptions = [5, 10, 30];
|
|
|
16535 |
this.skipTime = this.getSkipBackwardTime();
|
|
|
16536 |
if (this.skipTime && this.validOptions.includes(this.skipTime)) {
|
|
|
16537 |
this.setIcon(`replay-${this.skipTime}`);
|
|
|
16538 |
this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
|
|
|
16539 |
this.show();
|
|
|
16540 |
} else {
|
|
|
16541 |
this.hide();
|
|
|
16542 |
}
|
|
|
16543 |
}
|
|
|
16544 |
getSkipBackwardTime() {
|
|
|
16545 |
const playerOptions = this.options_.playerOptions;
|
|
|
16546 |
return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
|
|
|
16547 |
}
|
|
|
16548 |
buildCSSClass() {
|
|
|
16549 |
return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
|
|
|
16550 |
}
|
|
|
16551 |
|
|
|
16552 |
/**
|
|
|
16553 |
* On click, skips backward in the video by a configurable amount of seconds.
|
|
|
16554 |
* If the current time in the video is less than the configured 'skip backward' time,
|
|
|
16555 |
* skips to beginning of video or seekable range.
|
|
|
16556 |
*
|
|
|
16557 |
* Handle a click on a `SkipBackward` button
|
|
|
16558 |
*
|
|
|
16559 |
* @param {EventTarget~Event} event
|
|
|
16560 |
* The `click` event that caused this function
|
|
|
16561 |
* to be called
|
|
|
16562 |
*/
|
|
|
16563 |
handleClick(event) {
|
|
|
16564 |
const currentVideoTime = this.player_.currentTime();
|
|
|
16565 |
const liveTracker = this.player_.liveTracker;
|
|
|
16566 |
const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
|
|
|
16567 |
let newTime;
|
|
|
16568 |
if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
|
|
|
16569 |
newTime = seekableStart;
|
|
|
16570 |
} else if (currentVideoTime >= this.skipTime) {
|
|
|
16571 |
newTime = currentVideoTime - this.skipTime;
|
|
|
16572 |
} else {
|
|
|
16573 |
newTime = 0;
|
|
|
16574 |
}
|
|
|
16575 |
this.player_.currentTime(newTime);
|
|
|
16576 |
}
|
|
|
16577 |
|
|
|
16578 |
/**
|
|
|
16579 |
* Update control text on languagechange
|
|
|
16580 |
*/
|
|
|
16581 |
handleLanguagechange() {
|
|
|
16582 |
this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
|
|
|
16583 |
}
|
|
|
16584 |
}
|
|
|
16585 |
SkipBackward.prototype.controlText_ = 'Skip Backward';
|
|
|
16586 |
Component$1.registerComponent('SkipBackward', SkipBackward);
|
|
|
16587 |
|
|
|
16588 |
/**
|
|
|
16589 |
* @file menu.js
|
|
|
16590 |
*/
|
|
|
16591 |
|
|
|
16592 |
/**
|
|
|
16593 |
* The Menu component is used to build popup menus, including subtitle and
|
|
|
16594 |
* captions selection menus.
|
|
|
16595 |
*
|
|
|
16596 |
* @extends Component
|
|
|
16597 |
*/
|
|
|
16598 |
class Menu extends Component$1 {
|
|
|
16599 |
/**
|
|
|
16600 |
* Create an instance of this class.
|
|
|
16601 |
*
|
|
|
16602 |
* @param { import('../player').default } player
|
|
|
16603 |
* the player that this component should attach to
|
|
|
16604 |
*
|
|
|
16605 |
* @param {Object} [options]
|
|
|
16606 |
* Object of option names and values
|
|
|
16607 |
*
|
|
|
16608 |
*/
|
|
|
16609 |
constructor(player, options) {
|
|
|
16610 |
super(player, options);
|
|
|
16611 |
if (options) {
|
|
|
16612 |
this.menuButton_ = options.menuButton;
|
|
|
16613 |
}
|
|
|
16614 |
this.focusedChild_ = -1;
|
|
|
16615 |
this.on('keydown', e => this.handleKeyDown(e));
|
|
|
16616 |
|
|
|
16617 |
// All the menu item instances share the same blur handler provided by the menu container.
|
|
|
16618 |
this.boundHandleBlur_ = e => this.handleBlur(e);
|
|
|
16619 |
this.boundHandleTapClick_ = e => this.handleTapClick(e);
|
|
|
16620 |
}
|
|
|
16621 |
|
|
|
16622 |
/**
|
|
|
16623 |
* Add event listeners to the {@link MenuItem}.
|
|
|
16624 |
*
|
|
|
16625 |
* @param {Object} component
|
|
|
16626 |
* The instance of the `MenuItem` to add listeners to.
|
|
|
16627 |
*
|
|
|
16628 |
*/
|
|
|
16629 |
addEventListenerForItem(component) {
|
|
|
16630 |
if (!(component instanceof Component$1)) {
|
|
|
16631 |
return;
|
|
|
16632 |
}
|
|
|
16633 |
this.on(component, 'blur', this.boundHandleBlur_);
|
|
|
16634 |
this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
|
|
|
16635 |
}
|
|
|
16636 |
|
|
|
16637 |
/**
|
|
|
16638 |
* Remove event listeners from the {@link MenuItem}.
|
|
|
16639 |
*
|
|
|
16640 |
* @param {Object} component
|
|
|
16641 |
* The instance of the `MenuItem` to remove listeners.
|
|
|
16642 |
*
|
|
|
16643 |
*/
|
|
|
16644 |
removeEventListenerForItem(component) {
|
|
|
16645 |
if (!(component instanceof Component$1)) {
|
|
|
16646 |
return;
|
|
|
16647 |
}
|
|
|
16648 |
this.off(component, 'blur', this.boundHandleBlur_);
|
|
|
16649 |
this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
|
|
|
16650 |
}
|
|
|
16651 |
|
|
|
16652 |
/**
|
|
|
16653 |
* This method will be called indirectly when the component has been added
|
|
|
16654 |
* before the component adds to the new menu instance by `addItem`.
|
|
|
16655 |
* In this case, the original menu instance will remove the component
|
|
|
16656 |
* by calling `removeChild`.
|
|
|
16657 |
*
|
|
|
16658 |
* @param {Object} component
|
|
|
16659 |
* The instance of the `MenuItem`
|
|
|
16660 |
*/
|
|
|
16661 |
removeChild(component) {
|
|
|
16662 |
if (typeof component === 'string') {
|
|
|
16663 |
component = this.getChild(component);
|
|
|
16664 |
}
|
|
|
16665 |
this.removeEventListenerForItem(component);
|
|
|
16666 |
super.removeChild(component);
|
|
|
16667 |
}
|
|
|
16668 |
|
|
|
16669 |
/**
|
|
|
16670 |
* Add a {@link MenuItem} to the menu.
|
|
|
16671 |
*
|
|
|
16672 |
* @param {Object|string} component
|
|
|
16673 |
* The name or instance of the `MenuItem` to add.
|
|
|
16674 |
*
|
|
|
16675 |
*/
|
|
|
16676 |
addItem(component) {
|
|
|
16677 |
const childComponent = this.addChild(component);
|
|
|
16678 |
if (childComponent) {
|
|
|
16679 |
this.addEventListenerForItem(childComponent);
|
|
|
16680 |
}
|
|
|
16681 |
}
|
|
|
16682 |
|
|
|
16683 |
/**
|
|
|
16684 |
* Create the `Menu`s DOM element.
|
|
|
16685 |
*
|
|
|
16686 |
* @return {Element}
|
|
|
16687 |
* the element that was created
|
|
|
16688 |
*/
|
|
|
16689 |
createEl() {
|
|
|
16690 |
const contentElType = this.options_.contentElType || 'ul';
|
|
|
16691 |
this.contentEl_ = createEl(contentElType, {
|
|
|
16692 |
className: 'vjs-menu-content'
|
|
|
16693 |
});
|
|
|
16694 |
this.contentEl_.setAttribute('role', 'menu');
|
|
|
16695 |
const el = super.createEl('div', {
|
|
|
16696 |
append: this.contentEl_,
|
|
|
16697 |
className: 'vjs-menu'
|
|
|
16698 |
});
|
|
|
16699 |
el.appendChild(this.contentEl_);
|
|
|
16700 |
|
|
|
16701 |
// Prevent clicks from bubbling up. Needed for Menu Buttons,
|
|
|
16702 |
// where a click on the parent is significant
|
|
|
16703 |
on(el, 'click', function (event) {
|
|
|
16704 |
event.preventDefault();
|
|
|
16705 |
event.stopImmediatePropagation();
|
|
|
16706 |
});
|
|
|
16707 |
return el;
|
|
|
16708 |
}
|
|
|
16709 |
dispose() {
|
|
|
16710 |
this.contentEl_ = null;
|
|
|
16711 |
this.boundHandleBlur_ = null;
|
|
|
16712 |
this.boundHandleTapClick_ = null;
|
|
|
16713 |
super.dispose();
|
|
|
16714 |
}
|
|
|
16715 |
|
|
|
16716 |
/**
|
|
|
16717 |
* Called when a `MenuItem` loses focus.
|
|
|
16718 |
*
|
|
|
16719 |
* @param {Event} event
|
|
|
16720 |
* The `blur` event that caused this function to be called.
|
|
|
16721 |
*
|
|
|
16722 |
* @listens blur
|
|
|
16723 |
*/
|
|
|
16724 |
handleBlur(event) {
|
|
|
16725 |
const relatedTarget = event.relatedTarget || document.activeElement;
|
|
|
16726 |
|
|
|
16727 |
// Close menu popup when a user clicks outside the menu
|
|
|
16728 |
if (!this.children().some(element => {
|
|
|
16729 |
return element.el() === relatedTarget;
|
|
|
16730 |
})) {
|
|
|
16731 |
const btn = this.menuButton_;
|
|
|
16732 |
if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
|
|
|
16733 |
btn.unpressButton();
|
|
|
16734 |
}
|
|
|
16735 |
}
|
|
|
16736 |
}
|
|
|
16737 |
|
|
|
16738 |
/**
|
|
|
16739 |
* Called when a `MenuItem` gets clicked or tapped.
|
|
|
16740 |
*
|
|
|
16741 |
* @param {Event} event
|
|
|
16742 |
* The `click` or `tap` event that caused this function to be called.
|
|
|
16743 |
*
|
|
|
16744 |
* @listens click,tap
|
|
|
16745 |
*/
|
|
|
16746 |
handleTapClick(event) {
|
|
|
16747 |
// Unpress the associated MenuButton, and move focus back to it
|
|
|
16748 |
if (this.menuButton_) {
|
|
|
16749 |
this.menuButton_.unpressButton();
|
|
|
16750 |
const childComponents = this.children();
|
|
|
16751 |
if (!Array.isArray(childComponents)) {
|
|
|
16752 |
return;
|
|
|
16753 |
}
|
|
|
16754 |
const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
|
|
|
16755 |
if (!foundComponent) {
|
|
|
16756 |
return;
|
|
|
16757 |
}
|
|
|
16758 |
|
|
|
16759 |
// don't focus menu button if item is a caption settings item
|
|
|
16760 |
// because focus will move elsewhere
|
|
|
16761 |
if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
|
|
|
16762 |
this.menuButton_.focus();
|
|
|
16763 |
}
|
|
|
16764 |
}
|
|
|
16765 |
}
|
|
|
16766 |
|
|
|
16767 |
/**
|
|
|
16768 |
* Handle a `keydown` event on this menu. This listener is added in the constructor.
|
|
|
16769 |
*
|
|
|
16770 |
* @param {KeyboardEvent} event
|
|
|
16771 |
* A `keydown` event that happened on the menu.
|
|
|
16772 |
*
|
|
|
16773 |
* @listens keydown
|
|
|
16774 |
*/
|
|
|
16775 |
handleKeyDown(event) {
|
|
|
16776 |
// Left and Down Arrows
|
|
|
16777 |
if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
|
|
|
16778 |
event.preventDefault();
|
|
|
16779 |
event.stopPropagation();
|
|
|
16780 |
this.stepForward();
|
|
|
16781 |
|
|
|
16782 |
// Up and Right Arrows
|
|
|
16783 |
} else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
|
|
|
16784 |
event.preventDefault();
|
|
|
16785 |
event.stopPropagation();
|
|
|
16786 |
this.stepBack();
|
|
|
16787 |
}
|
|
|
16788 |
}
|
|
|
16789 |
|
|
|
16790 |
/**
|
|
|
16791 |
* Move to next (lower) menu item for keyboard users.
|
|
|
16792 |
*/
|
|
|
16793 |
stepForward() {
|
|
|
16794 |
let stepChild = 0;
|
|
|
16795 |
if (this.focusedChild_ !== undefined) {
|
|
|
16796 |
stepChild = this.focusedChild_ + 1;
|
|
|
16797 |
}
|
|
|
16798 |
this.focus(stepChild);
|
|
|
16799 |
}
|
|
|
16800 |
|
|
|
16801 |
/**
|
|
|
16802 |
* Move to previous (higher) menu item for keyboard users.
|
|
|
16803 |
*/
|
|
|
16804 |
stepBack() {
|
|
|
16805 |
let stepChild = 0;
|
|
|
16806 |
if (this.focusedChild_ !== undefined) {
|
|
|
16807 |
stepChild = this.focusedChild_ - 1;
|
|
|
16808 |
}
|
|
|
16809 |
this.focus(stepChild);
|
|
|
16810 |
}
|
|
|
16811 |
|
|
|
16812 |
/**
|
|
|
16813 |
* Set focus on a {@link MenuItem} in the `Menu`.
|
|
|
16814 |
*
|
|
|
16815 |
* @param {Object|string} [item=0]
|
|
|
16816 |
* Index of child item set focus on.
|
|
|
16817 |
*/
|
|
|
16818 |
focus(item = 0) {
|
|
|
16819 |
const children = this.children().slice();
|
|
|
16820 |
const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
|
|
|
16821 |
if (haveTitle) {
|
|
|
16822 |
children.shift();
|
|
|
16823 |
}
|
|
|
16824 |
if (children.length > 0) {
|
|
|
16825 |
if (item < 0) {
|
|
|
16826 |
item = 0;
|
|
|
16827 |
} else if (item >= children.length) {
|
|
|
16828 |
item = children.length - 1;
|
|
|
16829 |
}
|
|
|
16830 |
this.focusedChild_ = item;
|
|
|
16831 |
children[item].el_.focus();
|
|
|
16832 |
}
|
|
|
16833 |
}
|
|
|
16834 |
}
|
|
|
16835 |
Component$1.registerComponent('Menu', Menu);
|
|
|
16836 |
|
|
|
16837 |
/**
|
|
|
16838 |
* @file menu-button.js
|
|
|
16839 |
*/
|
|
|
16840 |
|
|
|
16841 |
/**
|
|
|
16842 |
* A `MenuButton` class for any popup {@link Menu}.
|
|
|
16843 |
*
|
|
|
16844 |
* @extends Component
|
|
|
16845 |
*/
|
|
|
16846 |
class MenuButton extends Component$1 {
|
|
|
16847 |
/**
|
|
|
16848 |
* Creates an instance of this class.
|
|
|
16849 |
*
|
|
|
16850 |
* @param { import('../player').default } player
|
|
|
16851 |
* The `Player` that this class should be attached to.
|
|
|
16852 |
*
|
|
|
16853 |
* @param {Object} [options={}]
|
|
|
16854 |
* The key/value store of player options.
|
|
|
16855 |
*/
|
|
|
16856 |
constructor(player, options = {}) {
|
|
|
16857 |
super(player, options);
|
|
|
16858 |
this.menuButton_ = new Button(player, options);
|
|
|
16859 |
this.menuButton_.controlText(this.controlText_);
|
|
|
16860 |
this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
|
|
|
16861 |
|
|
|
16862 |
// Add buildCSSClass values to the button, not the wrapper
|
|
|
16863 |
const buttonClass = Button.prototype.buildCSSClass();
|
|
|
16864 |
this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
|
|
|
16865 |
this.menuButton_.removeClass('vjs-control');
|
|
|
16866 |
this.addChild(this.menuButton_);
|
|
|
16867 |
this.update();
|
|
|
16868 |
this.enabled_ = true;
|
|
|
16869 |
const handleClick = e => this.handleClick(e);
|
|
|
16870 |
this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
|
|
|
16871 |
this.on(this.menuButton_, 'tap', handleClick);
|
|
|
16872 |
this.on(this.menuButton_, 'click', handleClick);
|
|
|
16873 |
this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
|
|
|
16874 |
this.on(this.menuButton_, 'mouseenter', () => {
|
|
|
16875 |
this.addClass('vjs-hover');
|
|
|
16876 |
this.menu.show();
|
|
|
16877 |
on(document, 'keyup', this.handleMenuKeyUp_);
|
|
|
16878 |
});
|
|
|
16879 |
this.on('mouseleave', e => this.handleMouseLeave(e));
|
|
|
16880 |
this.on('keydown', e => this.handleSubmenuKeyDown(e));
|
|
|
16881 |
}
|
|
|
16882 |
|
|
|
16883 |
/**
|
|
|
16884 |
* Update the menu based on the current state of its items.
|
|
|
16885 |
*/
|
|
|
16886 |
update() {
|
|
|
16887 |
const menu = this.createMenu();
|
|
|
16888 |
if (this.menu) {
|
|
|
16889 |
this.menu.dispose();
|
|
|
16890 |
this.removeChild(this.menu);
|
|
|
16891 |
}
|
|
|
16892 |
this.menu = menu;
|
|
|
16893 |
this.addChild(menu);
|
|
|
16894 |
|
|
|
16895 |
/**
|
|
|
16896 |
* Track the state of the menu button
|
|
|
16897 |
*
|
|
|
16898 |
* @type {Boolean}
|
|
|
16899 |
* @private
|
|
|
16900 |
*/
|
|
|
16901 |
this.buttonPressed_ = false;
|
|
|
16902 |
this.menuButton_.el_.setAttribute('aria-expanded', 'false');
|
|
|
16903 |
if (this.items && this.items.length <= this.hideThreshold_) {
|
|
|
16904 |
this.hide();
|
|
|
16905 |
this.menu.contentEl_.removeAttribute('role');
|
|
|
16906 |
} else {
|
|
|
16907 |
this.show();
|
|
|
16908 |
this.menu.contentEl_.setAttribute('role', 'menu');
|
|
|
16909 |
}
|
|
|
16910 |
}
|
|
|
16911 |
|
|
|
16912 |
/**
|
|
|
16913 |
* Create the menu and add all items to it.
|
|
|
16914 |
*
|
|
|
16915 |
* @return {Menu}
|
|
|
16916 |
* The constructed menu
|
|
|
16917 |
*/
|
|
|
16918 |
createMenu() {
|
|
|
16919 |
const menu = new Menu(this.player_, {
|
|
|
16920 |
menuButton: this
|
|
|
16921 |
});
|
|
|
16922 |
|
|
|
16923 |
/**
|
|
|
16924 |
* Hide the menu if the number of items is less than or equal to this threshold. This defaults
|
|
|
16925 |
* to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
|
|
|
16926 |
* it here because every time we run `createMenu` we need to reset the value.
|
|
|
16927 |
*
|
|
|
16928 |
* @protected
|
|
|
16929 |
* @type {Number}
|
|
|
16930 |
*/
|
|
|
16931 |
this.hideThreshold_ = 0;
|
|
|
16932 |
|
|
|
16933 |
// Add a title list item to the top
|
|
|
16934 |
if (this.options_.title) {
|
|
|
16935 |
const titleEl = createEl('li', {
|
|
|
16936 |
className: 'vjs-menu-title',
|
|
|
16937 |
textContent: toTitleCase$1(this.options_.title),
|
|
|
16938 |
tabIndex: -1
|
|
|
16939 |
});
|
|
|
16940 |
const titleComponent = new Component$1(this.player_, {
|
|
|
16941 |
el: titleEl
|
|
|
16942 |
});
|
|
|
16943 |
menu.addItem(titleComponent);
|
|
|
16944 |
}
|
|
|
16945 |
this.items = this.createItems();
|
|
|
16946 |
if (this.items) {
|
|
|
16947 |
// Add menu items to the menu
|
|
|
16948 |
for (let i = 0; i < this.items.length; i++) {
|
|
|
16949 |
menu.addItem(this.items[i]);
|
|
|
16950 |
}
|
|
|
16951 |
}
|
|
|
16952 |
return menu;
|
|
|
16953 |
}
|
|
|
16954 |
|
|
|
16955 |
/**
|
|
|
16956 |
* Create the list of menu items. Specific to each subclass.
|
|
|
16957 |
*
|
|
|
16958 |
* @abstract
|
|
|
16959 |
*/
|
|
|
16960 |
createItems() {}
|
|
|
16961 |
|
|
|
16962 |
/**
|
|
|
16963 |
* Create the `MenuButtons`s DOM element.
|
|
|
16964 |
*
|
|
|
16965 |
* @return {Element}
|
|
|
16966 |
* The element that gets created.
|
|
|
16967 |
*/
|
|
|
16968 |
createEl() {
|
|
|
16969 |
return super.createEl('div', {
|
|
|
16970 |
className: this.buildWrapperCSSClass()
|
|
|
16971 |
}, {});
|
|
|
16972 |
}
|
|
|
16973 |
|
|
|
16974 |
/**
|
|
|
16975 |
* Overwrites the `setIcon` method from `Component`.
|
|
|
16976 |
* In this case, we want the icon to be appended to the menuButton.
|
|
|
16977 |
*
|
|
|
16978 |
* @param {string} name
|
|
|
16979 |
* The icon name to be added.
|
|
|
16980 |
*/
|
|
|
16981 |
setIcon(name) {
|
|
|
16982 |
super.setIcon(name, this.menuButton_.el_);
|
|
|
16983 |
}
|
|
|
16984 |
|
|
|
16985 |
/**
|
|
|
16986 |
* Allow sub components to stack CSS class names for the wrapper element
|
|
|
16987 |
*
|
|
|
16988 |
* @return {string}
|
|
|
16989 |
* The constructed wrapper DOM `className`
|
|
|
16990 |
*/
|
|
|
16991 |
buildWrapperCSSClass() {
|
|
|
16992 |
let menuButtonClass = 'vjs-menu-button';
|
|
|
16993 |
|
|
|
16994 |
// If the inline option is passed, we want to use different styles altogether.
|
|
|
16995 |
if (this.options_.inline === true) {
|
|
|
16996 |
menuButtonClass += '-inline';
|
|
|
16997 |
} else {
|
|
|
16998 |
menuButtonClass += '-popup';
|
|
|
16999 |
}
|
|
|
17000 |
|
|
|
17001 |
// TODO: Fix the CSS so that this isn't necessary
|
|
|
17002 |
const buttonClass = Button.prototype.buildCSSClass();
|
|
|
17003 |
return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
|
|
|
17004 |
}
|
|
|
17005 |
|
|
|
17006 |
/**
|
|
|
17007 |
* Builds the default DOM `className`.
|
|
|
17008 |
*
|
|
|
17009 |
* @return {string}
|
|
|
17010 |
* The DOM `className` for this object.
|
|
|
17011 |
*/
|
|
|
17012 |
buildCSSClass() {
|
|
|
17013 |
let menuButtonClass = 'vjs-menu-button';
|
|
|
17014 |
|
|
|
17015 |
// If the inline option is passed, we want to use different styles altogether.
|
|
|
17016 |
if (this.options_.inline === true) {
|
|
|
17017 |
menuButtonClass += '-inline';
|
|
|
17018 |
} else {
|
|
|
17019 |
menuButtonClass += '-popup';
|
|
|
17020 |
}
|
|
|
17021 |
return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
|
|
|
17022 |
}
|
|
|
17023 |
|
|
|
17024 |
/**
|
|
|
17025 |
* Get or set the localized control text that will be used for accessibility.
|
|
|
17026 |
*
|
|
|
17027 |
* > NOTE: This will come from the internal `menuButton_` element.
|
|
|
17028 |
*
|
|
|
17029 |
* @param {string} [text]
|
|
|
17030 |
* Control text for element.
|
|
|
17031 |
*
|
|
|
17032 |
* @param {Element} [el=this.menuButton_.el()]
|
|
|
17033 |
* Element to set the title on.
|
|
|
17034 |
*
|
|
|
17035 |
* @return {string}
|
|
|
17036 |
* - The control text when getting
|
|
|
17037 |
*/
|
|
|
17038 |
controlText(text, el = this.menuButton_.el()) {
|
|
|
17039 |
return this.menuButton_.controlText(text, el);
|
|
|
17040 |
}
|
|
|
17041 |
|
|
|
17042 |
/**
|
|
|
17043 |
* Dispose of the `menu-button` and all child components.
|
|
|
17044 |
*/
|
|
|
17045 |
dispose() {
|
|
|
17046 |
this.handleMouseLeave();
|
|
|
17047 |
super.dispose();
|
|
|
17048 |
}
|
|
|
17049 |
|
|
|
17050 |
/**
|
|
|
17051 |
* Handle a click on a `MenuButton`.
|
|
|
17052 |
* See {@link ClickableComponent#handleClick} for instances where this is called.
|
|
|
17053 |
*
|
|
|
17054 |
* @param {Event} event
|
|
|
17055 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
17056 |
* called.
|
|
|
17057 |
*
|
|
|
17058 |
* @listens tap
|
|
|
17059 |
* @listens click
|
|
|
17060 |
*/
|
|
|
17061 |
handleClick(event) {
|
|
|
17062 |
if (this.buttonPressed_) {
|
|
|
17063 |
this.unpressButton();
|
|
|
17064 |
} else {
|
|
|
17065 |
this.pressButton();
|
|
|
17066 |
}
|
|
|
17067 |
}
|
|
|
17068 |
|
|
|
17069 |
/**
|
|
|
17070 |
* Handle `mouseleave` for `MenuButton`.
|
|
|
17071 |
*
|
|
|
17072 |
* @param {Event} event
|
|
|
17073 |
* The `mouseleave` event that caused this function to be called.
|
|
|
17074 |
*
|
|
|
17075 |
* @listens mouseleave
|
|
|
17076 |
*/
|
|
|
17077 |
handleMouseLeave(event) {
|
|
|
17078 |
this.removeClass('vjs-hover');
|
|
|
17079 |
off(document, 'keyup', this.handleMenuKeyUp_);
|
|
|
17080 |
}
|
|
|
17081 |
|
|
|
17082 |
/**
|
|
|
17083 |
* Set the focus to the actual button, not to this element
|
|
|
17084 |
*/
|
|
|
17085 |
focus() {
|
|
|
17086 |
this.menuButton_.focus();
|
|
|
17087 |
}
|
|
|
17088 |
|
|
|
17089 |
/**
|
|
|
17090 |
* Remove the focus from the actual button, not this element
|
|
|
17091 |
*/
|
|
|
17092 |
blur() {
|
|
|
17093 |
this.menuButton_.blur();
|
|
|
17094 |
}
|
|
|
17095 |
|
|
|
17096 |
/**
|
|
|
17097 |
* Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
|
|
|
17098 |
* {@link ClickableComponent#handleKeyDown} for instances where this is called.
|
|
|
17099 |
*
|
|
|
17100 |
* @param {Event} event
|
|
|
17101 |
* The `keydown` event that caused this function to be called.
|
|
|
17102 |
*
|
|
|
17103 |
* @listens keydown
|
|
|
17104 |
*/
|
|
|
17105 |
handleKeyDown(event) {
|
|
|
17106 |
// Escape or Tab unpress the 'button'
|
|
|
17107 |
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
|
|
|
17108 |
if (this.buttonPressed_) {
|
|
|
17109 |
this.unpressButton();
|
|
|
17110 |
}
|
|
|
17111 |
|
|
|
17112 |
// Don't preventDefault for Tab key - we still want to lose focus
|
|
|
17113 |
if (!keycode.isEventKey(event, 'Tab')) {
|
|
|
17114 |
event.preventDefault();
|
|
|
17115 |
// Set focus back to the menu button's button
|
|
|
17116 |
this.menuButton_.focus();
|
|
|
17117 |
}
|
|
|
17118 |
// Up Arrow or Down Arrow also 'press' the button to open the menu
|
|
|
17119 |
} else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) {
|
|
|
17120 |
if (!this.buttonPressed_) {
|
|
|
17121 |
event.preventDefault();
|
|
|
17122 |
this.pressButton();
|
|
|
17123 |
}
|
|
|
17124 |
}
|
|
|
17125 |
}
|
|
|
17126 |
|
|
|
17127 |
/**
|
|
|
17128 |
* Handle a `keyup` event on a `MenuButton`. The listener for this is added in
|
|
|
17129 |
* the constructor.
|
|
|
17130 |
*
|
|
|
17131 |
* @param {Event} event
|
|
|
17132 |
* Key press event
|
|
|
17133 |
*
|
|
|
17134 |
* @listens keyup
|
|
|
17135 |
*/
|
|
|
17136 |
handleMenuKeyUp(event) {
|
|
|
17137 |
// Escape hides popup menu
|
|
|
17138 |
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
|
|
|
17139 |
this.removeClass('vjs-hover');
|
|
|
17140 |
}
|
|
|
17141 |
}
|
|
|
17142 |
|
|
|
17143 |
/**
|
|
|
17144 |
* This method name now delegates to `handleSubmenuKeyDown`. This means
|
|
|
17145 |
* anyone calling `handleSubmenuKeyPress` will not see their method calls
|
|
|
17146 |
* stop working.
|
|
|
17147 |
*
|
|
|
17148 |
* @param {Event} event
|
|
|
17149 |
* The event that caused this function to be called.
|
|
|
17150 |
*/
|
|
|
17151 |
handleSubmenuKeyPress(event) {
|
|
|
17152 |
this.handleSubmenuKeyDown(event);
|
|
|
17153 |
}
|
|
|
17154 |
|
|
|
17155 |
/**
|
|
|
17156 |
* Handle a `keydown` event on a sub-menu. The listener for this is added in
|
|
|
17157 |
* the constructor.
|
|
|
17158 |
*
|
|
|
17159 |
* @param {Event} event
|
|
|
17160 |
* Key press event
|
|
|
17161 |
*
|
|
|
17162 |
* @listens keydown
|
|
|
17163 |
*/
|
|
|
17164 |
handleSubmenuKeyDown(event) {
|
|
|
17165 |
// Escape or Tab unpress the 'button'
|
|
|
17166 |
if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
|
|
|
17167 |
if (this.buttonPressed_) {
|
|
|
17168 |
this.unpressButton();
|
|
|
17169 |
}
|
|
|
17170 |
// Don't preventDefault for Tab key - we still want to lose focus
|
|
|
17171 |
if (!keycode.isEventKey(event, 'Tab')) {
|
|
|
17172 |
event.preventDefault();
|
|
|
17173 |
// Set focus back to the menu button's button
|
|
|
17174 |
this.menuButton_.focus();
|
|
|
17175 |
}
|
|
|
17176 |
}
|
|
|
17177 |
}
|
|
|
17178 |
|
|
|
17179 |
/**
|
|
|
17180 |
* Put the current `MenuButton` into a pressed state.
|
|
|
17181 |
*/
|
|
|
17182 |
pressButton() {
|
|
|
17183 |
if (this.enabled_) {
|
|
|
17184 |
this.buttonPressed_ = true;
|
|
|
17185 |
this.menu.show();
|
|
|
17186 |
this.menu.lockShowing();
|
|
|
17187 |
this.menuButton_.el_.setAttribute('aria-expanded', 'true');
|
|
|
17188 |
|
|
|
17189 |
// set the focus into the submenu, except on iOS where it is resulting in
|
|
|
17190 |
// undesired scrolling behavior when the player is in an iframe
|
|
|
17191 |
if (IS_IOS && isInFrame()) {
|
|
|
17192 |
// Return early so that the menu isn't focused
|
|
|
17193 |
return;
|
|
|
17194 |
}
|
|
|
17195 |
this.menu.focus();
|
|
|
17196 |
}
|
|
|
17197 |
}
|
|
|
17198 |
|
|
|
17199 |
/**
|
|
|
17200 |
* Take the current `MenuButton` out of a pressed state.
|
|
|
17201 |
*/
|
|
|
17202 |
unpressButton() {
|
|
|
17203 |
if (this.enabled_) {
|
|
|
17204 |
this.buttonPressed_ = false;
|
|
|
17205 |
this.menu.unlockShowing();
|
|
|
17206 |
this.menu.hide();
|
|
|
17207 |
this.menuButton_.el_.setAttribute('aria-expanded', 'false');
|
|
|
17208 |
}
|
|
|
17209 |
}
|
|
|
17210 |
|
|
|
17211 |
/**
|
|
|
17212 |
* Disable the `MenuButton`. Don't allow it to be clicked.
|
|
|
17213 |
*/
|
|
|
17214 |
disable() {
|
|
|
17215 |
this.unpressButton();
|
|
|
17216 |
this.enabled_ = false;
|
|
|
17217 |
this.addClass('vjs-disabled');
|
|
|
17218 |
this.menuButton_.disable();
|
|
|
17219 |
}
|
|
|
17220 |
|
|
|
17221 |
/**
|
|
|
17222 |
* Enable the `MenuButton`. Allow it to be clicked.
|
|
|
17223 |
*/
|
|
|
17224 |
enable() {
|
|
|
17225 |
this.enabled_ = true;
|
|
|
17226 |
this.removeClass('vjs-disabled');
|
|
|
17227 |
this.menuButton_.enable();
|
|
|
17228 |
}
|
|
|
17229 |
}
|
|
|
17230 |
Component$1.registerComponent('MenuButton', MenuButton);
|
|
|
17231 |
|
|
|
17232 |
/**
|
|
|
17233 |
* @file track-button.js
|
|
|
17234 |
*/
|
|
|
17235 |
|
|
|
17236 |
/**
|
|
|
17237 |
* The base class for buttons that toggle specific track types (e.g. subtitles).
|
|
|
17238 |
*
|
|
|
17239 |
* @extends MenuButton
|
|
|
17240 |
*/
|
|
|
17241 |
class TrackButton extends MenuButton {
|
|
|
17242 |
/**
|
|
|
17243 |
* Creates an instance of this class.
|
|
|
17244 |
*
|
|
|
17245 |
* @param { import('./player').default } player
|
|
|
17246 |
* The `Player` that this class should be attached to.
|
|
|
17247 |
*
|
|
|
17248 |
* @param {Object} [options]
|
|
|
17249 |
* The key/value store of player options.
|
|
|
17250 |
*/
|
|
|
17251 |
constructor(player, options) {
|
|
|
17252 |
const tracks = options.tracks;
|
|
|
17253 |
super(player, options);
|
|
|
17254 |
if (this.items.length <= 1) {
|
|
|
17255 |
this.hide();
|
|
|
17256 |
}
|
|
|
17257 |
if (!tracks) {
|
|
|
17258 |
return;
|
|
|
17259 |
}
|
|
|
17260 |
const updateHandler = bind_(this, this.update);
|
|
|
17261 |
tracks.addEventListener('removetrack', updateHandler);
|
|
|
17262 |
tracks.addEventListener('addtrack', updateHandler);
|
|
|
17263 |
tracks.addEventListener('labelchange', updateHandler);
|
|
|
17264 |
this.player_.on('ready', updateHandler);
|
|
|
17265 |
this.player_.on('dispose', function () {
|
|
|
17266 |
tracks.removeEventListener('removetrack', updateHandler);
|
|
|
17267 |
tracks.removeEventListener('addtrack', updateHandler);
|
|
|
17268 |
tracks.removeEventListener('labelchange', updateHandler);
|
|
|
17269 |
});
|
|
|
17270 |
}
|
|
|
17271 |
}
|
|
|
17272 |
Component$1.registerComponent('TrackButton', TrackButton);
|
|
|
17273 |
|
|
|
17274 |
/**
|
|
|
17275 |
* @file menu-keys.js
|
|
|
17276 |
*/
|
|
|
17277 |
|
|
|
17278 |
/**
|
|
|
17279 |
* All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`)
|
|
|
17280 |
* Note that 'Enter' and 'Space' are not included here (otherwise they would
|
|
|
17281 |
* prevent the `MenuButton` and `MenuItem` from being keyboard-clickable)
|
|
|
17282 |
*
|
|
|
17283 |
* @typedef MenuKeys
|
|
|
17284 |
* @array
|
|
|
17285 |
*/
|
|
|
17286 |
const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left'];
|
|
|
17287 |
|
|
|
17288 |
/**
|
|
|
17289 |
* @file menu-item.js
|
|
|
17290 |
*/
|
|
|
17291 |
|
|
|
17292 |
/**
|
|
|
17293 |
* The component for a menu item. `<li>`
|
|
|
17294 |
*
|
|
|
17295 |
* @extends ClickableComponent
|
|
|
17296 |
*/
|
|
|
17297 |
class MenuItem extends ClickableComponent {
|
|
|
17298 |
/**
|
|
|
17299 |
* Creates an instance of the this class.
|
|
|
17300 |
*
|
|
|
17301 |
* @param { import('../player').default } player
|
|
|
17302 |
* The `Player` that this class should be attached to.
|
|
|
17303 |
*
|
|
|
17304 |
* @param {Object} [options={}]
|
|
|
17305 |
* The key/value store of player options.
|
|
|
17306 |
*
|
|
|
17307 |
*/
|
|
|
17308 |
constructor(player, options) {
|
|
|
17309 |
super(player, options);
|
|
|
17310 |
this.selectable = options.selectable;
|
|
|
17311 |
this.isSelected_ = options.selected || false;
|
|
|
17312 |
this.multiSelectable = options.multiSelectable;
|
|
|
17313 |
this.selected(this.isSelected_);
|
|
|
17314 |
if (this.selectable) {
|
|
|
17315 |
if (this.multiSelectable) {
|
|
|
17316 |
this.el_.setAttribute('role', 'menuitemcheckbox');
|
|
|
17317 |
} else {
|
|
|
17318 |
this.el_.setAttribute('role', 'menuitemradio');
|
|
|
17319 |
}
|
|
|
17320 |
} else {
|
|
|
17321 |
this.el_.setAttribute('role', 'menuitem');
|
|
|
17322 |
}
|
|
|
17323 |
}
|
|
|
17324 |
|
|
|
17325 |
/**
|
|
|
17326 |
* Create the `MenuItem's DOM element
|
|
|
17327 |
*
|
|
|
17328 |
* @param {string} [type=li]
|
|
|
17329 |
* Element's node type, not actually used, always set to `li`.
|
|
|
17330 |
*
|
|
|
17331 |
* @param {Object} [props={}]
|
|
|
17332 |
* An object of properties that should be set on the element
|
|
|
17333 |
*
|
|
|
17334 |
* @param {Object} [attrs={}]
|
|
|
17335 |
* An object of attributes that should be set on the element
|
|
|
17336 |
*
|
|
|
17337 |
* @return {Element}
|
|
|
17338 |
* The element that gets created.
|
|
|
17339 |
*/
|
|
|
17340 |
createEl(type, props, attrs) {
|
|
|
17341 |
// The control is textual, not just an icon
|
|
|
17342 |
this.nonIconControl = true;
|
|
|
17343 |
const el = super.createEl('li', Object.assign({
|
|
|
17344 |
className: 'vjs-menu-item',
|
|
|
17345 |
tabIndex: -1
|
|
|
17346 |
}, props), attrs);
|
|
|
17347 |
|
|
|
17348 |
// swap icon with menu item text.
|
|
|
17349 |
const menuItemEl = createEl('span', {
|
|
|
17350 |
className: 'vjs-menu-item-text',
|
|
|
17351 |
textContent: this.localize(this.options_.label)
|
|
|
17352 |
});
|
|
|
17353 |
|
|
|
17354 |
// If using SVG icons, the element with vjs-icon-placeholder will be added separately.
|
|
|
17355 |
if (this.player_.options_.experimentalSvgIcons) {
|
|
|
17356 |
el.appendChild(menuItemEl);
|
|
|
17357 |
} else {
|
|
|
17358 |
el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder'));
|
|
|
17359 |
}
|
|
|
17360 |
return el;
|
|
|
17361 |
}
|
|
|
17362 |
|
|
|
17363 |
/**
|
|
|
17364 |
* Ignore keys which are used by the menu, but pass any other ones up. See
|
|
|
17365 |
* {@link ClickableComponent#handleKeyDown} for instances where this is called.
|
|
|
17366 |
*
|
|
|
17367 |
* @param {KeyboardEvent} event
|
|
|
17368 |
* The `keydown` event that caused this function to be called.
|
|
|
17369 |
*
|
|
|
17370 |
* @listens keydown
|
|
|
17371 |
*/
|
|
|
17372 |
handleKeyDown(event) {
|
|
|
17373 |
if (!MenuKeys.some(key => keycode.isEventKey(event, key))) {
|
|
|
17374 |
// Pass keydown handling up for unused keys
|
|
|
17375 |
super.handleKeyDown(event);
|
|
|
17376 |
}
|
|
|
17377 |
}
|
|
|
17378 |
|
|
|
17379 |
/**
|
|
|
17380 |
* Any click on a `MenuItem` puts it into the selected state.
|
|
|
17381 |
* See {@link ClickableComponent#handleClick} for instances where this is called.
|
|
|
17382 |
*
|
|
|
17383 |
* @param {Event} event
|
|
|
17384 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
17385 |
* called.
|
|
|
17386 |
*
|
|
|
17387 |
* @listens tap
|
|
|
17388 |
* @listens click
|
|
|
17389 |
*/
|
|
|
17390 |
handleClick(event) {
|
|
|
17391 |
this.selected(true);
|
|
|
17392 |
}
|
|
|
17393 |
|
|
|
17394 |
/**
|
|
|
17395 |
* Set the state for this menu item as selected or not.
|
|
|
17396 |
*
|
|
|
17397 |
* @param {boolean} selected
|
|
|
17398 |
* if the menu item is selected or not
|
|
|
17399 |
*/
|
|
|
17400 |
selected(selected) {
|
|
|
17401 |
if (this.selectable) {
|
|
|
17402 |
if (selected) {
|
|
|
17403 |
this.addClass('vjs-selected');
|
|
|
17404 |
this.el_.setAttribute('aria-checked', 'true');
|
|
|
17405 |
// aria-checked isn't fully supported by browsers/screen readers,
|
|
|
17406 |
// so indicate selected state to screen reader in the control text.
|
|
|
17407 |
this.controlText(', selected');
|
|
|
17408 |
this.isSelected_ = true;
|
|
|
17409 |
} else {
|
|
|
17410 |
this.removeClass('vjs-selected');
|
|
|
17411 |
this.el_.setAttribute('aria-checked', 'false');
|
|
|
17412 |
// Indicate un-selected state to screen reader
|
|
|
17413 |
this.controlText('');
|
|
|
17414 |
this.isSelected_ = false;
|
|
|
17415 |
}
|
|
|
17416 |
}
|
|
|
17417 |
}
|
|
|
17418 |
}
|
|
|
17419 |
Component$1.registerComponent('MenuItem', MenuItem);
|
|
|
17420 |
|
|
|
17421 |
/**
|
|
|
17422 |
* @file text-track-menu-item.js
|
|
|
17423 |
*/
|
|
|
17424 |
|
|
|
17425 |
/**
|
|
|
17426 |
* The specific menu item type for selecting a language within a text track kind
|
|
|
17427 |
*
|
|
|
17428 |
* @extends MenuItem
|
|
|
17429 |
*/
|
|
|
17430 |
class TextTrackMenuItem extends MenuItem {
|
|
|
17431 |
/**
|
|
|
17432 |
* Creates an instance of this class.
|
|
|
17433 |
*
|
|
|
17434 |
* @param { import('../../player').default } player
|
|
|
17435 |
* The `Player` that this class should be attached to.
|
|
|
17436 |
*
|
|
|
17437 |
* @param {Object} [options]
|
|
|
17438 |
* The key/value store of player options.
|
|
|
17439 |
*/
|
|
|
17440 |
constructor(player, options) {
|
|
|
17441 |
const track = options.track;
|
|
|
17442 |
const tracks = player.textTracks();
|
|
|
17443 |
|
|
|
17444 |
// Modify options for parent MenuItem class's init.
|
|
|
17445 |
options.label = track.label || track.language || 'Unknown';
|
|
|
17446 |
options.selected = track.mode === 'showing';
|
|
|
17447 |
super(player, options);
|
|
|
17448 |
this.track = track;
|
|
|
17449 |
// Determine the relevant kind(s) of tracks for this component and filter
|
|
|
17450 |
// out empty kinds.
|
|
|
17451 |
this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
|
|
|
17452 |
const changeHandler = (...args) => {
|
|
|
17453 |
this.handleTracksChange.apply(this, args);
|
|
|
17454 |
};
|
|
|
17455 |
const selectedLanguageChangeHandler = (...args) => {
|
|
|
17456 |
this.handleSelectedLanguageChange.apply(this, args);
|
|
|
17457 |
};
|
|
|
17458 |
player.on(['loadstart', 'texttrackchange'], changeHandler);
|
|
|
17459 |
tracks.addEventListener('change', changeHandler);
|
|
|
17460 |
tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
|
|
|
17461 |
this.on('dispose', function () {
|
|
|
17462 |
player.off(['loadstart', 'texttrackchange'], changeHandler);
|
|
|
17463 |
tracks.removeEventListener('change', changeHandler);
|
|
|
17464 |
tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
|
|
|
17465 |
});
|
|
|
17466 |
|
|
|
17467 |
// iOS7 doesn't dispatch change events to TextTrackLists when an
|
|
|
17468 |
// associated track's mode changes. Without something like
|
|
|
17469 |
// Object.observe() (also not present on iOS7), it's not
|
|
|
17470 |
// possible to detect changes to the mode attribute and polyfill
|
|
|
17471 |
// the change event. As a poor substitute, we manually dispatch
|
|
|
17472 |
// change events whenever the controls modify the mode.
|
|
|
17473 |
if (tracks.onchange === undefined) {
|
|
|
17474 |
let event;
|
|
|
17475 |
this.on(['tap', 'click'], function () {
|
|
|
17476 |
if (typeof window.Event !== 'object') {
|
|
|
17477 |
// Android 2.3 throws an Illegal Constructor error for window.Event
|
|
|
17478 |
try {
|
|
|
17479 |
event = new window.Event('change');
|
|
|
17480 |
} catch (err) {
|
|
|
17481 |
// continue regardless of error
|
|
|
17482 |
}
|
|
|
17483 |
}
|
|
|
17484 |
if (!event) {
|
|
|
17485 |
event = document.createEvent('Event');
|
|
|
17486 |
event.initEvent('change', true, true);
|
|
|
17487 |
}
|
|
|
17488 |
tracks.dispatchEvent(event);
|
|
|
17489 |
});
|
|
|
17490 |
}
|
|
|
17491 |
|
|
|
17492 |
// set the default state based on current tracks
|
|
|
17493 |
this.handleTracksChange();
|
|
|
17494 |
}
|
|
|
17495 |
|
|
|
17496 |
/**
|
|
|
17497 |
* This gets called when an `TextTrackMenuItem` is "clicked". See
|
|
|
17498 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
17499 |
*
|
|
|
17500 |
* @param {Event} event
|
|
|
17501 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
17502 |
* called.
|
|
|
17503 |
*
|
|
|
17504 |
* @listens tap
|
|
|
17505 |
* @listens click
|
|
|
17506 |
*/
|
|
|
17507 |
handleClick(event) {
|
|
|
17508 |
const referenceTrack = this.track;
|
|
|
17509 |
const tracks = this.player_.textTracks();
|
|
|
17510 |
super.handleClick(event);
|
|
|
17511 |
if (!tracks) {
|
|
|
17512 |
return;
|
|
|
17513 |
}
|
|
|
17514 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
17515 |
const track = tracks[i];
|
|
|
17516 |
|
|
|
17517 |
// If the track from the text tracks list is not of the right kind,
|
|
|
17518 |
// skip it. We do not want to affect tracks of incompatible kind(s).
|
|
|
17519 |
if (this.kinds.indexOf(track.kind) === -1) {
|
|
|
17520 |
continue;
|
|
|
17521 |
}
|
|
|
17522 |
|
|
|
17523 |
// If this text track is the component's track and it is not showing,
|
|
|
17524 |
// set it to showing.
|
|
|
17525 |
if (track === referenceTrack) {
|
|
|
17526 |
if (track.mode !== 'showing') {
|
|
|
17527 |
track.mode = 'showing';
|
|
|
17528 |
}
|
|
|
17529 |
|
|
|
17530 |
// If this text track is not the component's track and it is not
|
|
|
17531 |
// disabled, set it to disabled.
|
|
|
17532 |
} else if (track.mode !== 'disabled') {
|
|
|
17533 |
track.mode = 'disabled';
|
|
|
17534 |
}
|
|
|
17535 |
}
|
|
|
17536 |
}
|
|
|
17537 |
|
|
|
17538 |
/**
|
|
|
17539 |
* Handle text track list change
|
|
|
17540 |
*
|
|
|
17541 |
* @param {Event} event
|
|
|
17542 |
* The `change` event that caused this function to be called.
|
|
|
17543 |
*
|
|
|
17544 |
* @listens TextTrackList#change
|
|
|
17545 |
*/
|
|
|
17546 |
handleTracksChange(event) {
|
|
|
17547 |
const shouldBeSelected = this.track.mode === 'showing';
|
|
|
17548 |
|
|
|
17549 |
// Prevent redundant selected() calls because they may cause
|
|
|
17550 |
// screen readers to read the appended control text unnecessarily
|
|
|
17551 |
if (shouldBeSelected !== this.isSelected_) {
|
|
|
17552 |
this.selected(shouldBeSelected);
|
|
|
17553 |
}
|
|
|
17554 |
}
|
|
|
17555 |
handleSelectedLanguageChange(event) {
|
|
|
17556 |
if (this.track.mode === 'showing') {
|
|
|
17557 |
const selectedLanguage = this.player_.cache_.selectedLanguage;
|
|
|
17558 |
|
|
|
17559 |
// Don't replace the kind of track across the same language
|
|
|
17560 |
if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
|
|
|
17561 |
return;
|
|
|
17562 |
}
|
|
|
17563 |
this.player_.cache_.selectedLanguage = {
|
|
|
17564 |
enabled: true,
|
|
|
17565 |
language: this.track.language,
|
|
|
17566 |
kind: this.track.kind
|
|
|
17567 |
};
|
|
|
17568 |
}
|
|
|
17569 |
}
|
|
|
17570 |
dispose() {
|
|
|
17571 |
// remove reference to track object on dispose
|
|
|
17572 |
this.track = null;
|
|
|
17573 |
super.dispose();
|
|
|
17574 |
}
|
|
|
17575 |
}
|
|
|
17576 |
Component$1.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
|
|
|
17577 |
|
|
|
17578 |
/**
|
|
|
17579 |
* @file off-text-track-menu-item.js
|
|
|
17580 |
*/
|
|
|
17581 |
|
|
|
17582 |
/**
|
|
|
17583 |
* A special menu item for turning off a specific type of text track
|
|
|
17584 |
*
|
|
|
17585 |
* @extends TextTrackMenuItem
|
|
|
17586 |
*/
|
|
|
17587 |
class OffTextTrackMenuItem extends TextTrackMenuItem {
|
|
|
17588 |
/**
|
|
|
17589 |
* Creates an instance of this class.
|
|
|
17590 |
*
|
|
|
17591 |
* @param { import('../../player').default } player
|
|
|
17592 |
* The `Player` that this class should be attached to.
|
|
|
17593 |
*
|
|
|
17594 |
* @param {Object} [options]
|
|
|
17595 |
* The key/value store of player options.
|
|
|
17596 |
*/
|
|
|
17597 |
constructor(player, options) {
|
|
|
17598 |
// Create pseudo track info
|
|
|
17599 |
// Requires options['kind']
|
|
|
17600 |
options.track = {
|
|
|
17601 |
player,
|
|
|
17602 |
// it is no longer necessary to store `kind` or `kinds` on the track itself
|
|
|
17603 |
// since they are now stored in the `kinds` property of all instances of
|
|
|
17604 |
// TextTrackMenuItem, but this will remain for backwards compatibility
|
|
|
17605 |
kind: options.kind,
|
|
|
17606 |
kinds: options.kinds,
|
|
|
17607 |
default: false,
|
|
|
17608 |
mode: 'disabled'
|
|
|
17609 |
};
|
|
|
17610 |
if (!options.kinds) {
|
|
|
17611 |
options.kinds = [options.kind];
|
|
|
17612 |
}
|
|
|
17613 |
if (options.label) {
|
|
|
17614 |
options.track.label = options.label;
|
|
|
17615 |
} else {
|
|
|
17616 |
options.track.label = options.kinds.join(' and ') + ' off';
|
|
|
17617 |
}
|
|
|
17618 |
|
|
|
17619 |
// MenuItem is selectable
|
|
|
17620 |
options.selectable = true;
|
|
|
17621 |
// MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
|
|
|
17622 |
options.multiSelectable = false;
|
|
|
17623 |
super(player, options);
|
|
|
17624 |
}
|
|
|
17625 |
|
|
|
17626 |
/**
|
|
|
17627 |
* Handle text track change
|
|
|
17628 |
*
|
|
|
17629 |
* @param {Event} event
|
|
|
17630 |
* The event that caused this function to run
|
|
|
17631 |
*/
|
|
|
17632 |
handleTracksChange(event) {
|
|
|
17633 |
const tracks = this.player().textTracks();
|
|
|
17634 |
let shouldBeSelected = true;
|
|
|
17635 |
for (let i = 0, l = tracks.length; i < l; i++) {
|
|
|
17636 |
const track = tracks[i];
|
|
|
17637 |
if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
|
|
|
17638 |
shouldBeSelected = false;
|
|
|
17639 |
break;
|
|
|
17640 |
}
|
|
|
17641 |
}
|
|
|
17642 |
|
|
|
17643 |
// Prevent redundant selected() calls because they may cause
|
|
|
17644 |
// screen readers to read the appended control text unnecessarily
|
|
|
17645 |
if (shouldBeSelected !== this.isSelected_) {
|
|
|
17646 |
this.selected(shouldBeSelected);
|
|
|
17647 |
}
|
|
|
17648 |
}
|
|
|
17649 |
handleSelectedLanguageChange(event) {
|
|
|
17650 |
const tracks = this.player().textTracks();
|
|
|
17651 |
let allHidden = true;
|
|
|
17652 |
for (let i = 0, l = tracks.length; i < l; i++) {
|
|
|
17653 |
const track = tracks[i];
|
|
|
17654 |
if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
|
|
|
17655 |
allHidden = false;
|
|
|
17656 |
break;
|
|
|
17657 |
}
|
|
|
17658 |
}
|
|
|
17659 |
if (allHidden) {
|
|
|
17660 |
this.player_.cache_.selectedLanguage = {
|
|
|
17661 |
enabled: false
|
|
|
17662 |
};
|
|
|
17663 |
}
|
|
|
17664 |
}
|
|
|
17665 |
|
|
|
17666 |
/**
|
|
|
17667 |
* Update control text and label on languagechange
|
|
|
17668 |
*/
|
|
|
17669 |
handleLanguagechange() {
|
|
|
17670 |
this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
|
|
|
17671 |
super.handleLanguagechange();
|
|
|
17672 |
}
|
|
|
17673 |
}
|
|
|
17674 |
Component$1.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
|
|
|
17675 |
|
|
|
17676 |
/**
|
|
|
17677 |
* @file text-track-button.js
|
|
|
17678 |
*/
|
|
|
17679 |
|
|
|
17680 |
/**
|
|
|
17681 |
* The base class for buttons that toggle specific text track types (e.g. subtitles)
|
|
|
17682 |
*
|
|
|
17683 |
* @extends MenuButton
|
|
|
17684 |
*/
|
|
|
17685 |
class TextTrackButton extends TrackButton {
|
|
|
17686 |
/**
|
|
|
17687 |
* Creates an instance of this class.
|
|
|
17688 |
*
|
|
|
17689 |
* @param { import('../../player').default } player
|
|
|
17690 |
* The `Player` that this class should be attached to.
|
|
|
17691 |
*
|
|
|
17692 |
* @param {Object} [options={}]
|
|
|
17693 |
* The key/value store of player options.
|
|
|
17694 |
*/
|
|
|
17695 |
constructor(player, options = {}) {
|
|
|
17696 |
options.tracks = player.textTracks();
|
|
|
17697 |
super(player, options);
|
|
|
17698 |
}
|
|
|
17699 |
|
|
|
17700 |
/**
|
|
|
17701 |
* Create a menu item for each text track
|
|
|
17702 |
*
|
|
|
17703 |
* @param {TextTrackMenuItem[]} [items=[]]
|
|
|
17704 |
* Existing array of items to use during creation
|
|
|
17705 |
*
|
|
|
17706 |
* @return {TextTrackMenuItem[]}
|
|
|
17707 |
* Array of menu items that were created
|
|
|
17708 |
*/
|
|
|
17709 |
createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
|
|
|
17710 |
// Label is an override for the [track] off label
|
|
|
17711 |
// USed to localise captions/subtitles
|
|
|
17712 |
let label;
|
|
|
17713 |
if (this.label_) {
|
|
|
17714 |
label = `${this.label_} off`;
|
|
|
17715 |
}
|
|
|
17716 |
// Add an OFF menu item to turn all tracks off
|
|
|
17717 |
items.push(new OffTextTrackMenuItem(this.player_, {
|
|
|
17718 |
kinds: this.kinds_,
|
|
|
17719 |
kind: this.kind_,
|
|
|
17720 |
label
|
|
|
17721 |
}));
|
|
|
17722 |
this.hideThreshold_ += 1;
|
|
|
17723 |
const tracks = this.player_.textTracks();
|
|
|
17724 |
if (!Array.isArray(this.kinds_)) {
|
|
|
17725 |
this.kinds_ = [this.kind_];
|
|
|
17726 |
}
|
|
|
17727 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
17728 |
const track = tracks[i];
|
|
|
17729 |
|
|
|
17730 |
// only add tracks that are of an appropriate kind and have a label
|
|
|
17731 |
if (this.kinds_.indexOf(track.kind) > -1) {
|
|
|
17732 |
const item = new TrackMenuItem(this.player_, {
|
|
|
17733 |
track,
|
|
|
17734 |
kinds: this.kinds_,
|
|
|
17735 |
kind: this.kind_,
|
|
|
17736 |
// MenuItem is selectable
|
|
|
17737 |
selectable: true,
|
|
|
17738 |
// MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
|
|
|
17739 |
multiSelectable: false
|
|
|
17740 |
});
|
|
|
17741 |
item.addClass(`vjs-${track.kind}-menu-item`);
|
|
|
17742 |
items.push(item);
|
|
|
17743 |
}
|
|
|
17744 |
}
|
|
|
17745 |
return items;
|
|
|
17746 |
}
|
|
|
17747 |
}
|
|
|
17748 |
Component$1.registerComponent('TextTrackButton', TextTrackButton);
|
|
|
17749 |
|
|
|
17750 |
/**
|
|
|
17751 |
* @file chapters-track-menu-item.js
|
|
|
17752 |
*/
|
|
|
17753 |
|
|
|
17754 |
/**
|
|
|
17755 |
* The chapter track menu item
|
|
|
17756 |
*
|
|
|
17757 |
* @extends MenuItem
|
|
|
17758 |
*/
|
|
|
17759 |
class ChaptersTrackMenuItem extends MenuItem {
|
|
|
17760 |
/**
|
|
|
17761 |
* Creates an instance of this class.
|
|
|
17762 |
*
|
|
|
17763 |
* @param { import('../../player').default } player
|
|
|
17764 |
* The `Player` that this class should be attached to.
|
|
|
17765 |
*
|
|
|
17766 |
* @param {Object} [options]
|
|
|
17767 |
* The key/value store of player options.
|
|
|
17768 |
*/
|
|
|
17769 |
constructor(player, options) {
|
|
|
17770 |
const track = options.track;
|
|
|
17771 |
const cue = options.cue;
|
|
|
17772 |
const currentTime = player.currentTime();
|
|
|
17773 |
|
|
|
17774 |
// Modify options for parent MenuItem class's init.
|
|
|
17775 |
options.selectable = true;
|
|
|
17776 |
options.multiSelectable = false;
|
|
|
17777 |
options.label = cue.text;
|
|
|
17778 |
options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
|
|
|
17779 |
super(player, options);
|
|
|
17780 |
this.track = track;
|
|
|
17781 |
this.cue = cue;
|
|
|
17782 |
}
|
|
|
17783 |
|
|
|
17784 |
/**
|
|
|
17785 |
* This gets called when an `ChaptersTrackMenuItem` is "clicked". See
|
|
|
17786 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
17787 |
*
|
|
|
17788 |
* @param {Event} [event]
|
|
|
17789 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
17790 |
* called.
|
|
|
17791 |
*
|
|
|
17792 |
* @listens tap
|
|
|
17793 |
* @listens click
|
|
|
17794 |
*/
|
|
|
17795 |
handleClick(event) {
|
|
|
17796 |
super.handleClick();
|
|
|
17797 |
this.player_.currentTime(this.cue.startTime);
|
|
|
17798 |
}
|
|
|
17799 |
}
|
|
|
17800 |
Component$1.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
|
|
|
17801 |
|
|
|
17802 |
/**
|
|
|
17803 |
* @file chapters-button.js
|
|
|
17804 |
*/
|
|
|
17805 |
|
|
|
17806 |
/**
|
|
|
17807 |
* The button component for toggling and selecting chapters
|
|
|
17808 |
* Chapters act much differently than other text tracks
|
|
|
17809 |
* Cues are navigation vs. other tracks of alternative languages
|
|
|
17810 |
*
|
|
|
17811 |
* @extends TextTrackButton
|
|
|
17812 |
*/
|
|
|
17813 |
class ChaptersButton extends TextTrackButton {
|
|
|
17814 |
/**
|
|
|
17815 |
* Creates an instance of this class.
|
|
|
17816 |
*
|
|
|
17817 |
* @param { import('../../player').default } player
|
|
|
17818 |
* The `Player` that this class should be attached to.
|
|
|
17819 |
*
|
|
|
17820 |
* @param {Object} [options]
|
|
|
17821 |
* The key/value store of player options.
|
|
|
17822 |
*
|
|
|
17823 |
* @param {Function} [ready]
|
|
|
17824 |
* The function to call when this function is ready.
|
|
|
17825 |
*/
|
|
|
17826 |
constructor(player, options, ready) {
|
|
|
17827 |
super(player, options, ready);
|
|
|
17828 |
this.setIcon('chapters');
|
|
|
17829 |
this.selectCurrentItem_ = () => {
|
|
|
17830 |
this.items.forEach(item => {
|
|
|
17831 |
item.selected(this.track_.activeCues[0] === item.cue);
|
|
|
17832 |
});
|
|
|
17833 |
};
|
|
|
17834 |
}
|
|
|
17835 |
|
|
|
17836 |
/**
|
|
|
17837 |
* Builds the default DOM `className`.
|
|
|
17838 |
*
|
|
|
17839 |
* @return {string}
|
|
|
17840 |
* The DOM `className` for this object.
|
|
|
17841 |
*/
|
|
|
17842 |
buildCSSClass() {
|
|
|
17843 |
return `vjs-chapters-button ${super.buildCSSClass()}`;
|
|
|
17844 |
}
|
|
|
17845 |
buildWrapperCSSClass() {
|
|
|
17846 |
return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
|
|
|
17847 |
}
|
|
|
17848 |
|
|
|
17849 |
/**
|
|
|
17850 |
* Update the menu based on the current state of its items.
|
|
|
17851 |
*
|
|
|
17852 |
* @param {Event} [event]
|
|
|
17853 |
* An event that triggered this function to run.
|
|
|
17854 |
*
|
|
|
17855 |
* @listens TextTrackList#addtrack
|
|
|
17856 |
* @listens TextTrackList#removetrack
|
|
|
17857 |
* @listens TextTrackList#change
|
|
|
17858 |
*/
|
|
|
17859 |
update(event) {
|
|
|
17860 |
if (event && event.track && event.track.kind !== 'chapters') {
|
|
|
17861 |
return;
|
|
|
17862 |
}
|
|
|
17863 |
const track = this.findChaptersTrack();
|
|
|
17864 |
if (track !== this.track_) {
|
|
|
17865 |
this.setTrack(track);
|
|
|
17866 |
super.update();
|
|
|
17867 |
} else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
|
|
|
17868 |
// Update the menu initially or if the number of cues has changed since set
|
|
|
17869 |
super.update();
|
|
|
17870 |
}
|
|
|
17871 |
}
|
|
|
17872 |
|
|
|
17873 |
/**
|
|
|
17874 |
* Set the currently selected track for the chapters button.
|
|
|
17875 |
*
|
|
|
17876 |
* @param {TextTrack} track
|
|
|
17877 |
* The new track to select. Nothing will change if this is the currently selected
|
|
|
17878 |
* track.
|
|
|
17879 |
*/
|
|
|
17880 |
setTrack(track) {
|
|
|
17881 |
if (this.track_ === track) {
|
|
|
17882 |
return;
|
|
|
17883 |
}
|
|
|
17884 |
if (!this.updateHandler_) {
|
|
|
17885 |
this.updateHandler_ = this.update.bind(this);
|
|
|
17886 |
}
|
|
|
17887 |
|
|
|
17888 |
// here this.track_ refers to the old track instance
|
|
|
17889 |
if (this.track_) {
|
|
|
17890 |
const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
|
|
|
17891 |
if (remoteTextTrackEl) {
|
|
|
17892 |
remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
|
|
|
17893 |
}
|
|
|
17894 |
this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
|
|
|
17895 |
this.track_ = null;
|
|
|
17896 |
}
|
|
|
17897 |
this.track_ = track;
|
|
|
17898 |
|
|
|
17899 |
// here this.track_ refers to the new track instance
|
|
|
17900 |
if (this.track_) {
|
|
|
17901 |
this.track_.mode = 'hidden';
|
|
|
17902 |
const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
|
|
|
17903 |
if (remoteTextTrackEl) {
|
|
|
17904 |
remoteTextTrackEl.addEventListener('load', this.updateHandler_);
|
|
|
17905 |
}
|
|
|
17906 |
this.track_.addEventListener('cuechange', this.selectCurrentItem_);
|
|
|
17907 |
}
|
|
|
17908 |
}
|
|
|
17909 |
|
|
|
17910 |
/**
|
|
|
17911 |
* Find the track object that is currently in use by this ChaptersButton
|
|
|
17912 |
*
|
|
|
17913 |
* @return {TextTrack|undefined}
|
|
|
17914 |
* The current track or undefined if none was found.
|
|
|
17915 |
*/
|
|
|
17916 |
findChaptersTrack() {
|
|
|
17917 |
const tracks = this.player_.textTracks() || [];
|
|
|
17918 |
for (let i = tracks.length - 1; i >= 0; i--) {
|
|
|
17919 |
// We will always choose the last track as our chaptersTrack
|
|
|
17920 |
const track = tracks[i];
|
|
|
17921 |
if (track.kind === this.kind_) {
|
|
|
17922 |
return track;
|
|
|
17923 |
}
|
|
|
17924 |
}
|
|
|
17925 |
}
|
|
|
17926 |
|
|
|
17927 |
/**
|
|
|
17928 |
* Get the caption for the ChaptersButton based on the track label. This will also
|
|
|
17929 |
* use the current tracks localized kind as a fallback if a label does not exist.
|
|
|
17930 |
*
|
|
|
17931 |
* @return {string}
|
|
|
17932 |
* The tracks current label or the localized track kind.
|
|
|
17933 |
*/
|
|
|
17934 |
getMenuCaption() {
|
|
|
17935 |
if (this.track_ && this.track_.label) {
|
|
|
17936 |
return this.track_.label;
|
|
|
17937 |
}
|
|
|
17938 |
return this.localize(toTitleCase$1(this.kind_));
|
|
|
17939 |
}
|
|
|
17940 |
|
|
|
17941 |
/**
|
|
|
17942 |
* Create menu from chapter track
|
|
|
17943 |
*
|
|
|
17944 |
* @return { import('../../menu/menu').default }
|
|
|
17945 |
* New menu for the chapter buttons
|
|
|
17946 |
*/
|
|
|
17947 |
createMenu() {
|
|
|
17948 |
this.options_.title = this.getMenuCaption();
|
|
|
17949 |
return super.createMenu();
|
|
|
17950 |
}
|
|
|
17951 |
|
|
|
17952 |
/**
|
|
|
17953 |
* Create a menu item for each text track
|
|
|
17954 |
*
|
|
|
17955 |
* @return { import('./text-track-menu-item').default[] }
|
|
|
17956 |
* Array of menu items
|
|
|
17957 |
*/
|
|
|
17958 |
createItems() {
|
|
|
17959 |
const items = [];
|
|
|
17960 |
if (!this.track_) {
|
|
|
17961 |
return items;
|
|
|
17962 |
}
|
|
|
17963 |
const cues = this.track_.cues;
|
|
|
17964 |
if (!cues) {
|
|
|
17965 |
return items;
|
|
|
17966 |
}
|
|
|
17967 |
for (let i = 0, l = cues.length; i < l; i++) {
|
|
|
17968 |
const cue = cues[i];
|
|
|
17969 |
const mi = new ChaptersTrackMenuItem(this.player_, {
|
|
|
17970 |
track: this.track_,
|
|
|
17971 |
cue
|
|
|
17972 |
});
|
|
|
17973 |
items.push(mi);
|
|
|
17974 |
}
|
|
|
17975 |
return items;
|
|
|
17976 |
}
|
|
|
17977 |
}
|
|
|
17978 |
|
|
|
17979 |
/**
|
|
|
17980 |
* `kind` of TextTrack to look for to associate it with this menu.
|
|
|
17981 |
*
|
|
|
17982 |
* @type {string}
|
|
|
17983 |
* @private
|
|
|
17984 |
*/
|
|
|
17985 |
ChaptersButton.prototype.kind_ = 'chapters';
|
|
|
17986 |
|
|
|
17987 |
/**
|
|
|
17988 |
* The text that should display over the `ChaptersButton`s controls. Added for localization.
|
|
|
17989 |
*
|
|
|
17990 |
* @type {string}
|
|
|
17991 |
* @protected
|
|
|
17992 |
*/
|
|
|
17993 |
ChaptersButton.prototype.controlText_ = 'Chapters';
|
|
|
17994 |
Component$1.registerComponent('ChaptersButton', ChaptersButton);
|
|
|
17995 |
|
|
|
17996 |
/**
|
|
|
17997 |
* @file descriptions-button.js
|
|
|
17998 |
*/
|
|
|
17999 |
|
|
|
18000 |
/**
|
|
|
18001 |
* The button component for toggling and selecting descriptions
|
|
|
18002 |
*
|
|
|
18003 |
* @extends TextTrackButton
|
|
|
18004 |
*/
|
|
|
18005 |
class DescriptionsButton extends TextTrackButton {
|
|
|
18006 |
/**
|
|
|
18007 |
* Creates an instance of this class.
|
|
|
18008 |
*
|
|
|
18009 |
* @param { import('../../player').default } player
|
|
|
18010 |
* The `Player` that this class should be attached to.
|
|
|
18011 |
*
|
|
|
18012 |
* @param {Object} [options]
|
|
|
18013 |
* The key/value store of player options.
|
|
|
18014 |
*
|
|
|
18015 |
* @param {Function} [ready]
|
|
|
18016 |
* The function to call when this component is ready.
|
|
|
18017 |
*/
|
|
|
18018 |
constructor(player, options, ready) {
|
|
|
18019 |
super(player, options, ready);
|
|
|
18020 |
this.setIcon('audio-description');
|
|
|
18021 |
const tracks = player.textTracks();
|
|
|
18022 |
const changeHandler = bind_(this, this.handleTracksChange);
|
|
|
18023 |
tracks.addEventListener('change', changeHandler);
|
|
|
18024 |
this.on('dispose', function () {
|
|
|
18025 |
tracks.removeEventListener('change', changeHandler);
|
|
|
18026 |
});
|
|
|
18027 |
}
|
|
|
18028 |
|
|
|
18029 |
/**
|
|
|
18030 |
* Handle text track change
|
|
|
18031 |
*
|
|
|
18032 |
* @param {Event} event
|
|
|
18033 |
* The event that caused this function to run
|
|
|
18034 |
*
|
|
|
18035 |
* @listens TextTrackList#change
|
|
|
18036 |
*/
|
|
|
18037 |
handleTracksChange(event) {
|
|
|
18038 |
const tracks = this.player().textTracks();
|
|
|
18039 |
let disabled = false;
|
|
|
18040 |
|
|
|
18041 |
// Check whether a track of a different kind is showing
|
|
|
18042 |
for (let i = 0, l = tracks.length; i < l; i++) {
|
|
|
18043 |
const track = tracks[i];
|
|
|
18044 |
if (track.kind !== this.kind_ && track.mode === 'showing') {
|
|
|
18045 |
disabled = true;
|
|
|
18046 |
break;
|
|
|
18047 |
}
|
|
|
18048 |
}
|
|
|
18049 |
|
|
|
18050 |
// If another track is showing, disable this menu button
|
|
|
18051 |
if (disabled) {
|
|
|
18052 |
this.disable();
|
|
|
18053 |
} else {
|
|
|
18054 |
this.enable();
|
|
|
18055 |
}
|
|
|
18056 |
}
|
|
|
18057 |
|
|
|
18058 |
/**
|
|
|
18059 |
* Builds the default DOM `className`.
|
|
|
18060 |
*
|
|
|
18061 |
* @return {string}
|
|
|
18062 |
* The DOM `className` for this object.
|
|
|
18063 |
*/
|
|
|
18064 |
buildCSSClass() {
|
|
|
18065 |
return `vjs-descriptions-button ${super.buildCSSClass()}`;
|
|
|
18066 |
}
|
|
|
18067 |
buildWrapperCSSClass() {
|
|
|
18068 |
return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
|
|
|
18069 |
}
|
|
|
18070 |
}
|
|
|
18071 |
|
|
|
18072 |
/**
|
|
|
18073 |
* `kind` of TextTrack to look for to associate it with this menu.
|
|
|
18074 |
*
|
|
|
18075 |
* @type {string}
|
|
|
18076 |
* @private
|
|
|
18077 |
*/
|
|
|
18078 |
DescriptionsButton.prototype.kind_ = 'descriptions';
|
|
|
18079 |
|
|
|
18080 |
/**
|
|
|
18081 |
* The text that should display over the `DescriptionsButton`s controls. Added for localization.
|
|
|
18082 |
*
|
|
|
18083 |
* @type {string}
|
|
|
18084 |
* @protected
|
|
|
18085 |
*/
|
|
|
18086 |
DescriptionsButton.prototype.controlText_ = 'Descriptions';
|
|
|
18087 |
Component$1.registerComponent('DescriptionsButton', DescriptionsButton);
|
|
|
18088 |
|
|
|
18089 |
/**
|
|
|
18090 |
* @file subtitles-button.js
|
|
|
18091 |
*/
|
|
|
18092 |
|
|
|
18093 |
/**
|
|
|
18094 |
* The button component for toggling and selecting subtitles
|
|
|
18095 |
*
|
|
|
18096 |
* @extends TextTrackButton
|
|
|
18097 |
*/
|
|
|
18098 |
class SubtitlesButton extends TextTrackButton {
|
|
|
18099 |
/**
|
|
|
18100 |
* Creates an instance of this class.
|
|
|
18101 |
*
|
|
|
18102 |
* @param { import('../../player').default } player
|
|
|
18103 |
* The `Player` that this class should be attached to.
|
|
|
18104 |
*
|
|
|
18105 |
* @param {Object} [options]
|
|
|
18106 |
* The key/value store of player options.
|
|
|
18107 |
*
|
|
|
18108 |
* @param {Function} [ready]
|
|
|
18109 |
* The function to call when this component is ready.
|
|
|
18110 |
*/
|
|
|
18111 |
constructor(player, options, ready) {
|
|
|
18112 |
super(player, options, ready);
|
|
|
18113 |
this.setIcon('subtitles');
|
|
|
18114 |
}
|
|
|
18115 |
|
|
|
18116 |
/**
|
|
|
18117 |
* Builds the default DOM `className`.
|
|
|
18118 |
*
|
|
|
18119 |
* @return {string}
|
|
|
18120 |
* The DOM `className` for this object.
|
|
|
18121 |
*/
|
|
|
18122 |
buildCSSClass() {
|
|
|
18123 |
return `vjs-subtitles-button ${super.buildCSSClass()}`;
|
|
|
18124 |
}
|
|
|
18125 |
buildWrapperCSSClass() {
|
|
|
18126 |
return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
|
|
|
18127 |
}
|
|
|
18128 |
}
|
|
|
18129 |
|
|
|
18130 |
/**
|
|
|
18131 |
* `kind` of TextTrack to look for to associate it with this menu.
|
|
|
18132 |
*
|
|
|
18133 |
* @type {string}
|
|
|
18134 |
* @private
|
|
|
18135 |
*/
|
|
|
18136 |
SubtitlesButton.prototype.kind_ = 'subtitles';
|
|
|
18137 |
|
|
|
18138 |
/**
|
|
|
18139 |
* The text that should display over the `SubtitlesButton`s controls. Added for localization.
|
|
|
18140 |
*
|
|
|
18141 |
* @type {string}
|
|
|
18142 |
* @protected
|
|
|
18143 |
*/
|
|
|
18144 |
SubtitlesButton.prototype.controlText_ = 'Subtitles';
|
|
|
18145 |
Component$1.registerComponent('SubtitlesButton', SubtitlesButton);
|
|
|
18146 |
|
|
|
18147 |
/**
|
|
|
18148 |
* @file caption-settings-menu-item.js
|
|
|
18149 |
*/
|
|
|
18150 |
|
|
|
18151 |
/**
|
|
|
18152 |
* The menu item for caption track settings menu
|
|
|
18153 |
*
|
|
|
18154 |
* @extends TextTrackMenuItem
|
|
|
18155 |
*/
|
|
|
18156 |
class CaptionSettingsMenuItem extends TextTrackMenuItem {
|
|
|
18157 |
/**
|
|
|
18158 |
* Creates an instance of this class.
|
|
|
18159 |
*
|
|
|
18160 |
* @param { import('../../player').default } player
|
|
|
18161 |
* The `Player` that this class should be attached to.
|
|
|
18162 |
*
|
|
|
18163 |
* @param {Object} [options]
|
|
|
18164 |
* The key/value store of player options.
|
|
|
18165 |
*/
|
|
|
18166 |
constructor(player, options) {
|
|
|
18167 |
options.track = {
|
|
|
18168 |
player,
|
|
|
18169 |
kind: options.kind,
|
|
|
18170 |
label: options.kind + ' settings',
|
|
|
18171 |
selectable: false,
|
|
|
18172 |
default: false,
|
|
|
18173 |
mode: 'disabled'
|
|
|
18174 |
};
|
|
|
18175 |
|
|
|
18176 |
// CaptionSettingsMenuItem has no concept of 'selected'
|
|
|
18177 |
options.selectable = false;
|
|
|
18178 |
options.name = 'CaptionSettingsMenuItem';
|
|
|
18179 |
super(player, options);
|
|
|
18180 |
this.addClass('vjs-texttrack-settings');
|
|
|
18181 |
this.controlText(', opens ' + options.kind + ' settings dialog');
|
|
|
18182 |
}
|
|
|
18183 |
|
|
|
18184 |
/**
|
|
|
18185 |
* This gets called when an `CaptionSettingsMenuItem` is "clicked". See
|
|
|
18186 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
18187 |
*
|
|
|
18188 |
* @param {Event} [event]
|
|
|
18189 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
18190 |
* called.
|
|
|
18191 |
*
|
|
|
18192 |
* @listens tap
|
|
|
18193 |
* @listens click
|
|
|
18194 |
*/
|
|
|
18195 |
handleClick(event) {
|
|
|
18196 |
this.player().getChild('textTrackSettings').open();
|
|
|
18197 |
}
|
|
|
18198 |
|
|
|
18199 |
/**
|
|
|
18200 |
* Update control text and label on languagechange
|
|
|
18201 |
*/
|
|
|
18202 |
handleLanguagechange() {
|
|
|
18203 |
this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
|
|
|
18204 |
super.handleLanguagechange();
|
|
|
18205 |
}
|
|
|
18206 |
}
|
|
|
18207 |
Component$1.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
|
|
|
18208 |
|
|
|
18209 |
/**
|
|
|
18210 |
* @file captions-button.js
|
|
|
18211 |
*/
|
|
|
18212 |
|
|
|
18213 |
/**
|
|
|
18214 |
* The button component for toggling and selecting captions
|
|
|
18215 |
*
|
|
|
18216 |
* @extends TextTrackButton
|
|
|
18217 |
*/
|
|
|
18218 |
class CaptionsButton extends TextTrackButton {
|
|
|
18219 |
/**
|
|
|
18220 |
* Creates an instance of this class.
|
|
|
18221 |
*
|
|
|
18222 |
* @param { import('../../player').default } player
|
|
|
18223 |
* The `Player` that this class should be attached to.
|
|
|
18224 |
*
|
|
|
18225 |
* @param {Object} [options]
|
|
|
18226 |
* The key/value store of player options.
|
|
|
18227 |
*
|
|
|
18228 |
* @param {Function} [ready]
|
|
|
18229 |
* The function to call when this component is ready.
|
|
|
18230 |
*/
|
|
|
18231 |
constructor(player, options, ready) {
|
|
|
18232 |
super(player, options, ready);
|
|
|
18233 |
this.setIcon('captions');
|
|
|
18234 |
}
|
|
|
18235 |
|
|
|
18236 |
/**
|
|
|
18237 |
* Builds the default DOM `className`.
|
|
|
18238 |
*
|
|
|
18239 |
* @return {string}
|
|
|
18240 |
* The DOM `className` for this object.
|
|
|
18241 |
*/
|
|
|
18242 |
buildCSSClass() {
|
|
|
18243 |
return `vjs-captions-button ${super.buildCSSClass()}`;
|
|
|
18244 |
}
|
|
|
18245 |
buildWrapperCSSClass() {
|
|
|
18246 |
return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
|
|
|
18247 |
}
|
|
|
18248 |
|
|
|
18249 |
/**
|
|
|
18250 |
* Create caption menu items
|
|
|
18251 |
*
|
|
|
18252 |
* @return {CaptionSettingsMenuItem[]}
|
|
|
18253 |
* The array of current menu items.
|
|
|
18254 |
*/
|
|
|
18255 |
createItems() {
|
|
|
18256 |
const items = [];
|
|
|
18257 |
if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
|
|
|
18258 |
items.push(new CaptionSettingsMenuItem(this.player_, {
|
|
|
18259 |
kind: this.kind_
|
|
|
18260 |
}));
|
|
|
18261 |
this.hideThreshold_ += 1;
|
|
|
18262 |
}
|
|
|
18263 |
return super.createItems(items);
|
|
|
18264 |
}
|
|
|
18265 |
}
|
|
|
18266 |
|
|
|
18267 |
/**
|
|
|
18268 |
* `kind` of TextTrack to look for to associate it with this menu.
|
|
|
18269 |
*
|
|
|
18270 |
* @type {string}
|
|
|
18271 |
* @private
|
|
|
18272 |
*/
|
|
|
18273 |
CaptionsButton.prototype.kind_ = 'captions';
|
|
|
18274 |
|
|
|
18275 |
/**
|
|
|
18276 |
* The text that should display over the `CaptionsButton`s controls. Added for localization.
|
|
|
18277 |
*
|
|
|
18278 |
* @type {string}
|
|
|
18279 |
* @protected
|
|
|
18280 |
*/
|
|
|
18281 |
CaptionsButton.prototype.controlText_ = 'Captions';
|
|
|
18282 |
Component$1.registerComponent('CaptionsButton', CaptionsButton);
|
|
|
18283 |
|
|
|
18284 |
/**
|
|
|
18285 |
* @file subs-caps-menu-item.js
|
|
|
18286 |
*/
|
|
|
18287 |
|
|
|
18288 |
/**
|
|
|
18289 |
* SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
|
|
|
18290 |
* in the SubsCapsMenu.
|
|
|
18291 |
*
|
|
|
18292 |
* @extends TextTrackMenuItem
|
|
|
18293 |
*/
|
|
|
18294 |
class SubsCapsMenuItem extends TextTrackMenuItem {
|
|
|
18295 |
createEl(type, props, attrs) {
|
|
|
18296 |
const el = super.createEl(type, props, attrs);
|
|
|
18297 |
const parentSpan = el.querySelector('.vjs-menu-item-text');
|
|
|
18298 |
if (this.options_.track.kind === 'captions') {
|
|
|
18299 |
if (this.player_.options_.experimentalSvgIcons) {
|
|
|
18300 |
this.setIcon('captions', el);
|
|
|
18301 |
} else {
|
|
|
18302 |
parentSpan.appendChild(createEl('span', {
|
|
|
18303 |
className: 'vjs-icon-placeholder'
|
|
|
18304 |
}, {
|
|
|
18305 |
'aria-hidden': true
|
|
|
18306 |
}));
|
|
|
18307 |
}
|
|
|
18308 |
parentSpan.appendChild(createEl('span', {
|
|
|
18309 |
className: 'vjs-control-text',
|
|
|
18310 |
// space added as the text will visually flow with the
|
|
|
18311 |
// label
|
|
|
18312 |
textContent: ` ${this.localize('Captions')}`
|
|
|
18313 |
}));
|
|
|
18314 |
}
|
|
|
18315 |
return el;
|
|
|
18316 |
}
|
|
|
18317 |
}
|
|
|
18318 |
Component$1.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
|
|
|
18319 |
|
|
|
18320 |
/**
|
|
|
18321 |
* @file sub-caps-button.js
|
|
|
18322 |
*/
|
|
|
18323 |
|
|
|
18324 |
/**
|
|
|
18325 |
* The button component for toggling and selecting captions and/or subtitles
|
|
|
18326 |
*
|
|
|
18327 |
* @extends TextTrackButton
|
|
|
18328 |
*/
|
|
|
18329 |
class SubsCapsButton extends TextTrackButton {
|
|
|
18330 |
/**
|
|
|
18331 |
* Creates an instance of this class.
|
|
|
18332 |
*
|
|
|
18333 |
* @param { import('../../player').default } player
|
|
|
18334 |
* The `Player` that this class should be attached to.
|
|
|
18335 |
*
|
|
|
18336 |
* @param {Object} [options]
|
|
|
18337 |
* The key/value store of player options.
|
|
|
18338 |
*
|
|
|
18339 |
* @param {Function} [ready]
|
|
|
18340 |
* The function to call when this component is ready.
|
|
|
18341 |
*/
|
|
|
18342 |
constructor(player, options = {}) {
|
|
|
18343 |
super(player, options);
|
|
|
18344 |
|
|
|
18345 |
// Although North America uses "captions" in most cases for
|
|
|
18346 |
// "captions and subtitles" other locales use "subtitles"
|
|
|
18347 |
this.label_ = 'subtitles';
|
|
|
18348 |
this.setIcon('subtitles');
|
|
|
18349 |
if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
|
|
|
18350 |
this.label_ = 'captions';
|
|
|
18351 |
this.setIcon('captions');
|
|
|
18352 |
}
|
|
|
18353 |
this.menuButton_.controlText(toTitleCase$1(this.label_));
|
|
|
18354 |
}
|
|
|
18355 |
|
|
|
18356 |
/**
|
|
|
18357 |
* Builds the default DOM `className`.
|
|
|
18358 |
*
|
|
|
18359 |
* @return {string}
|
|
|
18360 |
* The DOM `className` for this object.
|
|
|
18361 |
*/
|
|
|
18362 |
buildCSSClass() {
|
|
|
18363 |
return `vjs-subs-caps-button ${super.buildCSSClass()}`;
|
|
|
18364 |
}
|
|
|
18365 |
buildWrapperCSSClass() {
|
|
|
18366 |
return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
|
|
|
18367 |
}
|
|
|
18368 |
|
|
|
18369 |
/**
|
|
|
18370 |
* Create caption/subtitles menu items
|
|
|
18371 |
*
|
|
|
18372 |
* @return {CaptionSettingsMenuItem[]}
|
|
|
18373 |
* The array of current menu items.
|
|
|
18374 |
*/
|
|
|
18375 |
createItems() {
|
|
|
18376 |
let items = [];
|
|
|
18377 |
if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
|
|
|
18378 |
items.push(new CaptionSettingsMenuItem(this.player_, {
|
|
|
18379 |
kind: this.label_
|
|
|
18380 |
}));
|
|
|
18381 |
this.hideThreshold_ += 1;
|
|
|
18382 |
}
|
|
|
18383 |
items = super.createItems(items, SubsCapsMenuItem);
|
|
|
18384 |
return items;
|
|
|
18385 |
}
|
|
|
18386 |
}
|
|
|
18387 |
|
|
|
18388 |
/**
|
|
|
18389 |
* `kind`s of TextTrack to look for to associate it with this menu.
|
|
|
18390 |
*
|
|
|
18391 |
* @type {array}
|
|
|
18392 |
* @private
|
|
|
18393 |
*/
|
|
|
18394 |
SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
|
|
|
18395 |
|
|
|
18396 |
/**
|
|
|
18397 |
* The text that should display over the `SubsCapsButton`s controls.
|
|
|
18398 |
*
|
|
|
18399 |
*
|
|
|
18400 |
* @type {string}
|
|
|
18401 |
* @protected
|
|
|
18402 |
*/
|
|
|
18403 |
SubsCapsButton.prototype.controlText_ = 'Subtitles';
|
|
|
18404 |
Component$1.registerComponent('SubsCapsButton', SubsCapsButton);
|
|
|
18405 |
|
|
|
18406 |
/**
|
|
|
18407 |
* @file audio-track-menu-item.js
|
|
|
18408 |
*/
|
|
|
18409 |
|
|
|
18410 |
/**
|
|
|
18411 |
* An {@link AudioTrack} {@link MenuItem}
|
|
|
18412 |
*
|
|
|
18413 |
* @extends MenuItem
|
|
|
18414 |
*/
|
|
|
18415 |
class AudioTrackMenuItem extends MenuItem {
|
|
|
18416 |
/**
|
|
|
18417 |
* Creates an instance of this class.
|
|
|
18418 |
*
|
|
|
18419 |
* @param { import('../../player').default } player
|
|
|
18420 |
* The `Player` that this class should be attached to.
|
|
|
18421 |
*
|
|
|
18422 |
* @param {Object} [options]
|
|
|
18423 |
* The key/value store of player options.
|
|
|
18424 |
*/
|
|
|
18425 |
constructor(player, options) {
|
|
|
18426 |
const track = options.track;
|
|
|
18427 |
const tracks = player.audioTracks();
|
|
|
18428 |
|
|
|
18429 |
// Modify options for parent MenuItem class's init.
|
|
|
18430 |
options.label = track.label || track.language || 'Unknown';
|
|
|
18431 |
options.selected = track.enabled;
|
|
|
18432 |
super(player, options);
|
|
|
18433 |
this.track = track;
|
|
|
18434 |
this.addClass(`vjs-${track.kind}-menu-item`);
|
|
|
18435 |
const changeHandler = (...args) => {
|
|
|
18436 |
this.handleTracksChange.apply(this, args);
|
|
|
18437 |
};
|
|
|
18438 |
tracks.addEventListener('change', changeHandler);
|
|
|
18439 |
this.on('dispose', () => {
|
|
|
18440 |
tracks.removeEventListener('change', changeHandler);
|
|
|
18441 |
});
|
|
|
18442 |
}
|
|
|
18443 |
createEl(type, props, attrs) {
|
|
|
18444 |
const el = super.createEl(type, props, attrs);
|
|
|
18445 |
const parentSpan = el.querySelector('.vjs-menu-item-text');
|
|
|
18446 |
if (['main-desc', 'description'].indexOf(this.options_.track.kind) >= 0) {
|
|
|
18447 |
parentSpan.appendChild(createEl('span', {
|
|
|
18448 |
className: 'vjs-icon-placeholder'
|
|
|
18449 |
}, {
|
|
|
18450 |
'aria-hidden': true
|
|
|
18451 |
}));
|
|
|
18452 |
parentSpan.appendChild(createEl('span', {
|
|
|
18453 |
className: 'vjs-control-text',
|
|
|
18454 |
textContent: ' ' + this.localize('Descriptions')
|
|
|
18455 |
}));
|
|
|
18456 |
}
|
|
|
18457 |
return el;
|
|
|
18458 |
}
|
|
|
18459 |
|
|
|
18460 |
/**
|
|
|
18461 |
* This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
|
|
|
18462 |
* for more detailed information on what a click can be.
|
|
|
18463 |
*
|
|
|
18464 |
* @param {Event} [event]
|
|
|
18465 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
18466 |
* called.
|
|
|
18467 |
*
|
|
|
18468 |
* @listens tap
|
|
|
18469 |
* @listens click
|
|
|
18470 |
*/
|
|
|
18471 |
handleClick(event) {
|
|
|
18472 |
super.handleClick(event);
|
|
|
18473 |
|
|
|
18474 |
// the audio track list will automatically toggle other tracks
|
|
|
18475 |
// off for us.
|
|
|
18476 |
this.track.enabled = true;
|
|
|
18477 |
|
|
|
18478 |
// when native audio tracks are used, we want to make sure that other tracks are turned off
|
|
|
18479 |
if (this.player_.tech_.featuresNativeAudioTracks) {
|
|
|
18480 |
const tracks = this.player_.audioTracks();
|
|
|
18481 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
18482 |
const track = tracks[i];
|
|
|
18483 |
|
|
|
18484 |
// skip the current track since we enabled it above
|
|
|
18485 |
if (track === this.track) {
|
|
|
18486 |
continue;
|
|
|
18487 |
}
|
|
|
18488 |
track.enabled = track === this.track;
|
|
|
18489 |
}
|
|
|
18490 |
}
|
|
|
18491 |
}
|
|
|
18492 |
|
|
|
18493 |
/**
|
|
|
18494 |
* Handle any {@link AudioTrack} change.
|
|
|
18495 |
*
|
|
|
18496 |
* @param {Event} [event]
|
|
|
18497 |
* The {@link AudioTrackList#change} event that caused this to run.
|
|
|
18498 |
*
|
|
|
18499 |
* @listens AudioTrackList#change
|
|
|
18500 |
*/
|
|
|
18501 |
handleTracksChange(event) {
|
|
|
18502 |
this.selected(this.track.enabled);
|
|
|
18503 |
}
|
|
|
18504 |
}
|
|
|
18505 |
Component$1.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
|
|
|
18506 |
|
|
|
18507 |
/**
|
|
|
18508 |
* @file audio-track-button.js
|
|
|
18509 |
*/
|
|
|
18510 |
|
|
|
18511 |
/**
|
|
|
18512 |
* The base class for buttons that toggle specific {@link AudioTrack} types.
|
|
|
18513 |
*
|
|
|
18514 |
* @extends TrackButton
|
|
|
18515 |
*/
|
|
|
18516 |
class AudioTrackButton extends TrackButton {
|
|
|
18517 |
/**
|
|
|
18518 |
* Creates an instance of this class.
|
|
|
18519 |
*
|
|
|
18520 |
* @param {Player} player
|
|
|
18521 |
* The `Player` that this class should be attached to.
|
|
|
18522 |
*
|
|
|
18523 |
* @param {Object} [options={}]
|
|
|
18524 |
* The key/value store of player options.
|
|
|
18525 |
*/
|
|
|
18526 |
constructor(player, options = {}) {
|
|
|
18527 |
options.tracks = player.audioTracks();
|
|
|
18528 |
super(player, options);
|
|
|
18529 |
this.setIcon('audio');
|
|
|
18530 |
}
|
|
|
18531 |
|
|
|
18532 |
/**
|
|
|
18533 |
* Builds the default DOM `className`.
|
|
|
18534 |
*
|
|
|
18535 |
* @return {string}
|
|
|
18536 |
* The DOM `className` for this object.
|
|
|
18537 |
*/
|
|
|
18538 |
buildCSSClass() {
|
|
|
18539 |
return `vjs-audio-button ${super.buildCSSClass()}`;
|
|
|
18540 |
}
|
|
|
18541 |
buildWrapperCSSClass() {
|
|
|
18542 |
return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
|
|
|
18543 |
}
|
|
|
18544 |
|
|
|
18545 |
/**
|
|
|
18546 |
* Create a menu item for each audio track
|
|
|
18547 |
*
|
|
|
18548 |
* @param {AudioTrackMenuItem[]} [items=[]]
|
|
|
18549 |
* An array of existing menu items to use.
|
|
|
18550 |
*
|
|
|
18551 |
* @return {AudioTrackMenuItem[]}
|
|
|
18552 |
* An array of menu items
|
|
|
18553 |
*/
|
|
|
18554 |
createItems(items = []) {
|
|
|
18555 |
// if there's only one audio track, there no point in showing it
|
|
|
18556 |
this.hideThreshold_ = 1;
|
|
|
18557 |
const tracks = this.player_.audioTracks();
|
|
|
18558 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
18559 |
const track = tracks[i];
|
|
|
18560 |
items.push(new AudioTrackMenuItem(this.player_, {
|
|
|
18561 |
track,
|
|
|
18562 |
// MenuItem is selectable
|
|
|
18563 |
selectable: true,
|
|
|
18564 |
// MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
|
|
|
18565 |
multiSelectable: false
|
|
|
18566 |
}));
|
|
|
18567 |
}
|
|
|
18568 |
return items;
|
|
|
18569 |
}
|
|
|
18570 |
}
|
|
|
18571 |
|
|
|
18572 |
/**
|
|
|
18573 |
* The text that should display over the `AudioTrackButton`s controls. Added for localization.
|
|
|
18574 |
*
|
|
|
18575 |
* @type {string}
|
|
|
18576 |
* @protected
|
|
|
18577 |
*/
|
|
|
18578 |
AudioTrackButton.prototype.controlText_ = 'Audio Track';
|
|
|
18579 |
Component$1.registerComponent('AudioTrackButton', AudioTrackButton);
|
|
|
18580 |
|
|
|
18581 |
/**
|
|
|
18582 |
* @file playback-rate-menu-item.js
|
|
|
18583 |
*/
|
|
|
18584 |
|
|
|
18585 |
/**
|
|
|
18586 |
* The specific menu item type for selecting a playback rate.
|
|
|
18587 |
*
|
|
|
18588 |
* @extends MenuItem
|
|
|
18589 |
*/
|
|
|
18590 |
class PlaybackRateMenuItem extends MenuItem {
|
|
|
18591 |
/**
|
|
|
18592 |
* Creates an instance of this class.
|
|
|
18593 |
*
|
|
|
18594 |
* @param { import('../../player').default } player
|
|
|
18595 |
* The `Player` that this class should be attached to.
|
|
|
18596 |
*
|
|
|
18597 |
* @param {Object} [options]
|
|
|
18598 |
* The key/value store of player options.
|
|
|
18599 |
*/
|
|
|
18600 |
constructor(player, options) {
|
|
|
18601 |
const label = options.rate;
|
|
|
18602 |
const rate = parseFloat(label, 10);
|
|
|
18603 |
|
|
|
18604 |
// Modify options for parent MenuItem class's init.
|
|
|
18605 |
options.label = label;
|
|
|
18606 |
options.selected = rate === player.playbackRate();
|
|
|
18607 |
options.selectable = true;
|
|
|
18608 |
options.multiSelectable = false;
|
|
|
18609 |
super(player, options);
|
|
|
18610 |
this.label = label;
|
|
|
18611 |
this.rate = rate;
|
|
|
18612 |
this.on(player, 'ratechange', e => this.update(e));
|
|
|
18613 |
}
|
|
|
18614 |
|
|
|
18615 |
/**
|
|
|
18616 |
* This gets called when an `PlaybackRateMenuItem` is "clicked". See
|
|
|
18617 |
* {@link ClickableComponent} for more detailed information on what a click can be.
|
|
|
18618 |
*
|
|
|
18619 |
* @param {Event} [event]
|
|
|
18620 |
* The `keydown`, `tap`, or `click` event that caused this function to be
|
|
|
18621 |
* called.
|
|
|
18622 |
*
|
|
|
18623 |
* @listens tap
|
|
|
18624 |
* @listens click
|
|
|
18625 |
*/
|
|
|
18626 |
handleClick(event) {
|
|
|
18627 |
super.handleClick();
|
|
|
18628 |
this.player().playbackRate(this.rate);
|
|
|
18629 |
}
|
|
|
18630 |
|
|
|
18631 |
/**
|
|
|
18632 |
* Update the PlaybackRateMenuItem when the playbackrate changes.
|
|
|
18633 |
*
|
|
|
18634 |
* @param {Event} [event]
|
|
|
18635 |
* The `ratechange` event that caused this function to run.
|
|
|
18636 |
*
|
|
|
18637 |
* @listens Player#ratechange
|
|
|
18638 |
*/
|
|
|
18639 |
update(event) {
|
|
|
18640 |
this.selected(this.player().playbackRate() === this.rate);
|
|
|
18641 |
}
|
|
|
18642 |
}
|
|
|
18643 |
|
|
|
18644 |
/**
|
|
|
18645 |
* The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
|
|
|
18646 |
*
|
|
|
18647 |
* @type {string}
|
|
|
18648 |
* @private
|
|
|
18649 |
*/
|
|
|
18650 |
PlaybackRateMenuItem.prototype.contentElType = 'button';
|
|
|
18651 |
Component$1.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
|
|
|
18652 |
|
|
|
18653 |
/**
|
|
|
18654 |
* @file playback-rate-menu-button.js
|
|
|
18655 |
*/
|
|
|
18656 |
|
|
|
18657 |
/**
|
|
|
18658 |
* The component for controlling the playback rate.
|
|
|
18659 |
*
|
|
|
18660 |
* @extends MenuButton
|
|
|
18661 |
*/
|
|
|
18662 |
class PlaybackRateMenuButton extends MenuButton {
|
|
|
18663 |
/**
|
|
|
18664 |
* Creates an instance of this class.
|
|
|
18665 |
*
|
|
|
18666 |
* @param { import('../../player').default } player
|
|
|
18667 |
* The `Player` that this class should be attached to.
|
|
|
18668 |
*
|
|
|
18669 |
* @param {Object} [options]
|
|
|
18670 |
* The key/value store of player options.
|
|
|
18671 |
*/
|
|
|
18672 |
constructor(player, options) {
|
|
|
18673 |
super(player, options);
|
|
|
18674 |
this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
|
|
|
18675 |
this.updateVisibility();
|
|
|
18676 |
this.updateLabel();
|
|
|
18677 |
this.on(player, 'loadstart', e => this.updateVisibility(e));
|
|
|
18678 |
this.on(player, 'ratechange', e => this.updateLabel(e));
|
|
|
18679 |
this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
|
|
|
18680 |
}
|
|
|
18681 |
|
|
|
18682 |
/**
|
|
|
18683 |
* Create the `Component`'s DOM element
|
|
|
18684 |
*
|
|
|
18685 |
* @return {Element}
|
|
|
18686 |
* The element that was created.
|
|
|
18687 |
*/
|
|
|
18688 |
createEl() {
|
|
|
18689 |
const el = super.createEl();
|
|
|
18690 |
this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
|
|
|
18691 |
this.labelEl_ = createEl('div', {
|
|
|
18692 |
className: 'vjs-playback-rate-value',
|
|
|
18693 |
id: this.labelElId_,
|
|
|
18694 |
textContent: '1x'
|
|
|
18695 |
});
|
|
|
18696 |
el.appendChild(this.labelEl_);
|
|
|
18697 |
return el;
|
|
|
18698 |
}
|
|
|
18699 |
dispose() {
|
|
|
18700 |
this.labelEl_ = null;
|
|
|
18701 |
super.dispose();
|
|
|
18702 |
}
|
|
|
18703 |
|
|
|
18704 |
/**
|
|
|
18705 |
* Builds the default DOM `className`.
|
|
|
18706 |
*
|
|
|
18707 |
* @return {string}
|
|
|
18708 |
* The DOM `className` for this object.
|
|
|
18709 |
*/
|
|
|
18710 |
buildCSSClass() {
|
|
|
18711 |
return `vjs-playback-rate ${super.buildCSSClass()}`;
|
|
|
18712 |
}
|
|
|
18713 |
buildWrapperCSSClass() {
|
|
|
18714 |
return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
|
|
|
18715 |
}
|
|
|
18716 |
|
|
|
18717 |
/**
|
|
|
18718 |
* Create the list of menu items. Specific to each subclass.
|
|
|
18719 |
*
|
|
|
18720 |
*/
|
|
|
18721 |
createItems() {
|
|
|
18722 |
const rates = this.playbackRates();
|
|
|
18723 |
const items = [];
|
|
|
18724 |
for (let i = rates.length - 1; i >= 0; i--) {
|
|
|
18725 |
items.push(new PlaybackRateMenuItem(this.player(), {
|
|
|
18726 |
rate: rates[i] + 'x'
|
|
|
18727 |
}));
|
|
|
18728 |
}
|
|
|
18729 |
return items;
|
|
|
18730 |
}
|
|
|
18731 |
|
|
|
18732 |
/**
|
|
|
18733 |
* On playbackrateschange, update the menu to account for the new items.
|
|
|
18734 |
*
|
|
|
18735 |
* @listens Player#playbackrateschange
|
|
|
18736 |
*/
|
|
|
18737 |
handlePlaybackRateschange(event) {
|
|
|
18738 |
this.update();
|
|
|
18739 |
}
|
|
|
18740 |
|
|
|
18741 |
/**
|
|
|
18742 |
* Get possible playback rates
|
|
|
18743 |
*
|
|
|
18744 |
* @return {Array}
|
|
|
18745 |
* All possible playback rates
|
|
|
18746 |
*/
|
|
|
18747 |
playbackRates() {
|
|
|
18748 |
const player = this.player();
|
|
|
18749 |
return player.playbackRates && player.playbackRates() || [];
|
|
|
18750 |
}
|
|
|
18751 |
|
|
|
18752 |
/**
|
|
|
18753 |
* Get whether playback rates is supported by the tech
|
|
|
18754 |
* and an array of playback rates exists
|
|
|
18755 |
*
|
|
|
18756 |
* @return {boolean}
|
|
|
18757 |
* Whether changing playback rate is supported
|
|
|
18758 |
*/
|
|
|
18759 |
playbackRateSupported() {
|
|
|
18760 |
return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
|
|
|
18761 |
}
|
|
|
18762 |
|
|
|
18763 |
/**
|
|
|
18764 |
* Hide playback rate controls when they're no playback rate options to select
|
|
|
18765 |
*
|
|
|
18766 |
* @param {Event} [event]
|
|
|
18767 |
* The event that caused this function to run.
|
|
|
18768 |
*
|
|
|
18769 |
* @listens Player#loadstart
|
|
|
18770 |
*/
|
|
|
18771 |
updateVisibility(event) {
|
|
|
18772 |
if (this.playbackRateSupported()) {
|
|
|
18773 |
this.removeClass('vjs-hidden');
|
|
|
18774 |
} else {
|
|
|
18775 |
this.addClass('vjs-hidden');
|
|
|
18776 |
}
|
|
|
18777 |
}
|
|
|
18778 |
|
|
|
18779 |
/**
|
|
|
18780 |
* Update button label when rate changed
|
|
|
18781 |
*
|
|
|
18782 |
* @param {Event} [event]
|
|
|
18783 |
* The event that caused this function to run.
|
|
|
18784 |
*
|
|
|
18785 |
* @listens Player#ratechange
|
|
|
18786 |
*/
|
|
|
18787 |
updateLabel(event) {
|
|
|
18788 |
if (this.playbackRateSupported()) {
|
|
|
18789 |
this.labelEl_.textContent = this.player().playbackRate() + 'x';
|
|
|
18790 |
}
|
|
|
18791 |
}
|
|
|
18792 |
}
|
|
|
18793 |
|
|
|
18794 |
/**
|
|
|
18795 |
* The text that should display over the `PlaybackRateMenuButton`s controls.
|
|
|
18796 |
*
|
|
|
18797 |
* Added for localization.
|
|
|
18798 |
*
|
|
|
18799 |
* @type {string}
|
|
|
18800 |
* @protected
|
|
|
18801 |
*/
|
|
|
18802 |
PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
|
|
|
18803 |
Component$1.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
|
|
|
18804 |
|
|
|
18805 |
/**
|
|
|
18806 |
* @file spacer.js
|
|
|
18807 |
*/
|
|
|
18808 |
|
|
|
18809 |
/**
|
|
|
18810 |
* Just an empty spacer element that can be used as an append point for plugins, etc.
|
|
|
18811 |
* Also can be used to create space between elements when necessary.
|
|
|
18812 |
*
|
|
|
18813 |
* @extends Component
|
|
|
18814 |
*/
|
|
|
18815 |
class Spacer extends Component$1 {
|
|
|
18816 |
/**
|
|
|
18817 |
* Builds the default DOM `className`.
|
|
|
18818 |
*
|
|
|
18819 |
* @return {string}
|
|
|
18820 |
* The DOM `className` for this object.
|
|
|
18821 |
*/
|
|
|
18822 |
buildCSSClass() {
|
|
|
18823 |
return `vjs-spacer ${super.buildCSSClass()}`;
|
|
|
18824 |
}
|
|
|
18825 |
|
|
|
18826 |
/**
|
|
|
18827 |
* Create the `Component`'s DOM element
|
|
|
18828 |
*
|
|
|
18829 |
* @return {Element}
|
|
|
18830 |
* The element that was created.
|
|
|
18831 |
*/
|
|
|
18832 |
createEl(tag = 'div', props = {}, attributes = {}) {
|
|
|
18833 |
if (!props.className) {
|
|
|
18834 |
props.className = this.buildCSSClass();
|
|
|
18835 |
}
|
|
|
18836 |
return super.createEl(tag, props, attributes);
|
|
|
18837 |
}
|
|
|
18838 |
}
|
|
|
18839 |
Component$1.registerComponent('Spacer', Spacer);
|
|
|
18840 |
|
|
|
18841 |
/**
|
|
|
18842 |
* @file custom-control-spacer.js
|
|
|
18843 |
*/
|
|
|
18844 |
|
|
|
18845 |
/**
|
|
|
18846 |
* Spacer specifically meant to be used as an insertion point for new plugins, etc.
|
|
|
18847 |
*
|
|
|
18848 |
* @extends Spacer
|
|
|
18849 |
*/
|
|
|
18850 |
class CustomControlSpacer extends Spacer {
|
|
|
18851 |
/**
|
|
|
18852 |
* Builds the default DOM `className`.
|
|
|
18853 |
*
|
|
|
18854 |
* @return {string}
|
|
|
18855 |
* The DOM `className` for this object.
|
|
|
18856 |
*/
|
|
|
18857 |
buildCSSClass() {
|
|
|
18858 |
return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
|
|
|
18859 |
}
|
|
|
18860 |
|
|
|
18861 |
/**
|
|
|
18862 |
* Create the `Component`'s DOM element
|
|
|
18863 |
*
|
|
|
18864 |
* @return {Element}
|
|
|
18865 |
* The element that was created.
|
|
|
18866 |
*/
|
|
|
18867 |
createEl() {
|
|
|
18868 |
return super.createEl('div', {
|
|
|
18869 |
className: this.buildCSSClass(),
|
|
|
18870 |
// No-flex/table-cell mode requires there be some content
|
|
|
18871 |
// in the cell to fill the remaining space of the table.
|
|
|
18872 |
textContent: '\u00a0'
|
|
|
18873 |
});
|
|
|
18874 |
}
|
|
|
18875 |
}
|
|
|
18876 |
Component$1.registerComponent('CustomControlSpacer', CustomControlSpacer);
|
|
|
18877 |
|
|
|
18878 |
/**
|
|
|
18879 |
* @file control-bar.js
|
|
|
18880 |
*/
|
|
|
18881 |
|
|
|
18882 |
/**
|
|
|
18883 |
* Container of main controls.
|
|
|
18884 |
*
|
|
|
18885 |
* @extends Component
|
|
|
18886 |
*/
|
|
|
18887 |
class ControlBar extends Component$1 {
|
|
|
18888 |
/**
|
|
|
18889 |
* Create the `Component`'s DOM element
|
|
|
18890 |
*
|
|
|
18891 |
* @return {Element}
|
|
|
18892 |
* The element that was created.
|
|
|
18893 |
*/
|
|
|
18894 |
createEl() {
|
|
|
18895 |
return super.createEl('div', {
|
|
|
18896 |
className: 'vjs-control-bar',
|
|
|
18897 |
dir: 'ltr'
|
|
|
18898 |
});
|
|
|
18899 |
}
|
|
|
18900 |
}
|
|
|
18901 |
|
|
|
18902 |
/**
|
|
|
18903 |
* Default options for `ControlBar`
|
|
|
18904 |
*
|
|
|
18905 |
* @type {Object}
|
|
|
18906 |
* @private
|
|
|
18907 |
*/
|
|
|
18908 |
ControlBar.prototype.options_ = {
|
|
|
18909 |
children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
|
|
|
18910 |
};
|
|
|
18911 |
Component$1.registerComponent('ControlBar', ControlBar);
|
|
|
18912 |
|
|
|
18913 |
/**
|
|
|
18914 |
* @file error-display.js
|
|
|
18915 |
*/
|
|
|
18916 |
|
|
|
18917 |
/**
|
|
|
18918 |
* A display that indicates an error has occurred. This means that the video
|
|
|
18919 |
* is unplayable.
|
|
|
18920 |
*
|
|
|
18921 |
* @extends ModalDialog
|
|
|
18922 |
*/
|
|
|
18923 |
class ErrorDisplay extends ModalDialog {
|
|
|
18924 |
/**
|
|
|
18925 |
* Creates an instance of this class.
|
|
|
18926 |
*
|
|
|
18927 |
* @param { import('./player').default } player
|
|
|
18928 |
* The `Player` that this class should be attached to.
|
|
|
18929 |
*
|
|
|
18930 |
* @param {Object} [options]
|
|
|
18931 |
* The key/value store of player options.
|
|
|
18932 |
*/
|
|
|
18933 |
constructor(player, options) {
|
|
|
18934 |
super(player, options);
|
|
|
18935 |
this.on(player, 'error', e => {
|
|
|
18936 |
this.close();
|
|
|
18937 |
this.open(e);
|
|
|
18938 |
});
|
|
|
18939 |
}
|
|
|
18940 |
|
|
|
18941 |
/**
|
|
|
18942 |
* Builds the default DOM `className`.
|
|
|
18943 |
*
|
|
|
18944 |
* @return {string}
|
|
|
18945 |
* The DOM `className` for this object.
|
|
|
18946 |
*
|
|
|
18947 |
* @deprecated Since version 5.
|
|
|
18948 |
*/
|
|
|
18949 |
buildCSSClass() {
|
|
|
18950 |
return `vjs-error-display ${super.buildCSSClass()}`;
|
|
|
18951 |
}
|
|
|
18952 |
|
|
|
18953 |
/**
|
|
|
18954 |
* Gets the localized error message based on the `Player`s error.
|
|
|
18955 |
*
|
|
|
18956 |
* @return {string}
|
|
|
18957 |
* The `Player`s error message localized or an empty string.
|
|
|
18958 |
*/
|
|
|
18959 |
content() {
|
|
|
18960 |
const error = this.player().error();
|
|
|
18961 |
return error ? this.localize(error.message) : '';
|
|
|
18962 |
}
|
|
|
18963 |
}
|
|
|
18964 |
|
|
|
18965 |
/**
|
|
|
18966 |
* The default options for an `ErrorDisplay`.
|
|
|
18967 |
*
|
|
|
18968 |
* @private
|
|
|
18969 |
*/
|
|
|
18970 |
ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
|
|
|
18971 |
pauseOnOpen: false,
|
|
|
18972 |
fillAlways: true,
|
|
|
18973 |
temporary: false,
|
|
|
18974 |
uncloseable: true
|
|
|
18975 |
});
|
|
|
18976 |
Component$1.registerComponent('ErrorDisplay', ErrorDisplay);
|
|
|
18977 |
|
|
|
18978 |
/**
|
|
|
18979 |
* @file text-track-settings.js
|
|
|
18980 |
*/
|
|
|
18981 |
const LOCAL_STORAGE_KEY$1 = 'vjs-text-track-settings';
|
|
|
18982 |
const COLOR_BLACK = ['#000', 'Black'];
|
|
|
18983 |
const COLOR_BLUE = ['#00F', 'Blue'];
|
|
|
18984 |
const COLOR_CYAN = ['#0FF', 'Cyan'];
|
|
|
18985 |
const COLOR_GREEN = ['#0F0', 'Green'];
|
|
|
18986 |
const COLOR_MAGENTA = ['#F0F', 'Magenta'];
|
|
|
18987 |
const COLOR_RED = ['#F00', 'Red'];
|
|
|
18988 |
const COLOR_WHITE = ['#FFF', 'White'];
|
|
|
18989 |
const COLOR_YELLOW = ['#FF0', 'Yellow'];
|
|
|
18990 |
const OPACITY_OPAQUE = ['1', 'Opaque'];
|
|
|
18991 |
const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
|
|
|
18992 |
const OPACITY_TRANS = ['0', 'Transparent'];
|
|
|
18993 |
|
|
|
18994 |
// Configuration for the various <select> elements in the DOM of this component.
|
|
|
18995 |
//
|
|
|
18996 |
// Possible keys include:
|
|
|
18997 |
//
|
|
|
18998 |
// `default`:
|
|
|
18999 |
// The default option index. Only needs to be provided if not zero.
|
|
|
19000 |
// `parser`:
|
|
|
19001 |
// A function which is used to parse the value from the selected option in
|
|
|
19002 |
// a customized way.
|
|
|
19003 |
// `selector`:
|
|
|
19004 |
// The selector used to find the associated <select> element.
|
|
|
19005 |
const selectConfigs = {
|
|
|
19006 |
backgroundColor: {
|
|
|
19007 |
selector: '.vjs-bg-color > select',
|
|
|
19008 |
id: 'captions-background-color-%s',
|
|
|
19009 |
label: 'Color',
|
|
|
19010 |
options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
|
|
|
19011 |
},
|
|
|
19012 |
backgroundOpacity: {
|
|
|
19013 |
selector: '.vjs-bg-opacity > select',
|
|
|
19014 |
id: 'captions-background-opacity-%s',
|
|
|
19015 |
label: 'Opacity',
|
|
|
19016 |
options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS]
|
|
|
19017 |
},
|
|
|
19018 |
color: {
|
|
|
19019 |
selector: '.vjs-text-color > select',
|
|
|
19020 |
id: 'captions-foreground-color-%s',
|
|
|
19021 |
label: 'Color',
|
|
|
19022 |
options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
|
|
|
19023 |
},
|
|
|
19024 |
edgeStyle: {
|
|
|
19025 |
selector: '.vjs-edge-style > select',
|
|
|
19026 |
id: '%s',
|
|
|
19027 |
label: 'Text Edge Style',
|
|
|
19028 |
options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']]
|
|
|
19029 |
},
|
|
|
19030 |
fontFamily: {
|
|
|
19031 |
selector: '.vjs-font-family > select',
|
|
|
19032 |
id: 'captions-font-family-%s',
|
|
|
19033 |
label: 'Font Family',
|
|
|
19034 |
options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
|
|
|
19035 |
},
|
|
|
19036 |
fontPercent: {
|
|
|
19037 |
selector: '.vjs-font-percent > select',
|
|
|
19038 |
id: 'captions-font-size-%s',
|
|
|
19039 |
label: 'Font Size',
|
|
|
19040 |
options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']],
|
|
|
19041 |
default: 2,
|
|
|
19042 |
parser: v => v === '1.00' ? null : Number(v)
|
|
|
19043 |
},
|
|
|
19044 |
textOpacity: {
|
|
|
19045 |
selector: '.vjs-text-opacity > select',
|
|
|
19046 |
id: 'captions-foreground-opacity-%s',
|
|
|
19047 |
label: 'Opacity',
|
|
|
19048 |
options: [OPACITY_OPAQUE, OPACITY_SEMI]
|
|
|
19049 |
},
|
|
|
19050 |
// Options for this object are defined below.
|
|
|
19051 |
windowColor: {
|
|
|
19052 |
selector: '.vjs-window-color > select',
|
|
|
19053 |
id: 'captions-window-color-%s',
|
|
|
19054 |
label: 'Color'
|
|
|
19055 |
},
|
|
|
19056 |
// Options for this object are defined below.
|
|
|
19057 |
windowOpacity: {
|
|
|
19058 |
selector: '.vjs-window-opacity > select',
|
|
|
19059 |
id: 'captions-window-opacity-%s',
|
|
|
19060 |
label: 'Opacity',
|
|
|
19061 |
options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE]
|
|
|
19062 |
}
|
|
|
19063 |
};
|
|
|
19064 |
selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
|
|
|
19065 |
|
|
|
19066 |
/**
|
|
|
19067 |
* Get the actual value of an option.
|
|
|
19068 |
*
|
|
|
19069 |
* @param {string} value
|
|
|
19070 |
* The value to get
|
|
|
19071 |
*
|
|
|
19072 |
* @param {Function} [parser]
|
|
|
19073 |
* Optional function to adjust the value.
|
|
|
19074 |
*
|
|
|
19075 |
* @return {*}
|
|
|
19076 |
* - Will be `undefined` if no value exists
|
|
|
19077 |
* - Will be `undefined` if the given value is "none".
|
|
|
19078 |
* - Will be the actual value otherwise.
|
|
|
19079 |
*
|
|
|
19080 |
* @private
|
|
|
19081 |
*/
|
|
|
19082 |
function parseOptionValue(value, parser) {
|
|
|
19083 |
if (parser) {
|
|
|
19084 |
value = parser(value);
|
|
|
19085 |
}
|
|
|
19086 |
if (value && value !== 'none') {
|
|
|
19087 |
return value;
|
|
|
19088 |
}
|
|
|
19089 |
}
|
|
|
19090 |
|
|
|
19091 |
/**
|
|
|
19092 |
* Gets the value of the selected <option> element within a <select> element.
|
|
|
19093 |
*
|
|
|
19094 |
* @param {Element} el
|
|
|
19095 |
* the element to look in
|
|
|
19096 |
*
|
|
|
19097 |
* @param {Function} [parser]
|
|
|
19098 |
* Optional function to adjust the value.
|
|
|
19099 |
*
|
|
|
19100 |
* @return {*}
|
|
|
19101 |
* - Will be `undefined` if no value exists
|
|
|
19102 |
* - Will be `undefined` if the given value is "none".
|
|
|
19103 |
* - Will be the actual value otherwise.
|
|
|
19104 |
*
|
|
|
19105 |
* @private
|
|
|
19106 |
*/
|
|
|
19107 |
function getSelectedOptionValue(el, parser) {
|
|
|
19108 |
const value = el.options[el.options.selectedIndex].value;
|
|
|
19109 |
return parseOptionValue(value, parser);
|
|
|
19110 |
}
|
|
|
19111 |
|
|
|
19112 |
/**
|
|
|
19113 |
* Sets the selected <option> element within a <select> element based on a
|
|
|
19114 |
* given value.
|
|
|
19115 |
*
|
|
|
19116 |
* @param {Element} el
|
|
|
19117 |
* The element to look in.
|
|
|
19118 |
*
|
|
|
19119 |
* @param {string} value
|
|
|
19120 |
* the property to look on.
|
|
|
19121 |
*
|
|
|
19122 |
* @param {Function} [parser]
|
|
|
19123 |
* Optional function to adjust the value before comparing.
|
|
|
19124 |
*
|
|
|
19125 |
* @private
|
|
|
19126 |
*/
|
|
|
19127 |
function setSelectedOption(el, value, parser) {
|
|
|
19128 |
if (!value) {
|
|
|
19129 |
return;
|
|
|
19130 |
}
|
|
|
19131 |
for (let i = 0; i < el.options.length; i++) {
|
|
|
19132 |
if (parseOptionValue(el.options[i].value, parser) === value) {
|
|
|
19133 |
el.selectedIndex = i;
|
|
|
19134 |
break;
|
|
|
19135 |
}
|
|
|
19136 |
}
|
|
|
19137 |
}
|
|
|
19138 |
|
|
|
19139 |
/**
|
|
|
19140 |
* Manipulate Text Tracks settings.
|
|
|
19141 |
*
|
|
|
19142 |
* @extends ModalDialog
|
|
|
19143 |
*/
|
|
|
19144 |
class TextTrackSettings extends ModalDialog {
|
|
|
19145 |
/**
|
|
|
19146 |
* Creates an instance of this class.
|
|
|
19147 |
*
|
|
|
19148 |
* @param { import('../player').default } player
|
|
|
19149 |
* The `Player` that this class should be attached to.
|
|
|
19150 |
*
|
|
|
19151 |
* @param {Object} [options]
|
|
|
19152 |
* The key/value store of player options.
|
|
|
19153 |
*/
|
|
|
19154 |
constructor(player, options) {
|
|
|
19155 |
options.temporary = false;
|
|
|
19156 |
super(player, options);
|
|
|
19157 |
this.updateDisplay = this.updateDisplay.bind(this);
|
|
|
19158 |
|
|
|
19159 |
// fill the modal and pretend we have opened it
|
|
|
19160 |
this.fill();
|
|
|
19161 |
this.hasBeenOpened_ = this.hasBeenFilled_ = true;
|
|
|
19162 |
this.endDialog = createEl('p', {
|
|
|
19163 |
className: 'vjs-control-text',
|
|
|
19164 |
textContent: this.localize('End of dialog window.')
|
|
|
19165 |
});
|
|
|
19166 |
this.el().appendChild(this.endDialog);
|
|
|
19167 |
this.setDefaults();
|
|
|
19168 |
|
|
|
19169 |
// Grab `persistTextTrackSettings` from the player options if not passed in child options
|
|
|
19170 |
if (options.persistTextTrackSettings === undefined) {
|
|
|
19171 |
this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
|
|
|
19172 |
}
|
|
|
19173 |
this.on(this.$('.vjs-done-button'), 'click', () => {
|
|
|
19174 |
this.saveSettings();
|
|
|
19175 |
this.close();
|
|
|
19176 |
});
|
|
|
19177 |
this.on(this.$('.vjs-default-button'), 'click', () => {
|
|
|
19178 |
this.setDefaults();
|
|
|
19179 |
this.updateDisplay();
|
|
|
19180 |
});
|
|
|
19181 |
each(selectConfigs, config => {
|
|
|
19182 |
this.on(this.$(config.selector), 'change', this.updateDisplay);
|
|
|
19183 |
});
|
|
|
19184 |
if (this.options_.persistTextTrackSettings) {
|
|
|
19185 |
this.restoreSettings();
|
|
|
19186 |
}
|
|
|
19187 |
}
|
|
|
19188 |
dispose() {
|
|
|
19189 |
this.endDialog = null;
|
|
|
19190 |
super.dispose();
|
|
|
19191 |
}
|
|
|
19192 |
|
|
|
19193 |
/**
|
|
|
19194 |
* Create a <select> element with configured options.
|
|
|
19195 |
*
|
|
|
19196 |
* @param {string} key
|
|
|
19197 |
* Configuration key to use during creation.
|
|
|
19198 |
*
|
|
|
19199 |
* @param {string} [legendId]
|
|
|
19200 |
* Id of associated <legend>.
|
|
|
19201 |
*
|
|
|
19202 |
* @param {string} [type=label]
|
|
|
19203 |
* Type of labelling element, `label` or `legend`
|
|
|
19204 |
*
|
|
|
19205 |
* @return {string}
|
|
|
19206 |
* An HTML string.
|
|
|
19207 |
*
|
|
|
19208 |
* @private
|
|
|
19209 |
*/
|
|
|
19210 |
createElSelect_(key, legendId = '', type = 'label') {
|
|
|
19211 |
const config = selectConfigs[key];
|
|
|
19212 |
const id = config.id.replace('%s', this.id_);
|
|
|
19213 |
const selectLabelledbyIds = [legendId, id].join(' ').trim();
|
|
|
19214 |
const guid = `vjs_select_${newGUID()}`;
|
|
|
19215 |
return [`<${type} id="${id}"${type === 'label' ? ` for="${guid}" class="vjs-label"` : ''}>`, this.localize(config.label), `</${type}>`, `<select aria-labelledby="${selectLabelledbyIds}" id="${guid}">`].concat(config.options.map(o => {
|
|
|
19216 |
const optionId = id + '-' + o[1].replace(/\W+/g, '');
|
|
|
19217 |
return [`<option id="${optionId}" value="${o[0]}" `, `aria-labelledby="${selectLabelledbyIds} ${optionId}">`, this.localize(o[1]), '</option>'].join('');
|
|
|
19218 |
})).concat('</select>').join('');
|
|
|
19219 |
}
|
|
|
19220 |
|
|
|
19221 |
/**
|
|
|
19222 |
* Create foreground color element for the component
|
|
|
19223 |
*
|
|
|
19224 |
* @return {string}
|
|
|
19225 |
* An HTML string.
|
|
|
19226 |
*
|
|
|
19227 |
* @private
|
|
|
19228 |
*/
|
|
|
19229 |
createElFgColor_() {
|
|
|
19230 |
const legendId = `captions-text-legend-${this.id_}`;
|
|
|
19231 |
return ['<fieldset class="vjs-fg vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Text'), '</legend>', '<span class="vjs-text-color">', this.createElSelect_('color', legendId), '</span>', '<span class="vjs-text-opacity vjs-opacity">', this.createElSelect_('textOpacity', legendId), '</span>', '</fieldset>'].join('');
|
|
|
19232 |
}
|
|
|
19233 |
|
|
|
19234 |
/**
|
|
|
19235 |
* Create background color element for the component
|
|
|
19236 |
*
|
|
|
19237 |
* @return {string}
|
|
|
19238 |
* An HTML string.
|
|
|
19239 |
*
|
|
|
19240 |
* @private
|
|
|
19241 |
*/
|
|
|
19242 |
createElBgColor_() {
|
|
|
19243 |
const legendId = `captions-background-${this.id_}`;
|
|
|
19244 |
return ['<fieldset class="vjs-bg vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Text Background'), '</legend>', '<span class="vjs-bg-color">', this.createElSelect_('backgroundColor', legendId), '</span>', '<span class="vjs-bg-opacity vjs-opacity">', this.createElSelect_('backgroundOpacity', legendId), '</span>', '</fieldset>'].join('');
|
|
|
19245 |
}
|
|
|
19246 |
|
|
|
19247 |
/**
|
|
|
19248 |
* Create window color element for the component
|
|
|
19249 |
*
|
|
|
19250 |
* @return {string}
|
|
|
19251 |
* An HTML string.
|
|
|
19252 |
*
|
|
|
19253 |
* @private
|
|
|
19254 |
*/
|
|
|
19255 |
createElWinColor_() {
|
|
|
19256 |
const legendId = `captions-window-${this.id_}`;
|
|
|
19257 |
return ['<fieldset class="vjs-window vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Caption Area Background'), '</legend>', '<span class="vjs-window-color">', this.createElSelect_('windowColor', legendId), '</span>', '<span class="vjs-window-opacity vjs-opacity">', this.createElSelect_('windowOpacity', legendId), '</span>', '</fieldset>'].join('');
|
|
|
19258 |
}
|
|
|
19259 |
|
|
|
19260 |
/**
|
|
|
19261 |
* Create color elements for the component
|
|
|
19262 |
*
|
|
|
19263 |
* @return {Element}
|
|
|
19264 |
* The element that was created
|
|
|
19265 |
*
|
|
|
19266 |
* @private
|
|
|
19267 |
*/
|
|
|
19268 |
createElColors_() {
|
|
|
19269 |
return createEl('div', {
|
|
|
19270 |
className: 'vjs-track-settings-colors',
|
|
|
19271 |
innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('')
|
|
|
19272 |
});
|
|
|
19273 |
}
|
|
|
19274 |
|
|
|
19275 |
/**
|
|
|
19276 |
* Create font elements for the component
|
|
|
19277 |
*
|
|
|
19278 |
* @return {Element}
|
|
|
19279 |
* The element that was created.
|
|
|
19280 |
*
|
|
|
19281 |
* @private
|
|
|
19282 |
*/
|
|
|
19283 |
createElFont_() {
|
|
|
19284 |
return createEl('div', {
|
|
|
19285 |
className: 'vjs-track-settings-font',
|
|
|
19286 |
innerHTML: ['<fieldset class="vjs-font-percent vjs-track-setting">', this.createElSelect_('fontPercent', '', 'legend'), '</fieldset>', '<fieldset class="vjs-edge-style vjs-track-setting">', this.createElSelect_('edgeStyle', '', 'legend'), '</fieldset>', '<fieldset class="vjs-font-family vjs-track-setting">', this.createElSelect_('fontFamily', '', 'legend'), '</fieldset>'].join('')
|
|
|
19287 |
});
|
|
|
19288 |
}
|
|
|
19289 |
|
|
|
19290 |
/**
|
|
|
19291 |
* Create controls for the component
|
|
|
19292 |
*
|
|
|
19293 |
* @return {Element}
|
|
|
19294 |
* The element that was created.
|
|
|
19295 |
*
|
|
|
19296 |
* @private
|
|
|
19297 |
*/
|
|
|
19298 |
createElControls_() {
|
|
|
19299 |
const defaultsDescription = this.localize('restore all settings to the default values');
|
|
|
19300 |
return createEl('div', {
|
|
|
19301 |
className: 'vjs-track-settings-controls',
|
|
|
19302 |
innerHTML: [`<button type="button" class="vjs-default-button" title="${defaultsDescription}">`, this.localize('Reset'), `<span class="vjs-control-text"> ${defaultsDescription}</span>`, '</button>', `<button type="button" class="vjs-done-button">${this.localize('Done')}</button>`].join('')
|
|
|
19303 |
});
|
|
|
19304 |
}
|
|
|
19305 |
content() {
|
|
|
19306 |
return [this.createElColors_(), this.createElFont_(), this.createElControls_()];
|
|
|
19307 |
}
|
|
|
19308 |
label() {
|
|
|
19309 |
return this.localize('Caption Settings Dialog');
|
|
|
19310 |
}
|
|
|
19311 |
description() {
|
|
|
19312 |
return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
|
|
|
19313 |
}
|
|
|
19314 |
buildCSSClass() {
|
|
|
19315 |
return super.buildCSSClass() + ' vjs-text-track-settings';
|
|
|
19316 |
}
|
|
|
19317 |
|
|
|
19318 |
/**
|
|
|
19319 |
* Gets an object of text track settings (or null).
|
|
|
19320 |
*
|
|
|
19321 |
* @return {Object}
|
|
|
19322 |
* An object with config values parsed from the DOM or localStorage.
|
|
|
19323 |
*/
|
|
|
19324 |
getValues() {
|
|
|
19325 |
return reduce(selectConfigs, (accum, config, key) => {
|
|
|
19326 |
const value = getSelectedOptionValue(this.$(config.selector), config.parser);
|
|
|
19327 |
if (value !== undefined) {
|
|
|
19328 |
accum[key] = value;
|
|
|
19329 |
}
|
|
|
19330 |
return accum;
|
|
|
19331 |
}, {});
|
|
|
19332 |
}
|
|
|
19333 |
|
|
|
19334 |
/**
|
|
|
19335 |
* Sets text track settings from an object of values.
|
|
|
19336 |
*
|
|
|
19337 |
* @param {Object} values
|
|
|
19338 |
* An object with config values parsed from the DOM or localStorage.
|
|
|
19339 |
*/
|
|
|
19340 |
setValues(values) {
|
|
|
19341 |
each(selectConfigs, (config, key) => {
|
|
|
19342 |
setSelectedOption(this.$(config.selector), values[key], config.parser);
|
|
|
19343 |
});
|
|
|
19344 |
}
|
|
|
19345 |
|
|
|
19346 |
/**
|
|
|
19347 |
* Sets all `<select>` elements to their default values.
|
|
|
19348 |
*/
|
|
|
19349 |
setDefaults() {
|
|
|
19350 |
each(selectConfigs, config => {
|
|
|
19351 |
const index = config.hasOwnProperty('default') ? config.default : 0;
|
|
|
19352 |
this.$(config.selector).selectedIndex = index;
|
|
|
19353 |
});
|
|
|
19354 |
}
|
|
|
19355 |
|
|
|
19356 |
/**
|
|
|
19357 |
* Restore texttrack settings from localStorage
|
|
|
19358 |
*/
|
|
|
19359 |
restoreSettings() {
|
|
|
19360 |
let values;
|
|
|
19361 |
try {
|
|
|
19362 |
values = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY$1));
|
|
|
19363 |
} catch (err) {
|
|
|
19364 |
log$1.warn(err);
|
|
|
19365 |
}
|
|
|
19366 |
if (values) {
|
|
|
19367 |
this.setValues(values);
|
|
|
19368 |
}
|
|
|
19369 |
}
|
|
|
19370 |
|
|
|
19371 |
/**
|
|
|
19372 |
* Save text track settings to localStorage
|
|
|
19373 |
*/
|
|
|
19374 |
saveSettings() {
|
|
|
19375 |
if (!this.options_.persistTextTrackSettings) {
|
|
|
19376 |
return;
|
|
|
19377 |
}
|
|
|
19378 |
const values = this.getValues();
|
|
|
19379 |
try {
|
|
|
19380 |
if (Object.keys(values).length) {
|
|
|
19381 |
window.localStorage.setItem(LOCAL_STORAGE_KEY$1, JSON.stringify(values));
|
|
|
19382 |
} else {
|
|
|
19383 |
window.localStorage.removeItem(LOCAL_STORAGE_KEY$1);
|
|
|
19384 |
}
|
|
|
19385 |
} catch (err) {
|
|
|
19386 |
log$1.warn(err);
|
|
|
19387 |
}
|
|
|
19388 |
}
|
|
|
19389 |
|
|
|
19390 |
/**
|
|
|
19391 |
* Update display of text track settings
|
|
|
19392 |
*/
|
|
|
19393 |
updateDisplay() {
|
|
|
19394 |
const ttDisplay = this.player_.getChild('textTrackDisplay');
|
|
|
19395 |
if (ttDisplay) {
|
|
|
19396 |
ttDisplay.updateDisplay();
|
|
|
19397 |
}
|
|
|
19398 |
}
|
|
|
19399 |
|
|
|
19400 |
/**
|
|
|
19401 |
* conditionally blur the element and refocus the captions button
|
|
|
19402 |
*
|
|
|
19403 |
* @private
|
|
|
19404 |
*/
|
|
|
19405 |
conditionalBlur_() {
|
|
|
19406 |
this.previouslyActiveEl_ = null;
|
|
|
19407 |
const cb = this.player_.controlBar;
|
|
|
19408 |
const subsCapsBtn = cb && cb.subsCapsButton;
|
|
|
19409 |
const ccBtn = cb && cb.captionsButton;
|
|
|
19410 |
if (subsCapsBtn) {
|
|
|
19411 |
subsCapsBtn.focus();
|
|
|
19412 |
} else if (ccBtn) {
|
|
|
19413 |
ccBtn.focus();
|
|
|
19414 |
}
|
|
|
19415 |
}
|
|
|
19416 |
|
|
|
19417 |
/**
|
|
|
19418 |
* Repopulate dialog with new localizations on languagechange
|
|
|
19419 |
*/
|
|
|
19420 |
handleLanguagechange() {
|
|
|
19421 |
this.fill();
|
|
|
19422 |
}
|
|
|
19423 |
}
|
|
|
19424 |
Component$1.registerComponent('TextTrackSettings', TextTrackSettings);
|
|
|
19425 |
|
|
|
19426 |
/**
|
|
|
19427 |
* @file resize-manager.js
|
|
|
19428 |
*/
|
|
|
19429 |
|
|
|
19430 |
/**
|
|
|
19431 |
* A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
|
|
|
19432 |
*
|
|
|
19433 |
* It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
|
|
|
19434 |
*
|
|
|
19435 |
* If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
|
|
|
19436 |
* If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
|
|
|
19437 |
*
|
|
|
19438 |
* @example <caption>How to disable the resize manager</caption>
|
|
|
19439 |
* const player = videojs('#vid', {
|
|
|
19440 |
* resizeManager: false
|
|
|
19441 |
* });
|
|
|
19442 |
*
|
|
|
19443 |
* @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
|
|
|
19444 |
*
|
|
|
19445 |
* @extends Component
|
|
|
19446 |
*/
|
|
|
19447 |
class ResizeManager extends Component$1 {
|
|
|
19448 |
/**
|
|
|
19449 |
* Create the ResizeManager.
|
|
|
19450 |
*
|
|
|
19451 |
* @param {Object} player
|
|
|
19452 |
* The `Player` that this class should be attached to.
|
|
|
19453 |
*
|
|
|
19454 |
* @param {Object} [options]
|
|
|
19455 |
* The key/value store of ResizeManager options.
|
|
|
19456 |
*
|
|
|
19457 |
* @param {Object} [options.ResizeObserver]
|
|
|
19458 |
* A polyfill for ResizeObserver can be passed in here.
|
|
|
19459 |
* If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
|
|
|
19460 |
*/
|
|
|
19461 |
constructor(player, options) {
|
|
|
19462 |
let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver;
|
|
|
19463 |
|
|
|
19464 |
// if `null` was passed, we want to disable the ResizeObserver
|
|
|
19465 |
if (options.ResizeObserver === null) {
|
|
|
19466 |
RESIZE_OBSERVER_AVAILABLE = false;
|
|
|
19467 |
}
|
|
|
19468 |
|
|
|
19469 |
// Only create an element when ResizeObserver isn't available
|
|
|
19470 |
const options_ = merge$2({
|
|
|
19471 |
createEl: !RESIZE_OBSERVER_AVAILABLE,
|
|
|
19472 |
reportTouchActivity: false
|
|
|
19473 |
}, options);
|
|
|
19474 |
super(player, options_);
|
|
|
19475 |
this.ResizeObserver = options.ResizeObserver || window.ResizeObserver;
|
|
|
19476 |
this.loadListener_ = null;
|
|
|
19477 |
this.resizeObserver_ = null;
|
|
|
19478 |
this.debouncedHandler_ = debounce(() => {
|
|
|
19479 |
this.resizeHandler();
|
|
|
19480 |
}, 100, false, this);
|
|
|
19481 |
if (RESIZE_OBSERVER_AVAILABLE) {
|
|
|
19482 |
this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
|
|
|
19483 |
this.resizeObserver_.observe(player.el());
|
|
|
19484 |
} else {
|
|
|
19485 |
this.loadListener_ = () => {
|
|
|
19486 |
if (!this.el_ || !this.el_.contentWindow) {
|
|
|
19487 |
return;
|
|
|
19488 |
}
|
|
|
19489 |
const debouncedHandler_ = this.debouncedHandler_;
|
|
|
19490 |
let unloadListener_ = this.unloadListener_ = function () {
|
|
|
19491 |
off(this, 'resize', debouncedHandler_);
|
|
|
19492 |
off(this, 'unload', unloadListener_);
|
|
|
19493 |
unloadListener_ = null;
|
|
|
19494 |
};
|
|
|
19495 |
|
|
|
19496 |
// safari and edge can unload the iframe before resizemanager dispose
|
|
|
19497 |
// we have to dispose of event handlers correctly before that happens
|
|
|
19498 |
on(this.el_.contentWindow, 'unload', unloadListener_);
|
|
|
19499 |
on(this.el_.contentWindow, 'resize', debouncedHandler_);
|
|
|
19500 |
};
|
|
|
19501 |
this.one('load', this.loadListener_);
|
|
|
19502 |
}
|
|
|
19503 |
}
|
|
|
19504 |
createEl() {
|
|
|
19505 |
return super.createEl('iframe', {
|
|
|
19506 |
className: 'vjs-resize-manager',
|
|
|
19507 |
tabIndex: -1,
|
|
|
19508 |
title: this.localize('No content')
|
|
|
19509 |
}, {
|
|
|
19510 |
'aria-hidden': 'true'
|
|
|
19511 |
});
|
|
|
19512 |
}
|
|
|
19513 |
|
|
|
19514 |
/**
|
|
|
19515 |
* Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
|
|
|
19516 |
*
|
|
|
19517 |
* @fires Player#playerresize
|
|
|
19518 |
*/
|
|
|
19519 |
resizeHandler() {
|
|
|
19520 |
/**
|
|
|
19521 |
* Called when the player size has changed
|
|
|
19522 |
*
|
|
|
19523 |
* @event Player#playerresize
|
|
|
19524 |
* @type {Event}
|
|
|
19525 |
*/
|
|
|
19526 |
// make sure player is still around to trigger
|
|
|
19527 |
// prevents this from causing an error after dispose
|
|
|
19528 |
if (!this.player_ || !this.player_.trigger) {
|
|
|
19529 |
return;
|
|
|
19530 |
}
|
|
|
19531 |
this.player_.trigger('playerresize');
|
|
|
19532 |
}
|
|
|
19533 |
dispose() {
|
|
|
19534 |
if (this.debouncedHandler_) {
|
|
|
19535 |
this.debouncedHandler_.cancel();
|
|
|
19536 |
}
|
|
|
19537 |
if (this.resizeObserver_) {
|
|
|
19538 |
if (this.player_.el()) {
|
|
|
19539 |
this.resizeObserver_.unobserve(this.player_.el());
|
|
|
19540 |
}
|
|
|
19541 |
this.resizeObserver_.disconnect();
|
|
|
19542 |
}
|
|
|
19543 |
if (this.loadListener_) {
|
|
|
19544 |
this.off('load', this.loadListener_);
|
|
|
19545 |
}
|
|
|
19546 |
if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
|
|
|
19547 |
this.unloadListener_.call(this.el_.contentWindow);
|
|
|
19548 |
}
|
|
|
19549 |
this.ResizeObserver = null;
|
|
|
19550 |
this.resizeObserver = null;
|
|
|
19551 |
this.debouncedHandler_ = null;
|
|
|
19552 |
this.loadListener_ = null;
|
|
|
19553 |
super.dispose();
|
|
|
19554 |
}
|
|
|
19555 |
}
|
|
|
19556 |
Component$1.registerComponent('ResizeManager', ResizeManager);
|
|
|
19557 |
|
|
|
19558 |
const defaults = {
|
|
|
19559 |
trackingThreshold: 20,
|
|
|
19560 |
liveTolerance: 15
|
|
|
19561 |
};
|
|
|
19562 |
|
|
|
19563 |
/*
|
|
|
19564 |
track when we are at the live edge, and other helpers for live playback */
|
|
|
19565 |
|
|
|
19566 |
/**
|
|
|
19567 |
* A class for checking live current time and determining when the player
|
|
|
19568 |
* is at or behind the live edge.
|
|
|
19569 |
*/
|
|
|
19570 |
class LiveTracker extends Component$1 {
|
|
|
19571 |
/**
|
|
|
19572 |
* Creates an instance of this class.
|
|
|
19573 |
*
|
|
|
19574 |
* @param { import('./player').default } player
|
|
|
19575 |
* The `Player` that this class should be attached to.
|
|
|
19576 |
*
|
|
|
19577 |
* @param {Object} [options]
|
|
|
19578 |
* The key/value store of player options.
|
|
|
19579 |
*
|
|
|
19580 |
* @param {number} [options.trackingThreshold=20]
|
|
|
19581 |
* Number of seconds of live window (seekableEnd - seekableStart) that
|
|
|
19582 |
* media needs to have before the liveui will be shown.
|
|
|
19583 |
*
|
|
|
19584 |
* @param {number} [options.liveTolerance=15]
|
|
|
19585 |
* Number of seconds behind live that we have to be
|
|
|
19586 |
* before we will be considered non-live. Note that this will only
|
|
|
19587 |
* be used when playing at the live edge. This allows large seekable end
|
|
|
19588 |
* changes to not effect whether we are live or not.
|
|
|
19589 |
*/
|
|
|
19590 |
constructor(player, options) {
|
|
|
19591 |
// LiveTracker does not need an element
|
|
|
19592 |
const options_ = merge$2(defaults, options, {
|
|
|
19593 |
createEl: false
|
|
|
19594 |
});
|
|
|
19595 |
super(player, options_);
|
|
|
19596 |
this.trackLiveHandler_ = () => this.trackLive_();
|
|
|
19597 |
this.handlePlay_ = e => this.handlePlay(e);
|
|
|
19598 |
this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
|
|
|
19599 |
this.handleSeeked_ = e => this.handleSeeked(e);
|
|
|
19600 |
this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
|
|
|
19601 |
this.reset_();
|
|
|
19602 |
this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
|
|
|
19603 |
// we should try to toggle tracking on canplay as native playback engines, like Safari
|
|
|
19604 |
// may not have the proper values for things like seekableEnd until then
|
|
|
19605 |
this.on(this.player_, 'canplay', () => this.toggleTracking());
|
|
|
19606 |
}
|
|
|
19607 |
|
|
|
19608 |
/**
|
|
|
19609 |
* all the functionality for tracking when seek end changes
|
|
|
19610 |
* and for tracking how far past seek end we should be
|
|
|
19611 |
*/
|
|
|
19612 |
trackLive_() {
|
|
|
19613 |
const seekable = this.player_.seekable();
|
|
|
19614 |
|
|
|
19615 |
// skip undefined seekable
|
|
|
19616 |
if (!seekable || !seekable.length) {
|
|
|
19617 |
return;
|
|
|
19618 |
}
|
|
|
19619 |
const newTime = Number(window.performance.now().toFixed(4));
|
|
|
19620 |
const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
|
|
|
19621 |
this.lastTime_ = newTime;
|
|
|
19622 |
this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
|
|
|
19623 |
const liveCurrentTime = this.liveCurrentTime();
|
|
|
19624 |
const currentTime = this.player_.currentTime();
|
|
|
19625 |
|
|
|
19626 |
// we are behind live if any are true
|
|
|
19627 |
// 1. the player is paused
|
|
|
19628 |
// 2. the user seeked to a location 2 seconds away from live
|
|
|
19629 |
// 3. the difference between live and current time is greater
|
|
|
19630 |
// liveTolerance which defaults to 15s
|
|
|
19631 |
let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
|
|
|
19632 |
|
|
|
19633 |
// we cannot be behind if
|
|
|
19634 |
// 1. until we have not seen a timeupdate yet
|
|
|
19635 |
// 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
|
|
|
19636 |
if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
|
|
|
19637 |
isBehind = false;
|
|
|
19638 |
}
|
|
|
19639 |
if (isBehind !== this.behindLiveEdge_) {
|
|
|
19640 |
this.behindLiveEdge_ = isBehind;
|
|
|
19641 |
this.trigger('liveedgechange');
|
|
|
19642 |
}
|
|
|
19643 |
}
|
|
|
19644 |
|
|
|
19645 |
/**
|
|
|
19646 |
* handle a durationchange event on the player
|
|
|
19647 |
* and start/stop tracking accordingly.
|
|
|
19648 |
*/
|
|
|
19649 |
handleDurationchange() {
|
|
|
19650 |
this.toggleTracking();
|
|
|
19651 |
}
|
|
|
19652 |
|
|
|
19653 |
/**
|
|
|
19654 |
* start/stop tracking
|
|
|
19655 |
*/
|
|
|
19656 |
toggleTracking() {
|
|
|
19657 |
if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
|
|
|
19658 |
if (this.player_.options_.liveui) {
|
|
|
19659 |
this.player_.addClass('vjs-liveui');
|
|
|
19660 |
}
|
|
|
19661 |
this.startTracking();
|
|
|
19662 |
} else {
|
|
|
19663 |
this.player_.removeClass('vjs-liveui');
|
|
|
19664 |
this.stopTracking();
|
|
|
19665 |
}
|
|
|
19666 |
}
|
|
|
19667 |
|
|
|
19668 |
/**
|
|
|
19669 |
* start tracking live playback
|
|
|
19670 |
*/
|
|
|
19671 |
startTracking() {
|
|
|
19672 |
if (this.isTracking()) {
|
|
|
19673 |
return;
|
|
|
19674 |
}
|
|
|
19675 |
|
|
|
19676 |
// If we haven't seen a timeupdate, we need to check whether playback
|
|
|
19677 |
// began before this component started tracking. This can happen commonly
|
|
|
19678 |
// when using autoplay.
|
|
|
19679 |
if (!this.timeupdateSeen_) {
|
|
|
19680 |
this.timeupdateSeen_ = this.player_.hasStarted();
|
|
|
19681 |
}
|
|
|
19682 |
this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
|
|
|
19683 |
this.trackLive_();
|
|
|
19684 |
this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
|
|
|
19685 |
if (!this.timeupdateSeen_) {
|
|
|
19686 |
this.one(this.player_, 'play', this.handlePlay_);
|
|
|
19687 |
this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
|
|
|
19688 |
} else {
|
|
|
19689 |
this.on(this.player_, 'seeked', this.handleSeeked_);
|
|
|
19690 |
}
|
|
|
19691 |
}
|
|
|
19692 |
|
|
|
19693 |
/**
|
|
|
19694 |
* handle the first timeupdate on the player if it wasn't already playing
|
|
|
19695 |
* when live tracker started tracking.
|
|
|
19696 |
*/
|
|
|
19697 |
handleFirstTimeupdate() {
|
|
|
19698 |
this.timeupdateSeen_ = true;
|
|
|
19699 |
this.on(this.player_, 'seeked', this.handleSeeked_);
|
|
|
19700 |
}
|
|
|
19701 |
|
|
|
19702 |
/**
|
|
|
19703 |
* Keep track of what time a seek starts, and listen for seeked
|
|
|
19704 |
* to find where a seek ends.
|
|
|
19705 |
*/
|
|
|
19706 |
handleSeeked() {
|
|
|
19707 |
const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
|
|
|
19708 |
this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
|
|
|
19709 |
this.nextSeekedFromUser_ = false;
|
|
|
19710 |
this.trackLive_();
|
|
|
19711 |
}
|
|
|
19712 |
|
|
|
19713 |
/**
|
|
|
19714 |
* handle the first play on the player, and make sure that we seek
|
|
|
19715 |
* right to the live edge.
|
|
|
19716 |
*/
|
|
|
19717 |
handlePlay() {
|
|
|
19718 |
this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
|
|
|
19719 |
}
|
|
|
19720 |
|
|
|
19721 |
/**
|
|
|
19722 |
* Stop tracking, and set all internal variables to
|
|
|
19723 |
* their initial value.
|
|
|
19724 |
*/
|
|
|
19725 |
reset_() {
|
|
|
19726 |
this.lastTime_ = -1;
|
|
|
19727 |
this.pastSeekEnd_ = 0;
|
|
|
19728 |
this.lastSeekEnd_ = -1;
|
|
|
19729 |
this.behindLiveEdge_ = true;
|
|
|
19730 |
this.timeupdateSeen_ = false;
|
|
|
19731 |
this.seekedBehindLive_ = false;
|
|
|
19732 |
this.nextSeekedFromUser_ = false;
|
|
|
19733 |
this.clearInterval(this.trackingInterval_);
|
|
|
19734 |
this.trackingInterval_ = null;
|
|
|
19735 |
this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
|
|
|
19736 |
this.off(this.player_, 'seeked', this.handleSeeked_);
|
|
|
19737 |
this.off(this.player_, 'play', this.handlePlay_);
|
|
|
19738 |
this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
|
|
|
19739 |
this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
|
|
|
19740 |
}
|
|
|
19741 |
|
|
|
19742 |
/**
|
|
|
19743 |
* The next seeked event is from the user. Meaning that any seek
|
|
|
19744 |
* > 2s behind live will be considered behind live for real and
|
|
|
19745 |
* liveTolerance will be ignored.
|
|
|
19746 |
*/
|
|
|
19747 |
nextSeekedFromUser() {
|
|
|
19748 |
this.nextSeekedFromUser_ = true;
|
|
|
19749 |
}
|
|
|
19750 |
|
|
|
19751 |
/**
|
|
|
19752 |
* stop tracking live playback
|
|
|
19753 |
*/
|
|
|
19754 |
stopTracking() {
|
|
|
19755 |
if (!this.isTracking()) {
|
|
|
19756 |
return;
|
|
|
19757 |
}
|
|
|
19758 |
this.reset_();
|
|
|
19759 |
this.trigger('liveedgechange');
|
|
|
19760 |
}
|
|
|
19761 |
|
|
|
19762 |
/**
|
|
|
19763 |
* A helper to get the player seekable end
|
|
|
19764 |
* so that we don't have to null check everywhere
|
|
|
19765 |
*
|
|
|
19766 |
* @return {number}
|
|
|
19767 |
* The furthest seekable end or Infinity.
|
|
|
19768 |
*/
|
|
|
19769 |
seekableEnd() {
|
|
|
19770 |
const seekable = this.player_.seekable();
|
|
|
19771 |
const seekableEnds = [];
|
|
|
19772 |
let i = seekable ? seekable.length : 0;
|
|
|
19773 |
while (i--) {
|
|
|
19774 |
seekableEnds.push(seekable.end(i));
|
|
|
19775 |
}
|
|
|
19776 |
|
|
|
19777 |
// grab the furthest seekable end after sorting, or if there are none
|
|
|
19778 |
// default to Infinity
|
|
|
19779 |
return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
|
|
|
19780 |
}
|
|
|
19781 |
|
|
|
19782 |
/**
|
|
|
19783 |
* A helper to get the player seekable start
|
|
|
19784 |
* so that we don't have to null check everywhere
|
|
|
19785 |
*
|
|
|
19786 |
* @return {number}
|
|
|
19787 |
* The earliest seekable start or 0.
|
|
|
19788 |
*/
|
|
|
19789 |
seekableStart() {
|
|
|
19790 |
const seekable = this.player_.seekable();
|
|
|
19791 |
const seekableStarts = [];
|
|
|
19792 |
let i = seekable ? seekable.length : 0;
|
|
|
19793 |
while (i--) {
|
|
|
19794 |
seekableStarts.push(seekable.start(i));
|
|
|
19795 |
}
|
|
|
19796 |
|
|
|
19797 |
// grab the first seekable start after sorting, or if there are none
|
|
|
19798 |
// default to 0
|
|
|
19799 |
return seekableStarts.length ? seekableStarts.sort()[0] : 0;
|
|
|
19800 |
}
|
|
|
19801 |
|
|
|
19802 |
/**
|
|
|
19803 |
* Get the live time window aka
|
|
|
19804 |
* the amount of time between seekable start and
|
|
|
19805 |
* live current time.
|
|
|
19806 |
*
|
|
|
19807 |
* @return {number}
|
|
|
19808 |
* The amount of seconds that are seekable in
|
|
|
19809 |
* the live video.
|
|
|
19810 |
*/
|
|
|
19811 |
liveWindow() {
|
|
|
19812 |
const liveCurrentTime = this.liveCurrentTime();
|
|
|
19813 |
|
|
|
19814 |
// if liveCurrenTime is Infinity then we don't have a liveWindow at all
|
|
|
19815 |
if (liveCurrentTime === Infinity) {
|
|
|
19816 |
return 0;
|
|
|
19817 |
}
|
|
|
19818 |
return liveCurrentTime - this.seekableStart();
|
|
|
19819 |
}
|
|
|
19820 |
|
|
|
19821 |
/**
|
|
|
19822 |
* Determines if the player is live, only checks if this component
|
|
|
19823 |
* is tracking live playback or not
|
|
|
19824 |
*
|
|
|
19825 |
* @return {boolean}
|
|
|
19826 |
* Whether liveTracker is tracking
|
|
|
19827 |
*/
|
|
|
19828 |
isLive() {
|
|
|
19829 |
return this.isTracking();
|
|
|
19830 |
}
|
|
|
19831 |
|
|
|
19832 |
/**
|
|
|
19833 |
* Determines if currentTime is at the live edge and won't fall behind
|
|
|
19834 |
* on each seekableendchange
|
|
|
19835 |
*
|
|
|
19836 |
* @return {boolean}
|
|
|
19837 |
* Whether playback is at the live edge
|
|
|
19838 |
*/
|
|
|
19839 |
atLiveEdge() {
|
|
|
19840 |
return !this.behindLiveEdge();
|
|
|
19841 |
}
|
|
|
19842 |
|
|
|
19843 |
/**
|
|
|
19844 |
* get what we expect the live current time to be
|
|
|
19845 |
*
|
|
|
19846 |
* @return {number}
|
|
|
19847 |
* The expected live current time
|
|
|
19848 |
*/
|
|
|
19849 |
liveCurrentTime() {
|
|
|
19850 |
return this.pastSeekEnd() + this.seekableEnd();
|
|
|
19851 |
}
|
|
|
19852 |
|
|
|
19853 |
/**
|
|
|
19854 |
* The number of seconds that have occurred after seekable end
|
|
|
19855 |
* changed. This will be reset to 0 once seekable end changes.
|
|
|
19856 |
*
|
|
|
19857 |
* @return {number}
|
|
|
19858 |
* Seconds past the current seekable end
|
|
|
19859 |
*/
|
|
|
19860 |
pastSeekEnd() {
|
|
|
19861 |
const seekableEnd = this.seekableEnd();
|
|
|
19862 |
if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
|
|
|
19863 |
this.pastSeekEnd_ = 0;
|
|
|
19864 |
}
|
|
|
19865 |
this.lastSeekEnd_ = seekableEnd;
|
|
|
19866 |
return this.pastSeekEnd_;
|
|
|
19867 |
}
|
|
|
19868 |
|
|
|
19869 |
/**
|
|
|
19870 |
* If we are currently behind the live edge, aka currentTime will be
|
|
|
19871 |
* behind on a seekableendchange
|
|
|
19872 |
*
|
|
|
19873 |
* @return {boolean}
|
|
|
19874 |
* If we are behind the live edge
|
|
|
19875 |
*/
|
|
|
19876 |
behindLiveEdge() {
|
|
|
19877 |
return this.behindLiveEdge_;
|
|
|
19878 |
}
|
|
|
19879 |
|
|
|
19880 |
/**
|
|
|
19881 |
* Whether live tracker is currently tracking or not.
|
|
|
19882 |
*/
|
|
|
19883 |
isTracking() {
|
|
|
19884 |
return typeof this.trackingInterval_ === 'number';
|
|
|
19885 |
}
|
|
|
19886 |
|
|
|
19887 |
/**
|
|
|
19888 |
* Seek to the live edge if we are behind the live edge
|
|
|
19889 |
*/
|
|
|
19890 |
seekToLiveEdge() {
|
|
|
19891 |
this.seekedBehindLive_ = false;
|
|
|
19892 |
if (this.atLiveEdge()) {
|
|
|
19893 |
return;
|
|
|
19894 |
}
|
|
|
19895 |
this.nextSeekedFromUser_ = false;
|
|
|
19896 |
this.player_.currentTime(this.liveCurrentTime());
|
|
|
19897 |
}
|
|
|
19898 |
|
|
|
19899 |
/**
|
|
|
19900 |
* Dispose of liveTracker
|
|
|
19901 |
*/
|
|
|
19902 |
dispose() {
|
|
|
19903 |
this.stopTracking();
|
|
|
19904 |
super.dispose();
|
|
|
19905 |
}
|
|
|
19906 |
}
|
|
|
19907 |
Component$1.registerComponent('LiveTracker', LiveTracker);
|
|
|
19908 |
|
|
|
19909 |
/**
|
|
|
19910 |
* Displays an element over the player which contains an optional title and
|
|
|
19911 |
* description for the current content.
|
|
|
19912 |
*
|
|
|
19913 |
* Much of the code for this component originated in the now obsolete
|
|
|
19914 |
* videojs-dock plugin: https://github.com/brightcove/videojs-dock/
|
|
|
19915 |
*
|
|
|
19916 |
* @extends Component
|
|
|
19917 |
*/
|
|
|
19918 |
class TitleBar extends Component$1 {
|
|
|
19919 |
constructor(player, options) {
|
|
|
19920 |
super(player, options);
|
|
|
19921 |
this.on('statechanged', e => this.updateDom_());
|
|
|
19922 |
this.updateDom_();
|
|
|
19923 |
}
|
|
|
19924 |
|
|
|
19925 |
/**
|
|
|
19926 |
* Create the `TitleBar`'s DOM element
|
|
|
19927 |
*
|
|
|
19928 |
* @return {Element}
|
|
|
19929 |
* The element that was created.
|
|
|
19930 |
*/
|
|
|
19931 |
createEl() {
|
|
|
19932 |
this.els = {
|
|
|
19933 |
title: createEl('div', {
|
|
|
19934 |
className: 'vjs-title-bar-title',
|
|
|
19935 |
id: `vjs-title-bar-title-${newGUID()}`
|
|
|
19936 |
}),
|
|
|
19937 |
description: createEl('div', {
|
|
|
19938 |
className: 'vjs-title-bar-description',
|
|
|
19939 |
id: `vjs-title-bar-description-${newGUID()}`
|
|
|
19940 |
})
|
|
|
19941 |
};
|
|
|
19942 |
return createEl('div', {
|
|
|
19943 |
className: 'vjs-title-bar'
|
|
|
19944 |
}, {}, values$1(this.els));
|
|
|
19945 |
}
|
|
|
19946 |
|
|
|
19947 |
/**
|
|
|
19948 |
* Updates the DOM based on the component's state object.
|
|
|
19949 |
*/
|
|
|
19950 |
updateDom_() {
|
|
|
19951 |
const tech = this.player_.tech_;
|
|
|
19952 |
const techEl = tech && tech.el_;
|
|
|
19953 |
const techAriaAttrs = {
|
|
|
19954 |
title: 'aria-labelledby',
|
|
|
19955 |
description: 'aria-describedby'
|
|
|
19956 |
};
|
|
|
19957 |
['title', 'description'].forEach(k => {
|
|
|
19958 |
const value = this.state[k];
|
|
|
19959 |
const el = this.els[k];
|
|
|
19960 |
const techAriaAttr = techAriaAttrs[k];
|
|
|
19961 |
emptyEl(el);
|
|
|
19962 |
if (value) {
|
|
|
19963 |
textContent(el, value);
|
|
|
19964 |
}
|
|
|
19965 |
|
|
|
19966 |
// If there is a tech element available, update its ARIA attributes
|
|
|
19967 |
// according to whether a title and/or description have been provided.
|
|
|
19968 |
if (techEl) {
|
|
|
19969 |
techEl.removeAttribute(techAriaAttr);
|
|
|
19970 |
if (value) {
|
|
|
19971 |
techEl.setAttribute(techAriaAttr, el.id);
|
|
|
19972 |
}
|
|
|
19973 |
}
|
|
|
19974 |
});
|
|
|
19975 |
if (this.state.title || this.state.description) {
|
|
|
19976 |
this.show();
|
|
|
19977 |
} else {
|
|
|
19978 |
this.hide();
|
|
|
19979 |
}
|
|
|
19980 |
}
|
|
|
19981 |
|
|
|
19982 |
/**
|
|
|
19983 |
* Update the contents of the title bar component with new title and
|
|
|
19984 |
* description text.
|
|
|
19985 |
*
|
|
|
19986 |
* If both title and description are missing, the title bar will be hidden.
|
|
|
19987 |
*
|
|
|
19988 |
* If either title or description are present, the title bar will be visible.
|
|
|
19989 |
*
|
|
|
19990 |
* NOTE: Any previously set value will be preserved. To unset a previously
|
|
|
19991 |
* set value, you must pass an empty string or null.
|
|
|
19992 |
*
|
|
|
19993 |
* For example:
|
|
|
19994 |
*
|
|
|
19995 |
* ```
|
|
|
19996 |
* update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
|
|
|
19997 |
* update({description: 'bar2'}) // title: 'foo', description: 'bar2'
|
|
|
19998 |
* update({title: ''}) // title: '', description: 'bar2'
|
|
|
19999 |
* update({title: 'foo', description: null}) // title: 'foo', description: null
|
|
|
20000 |
* ```
|
|
|
20001 |
*
|
|
|
20002 |
* @param {Object} [options={}]
|
|
|
20003 |
* An options object. When empty, the title bar will be hidden.
|
|
|
20004 |
*
|
|
|
20005 |
* @param {string} [options.title]
|
|
|
20006 |
* A title to display in the title bar.
|
|
|
20007 |
*
|
|
|
20008 |
* @param {string} [options.description]
|
|
|
20009 |
* A description to display in the title bar.
|
|
|
20010 |
*/
|
|
|
20011 |
update(options) {
|
|
|
20012 |
this.setState(options);
|
|
|
20013 |
}
|
|
|
20014 |
|
|
|
20015 |
/**
|
|
|
20016 |
* Dispose the component.
|
|
|
20017 |
*/
|
|
|
20018 |
dispose() {
|
|
|
20019 |
const tech = this.player_.tech_;
|
|
|
20020 |
const techEl = tech && tech.el_;
|
|
|
20021 |
if (techEl) {
|
|
|
20022 |
techEl.removeAttribute('aria-labelledby');
|
|
|
20023 |
techEl.removeAttribute('aria-describedby');
|
|
|
20024 |
}
|
|
|
20025 |
super.dispose();
|
|
|
20026 |
this.els = null;
|
|
|
20027 |
}
|
|
|
20028 |
}
|
|
|
20029 |
Component$1.registerComponent('TitleBar', TitleBar);
|
|
|
20030 |
|
|
|
20031 |
/**
|
|
|
20032 |
* This function is used to fire a sourceset when there is something
|
|
|
20033 |
* similar to `mediaEl.load()` being called. It will try to find the source via
|
|
|
20034 |
* the `src` attribute and then the `<source>` elements. It will then fire `sourceset`
|
|
|
20035 |
* with the source that was found or empty string if we cannot know. If it cannot
|
|
|
20036 |
* find a source then `sourceset` will not be fired.
|
|
|
20037 |
*
|
|
|
20038 |
* @param { import('./html5').default } tech
|
|
|
20039 |
* The tech object that sourceset was setup on
|
|
|
20040 |
*
|
|
|
20041 |
* @return {boolean}
|
|
|
20042 |
* returns false if the sourceset was not fired and true otherwise.
|
|
|
20043 |
*/
|
|
|
20044 |
const sourcesetLoad = tech => {
|
|
|
20045 |
const el = tech.el();
|
|
|
20046 |
|
|
|
20047 |
// if `el.src` is set, that source will be loaded.
|
|
|
20048 |
if (el.hasAttribute('src')) {
|
|
|
20049 |
tech.triggerSourceset(el.src);
|
|
|
20050 |
return true;
|
|
|
20051 |
}
|
|
|
20052 |
|
|
|
20053 |
/**
|
|
|
20054 |
* Since there isn't a src property on the media element, source elements will be used for
|
|
|
20055 |
* implementing the source selection algorithm. This happens asynchronously and
|
|
|
20056 |
* for most cases were there is more than one source we cannot tell what source will
|
|
|
20057 |
* be loaded, without re-implementing the source selection algorithm. At this time we are not
|
|
|
20058 |
* going to do that. There are three special cases that we do handle here though:
|
|
|
20059 |
*
|
|
|
20060 |
* 1. If there are no sources, do not fire `sourceset`.
|
|
|
20061 |
* 2. If there is only one `<source>` with a `src` property/attribute that is our `src`
|
|
|
20062 |
* 3. If there is more than one `<source>` but all of them have the same `src` url.
|
|
|
20063 |
* That will be our src.
|
|
|
20064 |
*/
|
|
|
20065 |
const sources = tech.$$('source');
|
|
|
20066 |
const srcUrls = [];
|
|
|
20067 |
let src = '';
|
|
|
20068 |
|
|
|
20069 |
// if there are no sources, do not fire sourceset
|
|
|
20070 |
if (!sources.length) {
|
|
|
20071 |
return false;
|
|
|
20072 |
}
|
|
|
20073 |
|
|
|
20074 |
// only count valid/non-duplicate source elements
|
|
|
20075 |
for (let i = 0; i < sources.length; i++) {
|
|
|
20076 |
const url = sources[i].src;
|
|
|
20077 |
if (url && srcUrls.indexOf(url) === -1) {
|
|
|
20078 |
srcUrls.push(url);
|
|
|
20079 |
}
|
|
|
20080 |
}
|
|
|
20081 |
|
|
|
20082 |
// there were no valid sources
|
|
|
20083 |
if (!srcUrls.length) {
|
|
|
20084 |
return false;
|
|
|
20085 |
}
|
|
|
20086 |
|
|
|
20087 |
// there is only one valid source element url
|
|
|
20088 |
// use that
|
|
|
20089 |
if (srcUrls.length === 1) {
|
|
|
20090 |
src = srcUrls[0];
|
|
|
20091 |
}
|
|
|
20092 |
tech.triggerSourceset(src);
|
|
|
20093 |
return true;
|
|
|
20094 |
};
|
|
|
20095 |
|
|
|
20096 |
/**
|
|
|
20097 |
* our implementation of an `innerHTML` descriptor for browsers
|
|
|
20098 |
* that do not have one.
|
|
|
20099 |
*/
|
|
|
20100 |
const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
|
|
|
20101 |
get() {
|
|
|
20102 |
return this.cloneNode(true).innerHTML;
|
|
|
20103 |
},
|
|
|
20104 |
set(v) {
|
|
|
20105 |
// make a dummy node to use innerHTML on
|
|
|
20106 |
const dummy = document.createElement(this.nodeName.toLowerCase());
|
|
|
20107 |
|
|
|
20108 |
// set innerHTML to the value provided
|
|
|
20109 |
dummy.innerHTML = v;
|
|
|
20110 |
|
|
|
20111 |
// make a document fragment to hold the nodes from dummy
|
|
|
20112 |
const docFrag = document.createDocumentFragment();
|
|
|
20113 |
|
|
|
20114 |
// copy all of the nodes created by the innerHTML on dummy
|
|
|
20115 |
// to the document fragment
|
|
|
20116 |
while (dummy.childNodes.length) {
|
|
|
20117 |
docFrag.appendChild(dummy.childNodes[0]);
|
|
|
20118 |
}
|
|
|
20119 |
|
|
|
20120 |
// remove content
|
|
|
20121 |
this.innerText = '';
|
|
|
20122 |
|
|
|
20123 |
// now we add all of that html in one by appending the
|
|
|
20124 |
// document fragment. This is how innerHTML does it.
|
|
|
20125 |
window.Element.prototype.appendChild.call(this, docFrag);
|
|
|
20126 |
|
|
|
20127 |
// then return the result that innerHTML's setter would
|
|
|
20128 |
return this.innerHTML;
|
|
|
20129 |
}
|
|
|
20130 |
});
|
|
|
20131 |
|
|
|
20132 |
/**
|
|
|
20133 |
* Get a property descriptor given a list of priorities and the
|
|
|
20134 |
* property to get.
|
|
|
20135 |
*/
|
|
|
20136 |
const getDescriptor = (priority, prop) => {
|
|
|
20137 |
let descriptor = {};
|
|
|
20138 |
for (let i = 0; i < priority.length; i++) {
|
|
|
20139 |
descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
|
|
|
20140 |
if (descriptor && descriptor.set && descriptor.get) {
|
|
|
20141 |
break;
|
|
|
20142 |
}
|
|
|
20143 |
}
|
|
|
20144 |
descriptor.enumerable = true;
|
|
|
20145 |
descriptor.configurable = true;
|
|
|
20146 |
return descriptor;
|
|
|
20147 |
};
|
|
|
20148 |
const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, window.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
|
|
|
20149 |
|
|
|
20150 |
/**
|
|
|
20151 |
* Patches browser internal functions so that we can tell synchronously
|
|
|
20152 |
* if a `<source>` was appended to the media element. For some reason this
|
|
|
20153 |
* causes a `sourceset` if the the media element is ready and has no source.
|
|
|
20154 |
* This happens when:
|
|
|
20155 |
* - The page has just loaded and the media element does not have a source.
|
|
|
20156 |
* - The media element was emptied of all sources, then `load()` was called.
|
|
|
20157 |
*
|
|
|
20158 |
* It does this by patching the following functions/properties when they are supported:
|
|
|
20159 |
*
|
|
|
20160 |
* - `append()` - can be used to add a `<source>` element to the media element
|
|
|
20161 |
* - `appendChild()` - can be used to add a `<source>` element to the media element
|
|
|
20162 |
* - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element
|
|
|
20163 |
* - `innerHTML` - can be used to add a `<source>` element to the media element
|
|
|
20164 |
*
|
|
|
20165 |
* @param {Html5} tech
|
|
|
20166 |
* The tech object that sourceset is being setup on.
|
|
|
20167 |
*/
|
|
|
20168 |
const firstSourceWatch = function (tech) {
|
|
|
20169 |
const el = tech.el();
|
|
|
20170 |
|
|
|
20171 |
// make sure firstSourceWatch isn't setup twice.
|
|
|
20172 |
if (el.resetSourceWatch_) {
|
|
|
20173 |
return;
|
|
|
20174 |
}
|
|
|
20175 |
const old = {};
|
|
|
20176 |
const innerDescriptor = getInnerHTMLDescriptor(tech);
|
|
|
20177 |
const appendWrapper = appendFn => (...args) => {
|
|
|
20178 |
const retval = appendFn.apply(el, args);
|
|
|
20179 |
sourcesetLoad(tech);
|
|
|
20180 |
return retval;
|
|
|
20181 |
};
|
|
|
20182 |
['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
|
|
|
20183 |
if (!el[k]) {
|
|
|
20184 |
return;
|
|
|
20185 |
}
|
|
|
20186 |
|
|
|
20187 |
// store the old function
|
|
|
20188 |
old[k] = el[k];
|
|
|
20189 |
|
|
|
20190 |
// call the old function with a sourceset if a source
|
|
|
20191 |
// was loaded
|
|
|
20192 |
el[k] = appendWrapper(old[k]);
|
|
|
20193 |
});
|
|
|
20194 |
Object.defineProperty(el, 'innerHTML', merge$2(innerDescriptor, {
|
|
|
20195 |
set: appendWrapper(innerDescriptor.set)
|
|
|
20196 |
}));
|
|
|
20197 |
el.resetSourceWatch_ = () => {
|
|
|
20198 |
el.resetSourceWatch_ = null;
|
|
|
20199 |
Object.keys(old).forEach(k => {
|
|
|
20200 |
el[k] = old[k];
|
|
|
20201 |
});
|
|
|
20202 |
Object.defineProperty(el, 'innerHTML', innerDescriptor);
|
|
|
20203 |
};
|
|
|
20204 |
|
|
|
20205 |
// on the first sourceset, we need to revert our changes
|
|
|
20206 |
tech.one('sourceset', el.resetSourceWatch_);
|
|
|
20207 |
};
|
|
|
20208 |
|
|
|
20209 |
/**
|
|
|
20210 |
* our implementation of a `src` descriptor for browsers
|
|
|
20211 |
* that do not have one
|
|
|
20212 |
*/
|
|
|
20213 |
const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
|
|
|
20214 |
get() {
|
|
|
20215 |
if (this.hasAttribute('src')) {
|
|
|
20216 |
return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src'));
|
|
|
20217 |
}
|
|
|
20218 |
return '';
|
|
|
20219 |
},
|
|
|
20220 |
set(v) {
|
|
|
20221 |
window.Element.prototype.setAttribute.call(this, 'src', v);
|
|
|
20222 |
return v;
|
|
|
20223 |
}
|
|
|
20224 |
});
|
|
|
20225 |
const getSrcDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
|
|
|
20226 |
|
|
|
20227 |
/**
|
|
|
20228 |
* setup `sourceset` handling on the `Html5` tech. This function
|
|
|
20229 |
* patches the following element properties/functions:
|
|
|
20230 |
*
|
|
|
20231 |
* - `src` - to determine when `src` is set
|
|
|
20232 |
* - `setAttribute()` - to determine when `src` is set
|
|
|
20233 |
* - `load()` - this re-triggers the source selection algorithm, and can
|
|
|
20234 |
* cause a sourceset.
|
|
|
20235 |
*
|
|
|
20236 |
* If there is no source when we are adding `sourceset` support or during a `load()`
|
|
|
20237 |
* we also patch the functions listed in `firstSourceWatch`.
|
|
|
20238 |
*
|
|
|
20239 |
* @param {Html5} tech
|
|
|
20240 |
* The tech to patch
|
|
|
20241 |
*/
|
|
|
20242 |
const setupSourceset = function (tech) {
|
|
|
20243 |
if (!tech.featuresSourceset) {
|
|
|
20244 |
return;
|
|
|
20245 |
}
|
|
|
20246 |
const el = tech.el();
|
|
|
20247 |
|
|
|
20248 |
// make sure sourceset isn't setup twice.
|
|
|
20249 |
if (el.resetSourceset_) {
|
|
|
20250 |
return;
|
|
|
20251 |
}
|
|
|
20252 |
const srcDescriptor = getSrcDescriptor(tech);
|
|
|
20253 |
const oldSetAttribute = el.setAttribute;
|
|
|
20254 |
const oldLoad = el.load;
|
|
|
20255 |
Object.defineProperty(el, 'src', merge$2(srcDescriptor, {
|
|
|
20256 |
set: v => {
|
|
|
20257 |
const retval = srcDescriptor.set.call(el, v);
|
|
|
20258 |
|
|
|
20259 |
// we use the getter here to get the actual value set on src
|
|
|
20260 |
tech.triggerSourceset(el.src);
|
|
|
20261 |
return retval;
|
|
|
20262 |
}
|
|
|
20263 |
}));
|
|
|
20264 |
el.setAttribute = (n, v) => {
|
|
|
20265 |
const retval = oldSetAttribute.call(el, n, v);
|
|
|
20266 |
if (/src/i.test(n)) {
|
|
|
20267 |
tech.triggerSourceset(el.src);
|
|
|
20268 |
}
|
|
|
20269 |
return retval;
|
|
|
20270 |
};
|
|
|
20271 |
el.load = () => {
|
|
|
20272 |
const retval = oldLoad.call(el);
|
|
|
20273 |
|
|
|
20274 |
// if load was called, but there was no source to fire
|
|
|
20275 |
// sourceset on. We have to watch for a source append
|
|
|
20276 |
// as that can trigger a `sourceset` when the media element
|
|
|
20277 |
// has no source
|
|
|
20278 |
if (!sourcesetLoad(tech)) {
|
|
|
20279 |
tech.triggerSourceset('');
|
|
|
20280 |
firstSourceWatch(tech);
|
|
|
20281 |
}
|
|
|
20282 |
return retval;
|
|
|
20283 |
};
|
|
|
20284 |
if (el.currentSrc) {
|
|
|
20285 |
tech.triggerSourceset(el.currentSrc);
|
|
|
20286 |
} else if (!sourcesetLoad(tech)) {
|
|
|
20287 |
firstSourceWatch(tech);
|
|
|
20288 |
}
|
|
|
20289 |
el.resetSourceset_ = () => {
|
|
|
20290 |
el.resetSourceset_ = null;
|
|
|
20291 |
el.load = oldLoad;
|
|
|
20292 |
el.setAttribute = oldSetAttribute;
|
|
|
20293 |
Object.defineProperty(el, 'src', srcDescriptor);
|
|
|
20294 |
if (el.resetSourceWatch_) {
|
|
|
20295 |
el.resetSourceWatch_();
|
|
|
20296 |
}
|
|
|
20297 |
};
|
|
|
20298 |
};
|
|
|
20299 |
|
|
|
20300 |
/**
|
|
|
20301 |
* @file html5.js
|
|
|
20302 |
*/
|
|
|
20303 |
|
|
|
20304 |
/**
|
|
|
20305 |
* HTML5 Media Controller - Wrapper for HTML5 Media API
|
|
|
20306 |
*
|
|
|
20307 |
* @mixes Tech~SourceHandlerAdditions
|
|
|
20308 |
* @extends Tech
|
|
|
20309 |
*/
|
|
|
20310 |
class Html5 extends Tech {
|
|
|
20311 |
/**
|
|
|
20312 |
* Create an instance of this Tech.
|
|
|
20313 |
*
|
|
|
20314 |
* @param {Object} [options]
|
|
|
20315 |
* The key/value store of player options.
|
|
|
20316 |
*
|
|
|
20317 |
* @param {Function} [ready]
|
|
|
20318 |
* Callback function to call when the `HTML5` Tech is ready.
|
|
|
20319 |
*/
|
|
|
20320 |
constructor(options, ready) {
|
|
|
20321 |
super(options, ready);
|
|
|
20322 |
const source = options.source;
|
|
|
20323 |
let crossoriginTracks = false;
|
|
|
20324 |
this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
|
|
|
20325 |
|
|
|
20326 |
// Set the source if one is provided
|
|
|
20327 |
// 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
|
|
|
20328 |
// 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
|
|
|
20329 |
// anyway so the error gets fired.
|
|
|
20330 |
if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
|
|
|
20331 |
this.setSource(source);
|
|
|
20332 |
} else {
|
|
|
20333 |
this.handleLateInit_(this.el_);
|
|
|
20334 |
}
|
|
|
20335 |
|
|
|
20336 |
// setup sourceset after late sourceset/init
|
|
|
20337 |
if (options.enableSourceset) {
|
|
|
20338 |
this.setupSourcesetHandling_();
|
|
|
20339 |
}
|
|
|
20340 |
this.isScrubbing_ = false;
|
|
|
20341 |
if (this.el_.hasChildNodes()) {
|
|
|
20342 |
const nodes = this.el_.childNodes;
|
|
|
20343 |
let nodesLength = nodes.length;
|
|
|
20344 |
const removeNodes = [];
|
|
|
20345 |
while (nodesLength--) {
|
|
|
20346 |
const node = nodes[nodesLength];
|
|
|
20347 |
const nodeName = node.nodeName.toLowerCase();
|
|
|
20348 |
if (nodeName === 'track') {
|
|
|
20349 |
if (!this.featuresNativeTextTracks) {
|
|
|
20350 |
// Empty video tag tracks so the built-in player doesn't use them also.
|
|
|
20351 |
// This may not be fast enough to stop HTML5 browsers from reading the tags
|
|
|
20352 |
// so we'll need to turn off any default tracks if we're manually doing
|
|
|
20353 |
// captions and subtitles. videoElement.textTracks
|
|
|
20354 |
removeNodes.push(node);
|
|
|
20355 |
} else {
|
|
|
20356 |
// store HTMLTrackElement and TextTrack to remote list
|
|
|
20357 |
this.remoteTextTrackEls().addTrackElement_(node);
|
|
|
20358 |
this.remoteTextTracks().addTrack(node.track);
|
|
|
20359 |
this.textTracks().addTrack(node.track);
|
|
|
20360 |
if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
|
|
|
20361 |
crossoriginTracks = true;
|
|
|
20362 |
}
|
|
|
20363 |
}
|
|
|
20364 |
}
|
|
|
20365 |
}
|
|
|
20366 |
for (let i = 0; i < removeNodes.length; i++) {
|
|
|
20367 |
this.el_.removeChild(removeNodes[i]);
|
|
|
20368 |
}
|
|
|
20369 |
}
|
|
|
20370 |
this.proxyNativeTracks_();
|
|
|
20371 |
if (this.featuresNativeTextTracks && crossoriginTracks) {
|
|
|
20372 |
log$1.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.');
|
|
|
20373 |
}
|
|
|
20374 |
|
|
|
20375 |
// prevent iOS Safari from disabling metadata text tracks during native playback
|
|
|
20376 |
this.restoreMetadataTracksInIOSNativePlayer_();
|
|
|
20377 |
|
|
|
20378 |
// Determine if native controls should be used
|
|
|
20379 |
// Our goal should be to get the custom controls on mobile solid everywhere
|
|
|
20380 |
// so we can remove this all together. Right now this will block custom
|
|
|
20381 |
// controls on touch enabled laptops like the Chrome Pixel
|
|
|
20382 |
if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
|
|
|
20383 |
this.setControls(true);
|
|
|
20384 |
}
|
|
|
20385 |
|
|
|
20386 |
// on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
|
|
|
20387 |
// into a `fullscreenchange` event
|
|
|
20388 |
this.proxyWebkitFullscreen_();
|
|
|
20389 |
this.triggerReady();
|
|
|
20390 |
}
|
|
|
20391 |
|
|
|
20392 |
/**
|
|
|
20393 |
* Dispose of `HTML5` media element and remove all tracks.
|
|
|
20394 |
*/
|
|
|
20395 |
dispose() {
|
|
|
20396 |
if (this.el_ && this.el_.resetSourceset_) {
|
|
|
20397 |
this.el_.resetSourceset_();
|
|
|
20398 |
}
|
|
|
20399 |
Html5.disposeMediaElement(this.el_);
|
|
|
20400 |
this.options_ = null;
|
|
|
20401 |
|
|
|
20402 |
// tech will handle clearing of the emulated track list
|
|
|
20403 |
super.dispose();
|
|
|
20404 |
}
|
|
|
20405 |
|
|
|
20406 |
/**
|
|
|
20407 |
* Modify the media element so that we can detect when
|
|
|
20408 |
* the source is changed. Fires `sourceset` just after the source has changed
|
|
|
20409 |
*/
|
|
|
20410 |
setupSourcesetHandling_() {
|
|
|
20411 |
setupSourceset(this);
|
|
|
20412 |
}
|
|
|
20413 |
|
|
|
20414 |
/**
|
|
|
20415 |
* When a captions track is enabled in the iOS Safari native player, all other
|
|
|
20416 |
* tracks are disabled (including metadata tracks), which nulls all of their
|
|
|
20417 |
* associated cue points. This will restore metadata tracks to their pre-fullscreen
|
|
|
20418 |
* state in those cases so that cue points are not needlessly lost.
|
|
|
20419 |
*
|
|
|
20420 |
* @private
|
|
|
20421 |
*/
|
|
|
20422 |
restoreMetadataTracksInIOSNativePlayer_() {
|
|
|
20423 |
const textTracks = this.textTracks();
|
|
|
20424 |
let metadataTracksPreFullscreenState;
|
|
|
20425 |
|
|
|
20426 |
// captures a snapshot of every metadata track's current state
|
|
|
20427 |
const takeMetadataTrackSnapshot = () => {
|
|
|
20428 |
metadataTracksPreFullscreenState = [];
|
|
|
20429 |
for (let i = 0; i < textTracks.length; i++) {
|
|
|
20430 |
const track = textTracks[i];
|
|
|
20431 |
if (track.kind === 'metadata') {
|
|
|
20432 |
metadataTracksPreFullscreenState.push({
|
|
|
20433 |
track,
|
|
|
20434 |
storedMode: track.mode
|
|
|
20435 |
});
|
|
|
20436 |
}
|
|
|
20437 |
}
|
|
|
20438 |
};
|
|
|
20439 |
|
|
|
20440 |
// snapshot each metadata track's initial state, and update the snapshot
|
|
|
20441 |
// each time there is a track 'change' event
|
|
|
20442 |
takeMetadataTrackSnapshot();
|
|
|
20443 |
textTracks.addEventListener('change', takeMetadataTrackSnapshot);
|
|
|
20444 |
this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
|
|
|
20445 |
const restoreTrackMode = () => {
|
|
|
20446 |
for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
|
|
|
20447 |
const storedTrack = metadataTracksPreFullscreenState[i];
|
|
|
20448 |
if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
|
|
|
20449 |
storedTrack.track.mode = storedTrack.storedMode;
|
|
|
20450 |
}
|
|
|
20451 |
}
|
|
|
20452 |
// we only want this handler to be executed on the first 'change' event
|
|
|
20453 |
textTracks.removeEventListener('change', restoreTrackMode);
|
|
|
20454 |
};
|
|
|
20455 |
|
|
|
20456 |
// when we enter fullscreen playback, stop updating the snapshot and
|
|
|
20457 |
// restore all track modes to their pre-fullscreen state
|
|
|
20458 |
this.on('webkitbeginfullscreen', () => {
|
|
|
20459 |
textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
|
|
|
20460 |
|
|
|
20461 |
// remove the listener before adding it just in case it wasn't previously removed
|
|
|
20462 |
textTracks.removeEventListener('change', restoreTrackMode);
|
|
|
20463 |
textTracks.addEventListener('change', restoreTrackMode);
|
|
|
20464 |
});
|
|
|
20465 |
|
|
|
20466 |
// start updating the snapshot again after leaving fullscreen
|
|
|
20467 |
this.on('webkitendfullscreen', () => {
|
|
|
20468 |
// remove the listener before adding it just in case it wasn't previously removed
|
|
|
20469 |
textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
|
|
|
20470 |
textTracks.addEventListener('change', takeMetadataTrackSnapshot);
|
|
|
20471 |
|
|
|
20472 |
// remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
|
|
|
20473 |
textTracks.removeEventListener('change', restoreTrackMode);
|
|
|
20474 |
});
|
|
|
20475 |
}
|
|
|
20476 |
|
|
|
20477 |
/**
|
|
|
20478 |
* Attempt to force override of tracks for the given type
|
|
|
20479 |
*
|
|
|
20480 |
* @param {string} type - Track type to override, possible values include 'Audio',
|
|
|
20481 |
* 'Video', and 'Text'.
|
|
|
20482 |
* @param {boolean} override - If set to true native audio/video will be overridden,
|
|
|
20483 |
* otherwise native audio/video will potentially be used.
|
|
|
20484 |
* @private
|
|
|
20485 |
*/
|
|
|
20486 |
overrideNative_(type, override) {
|
|
|
20487 |
// If there is no behavioral change don't add/remove listeners
|
|
|
20488 |
if (override !== this[`featuresNative${type}Tracks`]) {
|
|
|
20489 |
return;
|
|
|
20490 |
}
|
|
|
20491 |
const lowerCaseType = type.toLowerCase();
|
|
|
20492 |
if (this[`${lowerCaseType}TracksListeners_`]) {
|
|
|
20493 |
Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
|
|
|
20494 |
const elTracks = this.el()[`${lowerCaseType}Tracks`];
|
|
|
20495 |
elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
|
|
|
20496 |
});
|
|
|
20497 |
}
|
|
|
20498 |
this[`featuresNative${type}Tracks`] = !override;
|
|
|
20499 |
this[`${lowerCaseType}TracksListeners_`] = null;
|
|
|
20500 |
this.proxyNativeTracksForType_(lowerCaseType);
|
|
|
20501 |
}
|
|
|
20502 |
|
|
|
20503 |
/**
|
|
|
20504 |
* Attempt to force override of native audio tracks.
|
|
|
20505 |
*
|
|
|
20506 |
* @param {boolean} override - If set to true native audio will be overridden,
|
|
|
20507 |
* otherwise native audio will potentially be used.
|
|
|
20508 |
*/
|
|
|
20509 |
overrideNativeAudioTracks(override) {
|
|
|
20510 |
this.overrideNative_('Audio', override);
|
|
|
20511 |
}
|
|
|
20512 |
|
|
|
20513 |
/**
|
|
|
20514 |
* Attempt to force override of native video tracks.
|
|
|
20515 |
*
|
|
|
20516 |
* @param {boolean} override - If set to true native video will be overridden,
|
|
|
20517 |
* otherwise native video will potentially be used.
|
|
|
20518 |
*/
|
|
|
20519 |
overrideNativeVideoTracks(override) {
|
|
|
20520 |
this.overrideNative_('Video', override);
|
|
|
20521 |
}
|
|
|
20522 |
|
|
|
20523 |
/**
|
|
|
20524 |
* Proxy native track list events for the given type to our track
|
|
|
20525 |
* lists if the browser we are playing in supports that type of track list.
|
|
|
20526 |
*
|
|
|
20527 |
* @param {string} name - Track type; values include 'audio', 'video', and 'text'
|
|
|
20528 |
* @private
|
|
|
20529 |
*/
|
|
|
20530 |
proxyNativeTracksForType_(name) {
|
|
|
20531 |
const props = NORMAL[name];
|
|
|
20532 |
const elTracks = this.el()[props.getterName];
|
|
|
20533 |
const techTracks = this[props.getterName]();
|
|
|
20534 |
if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
|
|
|
20535 |
return;
|
|
|
20536 |
}
|
|
|
20537 |
const listeners = {
|
|
|
20538 |
change: e => {
|
|
|
20539 |
const event = {
|
|
|
20540 |
type: 'change',
|
|
|
20541 |
target: techTracks,
|
|
|
20542 |
currentTarget: techTracks,
|
|
|
20543 |
srcElement: techTracks
|
|
|
20544 |
};
|
|
|
20545 |
techTracks.trigger(event);
|
|
|
20546 |
|
|
|
20547 |
// if we are a text track change event, we should also notify the
|
|
|
20548 |
// remote text track list. This can potentially cause a false positive
|
|
|
20549 |
// if we were to get a change event on a non-remote track and
|
|
|
20550 |
// we triggered the event on the remote text track list which doesn't
|
|
|
20551 |
// contain that track. However, best practices mean looping through the
|
|
|
20552 |
// list of tracks and searching for the appropriate mode value, so,
|
|
|
20553 |
// this shouldn't pose an issue
|
|
|
20554 |
if (name === 'text') {
|
|
|
20555 |
this[REMOTE.remoteText.getterName]().trigger(event);
|
|
|
20556 |
}
|
|
|
20557 |
},
|
|
|
20558 |
addtrack(e) {
|
|
|
20559 |
techTracks.addTrack(e.track);
|
|
|
20560 |
},
|
|
|
20561 |
removetrack(e) {
|
|
|
20562 |
techTracks.removeTrack(e.track);
|
|
|
20563 |
}
|
|
|
20564 |
};
|
|
|
20565 |
const removeOldTracks = function () {
|
|
|
20566 |
const removeTracks = [];
|
|
|
20567 |
for (let i = 0; i < techTracks.length; i++) {
|
|
|
20568 |
let found = false;
|
|
|
20569 |
for (let j = 0; j < elTracks.length; j++) {
|
|
|
20570 |
if (elTracks[j] === techTracks[i]) {
|
|
|
20571 |
found = true;
|
|
|
20572 |
break;
|
|
|
20573 |
}
|
|
|
20574 |
}
|
|
|
20575 |
if (!found) {
|
|
|
20576 |
removeTracks.push(techTracks[i]);
|
|
|
20577 |
}
|
|
|
20578 |
}
|
|
|
20579 |
while (removeTracks.length) {
|
|
|
20580 |
techTracks.removeTrack(removeTracks.shift());
|
|
|
20581 |
}
|
|
|
20582 |
};
|
|
|
20583 |
this[props.getterName + 'Listeners_'] = listeners;
|
|
|
20584 |
Object.keys(listeners).forEach(eventName => {
|
|
|
20585 |
const listener = listeners[eventName];
|
|
|
20586 |
elTracks.addEventListener(eventName, listener);
|
|
|
20587 |
this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
|
|
|
20588 |
});
|
|
|
20589 |
|
|
|
20590 |
// Remove (native) tracks that are not used anymore
|
|
|
20591 |
this.on('loadstart', removeOldTracks);
|
|
|
20592 |
this.on('dispose', e => this.off('loadstart', removeOldTracks));
|
|
|
20593 |
}
|
|
|
20594 |
|
|
|
20595 |
/**
|
|
|
20596 |
* Proxy all native track list events to our track lists if the browser we are playing
|
|
|
20597 |
* in supports that type of track list.
|
|
|
20598 |
*
|
|
|
20599 |
* @private
|
|
|
20600 |
*/
|
|
|
20601 |
proxyNativeTracks_() {
|
|
|
20602 |
NORMAL.names.forEach(name => {
|
|
|
20603 |
this.proxyNativeTracksForType_(name);
|
|
|
20604 |
});
|
|
|
20605 |
}
|
|
|
20606 |
|
|
|
20607 |
/**
|
|
|
20608 |
* Create the `Html5` Tech's DOM element.
|
|
|
20609 |
*
|
|
|
20610 |
* @return {Element}
|
|
|
20611 |
* The element that gets created.
|
|
|
20612 |
*/
|
|
|
20613 |
createEl() {
|
|
|
20614 |
let el = this.options_.tag;
|
|
|
20615 |
|
|
|
20616 |
// Check if this browser supports moving the element into the box.
|
|
|
20617 |
// On the iPhone video will break if you move the element,
|
|
|
20618 |
// So we have to create a brand new element.
|
|
|
20619 |
// If we ingested the player div, we do not need to move the media element.
|
|
|
20620 |
if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
|
|
|
20621 |
// If the original tag is still there, clone and remove it.
|
|
|
20622 |
if (el) {
|
|
|
20623 |
const clone = el.cloneNode(true);
|
|
|
20624 |
if (el.parentNode) {
|
|
|
20625 |
el.parentNode.insertBefore(clone, el);
|
|
|
20626 |
}
|
|
|
20627 |
Html5.disposeMediaElement(el);
|
|
|
20628 |
el = clone;
|
|
|
20629 |
} else {
|
|
|
20630 |
el = document.createElement('video');
|
|
|
20631 |
|
|
|
20632 |
// determine if native controls should be used
|
|
|
20633 |
const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
|
|
|
20634 |
const attributes = merge$2({}, tagAttributes);
|
|
|
20635 |
if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
|
|
|
20636 |
delete attributes.controls;
|
|
|
20637 |
}
|
|
|
20638 |
setAttributes(el, Object.assign(attributes, {
|
|
|
20639 |
id: this.options_.techId,
|
|
|
20640 |
class: 'vjs-tech'
|
|
|
20641 |
}));
|
|
|
20642 |
}
|
|
|
20643 |
el.playerId = this.options_.playerId;
|
|
|
20644 |
}
|
|
|
20645 |
if (typeof this.options_.preload !== 'undefined') {
|
|
|
20646 |
setAttribute(el, 'preload', this.options_.preload);
|
|
|
20647 |
}
|
|
|
20648 |
if (this.options_.disablePictureInPicture !== undefined) {
|
|
|
20649 |
el.disablePictureInPicture = this.options_.disablePictureInPicture;
|
|
|
20650 |
}
|
|
|
20651 |
|
|
|
20652 |
// Update specific tag settings, in case they were overridden
|
|
|
20653 |
// `autoplay` has to be *last* so that `muted` and `playsinline` are present
|
|
|
20654 |
// when iOS/Safari or other browsers attempt to autoplay.
|
|
|
20655 |
const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
|
|
|
20656 |
for (let i = 0; i < settingsAttrs.length; i++) {
|
|
|
20657 |
const attr = settingsAttrs[i];
|
|
|
20658 |
const value = this.options_[attr];
|
|
|
20659 |
if (typeof value !== 'undefined') {
|
|
|
20660 |
if (value) {
|
|
|
20661 |
setAttribute(el, attr, attr);
|
|
|
20662 |
} else {
|
|
|
20663 |
removeAttribute(el, attr);
|
|
|
20664 |
}
|
|
|
20665 |
el[attr] = value;
|
|
|
20666 |
}
|
|
|
20667 |
}
|
|
|
20668 |
return el;
|
|
|
20669 |
}
|
|
|
20670 |
|
|
|
20671 |
/**
|
|
|
20672 |
* This will be triggered if the loadstart event has already fired, before videojs was
|
|
|
20673 |
* ready. Two known examples of when this can happen are:
|
|
|
20674 |
* 1. If we're loading the playback object after it has started loading
|
|
|
20675 |
* 2. The media is already playing the (often with autoplay on) then
|
|
|
20676 |
*
|
|
|
20677 |
* This function will fire another loadstart so that videojs can catchup.
|
|
|
20678 |
*
|
|
|
20679 |
* @fires Tech#loadstart
|
|
|
20680 |
*
|
|
|
20681 |
* @return {undefined}
|
|
|
20682 |
* returns nothing.
|
|
|
20683 |
*/
|
|
|
20684 |
handleLateInit_(el) {
|
|
|
20685 |
if (el.networkState === 0 || el.networkState === 3) {
|
|
|
20686 |
// The video element hasn't started loading the source yet
|
|
|
20687 |
// or didn't find a source
|
|
|
20688 |
return;
|
|
|
20689 |
}
|
|
|
20690 |
if (el.readyState === 0) {
|
|
|
20691 |
// NetworkState is set synchronously BUT loadstart is fired at the
|
|
|
20692 |
// end of the current stack, usually before setInterval(fn, 0).
|
|
|
20693 |
// So at this point we know loadstart may have already fired or is
|
|
|
20694 |
// about to fire, and either way the player hasn't seen it yet.
|
|
|
20695 |
// We don't want to fire loadstart prematurely here and cause a
|
|
|
20696 |
// double loadstart so we'll wait and see if it happens between now
|
|
|
20697 |
// and the next loop, and fire it if not.
|
|
|
20698 |
// HOWEVER, we also want to make sure it fires before loadedmetadata
|
|
|
20699 |
// which could also happen between now and the next loop, so we'll
|
|
|
20700 |
// watch for that also.
|
|
|
20701 |
let loadstartFired = false;
|
|
|
20702 |
const setLoadstartFired = function () {
|
|
|
20703 |
loadstartFired = true;
|
|
|
20704 |
};
|
|
|
20705 |
this.on('loadstart', setLoadstartFired);
|
|
|
20706 |
const triggerLoadstart = function () {
|
|
|
20707 |
// We did miss the original loadstart. Make sure the player
|
|
|
20708 |
// sees loadstart before loadedmetadata
|
|
|
20709 |
if (!loadstartFired) {
|
|
|
20710 |
this.trigger('loadstart');
|
|
|
20711 |
}
|
|
|
20712 |
};
|
|
|
20713 |
this.on('loadedmetadata', triggerLoadstart);
|
|
|
20714 |
this.ready(function () {
|
|
|
20715 |
this.off('loadstart', setLoadstartFired);
|
|
|
20716 |
this.off('loadedmetadata', triggerLoadstart);
|
|
|
20717 |
if (!loadstartFired) {
|
|
|
20718 |
// We did miss the original native loadstart. Fire it now.
|
|
|
20719 |
this.trigger('loadstart');
|
|
|
20720 |
}
|
|
|
20721 |
});
|
|
|
20722 |
return;
|
|
|
20723 |
}
|
|
|
20724 |
|
|
|
20725 |
// From here on we know that loadstart already fired and we missed it.
|
|
|
20726 |
// The other readyState events aren't as much of a problem if we double
|
|
|
20727 |
// them, so not going to go to as much trouble as loadstart to prevent
|
|
|
20728 |
// that unless we find reason to.
|
|
|
20729 |
const eventsToTrigger = ['loadstart'];
|
|
|
20730 |
|
|
|
20731 |
// loadedmetadata: newly equal to HAVE_METADATA (1) or greater
|
|
|
20732 |
eventsToTrigger.push('loadedmetadata');
|
|
|
20733 |
|
|
|
20734 |
// loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
|
|
|
20735 |
if (el.readyState >= 2) {
|
|
|
20736 |
eventsToTrigger.push('loadeddata');
|
|
|
20737 |
}
|
|
|
20738 |
|
|
|
20739 |
// canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
|
|
|
20740 |
if (el.readyState >= 3) {
|
|
|
20741 |
eventsToTrigger.push('canplay');
|
|
|
20742 |
}
|
|
|
20743 |
|
|
|
20744 |
// canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
|
|
|
20745 |
if (el.readyState >= 4) {
|
|
|
20746 |
eventsToTrigger.push('canplaythrough');
|
|
|
20747 |
}
|
|
|
20748 |
|
|
|
20749 |
// We still need to give the player time to add event listeners
|
|
|
20750 |
this.ready(function () {
|
|
|
20751 |
eventsToTrigger.forEach(function (type) {
|
|
|
20752 |
this.trigger(type);
|
|
|
20753 |
}, this);
|
|
|
20754 |
});
|
|
|
20755 |
}
|
|
|
20756 |
|
|
|
20757 |
/**
|
|
|
20758 |
* Set whether we are scrubbing or not.
|
|
|
20759 |
* This is used to decide whether we should use `fastSeek` or not.
|
|
|
20760 |
* `fastSeek` is used to provide trick play on Safari browsers.
|
|
|
20761 |
*
|
|
|
20762 |
* @param {boolean} isScrubbing
|
|
|
20763 |
* - true for we are currently scrubbing
|
|
|
20764 |
* - false for we are no longer scrubbing
|
|
|
20765 |
*/
|
|
|
20766 |
setScrubbing(isScrubbing) {
|
|
|
20767 |
this.isScrubbing_ = isScrubbing;
|
|
|
20768 |
}
|
|
|
20769 |
|
|
|
20770 |
/**
|
|
|
20771 |
* Get whether we are scrubbing or not.
|
|
|
20772 |
*
|
|
|
20773 |
* @return {boolean} isScrubbing
|
|
|
20774 |
* - true for we are currently scrubbing
|
|
|
20775 |
* - false for we are no longer scrubbing
|
|
|
20776 |
*/
|
|
|
20777 |
scrubbing() {
|
|
|
20778 |
return this.isScrubbing_;
|
|
|
20779 |
}
|
|
|
20780 |
|
|
|
20781 |
/**
|
|
|
20782 |
* Set current time for the `HTML5` tech.
|
|
|
20783 |
*
|
|
|
20784 |
* @param {number} seconds
|
|
|
20785 |
* Set the current time of the media to this.
|
|
|
20786 |
*/
|
|
|
20787 |
setCurrentTime(seconds) {
|
|
|
20788 |
try {
|
|
|
20789 |
if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
|
|
|
20790 |
this.el_.fastSeek(seconds);
|
|
|
20791 |
} else {
|
|
|
20792 |
this.el_.currentTime = seconds;
|
|
|
20793 |
}
|
|
|
20794 |
} catch (e) {
|
|
|
20795 |
log$1(e, 'Video is not ready. (Video.js)');
|
|
|
20796 |
// this.warning(VideoJS.warnings.videoNotReady);
|
|
|
20797 |
}
|
|
|
20798 |
}
|
|
|
20799 |
|
|
|
20800 |
/**
|
|
|
20801 |
* Get the current duration of the HTML5 media element.
|
|
|
20802 |
*
|
|
|
20803 |
* @return {number}
|
|
|
20804 |
* The duration of the media or 0 if there is no duration.
|
|
|
20805 |
*/
|
|
|
20806 |
duration() {
|
|
|
20807 |
// Android Chrome will report duration as Infinity for VOD HLS until after
|
|
|
20808 |
// playback has started, which triggers the live display erroneously.
|
|
|
20809 |
// Return NaN if playback has not started and trigger a durationupdate once
|
|
|
20810 |
// the duration can be reliably known.
|
|
|
20811 |
if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
|
|
|
20812 |
// Wait for the first `timeupdate` with currentTime > 0 - there may be
|
|
|
20813 |
// several with 0
|
|
|
20814 |
const checkProgress = () => {
|
|
|
20815 |
if (this.el_.currentTime > 0) {
|
|
|
20816 |
// Trigger durationchange for genuinely live video
|
|
|
20817 |
if (this.el_.duration === Infinity) {
|
|
|
20818 |
this.trigger('durationchange');
|
|
|
20819 |
}
|
|
|
20820 |
this.off('timeupdate', checkProgress);
|
|
|
20821 |
}
|
|
|
20822 |
};
|
|
|
20823 |
this.on('timeupdate', checkProgress);
|
|
|
20824 |
return NaN;
|
|
|
20825 |
}
|
|
|
20826 |
return this.el_.duration || NaN;
|
|
|
20827 |
}
|
|
|
20828 |
|
|
|
20829 |
/**
|
|
|
20830 |
* Get the current width of the HTML5 media element.
|
|
|
20831 |
*
|
|
|
20832 |
* @return {number}
|
|
|
20833 |
* The width of the HTML5 media element.
|
|
|
20834 |
*/
|
|
|
20835 |
width() {
|
|
|
20836 |
return this.el_.offsetWidth;
|
|
|
20837 |
}
|
|
|
20838 |
|
|
|
20839 |
/**
|
|
|
20840 |
* Get the current height of the HTML5 media element.
|
|
|
20841 |
*
|
|
|
20842 |
* @return {number}
|
|
|
20843 |
* The height of the HTML5 media element.
|
|
|
20844 |
*/
|
|
|
20845 |
height() {
|
|
|
20846 |
return this.el_.offsetHeight;
|
|
|
20847 |
}
|
|
|
20848 |
|
|
|
20849 |
/**
|
|
|
20850 |
* Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
|
|
|
20851 |
* `fullscreenchange` event.
|
|
|
20852 |
*
|
|
|
20853 |
* @private
|
|
|
20854 |
* @fires fullscreenchange
|
|
|
20855 |
* @listens webkitendfullscreen
|
|
|
20856 |
* @listens webkitbeginfullscreen
|
|
|
20857 |
* @listens webkitbeginfullscreen
|
|
|
20858 |
*/
|
|
|
20859 |
proxyWebkitFullscreen_() {
|
|
|
20860 |
if (!('webkitDisplayingFullscreen' in this.el_)) {
|
|
|
20861 |
return;
|
|
|
20862 |
}
|
|
|
20863 |
const endFn = function () {
|
|
|
20864 |
this.trigger('fullscreenchange', {
|
|
|
20865 |
isFullscreen: false
|
|
|
20866 |
});
|
|
|
20867 |
// Safari will sometimes set controls on the videoelement when existing fullscreen.
|
|
|
20868 |
if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
|
|
|
20869 |
this.el_.controls = false;
|
|
|
20870 |
}
|
|
|
20871 |
};
|
|
|
20872 |
const beginFn = function () {
|
|
|
20873 |
if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
|
|
|
20874 |
this.one('webkitendfullscreen', endFn);
|
|
|
20875 |
this.trigger('fullscreenchange', {
|
|
|
20876 |
isFullscreen: true,
|
|
|
20877 |
// set a flag in case another tech triggers fullscreenchange
|
|
|
20878 |
nativeIOSFullscreen: true
|
|
|
20879 |
});
|
|
|
20880 |
}
|
|
|
20881 |
};
|
|
|
20882 |
this.on('webkitbeginfullscreen', beginFn);
|
|
|
20883 |
this.on('dispose', () => {
|
|
|
20884 |
this.off('webkitbeginfullscreen', beginFn);
|
|
|
20885 |
this.off('webkitendfullscreen', endFn);
|
|
|
20886 |
});
|
|
|
20887 |
}
|
|
|
20888 |
|
|
|
20889 |
/**
|
|
|
20890 |
* Check if fullscreen is supported on the video el.
|
|
|
20891 |
*
|
|
|
20892 |
* @return {boolean}
|
|
|
20893 |
* - True if fullscreen is supported.
|
|
|
20894 |
* - False if fullscreen is not supported.
|
|
|
20895 |
*/
|
|
|
20896 |
supportsFullScreen() {
|
|
|
20897 |
return typeof this.el_.webkitEnterFullScreen === 'function';
|
|
|
20898 |
}
|
|
|
20899 |
|
|
|
20900 |
/**
|
|
|
20901 |
* Request that the `HTML5` Tech enter fullscreen.
|
|
|
20902 |
*/
|
|
|
20903 |
enterFullScreen() {
|
|
|
20904 |
const video = this.el_;
|
|
|
20905 |
if (video.paused && video.networkState <= video.HAVE_METADATA) {
|
|
|
20906 |
// attempt to prime the video element for programmatic access
|
|
|
20907 |
// this isn't necessary on the desktop but shouldn't hurt
|
|
|
20908 |
silencePromise(this.el_.play());
|
|
|
20909 |
|
|
|
20910 |
// playing and pausing synchronously during the transition to fullscreen
|
|
|
20911 |
// can get iOS ~6.1 devices into a play/pause loop
|
|
|
20912 |
this.setTimeout(function () {
|
|
|
20913 |
video.pause();
|
|
|
20914 |
try {
|
|
|
20915 |
video.webkitEnterFullScreen();
|
|
|
20916 |
} catch (e) {
|
|
|
20917 |
this.trigger('fullscreenerror', e);
|
|
|
20918 |
}
|
|
|
20919 |
}, 0);
|
|
|
20920 |
} else {
|
|
|
20921 |
try {
|
|
|
20922 |
video.webkitEnterFullScreen();
|
|
|
20923 |
} catch (e) {
|
|
|
20924 |
this.trigger('fullscreenerror', e);
|
|
|
20925 |
}
|
|
|
20926 |
}
|
|
|
20927 |
}
|
|
|
20928 |
|
|
|
20929 |
/**
|
|
|
20930 |
* Request that the `HTML5` Tech exit fullscreen.
|
|
|
20931 |
*/
|
|
|
20932 |
exitFullScreen() {
|
|
|
20933 |
if (!this.el_.webkitDisplayingFullscreen) {
|
|
|
20934 |
this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
|
|
|
20935 |
return;
|
|
|
20936 |
}
|
|
|
20937 |
this.el_.webkitExitFullScreen();
|
|
|
20938 |
}
|
|
|
20939 |
|
|
|
20940 |
/**
|
|
|
20941 |
* Create a floating video window always on top of other windows so that users may
|
|
|
20942 |
* continue consuming media while they interact with other content sites, or
|
|
|
20943 |
* applications on their device.
|
|
|
20944 |
*
|
|
|
20945 |
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
|
|
|
20946 |
*
|
|
|
20947 |
* @return {Promise}
|
|
|
20948 |
* A promise with a Picture-in-Picture window.
|
|
|
20949 |
*/
|
|
|
20950 |
requestPictureInPicture() {
|
|
|
20951 |
return this.el_.requestPictureInPicture();
|
|
|
20952 |
}
|
|
|
20953 |
|
|
|
20954 |
/**
|
|
|
20955 |
* Native requestVideoFrameCallback if supported by browser/tech, or fallback
|
|
|
20956 |
* Don't use rVCF on Safari when DRM is playing, as it doesn't fire
|
|
|
20957 |
* Needs to be checked later than the constructor
|
|
|
20958 |
* This will be a false positive for clear sources loaded after a Fairplay source
|
|
|
20959 |
*
|
|
|
20960 |
* @param {function} cb function to call
|
|
|
20961 |
* @return {number} id of request
|
|
|
20962 |
*/
|
|
|
20963 |
requestVideoFrameCallback(cb) {
|
|
|
20964 |
if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
|
|
|
20965 |
return this.el_.requestVideoFrameCallback(cb);
|
|
|
20966 |
}
|
|
|
20967 |
return super.requestVideoFrameCallback(cb);
|
|
|
20968 |
}
|
|
|
20969 |
|
|
|
20970 |
/**
|
|
|
20971 |
* Native or fallback requestVideoFrameCallback
|
|
|
20972 |
*
|
|
|
20973 |
* @param {number} id request id to cancel
|
|
|
20974 |
*/
|
|
|
20975 |
cancelVideoFrameCallback(id) {
|
|
|
20976 |
if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
|
|
|
20977 |
this.el_.cancelVideoFrameCallback(id);
|
|
|
20978 |
} else {
|
|
|
20979 |
super.cancelVideoFrameCallback(id);
|
|
|
20980 |
}
|
|
|
20981 |
}
|
|
|
20982 |
|
|
|
20983 |
/**
|
|
|
20984 |
* A getter/setter for the `Html5` Tech's source object.
|
|
|
20985 |
* > Note: Please use {@link Html5#setSource}
|
|
|
20986 |
*
|
|
|
20987 |
* @param {Tech~SourceObject} [src]
|
|
|
20988 |
* The source object you want to set on the `HTML5` techs element.
|
|
|
20989 |
*
|
|
|
20990 |
* @return {Tech~SourceObject|undefined}
|
|
|
20991 |
* - The current source object when a source is not passed in.
|
|
|
20992 |
* - undefined when setting
|
|
|
20993 |
*
|
|
|
20994 |
* @deprecated Since version 5.
|
|
|
20995 |
*/
|
|
|
20996 |
src(src) {
|
|
|
20997 |
if (src === undefined) {
|
|
|
20998 |
return this.el_.src;
|
|
|
20999 |
}
|
|
|
21000 |
|
|
|
21001 |
// Setting src through `src` instead of `setSrc` will be deprecated
|
|
|
21002 |
this.setSrc(src);
|
|
|
21003 |
}
|
|
|
21004 |
|
|
|
21005 |
/**
|
|
|
21006 |
* Reset the tech by removing all sources and then calling
|
|
|
21007 |
* {@link Html5.resetMediaElement}.
|
|
|
21008 |
*/
|
|
|
21009 |
reset() {
|
|
|
21010 |
Html5.resetMediaElement(this.el_);
|
|
|
21011 |
}
|
|
|
21012 |
|
|
|
21013 |
/**
|
|
|
21014 |
* Get the current source on the `HTML5` Tech. Falls back to returning the source from
|
|
|
21015 |
* the HTML5 media element.
|
|
|
21016 |
*
|
|
|
21017 |
* @return {Tech~SourceObject}
|
|
|
21018 |
* The current source object from the HTML5 tech. With a fallback to the
|
|
|
21019 |
* elements source.
|
|
|
21020 |
*/
|
|
|
21021 |
currentSrc() {
|
|
|
21022 |
if (this.currentSource_) {
|
|
|
21023 |
return this.currentSource_.src;
|
|
|
21024 |
}
|
|
|
21025 |
return this.el_.currentSrc;
|
|
|
21026 |
}
|
|
|
21027 |
|
|
|
21028 |
/**
|
|
|
21029 |
* Set controls attribute for the HTML5 media Element.
|
|
|
21030 |
*
|
|
|
21031 |
* @param {string} val
|
|
|
21032 |
* Value to set the controls attribute to
|
|
|
21033 |
*/
|
|
|
21034 |
setControls(val) {
|
|
|
21035 |
this.el_.controls = !!val;
|
|
|
21036 |
}
|
|
|
21037 |
|
|
|
21038 |
/**
|
|
|
21039 |
* Create and returns a remote {@link TextTrack} object.
|
|
|
21040 |
*
|
|
|
21041 |
* @param {string} kind
|
|
|
21042 |
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
|
|
|
21043 |
*
|
|
|
21044 |
* @param {string} [label]
|
|
|
21045 |
* Label to identify the text track
|
|
|
21046 |
*
|
|
|
21047 |
* @param {string} [language]
|
|
|
21048 |
* Two letter language abbreviation
|
|
|
21049 |
*
|
|
|
21050 |
* @return {TextTrack}
|
|
|
21051 |
* The TextTrack that gets created.
|
|
|
21052 |
*/
|
|
|
21053 |
addTextTrack(kind, label, language) {
|
|
|
21054 |
if (!this.featuresNativeTextTracks) {
|
|
|
21055 |
return super.addTextTrack(kind, label, language);
|
|
|
21056 |
}
|
|
|
21057 |
return this.el_.addTextTrack(kind, label, language);
|
|
|
21058 |
}
|
|
|
21059 |
|
|
|
21060 |
/**
|
|
|
21061 |
* Creates either native TextTrack or an emulated TextTrack depending
|
|
|
21062 |
* on the value of `featuresNativeTextTracks`
|
|
|
21063 |
*
|
|
|
21064 |
* @param {Object} options
|
|
|
21065 |
* The object should contain the options to initialize the TextTrack with.
|
|
|
21066 |
*
|
|
|
21067 |
* @param {string} [options.kind]
|
|
|
21068 |
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
|
|
|
21069 |
*
|
|
|
21070 |
* @param {string} [options.label]
|
|
|
21071 |
* Label to identify the text track
|
|
|
21072 |
*
|
|
|
21073 |
* @param {string} [options.language]
|
|
|
21074 |
* Two letter language abbreviation.
|
|
|
21075 |
*
|
|
|
21076 |
* @param {boolean} [options.default]
|
|
|
21077 |
* Default this track to on.
|
|
|
21078 |
*
|
|
|
21079 |
* @param {string} [options.id]
|
|
|
21080 |
* The internal id to assign this track.
|
|
|
21081 |
*
|
|
|
21082 |
* @param {string} [options.src]
|
|
|
21083 |
* A source url for the track.
|
|
|
21084 |
*
|
|
|
21085 |
* @return {HTMLTrackElement}
|
|
|
21086 |
* The track element that gets created.
|
|
|
21087 |
*/
|
|
|
21088 |
createRemoteTextTrack(options) {
|
|
|
21089 |
if (!this.featuresNativeTextTracks) {
|
|
|
21090 |
return super.createRemoteTextTrack(options);
|
|
|
21091 |
}
|
|
|
21092 |
const htmlTrackElement = document.createElement('track');
|
|
|
21093 |
if (options.kind) {
|
|
|
21094 |
htmlTrackElement.kind = options.kind;
|
|
|
21095 |
}
|
|
|
21096 |
if (options.label) {
|
|
|
21097 |
htmlTrackElement.label = options.label;
|
|
|
21098 |
}
|
|
|
21099 |
if (options.language || options.srclang) {
|
|
|
21100 |
htmlTrackElement.srclang = options.language || options.srclang;
|
|
|
21101 |
}
|
|
|
21102 |
if (options.default) {
|
|
|
21103 |
htmlTrackElement.default = options.default;
|
|
|
21104 |
}
|
|
|
21105 |
if (options.id) {
|
|
|
21106 |
htmlTrackElement.id = options.id;
|
|
|
21107 |
}
|
|
|
21108 |
if (options.src) {
|
|
|
21109 |
htmlTrackElement.src = options.src;
|
|
|
21110 |
}
|
|
|
21111 |
return htmlTrackElement;
|
|
|
21112 |
}
|
|
|
21113 |
|
|
|
21114 |
/**
|
|
|
21115 |
* Creates a remote text track object and returns an html track element.
|
|
|
21116 |
*
|
|
|
21117 |
* @param {Object} options The object should contain values for
|
|
|
21118 |
* kind, language, label, and src (location of the WebVTT file)
|
|
|
21119 |
* @param {boolean} [manualCleanup=false] if set to true, the TextTrack
|
|
|
21120 |
* will not be removed from the TextTrackList and HtmlTrackElementList
|
|
|
21121 |
* after a source change
|
|
|
21122 |
* @return {HTMLTrackElement} An Html Track Element.
|
|
|
21123 |
* This can be an emulated {@link HTMLTrackElement} or a native one.
|
|
|
21124 |
*
|
|
|
21125 |
*/
|
|
|
21126 |
addRemoteTextTrack(options, manualCleanup) {
|
|
|
21127 |
const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
|
|
|
21128 |
if (this.featuresNativeTextTracks) {
|
|
|
21129 |
this.el().appendChild(htmlTrackElement);
|
|
|
21130 |
}
|
|
|
21131 |
return htmlTrackElement;
|
|
|
21132 |
}
|
|
|
21133 |
|
|
|
21134 |
/**
|
|
|
21135 |
* Remove remote `TextTrack` from `TextTrackList` object
|
|
|
21136 |
*
|
|
|
21137 |
* @param {TextTrack} track
|
|
|
21138 |
* `TextTrack` object to remove
|
|
|
21139 |
*/
|
|
|
21140 |
removeRemoteTextTrack(track) {
|
|
|
21141 |
super.removeRemoteTextTrack(track);
|
|
|
21142 |
if (this.featuresNativeTextTracks) {
|
|
|
21143 |
const tracks = this.$$('track');
|
|
|
21144 |
let i = tracks.length;
|
|
|
21145 |
while (i--) {
|
|
|
21146 |
if (track === tracks[i] || track === tracks[i].track) {
|
|
|
21147 |
this.el().removeChild(tracks[i]);
|
|
|
21148 |
}
|
|
|
21149 |
}
|
|
|
21150 |
}
|
|
|
21151 |
}
|
|
|
21152 |
|
|
|
21153 |
/**
|
|
|
21154 |
* Gets available media playback quality metrics as specified by the W3C's Media
|
|
|
21155 |
* Playback Quality API.
|
|
|
21156 |
*
|
|
|
21157 |
* @see [Spec]{@link https://wicg.github.io/media-playback-quality}
|
|
|
21158 |
*
|
|
|
21159 |
* @return {Object}
|
|
|
21160 |
* An object with supported media playback quality metrics
|
|
|
21161 |
*/
|
|
|
21162 |
getVideoPlaybackQuality() {
|
|
|
21163 |
if (typeof this.el().getVideoPlaybackQuality === 'function') {
|
|
|
21164 |
return this.el().getVideoPlaybackQuality();
|
|
|
21165 |
}
|
|
|
21166 |
const videoPlaybackQuality = {};
|
|
|
21167 |
if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
|
|
|
21168 |
videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
|
|
|
21169 |
videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
|
|
|
21170 |
}
|
|
|
21171 |
if (window.performance) {
|
|
|
21172 |
videoPlaybackQuality.creationTime = window.performance.now();
|
|
|
21173 |
}
|
|
|
21174 |
return videoPlaybackQuality;
|
|
|
21175 |
}
|
|
|
21176 |
}
|
|
|
21177 |
|
|
|
21178 |
/* HTML5 Support Testing ---------------------------------------------------- */
|
|
|
21179 |
|
|
|
21180 |
/**
|
|
|
21181 |
* Element for testing browser HTML5 media capabilities
|
|
|
21182 |
*
|
|
|
21183 |
* @type {Element}
|
|
|
21184 |
* @constant
|
|
|
21185 |
* @private
|
|
|
21186 |
*/
|
|
|
21187 |
defineLazyProperty(Html5, 'TEST_VID', function () {
|
|
|
21188 |
if (!isReal()) {
|
|
|
21189 |
return;
|
|
|
21190 |
}
|
|
|
21191 |
const video = document.createElement('video');
|
|
|
21192 |
const track = document.createElement('track');
|
|
|
21193 |
track.kind = 'captions';
|
|
|
21194 |
track.srclang = 'en';
|
|
|
21195 |
track.label = 'English';
|
|
|
21196 |
video.appendChild(track);
|
|
|
21197 |
return video;
|
|
|
21198 |
});
|
|
|
21199 |
|
|
|
21200 |
/**
|
|
|
21201 |
* Check if HTML5 media is supported by this browser/device.
|
|
|
21202 |
*
|
|
|
21203 |
* @return {boolean}
|
|
|
21204 |
* - True if HTML5 media is supported.
|
|
|
21205 |
* - False if HTML5 media is not supported.
|
|
|
21206 |
*/
|
|
|
21207 |
Html5.isSupported = function () {
|
|
|
21208 |
// IE with no Media Player is a LIAR! (#984)
|
|
|
21209 |
try {
|
|
|
21210 |
Html5.TEST_VID.volume = 0.5;
|
|
|
21211 |
} catch (e) {
|
|
|
21212 |
return false;
|
|
|
21213 |
}
|
|
|
21214 |
return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
|
|
|
21215 |
};
|
|
|
21216 |
|
|
|
21217 |
/**
|
|
|
21218 |
* Check if the tech can support the given type
|
|
|
21219 |
*
|
|
|
21220 |
* @param {string} type
|
|
|
21221 |
* The mimetype to check
|
|
|
21222 |
* @return {string} 'probably', 'maybe', or '' (empty string)
|
|
|
21223 |
*/
|
|
|
21224 |
Html5.canPlayType = function (type) {
|
|
|
21225 |
return Html5.TEST_VID.canPlayType(type);
|
|
|
21226 |
};
|
|
|
21227 |
|
|
|
21228 |
/**
|
|
|
21229 |
* Check if the tech can support the given source
|
|
|
21230 |
*
|
|
|
21231 |
* @param {Object} srcObj
|
|
|
21232 |
* The source object
|
|
|
21233 |
* @param {Object} options
|
|
|
21234 |
* The options passed to the tech
|
|
|
21235 |
* @return {string} 'probably', 'maybe', or '' (empty string)
|
|
|
21236 |
*/
|
|
|
21237 |
Html5.canPlaySource = function (srcObj, options) {
|
|
|
21238 |
return Html5.canPlayType(srcObj.type);
|
|
|
21239 |
};
|
|
|
21240 |
|
|
|
21241 |
/**
|
|
|
21242 |
* Check if the volume can be changed in this browser/device.
|
|
|
21243 |
* Volume cannot be changed in a lot of mobile devices.
|
|
|
21244 |
* Specifically, it can't be changed from 1 on iOS.
|
|
|
21245 |
*
|
|
|
21246 |
* @return {boolean}
|
|
|
21247 |
* - True if volume can be controlled
|
|
|
21248 |
* - False otherwise
|
|
|
21249 |
*/
|
|
|
21250 |
Html5.canControlVolume = function () {
|
|
|
21251 |
// IE will error if Windows Media Player not installed #3315
|
|
|
21252 |
try {
|
|
|
21253 |
const volume = Html5.TEST_VID.volume;
|
|
|
21254 |
Html5.TEST_VID.volume = volume / 2 + 0.1;
|
|
|
21255 |
const canControl = volume !== Html5.TEST_VID.volume;
|
|
|
21256 |
|
|
|
21257 |
// With the introduction of iOS 15, there are cases where the volume is read as
|
|
|
21258 |
// changed but reverts back to its original state at the start of the next tick.
|
|
|
21259 |
// To determine whether volume can be controlled on iOS,
|
|
|
21260 |
// a timeout is set and the volume is checked asynchronously.
|
|
|
21261 |
// Since `features` doesn't currently work asynchronously, the value is manually set.
|
|
|
21262 |
if (canControl && IS_IOS) {
|
|
|
21263 |
window.setTimeout(() => {
|
|
|
21264 |
if (Html5 && Html5.prototype) {
|
|
|
21265 |
Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
|
|
|
21266 |
}
|
|
|
21267 |
});
|
|
|
21268 |
|
|
|
21269 |
// default iOS to false, which will be updated in the timeout above.
|
|
|
21270 |
return false;
|
|
|
21271 |
}
|
|
|
21272 |
return canControl;
|
|
|
21273 |
} catch (e) {
|
|
|
21274 |
return false;
|
|
|
21275 |
}
|
|
|
21276 |
};
|
|
|
21277 |
|
|
|
21278 |
/**
|
|
|
21279 |
* Check if the volume can be muted in this browser/device.
|
|
|
21280 |
* Some devices, e.g. iOS, don't allow changing volume
|
|
|
21281 |
* but permits muting/unmuting.
|
|
|
21282 |
*
|
|
|
21283 |
* @return {boolean}
|
|
|
21284 |
* - True if volume can be muted
|
|
|
21285 |
* - False otherwise
|
|
|
21286 |
*/
|
|
|
21287 |
Html5.canMuteVolume = function () {
|
|
|
21288 |
try {
|
|
|
21289 |
const muted = Html5.TEST_VID.muted;
|
|
|
21290 |
|
|
|
21291 |
// in some versions of iOS muted property doesn't always
|
|
|
21292 |
// work, so we want to set both property and attribute
|
|
|
21293 |
Html5.TEST_VID.muted = !muted;
|
|
|
21294 |
if (Html5.TEST_VID.muted) {
|
|
|
21295 |
setAttribute(Html5.TEST_VID, 'muted', 'muted');
|
|
|
21296 |
} else {
|
|
|
21297 |
removeAttribute(Html5.TEST_VID, 'muted', 'muted');
|
|
|
21298 |
}
|
|
|
21299 |
return muted !== Html5.TEST_VID.muted;
|
|
|
21300 |
} catch (e) {
|
|
|
21301 |
return false;
|
|
|
21302 |
}
|
|
|
21303 |
};
|
|
|
21304 |
|
|
|
21305 |
/**
|
|
|
21306 |
* Check if the playback rate can be changed in this browser/device.
|
|
|
21307 |
*
|
|
|
21308 |
* @return {boolean}
|
|
|
21309 |
* - True if playback rate can be controlled
|
|
|
21310 |
* - False otherwise
|
|
|
21311 |
*/
|
|
|
21312 |
Html5.canControlPlaybackRate = function () {
|
|
|
21313 |
// Playback rate API is implemented in Android Chrome, but doesn't do anything
|
|
|
21314 |
// https://github.com/videojs/video.js/issues/3180
|
|
|
21315 |
if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
|
|
|
21316 |
return false;
|
|
|
21317 |
}
|
|
|
21318 |
// IE will error if Windows Media Player not installed #3315
|
|
|
21319 |
try {
|
|
|
21320 |
const playbackRate = Html5.TEST_VID.playbackRate;
|
|
|
21321 |
Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
|
|
|
21322 |
return playbackRate !== Html5.TEST_VID.playbackRate;
|
|
|
21323 |
} catch (e) {
|
|
|
21324 |
return false;
|
|
|
21325 |
}
|
|
|
21326 |
};
|
|
|
21327 |
|
|
|
21328 |
/**
|
|
|
21329 |
* Check if we can override a video/audio elements attributes, with
|
|
|
21330 |
* Object.defineProperty.
|
|
|
21331 |
*
|
|
|
21332 |
* @return {boolean}
|
|
|
21333 |
* - True if builtin attributes can be overridden
|
|
|
21334 |
* - False otherwise
|
|
|
21335 |
*/
|
|
|
21336 |
Html5.canOverrideAttributes = function () {
|
|
|
21337 |
// if we cannot overwrite the src/innerHTML property, there is no support
|
|
|
21338 |
// iOS 7 safari for instance cannot do this.
|
|
|
21339 |
try {
|
|
|
21340 |
const noop = () => {};
|
|
|
21341 |
Object.defineProperty(document.createElement('video'), 'src', {
|
|
|
21342 |
get: noop,
|
|
|
21343 |
set: noop
|
|
|
21344 |
});
|
|
|
21345 |
Object.defineProperty(document.createElement('audio'), 'src', {
|
|
|
21346 |
get: noop,
|
|
|
21347 |
set: noop
|
|
|
21348 |
});
|
|
|
21349 |
Object.defineProperty(document.createElement('video'), 'innerHTML', {
|
|
|
21350 |
get: noop,
|
|
|
21351 |
set: noop
|
|
|
21352 |
});
|
|
|
21353 |
Object.defineProperty(document.createElement('audio'), 'innerHTML', {
|
|
|
21354 |
get: noop,
|
|
|
21355 |
set: noop
|
|
|
21356 |
});
|
|
|
21357 |
} catch (e) {
|
|
|
21358 |
return false;
|
|
|
21359 |
}
|
|
|
21360 |
return true;
|
|
|
21361 |
};
|
|
|
21362 |
|
|
|
21363 |
/**
|
|
|
21364 |
* Check to see if native `TextTrack`s are supported by this browser/device.
|
|
|
21365 |
*
|
|
|
21366 |
* @return {boolean}
|
|
|
21367 |
* - True if native `TextTrack`s are supported.
|
|
|
21368 |
* - False otherwise
|
|
|
21369 |
*/
|
|
|
21370 |
Html5.supportsNativeTextTracks = function () {
|
|
|
21371 |
return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
|
|
|
21372 |
};
|
|
|
21373 |
|
|
|
21374 |
/**
|
|
|
21375 |
* Check to see if native `VideoTrack`s are supported by this browser/device
|
|
|
21376 |
*
|
|
|
21377 |
* @return {boolean}
|
|
|
21378 |
* - True if native `VideoTrack`s are supported.
|
|
|
21379 |
* - False otherwise
|
|
|
21380 |
*/
|
|
|
21381 |
Html5.supportsNativeVideoTracks = function () {
|
|
|
21382 |
return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
|
|
|
21383 |
};
|
|
|
21384 |
|
|
|
21385 |
/**
|
|
|
21386 |
* Check to see if native `AudioTrack`s are supported by this browser/device
|
|
|
21387 |
*
|
|
|
21388 |
* @return {boolean}
|
|
|
21389 |
* - True if native `AudioTrack`s are supported.
|
|
|
21390 |
* - False otherwise
|
|
|
21391 |
*/
|
|
|
21392 |
Html5.supportsNativeAudioTracks = function () {
|
|
|
21393 |
return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
|
|
|
21394 |
};
|
|
|
21395 |
|
|
|
21396 |
/**
|
|
|
21397 |
* An array of events available on the Html5 tech.
|
|
|
21398 |
*
|
|
|
21399 |
* @private
|
|
|
21400 |
* @type {Array}
|
|
|
21401 |
*/
|
|
|
21402 |
Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange'];
|
|
|
21403 |
|
|
|
21404 |
/**
|
|
|
21405 |
* Boolean indicating whether the `Tech` supports volume control.
|
|
|
21406 |
*
|
|
|
21407 |
* @type {boolean}
|
|
|
21408 |
* @default {@link Html5.canControlVolume}
|
|
|
21409 |
*/
|
|
|
21410 |
/**
|
|
|
21411 |
* Boolean indicating whether the `Tech` supports muting volume.
|
|
|
21412 |
*
|
|
|
21413 |
* @type {boolean}
|
|
|
21414 |
* @default {@link Html5.canMuteVolume}
|
|
|
21415 |
*/
|
|
|
21416 |
|
|
|
21417 |
/**
|
|
|
21418 |
* Boolean indicating whether the `Tech` supports changing the speed at which the media
|
|
|
21419 |
* plays. Examples:
|
|
|
21420 |
* - Set player to play 2x (twice) as fast
|
|
|
21421 |
* - Set player to play 0.5x (half) as fast
|
|
|
21422 |
*
|
|
|
21423 |
* @type {boolean}
|
|
|
21424 |
* @default {@link Html5.canControlPlaybackRate}
|
|
|
21425 |
*/
|
|
|
21426 |
|
|
|
21427 |
/**
|
|
|
21428 |
* Boolean indicating whether the `Tech` supports the `sourceset` event.
|
|
|
21429 |
*
|
|
|
21430 |
* @type {boolean}
|
|
|
21431 |
* @default
|
|
|
21432 |
*/
|
|
|
21433 |
/**
|
|
|
21434 |
* Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
|
|
|
21435 |
*
|
|
|
21436 |
* @type {boolean}
|
|
|
21437 |
* @default {@link Html5.supportsNativeTextTracks}
|
|
|
21438 |
*/
|
|
|
21439 |
/**
|
|
|
21440 |
* Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
|
|
|
21441 |
*
|
|
|
21442 |
* @type {boolean}
|
|
|
21443 |
* @default {@link Html5.supportsNativeVideoTracks}
|
|
|
21444 |
*/
|
|
|
21445 |
/**
|
|
|
21446 |
* Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
|
|
|
21447 |
*
|
|
|
21448 |
* @type {boolean}
|
|
|
21449 |
* @default {@link Html5.supportsNativeAudioTracks}
|
|
|
21450 |
*/
|
|
|
21451 |
[['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
|
|
|
21452 |
defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
|
|
|
21453 |
});
|
|
|
21454 |
Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
|
|
|
21455 |
|
|
|
21456 |
/**
|
|
|
21457 |
* Boolean indicating whether the `HTML5` tech currently supports the media element
|
|
|
21458 |
* moving in the DOM. iOS breaks if you move the media element, so this is set this to
|
|
|
21459 |
* false there. Everywhere else this should be true.
|
|
|
21460 |
*
|
|
|
21461 |
* @type {boolean}
|
|
|
21462 |
* @default
|
|
|
21463 |
*/
|
|
|
21464 |
Html5.prototype.movingMediaElementInDOM = !IS_IOS;
|
|
|
21465 |
|
|
|
21466 |
// TODO: Previous comment: No longer appears to be used. Can probably be removed.
|
|
|
21467 |
// Is this true?
|
|
|
21468 |
/**
|
|
|
21469 |
* Boolean indicating whether the `HTML5` tech currently supports automatic media resize
|
|
|
21470 |
* when going into fullscreen.
|
|
|
21471 |
*
|
|
|
21472 |
* @type {boolean}
|
|
|
21473 |
* @default
|
|
|
21474 |
*/
|
|
|
21475 |
Html5.prototype.featuresFullscreenResize = true;
|
|
|
21476 |
|
|
|
21477 |
/**
|
|
|
21478 |
* Boolean indicating whether the `HTML5` tech currently supports the progress event.
|
|
|
21479 |
* If this is false, manual `progress` events will be triggered instead.
|
|
|
21480 |
*
|
|
|
21481 |
* @type {boolean}
|
|
|
21482 |
* @default
|
|
|
21483 |
*/
|
|
|
21484 |
Html5.prototype.featuresProgressEvents = true;
|
|
|
21485 |
|
|
|
21486 |
/**
|
|
|
21487 |
* Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
|
|
|
21488 |
* If this is false, manual `timeupdate` events will be triggered instead.
|
|
|
21489 |
*
|
|
|
21490 |
* @default
|
|
|
21491 |
*/
|
|
|
21492 |
Html5.prototype.featuresTimeupdateEvents = true;
|
|
|
21493 |
|
|
|
21494 |
/**
|
|
|
21495 |
* Whether the HTML5 el supports `requestVideoFrameCallback`
|
|
|
21496 |
*
|
|
|
21497 |
* @type {boolean}
|
|
|
21498 |
*/
|
|
|
21499 |
Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
|
|
|
21500 |
Html5.disposeMediaElement = function (el) {
|
|
|
21501 |
if (!el) {
|
|
|
21502 |
return;
|
|
|
21503 |
}
|
|
|
21504 |
if (el.parentNode) {
|
|
|
21505 |
el.parentNode.removeChild(el);
|
|
|
21506 |
}
|
|
|
21507 |
|
|
|
21508 |
// remove any child track or source nodes to prevent their loading
|
|
|
21509 |
while (el.hasChildNodes()) {
|
|
|
21510 |
el.removeChild(el.firstChild);
|
|
|
21511 |
}
|
|
|
21512 |
|
|
|
21513 |
// remove any src reference. not setting `src=''` because that causes a warning
|
|
|
21514 |
// in firefox
|
|
|
21515 |
el.removeAttribute('src');
|
|
|
21516 |
|
|
|
21517 |
// force the media element to update its loading state by calling load()
|
|
|
21518 |
// however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
|
|
|
21519 |
if (typeof el.load === 'function') {
|
|
|
21520 |
// wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
|
|
|
21521 |
(function () {
|
|
|
21522 |
try {
|
|
|
21523 |
el.load();
|
|
|
21524 |
} catch (e) {
|
|
|
21525 |
// not supported
|
|
|
21526 |
}
|
|
|
21527 |
})();
|
|
|
21528 |
}
|
|
|
21529 |
};
|
|
|
21530 |
Html5.resetMediaElement = function (el) {
|
|
|
21531 |
if (!el) {
|
|
|
21532 |
return;
|
|
|
21533 |
}
|
|
|
21534 |
const sources = el.querySelectorAll('source');
|
|
|
21535 |
let i = sources.length;
|
|
|
21536 |
while (i--) {
|
|
|
21537 |
el.removeChild(sources[i]);
|
|
|
21538 |
}
|
|
|
21539 |
|
|
|
21540 |
// remove any src reference.
|
|
|
21541 |
// not setting `src=''` because that throws an error
|
|
|
21542 |
el.removeAttribute('src');
|
|
|
21543 |
if (typeof el.load === 'function') {
|
|
|
21544 |
// wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
|
|
|
21545 |
(function () {
|
|
|
21546 |
try {
|
|
|
21547 |
el.load();
|
|
|
21548 |
} catch (e) {
|
|
|
21549 |
// satisfy linter
|
|
|
21550 |
}
|
|
|
21551 |
})();
|
|
|
21552 |
}
|
|
|
21553 |
};
|
|
|
21554 |
|
|
|
21555 |
/* Native HTML5 element property wrapping ----------------------------------- */
|
|
|
21556 |
// Wrap native boolean attributes with getters that check both property and attribute
|
|
|
21557 |
// The list is as followed:
|
|
|
21558 |
// muted, defaultMuted, autoplay, controls, loop, playsinline
|
|
|
21559 |
[
|
|
|
21560 |
/**
|
|
|
21561 |
* Get the value of `muted` from the media element. `muted` indicates
|
|
|
21562 |
* that the volume for the media should be set to silent. This does not actually change
|
|
|
21563 |
* the `volume` attribute.
|
|
|
21564 |
*
|
|
|
21565 |
* @method Html5#muted
|
|
|
21566 |
* @return {boolean}
|
|
|
21567 |
* - True if the value of `volume` should be ignored and the audio set to silent.
|
|
|
21568 |
* - False if the value of `volume` should be used.
|
|
|
21569 |
*
|
|
|
21570 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
|
|
|
21571 |
*/
|
|
|
21572 |
'muted',
|
|
|
21573 |
/**
|
|
|
21574 |
* Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
|
|
|
21575 |
* whether the media should start muted or not. Only changes the default state of the
|
|
|
21576 |
* media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
|
|
|
21577 |
* current state.
|
|
|
21578 |
*
|
|
|
21579 |
* @method Html5#defaultMuted
|
|
|
21580 |
* @return {boolean}
|
|
|
21581 |
* - The value of `defaultMuted` from the media element.
|
|
|
21582 |
* - True indicates that the media should start muted.
|
|
|
21583 |
* - False indicates that the media should not start muted
|
|
|
21584 |
*
|
|
|
21585 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
|
|
|
21586 |
*/
|
|
|
21587 |
'defaultMuted',
|
|
|
21588 |
/**
|
|
|
21589 |
* Get the value of `autoplay` from the media element. `autoplay` indicates
|
|
|
21590 |
* that the media should start to play as soon as the page is ready.
|
|
|
21591 |
*
|
|
|
21592 |
* @method Html5#autoplay
|
|
|
21593 |
* @return {boolean}
|
|
|
21594 |
* - The value of `autoplay` from the media element.
|
|
|
21595 |
* - True indicates that the media should start as soon as the page loads.
|
|
|
21596 |
* - False indicates that the media should not start as soon as the page loads.
|
|
|
21597 |
*
|
|
|
21598 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
|
|
|
21599 |
*/
|
|
|
21600 |
'autoplay',
|
|
|
21601 |
/**
|
|
|
21602 |
* Get the value of `controls` from the media element. `controls` indicates
|
|
|
21603 |
* whether the native media controls should be shown or hidden.
|
|
|
21604 |
*
|
|
|
21605 |
* @method Html5#controls
|
|
|
21606 |
* @return {boolean}
|
|
|
21607 |
* - The value of `controls` from the media element.
|
|
|
21608 |
* - True indicates that native controls should be showing.
|
|
|
21609 |
* - False indicates that native controls should be hidden.
|
|
|
21610 |
*
|
|
|
21611 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
|
|
|
21612 |
*/
|
|
|
21613 |
'controls',
|
|
|
21614 |
/**
|
|
|
21615 |
* Get the value of `loop` from the media element. `loop` indicates
|
|
|
21616 |
* that the media should return to the start of the media and continue playing once
|
|
|
21617 |
* it reaches the end.
|
|
|
21618 |
*
|
|
|
21619 |
* @method Html5#loop
|
|
|
21620 |
* @return {boolean}
|
|
|
21621 |
* - The value of `loop` from the media element.
|
|
|
21622 |
* - True indicates that playback should seek back to start once
|
|
|
21623 |
* the end of a media is reached.
|
|
|
21624 |
* - False indicates that playback should not loop back to the start when the
|
|
|
21625 |
* end of the media is reached.
|
|
|
21626 |
*
|
|
|
21627 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
|
|
|
21628 |
*/
|
|
|
21629 |
'loop',
|
|
|
21630 |
/**
|
|
|
21631 |
* Get the value of `playsinline` from the media element. `playsinline` indicates
|
|
|
21632 |
* to the browser that non-fullscreen playback is preferred when fullscreen
|
|
|
21633 |
* playback is the native default, such as in iOS Safari.
|
|
|
21634 |
*
|
|
|
21635 |
* @method Html5#playsinline
|
|
|
21636 |
* @return {boolean}
|
|
|
21637 |
* - The value of `playsinline` from the media element.
|
|
|
21638 |
* - True indicates that the media should play inline.
|
|
|
21639 |
* - False indicates that the media should not play inline.
|
|
|
21640 |
*
|
|
|
21641 |
* @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
|
|
|
21642 |
*/
|
|
|
21643 |
'playsinline'].forEach(function (prop) {
|
|
|
21644 |
Html5.prototype[prop] = function () {
|
|
|
21645 |
return this.el_[prop] || this.el_.hasAttribute(prop);
|
|
|
21646 |
};
|
|
|
21647 |
});
|
|
|
21648 |
|
|
|
21649 |
// Wrap native boolean attributes with setters that set both property and attribute
|
|
|
21650 |
// The list is as followed:
|
|
|
21651 |
// setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
|
|
|
21652 |
// setControls is special-cased above
|
|
|
21653 |
[
|
|
|
21654 |
/**
|
|
|
21655 |
* Set the value of `muted` on the media element. `muted` indicates that the current
|
|
|
21656 |
* audio level should be silent.
|
|
|
21657 |
*
|
|
|
21658 |
* @method Html5#setMuted
|
|
|
21659 |
* @param {boolean} muted
|
|
|
21660 |
* - True if the audio should be set to silent
|
|
|
21661 |
* - False otherwise
|
|
|
21662 |
*
|
|
|
21663 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
|
|
|
21664 |
*/
|
|
|
21665 |
'muted',
|
|
|
21666 |
/**
|
|
|
21667 |
* Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
|
|
|
21668 |
* audio level should be silent, but will only effect the muted level on initial playback..
|
|
|
21669 |
*
|
|
|
21670 |
* @method Html5.prototype.setDefaultMuted
|
|
|
21671 |
* @param {boolean} defaultMuted
|
|
|
21672 |
* - True if the audio should be set to silent
|
|
|
21673 |
* - False otherwise
|
|
|
21674 |
*
|
|
|
21675 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
|
|
|
21676 |
*/
|
|
|
21677 |
'defaultMuted',
|
|
|
21678 |
/**
|
|
|
21679 |
* Set the value of `autoplay` on the media element. `autoplay` indicates
|
|
|
21680 |
* that the media should start to play as soon as the page is ready.
|
|
|
21681 |
*
|
|
|
21682 |
* @method Html5#setAutoplay
|
|
|
21683 |
* @param {boolean} autoplay
|
|
|
21684 |
* - True indicates that the media should start as soon as the page loads.
|
|
|
21685 |
* - False indicates that the media should not start as soon as the page loads.
|
|
|
21686 |
*
|
|
|
21687 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
|
|
|
21688 |
*/
|
|
|
21689 |
'autoplay',
|
|
|
21690 |
/**
|
|
|
21691 |
* Set the value of `loop` on the media element. `loop` indicates
|
|
|
21692 |
* that the media should return to the start of the media and continue playing once
|
|
|
21693 |
* it reaches the end.
|
|
|
21694 |
*
|
|
|
21695 |
* @method Html5#setLoop
|
|
|
21696 |
* @param {boolean} loop
|
|
|
21697 |
* - True indicates that playback should seek back to start once
|
|
|
21698 |
* the end of a media is reached.
|
|
|
21699 |
* - False indicates that playback should not loop back to the start when the
|
|
|
21700 |
* end of the media is reached.
|
|
|
21701 |
*
|
|
|
21702 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
|
|
|
21703 |
*/
|
|
|
21704 |
'loop',
|
|
|
21705 |
/**
|
|
|
21706 |
* Set the value of `playsinline` from the media element. `playsinline` indicates
|
|
|
21707 |
* to the browser that non-fullscreen playback is preferred when fullscreen
|
|
|
21708 |
* playback is the native default, such as in iOS Safari.
|
|
|
21709 |
*
|
|
|
21710 |
* @method Html5#setPlaysinline
|
|
|
21711 |
* @param {boolean} playsinline
|
|
|
21712 |
* - True indicates that the media should play inline.
|
|
|
21713 |
* - False indicates that the media should not play inline.
|
|
|
21714 |
*
|
|
|
21715 |
* @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
|
|
|
21716 |
*/
|
|
|
21717 |
'playsinline'].forEach(function (prop) {
|
|
|
21718 |
Html5.prototype['set' + toTitleCase$1(prop)] = function (v) {
|
|
|
21719 |
this.el_[prop] = v;
|
|
|
21720 |
if (v) {
|
|
|
21721 |
this.el_.setAttribute(prop, prop);
|
|
|
21722 |
} else {
|
|
|
21723 |
this.el_.removeAttribute(prop);
|
|
|
21724 |
}
|
|
|
21725 |
};
|
|
|
21726 |
});
|
|
|
21727 |
|
|
|
21728 |
// Wrap native properties with a getter
|
|
|
21729 |
// The list is as followed
|
|
|
21730 |
// paused, currentTime, buffered, volume, poster, preload, error, seeking
|
|
|
21731 |
// seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
|
|
|
21732 |
// played, networkState, readyState, videoWidth, videoHeight, crossOrigin
|
|
|
21733 |
[
|
|
|
21734 |
/**
|
|
|
21735 |
* Get the value of `paused` from the media element. `paused` indicates whether the media element
|
|
|
21736 |
* is currently paused or not.
|
|
|
21737 |
*
|
|
|
21738 |
* @method Html5#paused
|
|
|
21739 |
* @return {boolean}
|
|
|
21740 |
* The value of `paused` from the media element.
|
|
|
21741 |
*
|
|
|
21742 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
|
|
|
21743 |
*/
|
|
|
21744 |
'paused',
|
|
|
21745 |
/**
|
|
|
21746 |
* Get the value of `currentTime` from the media element. `currentTime` indicates
|
|
|
21747 |
* the current second that the media is at in playback.
|
|
|
21748 |
*
|
|
|
21749 |
* @method Html5#currentTime
|
|
|
21750 |
* @return {number}
|
|
|
21751 |
* The value of `currentTime` from the media element.
|
|
|
21752 |
*
|
|
|
21753 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
|
|
|
21754 |
*/
|
|
|
21755 |
'currentTime',
|
|
|
21756 |
/**
|
|
|
21757 |
* Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
|
|
|
21758 |
* object that represents the parts of the media that are already downloaded and
|
|
|
21759 |
* available for playback.
|
|
|
21760 |
*
|
|
|
21761 |
* @method Html5#buffered
|
|
|
21762 |
* @return {TimeRange}
|
|
|
21763 |
* The value of `buffered` from the media element.
|
|
|
21764 |
*
|
|
|
21765 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
|
|
|
21766 |
*/
|
|
|
21767 |
'buffered',
|
|
|
21768 |
/**
|
|
|
21769 |
* Get the value of `volume` from the media element. `volume` indicates
|
|
|
21770 |
* the current playback volume of audio for a media. `volume` will be a value from 0
|
|
|
21771 |
* (silent) to 1 (loudest and default).
|
|
|
21772 |
*
|
|
|
21773 |
* @method Html5#volume
|
|
|
21774 |
* @return {number}
|
|
|
21775 |
* The value of `volume` from the media element. Value will be between 0-1.
|
|
|
21776 |
*
|
|
|
21777 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
|
|
|
21778 |
*/
|
|
|
21779 |
'volume',
|
|
|
21780 |
/**
|
|
|
21781 |
* Get the value of `poster` from the media element. `poster` indicates
|
|
|
21782 |
* that the url of an image file that can/will be shown when no media data is available.
|
|
|
21783 |
*
|
|
|
21784 |
* @method Html5#poster
|
|
|
21785 |
* @return {string}
|
|
|
21786 |
* The value of `poster` from the media element. Value will be a url to an
|
|
|
21787 |
* image.
|
|
|
21788 |
*
|
|
|
21789 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
|
|
|
21790 |
*/
|
|
|
21791 |
'poster',
|
|
|
21792 |
/**
|
|
|
21793 |
* Get the value of `preload` from the media element. `preload` indicates
|
|
|
21794 |
* what should download before the media is interacted with. It can have the following
|
|
|
21795 |
* values:
|
|
|
21796 |
* - none: nothing should be downloaded
|
|
|
21797 |
* - metadata: poster and the first few frames of the media may be downloaded to get
|
|
|
21798 |
* media dimensions and other metadata
|
|
|
21799 |
* - auto: allow the media and metadata for the media to be downloaded before
|
|
|
21800 |
* interaction
|
|
|
21801 |
*
|
|
|
21802 |
* @method Html5#preload
|
|
|
21803 |
* @return {string}
|
|
|
21804 |
* The value of `preload` from the media element. Will be 'none', 'metadata',
|
|
|
21805 |
* or 'auto'.
|
|
|
21806 |
*
|
|
|
21807 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
|
|
|
21808 |
*/
|
|
|
21809 |
'preload',
|
|
|
21810 |
/**
|
|
|
21811 |
* Get the value of the `error` from the media element. `error` indicates any
|
|
|
21812 |
* MediaError that may have occurred during playback. If error returns null there is no
|
|
|
21813 |
* current error.
|
|
|
21814 |
*
|
|
|
21815 |
* @method Html5#error
|
|
|
21816 |
* @return {MediaError|null}
|
|
|
21817 |
* The value of `error` from the media element. Will be `MediaError` if there
|
|
|
21818 |
* is a current error and null otherwise.
|
|
|
21819 |
*
|
|
|
21820 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
|
|
|
21821 |
*/
|
|
|
21822 |
'error',
|
|
|
21823 |
/**
|
|
|
21824 |
* Get the value of `seeking` from the media element. `seeking` indicates whether the
|
|
|
21825 |
* media is currently seeking to a new position or not.
|
|
|
21826 |
*
|
|
|
21827 |
* @method Html5#seeking
|
|
|
21828 |
* @return {boolean}
|
|
|
21829 |
* - The value of `seeking` from the media element.
|
|
|
21830 |
* - True indicates that the media is currently seeking to a new position.
|
|
|
21831 |
* - False indicates that the media is not seeking to a new position at this time.
|
|
|
21832 |
*
|
|
|
21833 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
|
|
|
21834 |
*/
|
|
|
21835 |
'seeking',
|
|
|
21836 |
/**
|
|
|
21837 |
* Get the value of `seekable` from the media element. `seekable` returns a
|
|
|
21838 |
* `TimeRange` object indicating ranges of time that can currently be `seeked` to.
|
|
|
21839 |
*
|
|
|
21840 |
* @method Html5#seekable
|
|
|
21841 |
* @return {TimeRange}
|
|
|
21842 |
* The value of `seekable` from the media element. A `TimeRange` object
|
|
|
21843 |
* indicating the current ranges of time that can be seeked to.
|
|
|
21844 |
*
|
|
|
21845 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
|
|
|
21846 |
*/
|
|
|
21847 |
'seekable',
|
|
|
21848 |
/**
|
|
|
21849 |
* Get the value of `ended` from the media element. `ended` indicates whether
|
|
|
21850 |
* the media has reached the end or not.
|
|
|
21851 |
*
|
|
|
21852 |
* @method Html5#ended
|
|
|
21853 |
* @return {boolean}
|
|
|
21854 |
* - The value of `ended` from the media element.
|
|
|
21855 |
* - True indicates that the media has ended.
|
|
|
21856 |
* - False indicates that the media has not ended.
|
|
|
21857 |
*
|
|
|
21858 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
|
|
|
21859 |
*/
|
|
|
21860 |
'ended',
|
|
|
21861 |
/**
|
|
|
21862 |
* Get the value of `playbackRate` from the media element. `playbackRate` indicates
|
|
|
21863 |
* the rate at which the media is currently playing back. Examples:
|
|
|
21864 |
* - if playbackRate is set to 2, media will play twice as fast.
|
|
|
21865 |
* - if playbackRate is set to 0.5, media will play half as fast.
|
|
|
21866 |
*
|
|
|
21867 |
* @method Html5#playbackRate
|
|
|
21868 |
* @return {number}
|
|
|
21869 |
* The value of `playbackRate` from the media element. A number indicating
|
|
|
21870 |
* the current playback speed of the media, where 1 is normal speed.
|
|
|
21871 |
*
|
|
|
21872 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
|
|
|
21873 |
*/
|
|
|
21874 |
'playbackRate',
|
|
|
21875 |
/**
|
|
|
21876 |
* Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
|
|
|
21877 |
* the rate at which the media is currently playing back. This value will not indicate the current
|
|
|
21878 |
* `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
|
|
|
21879 |
*
|
|
|
21880 |
* Examples:
|
|
|
21881 |
* - if defaultPlaybackRate is set to 2, media will play twice as fast.
|
|
|
21882 |
* - if defaultPlaybackRate is set to 0.5, media will play half as fast.
|
|
|
21883 |
*
|
|
|
21884 |
* @method Html5.prototype.defaultPlaybackRate
|
|
|
21885 |
* @return {number}
|
|
|
21886 |
* The value of `defaultPlaybackRate` from the media element. A number indicating
|
|
|
21887 |
* the current playback speed of the media, where 1 is normal speed.
|
|
|
21888 |
*
|
|
|
21889 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
|
|
|
21890 |
*/
|
|
|
21891 |
'defaultPlaybackRate',
|
|
|
21892 |
/**
|
|
|
21893 |
* Get the value of 'disablePictureInPicture' from the video element.
|
|
|
21894 |
*
|
|
|
21895 |
* @method Html5#disablePictureInPicture
|
|
|
21896 |
* @return {boolean} value
|
|
|
21897 |
* - The value of `disablePictureInPicture` from the video element.
|
|
|
21898 |
* - True indicates that the video can't be played in Picture-In-Picture mode
|
|
|
21899 |
* - False indicates that the video can be played in Picture-In-Picture mode
|
|
|
21900 |
*
|
|
|
21901 |
* @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
|
|
|
21902 |
*/
|
|
|
21903 |
'disablePictureInPicture',
|
|
|
21904 |
/**
|
|
|
21905 |
* Get the value of `played` from the media element. `played` returns a `TimeRange`
|
|
|
21906 |
* object representing points in the media timeline that have been played.
|
|
|
21907 |
*
|
|
|
21908 |
* @method Html5#played
|
|
|
21909 |
* @return {TimeRange}
|
|
|
21910 |
* The value of `played` from the media element. A `TimeRange` object indicating
|
|
|
21911 |
* the ranges of time that have been played.
|
|
|
21912 |
*
|
|
|
21913 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
|
|
|
21914 |
*/
|
|
|
21915 |
'played',
|
|
|
21916 |
/**
|
|
|
21917 |
* Get the value of `networkState` from the media element. `networkState` indicates
|
|
|
21918 |
* the current network state. It returns an enumeration from the following list:
|
|
|
21919 |
* - 0: NETWORK_EMPTY
|
|
|
21920 |
* - 1: NETWORK_IDLE
|
|
|
21921 |
* - 2: NETWORK_LOADING
|
|
|
21922 |
* - 3: NETWORK_NO_SOURCE
|
|
|
21923 |
*
|
|
|
21924 |
* @method Html5#networkState
|
|
|
21925 |
* @return {number}
|
|
|
21926 |
* The value of `networkState` from the media element. This will be a number
|
|
|
21927 |
* from the list in the description.
|
|
|
21928 |
*
|
|
|
21929 |
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
|
|
|
21930 |
*/
|
|
|
21931 |
'networkState',
|
|
|
21932 |
/**
|
|
|
21933 |
* Get the value of `readyState` from the media element. `readyState` indicates
|
|
|
21934 |
* the current state of the media element. It returns an enumeration from the
|
|
|
21935 |
* following list:
|
|
|
21936 |
* - 0: HAVE_NOTHING
|
|
|
21937 |
* - 1: HAVE_METADATA
|
|
|
21938 |
* - 2: HAVE_CURRENT_DATA
|
|
|
21939 |
* - 3: HAVE_FUTURE_DATA
|
|
|
21940 |
* - 4: HAVE_ENOUGH_DATA
|
|
|
21941 |
*
|
|
|
21942 |
* @method Html5#readyState
|
|
|
21943 |
* @return {number}
|
|
|
21944 |
* The value of `readyState` from the media element. This will be a number
|
|
|
21945 |
* from the list in the description.
|
|
|
21946 |
*
|
|
|
21947 |
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
|
|
|
21948 |
*/
|
|
|
21949 |
'readyState',
|
|
|
21950 |
/**
|
|
|
21951 |
* Get the value of `videoWidth` from the video element. `videoWidth` indicates
|
|
|
21952 |
* the current width of the video in css pixels.
|
|
|
21953 |
*
|
|
|
21954 |
* @method Html5#videoWidth
|
|
|
21955 |
* @return {number}
|
|
|
21956 |
* The value of `videoWidth` from the video element. This will be a number
|
|
|
21957 |
* in css pixels.
|
|
|
21958 |
*
|
|
|
21959 |
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
|
|
|
21960 |
*/
|
|
|
21961 |
'videoWidth',
|
|
|
21962 |
/**
|
|
|
21963 |
* Get the value of `videoHeight` from the video element. `videoHeight` indicates
|
|
|
21964 |
* the current height of the video in css pixels.
|
|
|
21965 |
*
|
|
|
21966 |
* @method Html5#videoHeight
|
|
|
21967 |
* @return {number}
|
|
|
21968 |
* The value of `videoHeight` from the video element. This will be a number
|
|
|
21969 |
* in css pixels.
|
|
|
21970 |
*
|
|
|
21971 |
* @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
|
|
|
21972 |
*/
|
|
|
21973 |
'videoHeight',
|
|
|
21974 |
/**
|
|
|
21975 |
* Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
|
|
|
21976 |
* to the browser that should sent the cookies along with the requests for the
|
|
|
21977 |
* different assets/playlists
|
|
|
21978 |
*
|
|
|
21979 |
* @method Html5#crossOrigin
|
|
|
21980 |
* @return {string}
|
|
|
21981 |
* - anonymous indicates that the media should not sent cookies.
|
|
|
21982 |
* - use-credentials indicates that the media should sent cookies along the requests.
|
|
|
21983 |
*
|
|
|
21984 |
* @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
|
|
|
21985 |
*/
|
|
|
21986 |
'crossOrigin'].forEach(function (prop) {
|
|
|
21987 |
Html5.prototype[prop] = function () {
|
|
|
21988 |
return this.el_[prop];
|
|
|
21989 |
};
|
|
|
21990 |
});
|
|
|
21991 |
|
|
|
21992 |
// Wrap native properties with a setter in this format:
|
|
|
21993 |
// set + toTitleCase(name)
|
|
|
21994 |
// The list is as follows:
|
|
|
21995 |
// setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
|
|
|
21996 |
// setDisablePictureInPicture, setCrossOrigin
|
|
|
21997 |
[
|
|
|
21998 |
/**
|
|
|
21999 |
* Set the value of `volume` on the media element. `volume` indicates the current
|
|
|
22000 |
* audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
|
|
|
22001 |
* so on.
|
|
|
22002 |
*
|
|
|
22003 |
* @method Html5#setVolume
|
|
|
22004 |
* @param {number} percentAsDecimal
|
|
|
22005 |
* The volume percent as a decimal. Valid range is from 0-1.
|
|
|
22006 |
*
|
|
|
22007 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
|
|
|
22008 |
*/
|
|
|
22009 |
'volume',
|
|
|
22010 |
/**
|
|
|
22011 |
* Set the value of `src` on the media element. `src` indicates the current
|
|
|
22012 |
* {@link Tech~SourceObject} for the media.
|
|
|
22013 |
*
|
|
|
22014 |
* @method Html5#setSrc
|
|
|
22015 |
* @param {Tech~SourceObject} src
|
|
|
22016 |
* The source object to set as the current source.
|
|
|
22017 |
*
|
|
|
22018 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
|
|
|
22019 |
*/
|
|
|
22020 |
'src',
|
|
|
22021 |
/**
|
|
|
22022 |
* Set the value of `poster` on the media element. `poster` is the url to
|
|
|
22023 |
* an image file that can/will be shown when no media data is available.
|
|
|
22024 |
*
|
|
|
22025 |
* @method Html5#setPoster
|
|
|
22026 |
* @param {string} poster
|
|
|
22027 |
* The url to an image that should be used as the `poster` for the media
|
|
|
22028 |
* element.
|
|
|
22029 |
*
|
|
|
22030 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
|
|
|
22031 |
*/
|
|
|
22032 |
'poster',
|
|
|
22033 |
/**
|
|
|
22034 |
* Set the value of `preload` on the media element. `preload` indicates
|
|
|
22035 |
* what should download before the media is interacted with. It can have the following
|
|
|
22036 |
* values:
|
|
|
22037 |
* - none: nothing should be downloaded
|
|
|
22038 |
* - metadata: poster and the first few frames of the media may be downloaded to get
|
|
|
22039 |
* media dimensions and other metadata
|
|
|
22040 |
* - auto: allow the media and metadata for the media to be downloaded before
|
|
|
22041 |
* interaction
|
|
|
22042 |
*
|
|
|
22043 |
* @method Html5#setPreload
|
|
|
22044 |
* @param {string} preload
|
|
|
22045 |
* The value of `preload` to set on the media element. Must be 'none', 'metadata',
|
|
|
22046 |
* or 'auto'.
|
|
|
22047 |
*
|
|
|
22048 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
|
|
|
22049 |
*/
|
|
|
22050 |
'preload',
|
|
|
22051 |
/**
|
|
|
22052 |
* Set the value of `playbackRate` on the media element. `playbackRate` indicates
|
|
|
22053 |
* the rate at which the media should play back. Examples:
|
|
|
22054 |
* - if playbackRate is set to 2, media will play twice as fast.
|
|
|
22055 |
* - if playbackRate is set to 0.5, media will play half as fast.
|
|
|
22056 |
*
|
|
|
22057 |
* @method Html5#setPlaybackRate
|
|
|
22058 |
* @return {number}
|
|
|
22059 |
* The value of `playbackRate` from the media element. A number indicating
|
|
|
22060 |
* the current playback speed of the media, where 1 is normal speed.
|
|
|
22061 |
*
|
|
|
22062 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
|
|
|
22063 |
*/
|
|
|
22064 |
'playbackRate',
|
|
|
22065 |
/**
|
|
|
22066 |
* Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
|
|
|
22067 |
* the rate at which the media should play back upon initial startup. Changing this value
|
|
|
22068 |
* after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
|
|
|
22069 |
*
|
|
|
22070 |
* Example Values:
|
|
|
22071 |
* - if playbackRate is set to 2, media will play twice as fast.
|
|
|
22072 |
* - if playbackRate is set to 0.5, media will play half as fast.
|
|
|
22073 |
*
|
|
|
22074 |
* @method Html5.prototype.setDefaultPlaybackRate
|
|
|
22075 |
* @return {number}
|
|
|
22076 |
* The value of `defaultPlaybackRate` from the media element. A number indicating
|
|
|
22077 |
* the current playback speed of the media, where 1 is normal speed.
|
|
|
22078 |
*
|
|
|
22079 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
|
|
|
22080 |
*/
|
|
|
22081 |
'defaultPlaybackRate',
|
|
|
22082 |
/**
|
|
|
22083 |
* Prevents the browser from suggesting a Picture-in-Picture context menu
|
|
|
22084 |
* or to request Picture-in-Picture automatically in some cases.
|
|
|
22085 |
*
|
|
|
22086 |
* @method Html5#setDisablePictureInPicture
|
|
|
22087 |
* @param {boolean} value
|
|
|
22088 |
* The true value will disable Picture-in-Picture mode.
|
|
|
22089 |
*
|
|
|
22090 |
* @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
|
|
|
22091 |
*/
|
|
|
22092 |
'disablePictureInPicture',
|
|
|
22093 |
/**
|
|
|
22094 |
* Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
|
|
|
22095 |
* to the browser that should sent the cookies along with the requests for the
|
|
|
22096 |
* different assets/playlists
|
|
|
22097 |
*
|
|
|
22098 |
* @method Html5#setCrossOrigin
|
|
|
22099 |
* @param {string} crossOrigin
|
|
|
22100 |
* - anonymous indicates that the media should not sent cookies.
|
|
|
22101 |
* - use-credentials indicates that the media should sent cookies along the requests.
|
|
|
22102 |
*
|
|
|
22103 |
* @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
|
|
|
22104 |
*/
|
|
|
22105 |
'crossOrigin'].forEach(function (prop) {
|
|
|
22106 |
Html5.prototype['set' + toTitleCase$1(prop)] = function (v) {
|
|
|
22107 |
this.el_[prop] = v;
|
|
|
22108 |
};
|
|
|
22109 |
});
|
|
|
22110 |
|
|
|
22111 |
// wrap native functions with a function
|
|
|
22112 |
// The list is as follows:
|
|
|
22113 |
// pause, load, play
|
|
|
22114 |
[
|
|
|
22115 |
/**
|
|
|
22116 |
* A wrapper around the media elements `pause` function. This will call the `HTML5`
|
|
|
22117 |
* media elements `pause` function.
|
|
|
22118 |
*
|
|
|
22119 |
* @method Html5#pause
|
|
|
22120 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
|
|
|
22121 |
*/
|
|
|
22122 |
'pause',
|
|
|
22123 |
/**
|
|
|
22124 |
* A wrapper around the media elements `load` function. This will call the `HTML5`s
|
|
|
22125 |
* media element `load` function.
|
|
|
22126 |
*
|
|
|
22127 |
* @method Html5#load
|
|
|
22128 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
|
|
|
22129 |
*/
|
|
|
22130 |
'load',
|
|
|
22131 |
/**
|
|
|
22132 |
* A wrapper around the media elements `play` function. This will call the `HTML5`s
|
|
|
22133 |
* media element `play` function.
|
|
|
22134 |
*
|
|
|
22135 |
* @method Html5#play
|
|
|
22136 |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
|
|
|
22137 |
*/
|
|
|
22138 |
'play'].forEach(function (prop) {
|
|
|
22139 |
Html5.prototype[prop] = function () {
|
|
|
22140 |
return this.el_[prop]();
|
|
|
22141 |
};
|
|
|
22142 |
});
|
|
|
22143 |
Tech.withSourceHandlers(Html5);
|
|
|
22144 |
|
|
|
22145 |
/**
|
|
|
22146 |
* Native source handler for Html5, simply passes the source to the media element.
|
|
|
22147 |
*
|
|
|
22148 |
* @property {Tech~SourceObject} source
|
|
|
22149 |
* The source object
|
|
|
22150 |
*
|
|
|
22151 |
* @property {Html5} tech
|
|
|
22152 |
* The instance of the HTML5 tech.
|
|
|
22153 |
*/
|
|
|
22154 |
Html5.nativeSourceHandler = {};
|
|
|
22155 |
|
|
|
22156 |
/**
|
|
|
22157 |
* Check if the media element can play the given mime type.
|
|
|
22158 |
*
|
|
|
22159 |
* @param {string} type
|
|
|
22160 |
* The mimetype to check
|
|
|
22161 |
*
|
|
|
22162 |
* @return {string}
|
|
|
22163 |
* 'probably', 'maybe', or '' (empty string)
|
|
|
22164 |
*/
|
|
|
22165 |
Html5.nativeSourceHandler.canPlayType = function (type) {
|
|
|
22166 |
// IE without MediaPlayer throws an error (#519)
|
|
|
22167 |
try {
|
|
|
22168 |
return Html5.TEST_VID.canPlayType(type);
|
|
|
22169 |
} catch (e) {
|
|
|
22170 |
return '';
|
|
|
22171 |
}
|
|
|
22172 |
};
|
|
|
22173 |
|
|
|
22174 |
/**
|
|
|
22175 |
* Check if the media element can handle a source natively.
|
|
|
22176 |
*
|
|
|
22177 |
* @param {Tech~SourceObject} source
|
|
|
22178 |
* The source object
|
|
|
22179 |
*
|
|
|
22180 |
* @param {Object} [options]
|
|
|
22181 |
* Options to be passed to the tech.
|
|
|
22182 |
*
|
|
|
22183 |
* @return {string}
|
|
|
22184 |
* 'probably', 'maybe', or '' (empty string).
|
|
|
22185 |
*/
|
|
|
22186 |
Html5.nativeSourceHandler.canHandleSource = function (source, options) {
|
|
|
22187 |
// If a type was provided we should rely on that
|
|
|
22188 |
if (source.type) {
|
|
|
22189 |
return Html5.nativeSourceHandler.canPlayType(source.type);
|
|
|
22190 |
|
|
|
22191 |
// If no type, fall back to checking 'video/[EXTENSION]'
|
|
|
22192 |
} else if (source.src) {
|
|
|
22193 |
const ext = getFileExtension(source.src);
|
|
|
22194 |
return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
|
|
|
22195 |
}
|
|
|
22196 |
return '';
|
|
|
22197 |
};
|
|
|
22198 |
|
|
|
22199 |
/**
|
|
|
22200 |
* Pass the source to the native media element.
|
|
|
22201 |
*
|
|
|
22202 |
* @param {Tech~SourceObject} source
|
|
|
22203 |
* The source object
|
|
|
22204 |
*
|
|
|
22205 |
* @param {Html5} tech
|
|
|
22206 |
* The instance of the Html5 tech
|
|
|
22207 |
*
|
|
|
22208 |
* @param {Object} [options]
|
|
|
22209 |
* The options to pass to the source
|
|
|
22210 |
*/
|
|
|
22211 |
Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
|
|
|
22212 |
tech.setSrc(source.src);
|
|
|
22213 |
};
|
|
|
22214 |
|
|
|
22215 |
/**
|
|
|
22216 |
* A noop for the native dispose function, as cleanup is not needed.
|
|
|
22217 |
*/
|
|
|
22218 |
Html5.nativeSourceHandler.dispose = function () {};
|
|
|
22219 |
|
|
|
22220 |
// Register the native source handler
|
|
|
22221 |
Html5.registerSourceHandler(Html5.nativeSourceHandler);
|
|
|
22222 |
Tech.registerTech('Html5', Html5);
|
|
|
22223 |
|
|
|
22224 |
/**
|
|
|
22225 |
* @file player.js
|
|
|
22226 |
*/
|
|
|
22227 |
|
|
|
22228 |
// The following tech events are simply re-triggered
|
|
|
22229 |
// on the player when they happen
|
|
|
22230 |
const TECH_EVENTS_RETRIGGER = [
|
|
|
22231 |
/**
|
|
|
22232 |
* Fired while the user agent is downloading media data.
|
|
|
22233 |
*
|
|
|
22234 |
* @event Player#progress
|
|
|
22235 |
* @type {Event}
|
|
|
22236 |
*/
|
|
|
22237 |
/**
|
|
|
22238 |
* Retrigger the `progress` event that was triggered by the {@link Tech}.
|
|
|
22239 |
*
|
|
|
22240 |
* @private
|
|
|
22241 |
* @method Player#handleTechProgress_
|
|
|
22242 |
* @fires Player#progress
|
|
|
22243 |
* @listens Tech#progress
|
|
|
22244 |
*/
|
|
|
22245 |
'progress',
|
|
|
22246 |
/**
|
|
|
22247 |
* Fires when the loading of an audio/video is aborted.
|
|
|
22248 |
*
|
|
|
22249 |
* @event Player#abort
|
|
|
22250 |
* @type {Event}
|
|
|
22251 |
*/
|
|
|
22252 |
/**
|
|
|
22253 |
* Retrigger the `abort` event that was triggered by the {@link Tech}.
|
|
|
22254 |
*
|
|
|
22255 |
* @private
|
|
|
22256 |
* @method Player#handleTechAbort_
|
|
|
22257 |
* @fires Player#abort
|
|
|
22258 |
* @listens Tech#abort
|
|
|
22259 |
*/
|
|
|
22260 |
'abort',
|
|
|
22261 |
/**
|
|
|
22262 |
* Fires when the browser is intentionally not getting media data.
|
|
|
22263 |
*
|
|
|
22264 |
* @event Player#suspend
|
|
|
22265 |
* @type {Event}
|
|
|
22266 |
*/
|
|
|
22267 |
/**
|
|
|
22268 |
* Retrigger the `suspend` event that was triggered by the {@link Tech}.
|
|
|
22269 |
*
|
|
|
22270 |
* @private
|
|
|
22271 |
* @method Player#handleTechSuspend_
|
|
|
22272 |
* @fires Player#suspend
|
|
|
22273 |
* @listens Tech#suspend
|
|
|
22274 |
*/
|
|
|
22275 |
'suspend',
|
|
|
22276 |
/**
|
|
|
22277 |
* Fires when the current playlist is empty.
|
|
|
22278 |
*
|
|
|
22279 |
* @event Player#emptied
|
|
|
22280 |
* @type {Event}
|
|
|
22281 |
*/
|
|
|
22282 |
/**
|
|
|
22283 |
* Retrigger the `emptied` event that was triggered by the {@link Tech}.
|
|
|
22284 |
*
|
|
|
22285 |
* @private
|
|
|
22286 |
* @method Player#handleTechEmptied_
|
|
|
22287 |
* @fires Player#emptied
|
|
|
22288 |
* @listens Tech#emptied
|
|
|
22289 |
*/
|
|
|
22290 |
'emptied',
|
|
|
22291 |
/**
|
|
|
22292 |
* Fires when the browser is trying to get media data, but data is not available.
|
|
|
22293 |
*
|
|
|
22294 |
* @event Player#stalled
|
|
|
22295 |
* @type {Event}
|
|
|
22296 |
*/
|
|
|
22297 |
/**
|
|
|
22298 |
* Retrigger the `stalled` event that was triggered by the {@link Tech}.
|
|
|
22299 |
*
|
|
|
22300 |
* @private
|
|
|
22301 |
* @method Player#handleTechStalled_
|
|
|
22302 |
* @fires Player#stalled
|
|
|
22303 |
* @listens Tech#stalled
|
|
|
22304 |
*/
|
|
|
22305 |
'stalled',
|
|
|
22306 |
/**
|
|
|
22307 |
* Fires when the browser has loaded meta data for the audio/video.
|
|
|
22308 |
*
|
|
|
22309 |
* @event Player#loadedmetadata
|
|
|
22310 |
* @type {Event}
|
|
|
22311 |
*/
|
|
|
22312 |
/**
|
|
|
22313 |
* Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
|
|
|
22314 |
*
|
|
|
22315 |
* @private
|
|
|
22316 |
* @method Player#handleTechLoadedmetadata_
|
|
|
22317 |
* @fires Player#loadedmetadata
|
|
|
22318 |
* @listens Tech#loadedmetadata
|
|
|
22319 |
*/
|
|
|
22320 |
'loadedmetadata',
|
|
|
22321 |
/**
|
|
|
22322 |
* Fires when the browser has loaded the current frame of the audio/video.
|
|
|
22323 |
*
|
|
|
22324 |
* @event Player#loadeddata
|
|
|
22325 |
* @type {event}
|
|
|
22326 |
*/
|
|
|
22327 |
/**
|
|
|
22328 |
* Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
|
|
|
22329 |
*
|
|
|
22330 |
* @private
|
|
|
22331 |
* @method Player#handleTechLoaddeddata_
|
|
|
22332 |
* @fires Player#loadeddata
|
|
|
22333 |
* @listens Tech#loadeddata
|
|
|
22334 |
*/
|
|
|
22335 |
'loadeddata',
|
|
|
22336 |
/**
|
|
|
22337 |
* Fires when the current playback position has changed.
|
|
|
22338 |
*
|
|
|
22339 |
* @event Player#timeupdate
|
|
|
22340 |
* @type {event}
|
|
|
22341 |
*/
|
|
|
22342 |
/**
|
|
|
22343 |
* Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
|
|
|
22344 |
*
|
|
|
22345 |
* @private
|
|
|
22346 |
* @method Player#handleTechTimeUpdate_
|
|
|
22347 |
* @fires Player#timeupdate
|
|
|
22348 |
* @listens Tech#timeupdate
|
|
|
22349 |
*/
|
|
|
22350 |
'timeupdate',
|
|
|
22351 |
/**
|
|
|
22352 |
* Fires when the video's intrinsic dimensions change
|
|
|
22353 |
*
|
|
|
22354 |
* @event Player#resize
|
|
|
22355 |
* @type {event}
|
|
|
22356 |
*/
|
|
|
22357 |
/**
|
|
|
22358 |
* Retrigger the `resize` event that was triggered by the {@link Tech}.
|
|
|
22359 |
*
|
|
|
22360 |
* @private
|
|
|
22361 |
* @method Player#handleTechResize_
|
|
|
22362 |
* @fires Player#resize
|
|
|
22363 |
* @listens Tech#resize
|
|
|
22364 |
*/
|
|
|
22365 |
'resize',
|
|
|
22366 |
/**
|
|
|
22367 |
* Fires when the volume has been changed
|
|
|
22368 |
*
|
|
|
22369 |
* @event Player#volumechange
|
|
|
22370 |
* @type {event}
|
|
|
22371 |
*/
|
|
|
22372 |
/**
|
|
|
22373 |
* Retrigger the `volumechange` event that was triggered by the {@link Tech}.
|
|
|
22374 |
*
|
|
|
22375 |
* @private
|
|
|
22376 |
* @method Player#handleTechVolumechange_
|
|
|
22377 |
* @fires Player#volumechange
|
|
|
22378 |
* @listens Tech#volumechange
|
|
|
22379 |
*/
|
|
|
22380 |
'volumechange',
|
|
|
22381 |
/**
|
|
|
22382 |
* Fires when the text track has been changed
|
|
|
22383 |
*
|
|
|
22384 |
* @event Player#texttrackchange
|
|
|
22385 |
* @type {event}
|
|
|
22386 |
*/
|
|
|
22387 |
/**
|
|
|
22388 |
* Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
|
|
|
22389 |
*
|
|
|
22390 |
* @private
|
|
|
22391 |
* @method Player#handleTechTexttrackchange_
|
|
|
22392 |
* @fires Player#texttrackchange
|
|
|
22393 |
* @listens Tech#texttrackchange
|
|
|
22394 |
*/
|
|
|
22395 |
'texttrackchange'];
|
|
|
22396 |
|
|
|
22397 |
// events to queue when playback rate is zero
|
|
|
22398 |
// this is a hash for the sole purpose of mapping non-camel-cased event names
|
|
|
22399 |
// to camel-cased function names
|
|
|
22400 |
const TECH_EVENTS_QUEUE = {
|
|
|
22401 |
canplay: 'CanPlay',
|
|
|
22402 |
canplaythrough: 'CanPlayThrough',
|
|
|
22403 |
playing: 'Playing',
|
|
|
22404 |
seeked: 'Seeked'
|
|
|
22405 |
};
|
|
|
22406 |
const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
|
|
|
22407 |
const BREAKPOINT_CLASSES = {};
|
|
|
22408 |
|
|
|
22409 |
// grep: vjs-layout-tiny
|
|
|
22410 |
// grep: vjs-layout-x-small
|
|
|
22411 |
// grep: vjs-layout-small
|
|
|
22412 |
// grep: vjs-layout-medium
|
|
|
22413 |
// grep: vjs-layout-large
|
|
|
22414 |
// grep: vjs-layout-x-large
|
|
|
22415 |
// grep: vjs-layout-huge
|
|
|
22416 |
BREAKPOINT_ORDER.forEach(k => {
|
|
|
22417 |
const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
|
|
|
22418 |
BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
|
|
|
22419 |
});
|
|
|
22420 |
const DEFAULT_BREAKPOINTS = {
|
|
|
22421 |
tiny: 210,
|
|
|
22422 |
xsmall: 320,
|
|
|
22423 |
small: 425,
|
|
|
22424 |
medium: 768,
|
|
|
22425 |
large: 1440,
|
|
|
22426 |
xlarge: 2560,
|
|
|
22427 |
huge: Infinity
|
|
|
22428 |
};
|
|
|
22429 |
|
|
|
22430 |
/**
|
|
|
22431 |
* An instance of the `Player` class is created when any of the Video.js setup methods
|
|
|
22432 |
* are used to initialize a video.
|
|
|
22433 |
*
|
|
|
22434 |
* After an instance has been created it can be accessed globally in three ways:
|
|
|
22435 |
* 1. By calling `videojs.getPlayer('example_video_1');`
|
|
|
22436 |
* 2. By calling `videojs('example_video_1');` (not recommended)
|
|
|
22437 |
* 2. By using it directly via `videojs.players.example_video_1;`
|
|
|
22438 |
*
|
|
|
22439 |
* @extends Component
|
|
|
22440 |
* @global
|
|
|
22441 |
*/
|
|
|
22442 |
class Player extends Component$1 {
|
|
|
22443 |
/**
|
|
|
22444 |
* Create an instance of this class.
|
|
|
22445 |
*
|
|
|
22446 |
* @param {Element} tag
|
|
|
22447 |
* The original video DOM element used for configuring options.
|
|
|
22448 |
*
|
|
|
22449 |
* @param {Object} [options]
|
|
|
22450 |
* Object of option names and values.
|
|
|
22451 |
*
|
|
|
22452 |
* @param {Function} [ready]
|
|
|
22453 |
* Ready callback function.
|
|
|
22454 |
*/
|
|
|
22455 |
constructor(tag, options, ready) {
|
|
|
22456 |
// Make sure tag ID exists
|
|
|
22457 |
// also here.. probably better
|
|
|
22458 |
tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
|
|
|
22459 |
|
|
|
22460 |
// Set Options
|
|
|
22461 |
// The options argument overrides options set in the video tag
|
|
|
22462 |
// which overrides globally set options.
|
|
|
22463 |
// This latter part coincides with the load order
|
|
|
22464 |
// (tag must exist before Player)
|
|
|
22465 |
options = Object.assign(Player.getTagSettings(tag), options);
|
|
|
22466 |
|
|
|
22467 |
// Delay the initialization of children because we need to set up
|
|
|
22468 |
// player properties first, and can't use `this` before `super()`
|
|
|
22469 |
options.initChildren = false;
|
|
|
22470 |
|
|
|
22471 |
// Same with creating the element
|
|
|
22472 |
options.createEl = false;
|
|
|
22473 |
|
|
|
22474 |
// don't auto mixin the evented mixin
|
|
|
22475 |
options.evented = false;
|
|
|
22476 |
|
|
|
22477 |
// we don't want the player to report touch activity on itself
|
|
|
22478 |
// see enableTouchActivity in Component
|
|
|
22479 |
options.reportTouchActivity = false;
|
|
|
22480 |
|
|
|
22481 |
// If language is not set, get the closest lang attribute
|
|
|
22482 |
if (!options.language) {
|
|
|
22483 |
const closest = tag.closest('[lang]');
|
|
|
22484 |
if (closest) {
|
|
|
22485 |
options.language = closest.getAttribute('lang');
|
|
|
22486 |
}
|
|
|
22487 |
}
|
|
|
22488 |
|
|
|
22489 |
// Run base component initializing with new options
|
|
|
22490 |
super(null, options, ready);
|
|
|
22491 |
|
|
|
22492 |
// Create bound methods for document listeners.
|
|
|
22493 |
this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
|
|
|
22494 |
this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
|
|
|
22495 |
this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
|
|
|
22496 |
this.boundApplyInitTime_ = e => this.applyInitTime_(e);
|
|
|
22497 |
this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
|
|
|
22498 |
this.boundHandleTechClick_ = e => this.handleTechClick_(e);
|
|
|
22499 |
this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
|
|
|
22500 |
this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
|
|
|
22501 |
this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
|
|
|
22502 |
this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
|
|
|
22503 |
this.boundHandleTechTap_ = e => this.handleTechTap_(e);
|
|
|
22504 |
|
|
|
22505 |
// default isFullscreen_ to false
|
|
|
22506 |
this.isFullscreen_ = false;
|
|
|
22507 |
|
|
|
22508 |
// create logger
|
|
|
22509 |
this.log = createLogger(this.id_);
|
|
|
22510 |
|
|
|
22511 |
// Hold our own reference to fullscreen api so it can be mocked in tests
|
|
|
22512 |
this.fsApi_ = FullscreenApi;
|
|
|
22513 |
|
|
|
22514 |
// Tracks when a tech changes the poster
|
|
|
22515 |
this.isPosterFromTech_ = false;
|
|
|
22516 |
|
|
|
22517 |
// Holds callback info that gets queued when playback rate is zero
|
|
|
22518 |
// and a seek is happening
|
|
|
22519 |
this.queuedCallbacks_ = [];
|
|
|
22520 |
|
|
|
22521 |
// Turn off API access because we're loading a new tech that might load asynchronously
|
|
|
22522 |
this.isReady_ = false;
|
|
|
22523 |
|
|
|
22524 |
// Init state hasStarted_
|
|
|
22525 |
this.hasStarted_ = false;
|
|
|
22526 |
|
|
|
22527 |
// Init state userActive_
|
|
|
22528 |
this.userActive_ = false;
|
|
|
22529 |
|
|
|
22530 |
// Init debugEnabled_
|
|
|
22531 |
this.debugEnabled_ = false;
|
|
|
22532 |
|
|
|
22533 |
// Init state audioOnlyMode_
|
|
|
22534 |
this.audioOnlyMode_ = false;
|
|
|
22535 |
|
|
|
22536 |
// Init state audioPosterMode_
|
|
|
22537 |
this.audioPosterMode_ = false;
|
|
|
22538 |
|
|
|
22539 |
// Init state audioOnlyCache_
|
|
|
22540 |
this.audioOnlyCache_ = {
|
|
|
22541 |
playerHeight: null,
|
|
|
22542 |
hiddenChildren: []
|
|
|
22543 |
};
|
|
|
22544 |
|
|
|
22545 |
// if the global option object was accidentally blown away by
|
|
|
22546 |
// someone, bail early with an informative error
|
|
|
22547 |
if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
|
|
|
22548 |
throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
|
|
|
22549 |
}
|
|
|
22550 |
|
|
|
22551 |
// Store the original tag used to set options
|
|
|
22552 |
this.tag = tag;
|
|
|
22553 |
|
|
|
22554 |
// Store the tag attributes used to restore html5 element
|
|
|
22555 |
this.tagAttributes = tag && getAttributes(tag);
|
|
|
22556 |
|
|
|
22557 |
// Update current language
|
|
|
22558 |
this.language(this.options_.language);
|
|
|
22559 |
|
|
|
22560 |
// Update Supported Languages
|
|
|
22561 |
if (options.languages) {
|
|
|
22562 |
// Normalise player option languages to lowercase
|
|
|
22563 |
const languagesToLower = {};
|
|
|
22564 |
Object.getOwnPropertyNames(options.languages).forEach(function (name) {
|
|
|
22565 |
languagesToLower[name.toLowerCase()] = options.languages[name];
|
|
|
22566 |
});
|
|
|
22567 |
this.languages_ = languagesToLower;
|
|
|
22568 |
} else {
|
|
|
22569 |
this.languages_ = Player.prototype.options_.languages;
|
|
|
22570 |
}
|
|
|
22571 |
this.resetCache_();
|
|
|
22572 |
|
|
|
22573 |
// Set poster
|
|
|
22574 |
/** @type string */
|
|
|
22575 |
this.poster_ = options.poster || '';
|
|
|
22576 |
|
|
|
22577 |
// Set controls
|
|
|
22578 |
/** @type {boolean} */
|
|
|
22579 |
this.controls_ = !!options.controls;
|
|
|
22580 |
|
|
|
22581 |
// Original tag settings stored in options
|
|
|
22582 |
// now remove immediately so native controls don't flash.
|
|
|
22583 |
// May be turned back on by HTML5 tech if nativeControlsForTouch is true
|
|
|
22584 |
tag.controls = false;
|
|
|
22585 |
tag.removeAttribute('controls');
|
|
|
22586 |
this.changingSrc_ = false;
|
|
|
22587 |
this.playCallbacks_ = [];
|
|
|
22588 |
this.playTerminatedQueue_ = [];
|
|
|
22589 |
|
|
|
22590 |
// the attribute overrides the option
|
|
|
22591 |
if (tag.hasAttribute('autoplay')) {
|
|
|
22592 |
this.autoplay(true);
|
|
|
22593 |
} else {
|
|
|
22594 |
// otherwise use the setter to validate and
|
|
|
22595 |
// set the correct value.
|
|
|
22596 |
this.autoplay(this.options_.autoplay);
|
|
|
22597 |
}
|
|
|
22598 |
|
|
|
22599 |
// check plugins
|
|
|
22600 |
if (options.plugins) {
|
|
|
22601 |
Object.keys(options.plugins).forEach(name => {
|
|
|
22602 |
if (typeof this[name] !== 'function') {
|
|
|
22603 |
throw new Error(`plugin "${name}" does not exist`);
|
|
|
22604 |
}
|
|
|
22605 |
});
|
|
|
22606 |
}
|
|
|
22607 |
|
|
|
22608 |
/*
|
|
|
22609 |
* Store the internal state of scrubbing
|
|
|
22610 |
*
|
|
|
22611 |
* @private
|
|
|
22612 |
* @return {Boolean} True if the user is scrubbing
|
|
|
22613 |
*/
|
|
|
22614 |
this.scrubbing_ = false;
|
|
|
22615 |
this.el_ = this.createEl();
|
|
|
22616 |
|
|
|
22617 |
// Make this an evented object and use `el_` as its event bus.
|
|
|
22618 |
evented(this, {
|
|
|
22619 |
eventBusKey: 'el_'
|
|
|
22620 |
});
|
|
|
22621 |
|
|
|
22622 |
// listen to document and player fullscreenchange handlers so we receive those events
|
|
|
22623 |
// before a user can receive them so we can update isFullscreen appropriately.
|
|
|
22624 |
// make sure that we listen to fullscreenchange events before everything else to make sure that
|
|
|
22625 |
// our isFullscreen method is updated properly for internal components as well as external.
|
|
|
22626 |
if (this.fsApi_.requestFullscreen) {
|
|
|
22627 |
on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
|
|
|
22628 |
this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
|
|
|
22629 |
}
|
|
|
22630 |
if (this.fluid_) {
|
|
|
22631 |
this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
|
|
|
22632 |
}
|
|
|
22633 |
// We also want to pass the original player options to each component and plugin
|
|
|
22634 |
// as well so they don't need to reach back into the player for options later.
|
|
|
22635 |
// We also need to do another copy of this.options_ so we don't end up with
|
|
|
22636 |
// an infinite loop.
|
|
|
22637 |
const playerOptionsCopy = merge$2(this.options_);
|
|
|
22638 |
|
|
|
22639 |
// Load plugins
|
|
|
22640 |
if (options.plugins) {
|
|
|
22641 |
Object.keys(options.plugins).forEach(name => {
|
|
|
22642 |
this[name](options.plugins[name]);
|
|
|
22643 |
});
|
|
|
22644 |
}
|
|
|
22645 |
|
|
|
22646 |
// Enable debug mode to fire debugon event for all plugins.
|
|
|
22647 |
if (options.debug) {
|
|
|
22648 |
this.debug(true);
|
|
|
22649 |
}
|
|
|
22650 |
this.options_.playerOptions = playerOptionsCopy;
|
|
|
22651 |
this.middleware_ = [];
|
|
|
22652 |
this.playbackRates(options.playbackRates);
|
|
|
22653 |
if (options.experimentalSvgIcons) {
|
|
|
22654 |
// Add SVG Sprite to the DOM
|
|
|
22655 |
const parser = new window.DOMParser();
|
|
|
22656 |
const parsedSVG = parser.parseFromString(icons, 'image/svg+xml');
|
|
|
22657 |
const errorNode = parsedSVG.querySelector('parsererror');
|
|
|
22658 |
if (errorNode) {
|
|
|
22659 |
log$1.warn('Failed to load SVG Icons. Falling back to Font Icons.');
|
|
|
22660 |
this.options_.experimentalSvgIcons = null;
|
|
|
22661 |
} else {
|
|
|
22662 |
const sprite = parsedSVG.documentElement;
|
|
|
22663 |
sprite.style.display = 'none';
|
|
|
22664 |
this.el_.appendChild(sprite);
|
|
|
22665 |
this.addClass('vjs-svg-icons-enabled');
|
|
|
22666 |
}
|
|
|
22667 |
}
|
|
|
22668 |
this.initChildren();
|
|
|
22669 |
|
|
|
22670 |
// Set isAudio based on whether or not an audio tag was used
|
|
|
22671 |
this.isAudio(tag.nodeName.toLowerCase() === 'audio');
|
|
|
22672 |
|
|
|
22673 |
// Update controls className. Can't do this when the controls are initially
|
|
|
22674 |
// set because the element doesn't exist yet.
|
|
|
22675 |
if (this.controls()) {
|
|
|
22676 |
this.addClass('vjs-controls-enabled');
|
|
|
22677 |
} else {
|
|
|
22678 |
this.addClass('vjs-controls-disabled');
|
|
|
22679 |
}
|
|
|
22680 |
|
|
|
22681 |
// Set ARIA label and region role depending on player type
|
|
|
22682 |
this.el_.setAttribute('role', 'region');
|
|
|
22683 |
if (this.isAudio()) {
|
|
|
22684 |
this.el_.setAttribute('aria-label', this.localize('Audio Player'));
|
|
|
22685 |
} else {
|
|
|
22686 |
this.el_.setAttribute('aria-label', this.localize('Video Player'));
|
|
|
22687 |
}
|
|
|
22688 |
if (this.isAudio()) {
|
|
|
22689 |
this.addClass('vjs-audio');
|
|
|
22690 |
}
|
|
|
22691 |
|
|
|
22692 |
// TODO: Make this smarter. Toggle user state between touching/mousing
|
|
|
22693 |
// using events, since devices can have both touch and mouse events.
|
|
|
22694 |
// TODO: Make this check be performed again when the window switches between monitors
|
|
|
22695 |
// (See https://github.com/videojs/video.js/issues/5683)
|
|
|
22696 |
if (TOUCH_ENABLED) {
|
|
|
22697 |
this.addClass('vjs-touch-enabled');
|
|
|
22698 |
}
|
|
|
22699 |
|
|
|
22700 |
// iOS Safari has broken hover handling
|
|
|
22701 |
if (!IS_IOS) {
|
|
|
22702 |
this.addClass('vjs-workinghover');
|
|
|
22703 |
}
|
|
|
22704 |
|
|
|
22705 |
// Make player easily findable by ID
|
|
|
22706 |
Player.players[this.id_] = this;
|
|
|
22707 |
|
|
|
22708 |
// Add a major version class to aid css in plugins
|
|
|
22709 |
const majorVersion = version$5.split('.')[0];
|
|
|
22710 |
this.addClass(`vjs-v${majorVersion}`);
|
|
|
22711 |
|
|
|
22712 |
// When the player is first initialized, trigger activity so components
|
|
|
22713 |
// like the control bar show themselves if needed
|
|
|
22714 |
this.userActive(true);
|
|
|
22715 |
this.reportUserActivity();
|
|
|
22716 |
this.one('play', e => this.listenForUserActivity_(e));
|
|
|
22717 |
this.on('keydown', e => this.handleKeyDown(e));
|
|
|
22718 |
this.on('languagechange', e => this.handleLanguagechange(e));
|
|
|
22719 |
this.breakpoints(this.options_.breakpoints);
|
|
|
22720 |
this.responsive(this.options_.responsive);
|
|
|
22721 |
|
|
|
22722 |
// Calling both the audio mode methods after the player is fully
|
|
|
22723 |
// setup to be able to listen to the events triggered by them
|
|
|
22724 |
this.on('ready', () => {
|
|
|
22725 |
// Calling the audioPosterMode method first so that
|
|
|
22726 |
// the audioOnlyMode can take precedence when both options are set to true
|
|
|
22727 |
this.audioPosterMode(this.options_.audioPosterMode);
|
|
|
22728 |
this.audioOnlyMode(this.options_.audioOnlyMode);
|
|
|
22729 |
});
|
|
|
22730 |
}
|
|
|
22731 |
|
|
|
22732 |
/**
|
|
|
22733 |
* Destroys the video player and does any necessary cleanup.
|
|
|
22734 |
*
|
|
|
22735 |
* This is especially helpful if you are dynamically adding and removing videos
|
|
|
22736 |
* to/from the DOM.
|
|
|
22737 |
*
|
|
|
22738 |
* @fires Player#dispose
|
|
|
22739 |
*/
|
|
|
22740 |
dispose() {
|
|
|
22741 |
/**
|
|
|
22742 |
* Called when the player is being disposed of.
|
|
|
22743 |
*
|
|
|
22744 |
* @event Player#dispose
|
|
|
22745 |
* @type {Event}
|
|
|
22746 |
*/
|
|
|
22747 |
this.trigger('dispose');
|
|
|
22748 |
// prevent dispose from being called twice
|
|
|
22749 |
this.off('dispose');
|
|
|
22750 |
|
|
|
22751 |
// Make sure all player-specific document listeners are unbound. This is
|
|
|
22752 |
off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
|
|
|
22753 |
off(document, 'keydown', this.boundFullWindowOnEscKey_);
|
|
|
22754 |
if (this.styleEl_ && this.styleEl_.parentNode) {
|
|
|
22755 |
this.styleEl_.parentNode.removeChild(this.styleEl_);
|
|
|
22756 |
this.styleEl_ = null;
|
|
|
22757 |
}
|
|
|
22758 |
|
|
|
22759 |
// Kill reference to this player
|
|
|
22760 |
Player.players[this.id_] = null;
|
|
|
22761 |
if (this.tag && this.tag.player) {
|
|
|
22762 |
this.tag.player = null;
|
|
|
22763 |
}
|
|
|
22764 |
if (this.el_ && this.el_.player) {
|
|
|
22765 |
this.el_.player = null;
|
|
|
22766 |
}
|
|
|
22767 |
if (this.tech_) {
|
|
|
22768 |
this.tech_.dispose();
|
|
|
22769 |
this.isPosterFromTech_ = false;
|
|
|
22770 |
this.poster_ = '';
|
|
|
22771 |
}
|
|
|
22772 |
if (this.playerElIngest_) {
|
|
|
22773 |
this.playerElIngest_ = null;
|
|
|
22774 |
}
|
|
|
22775 |
if (this.tag) {
|
|
|
22776 |
this.tag = null;
|
|
|
22777 |
}
|
|
|
22778 |
clearCacheForPlayer(this);
|
|
|
22779 |
|
|
|
22780 |
// remove all event handlers for track lists
|
|
|
22781 |
// all tracks and track listeners are removed on
|
|
|
22782 |
// tech dispose
|
|
|
22783 |
ALL.names.forEach(name => {
|
|
|
22784 |
const props = ALL[name];
|
|
|
22785 |
const list = this[props.getterName]();
|
|
|
22786 |
|
|
|
22787 |
// if it is not a native list
|
|
|
22788 |
// we have to manually remove event listeners
|
|
|
22789 |
if (list && list.off) {
|
|
|
22790 |
list.off();
|
|
|
22791 |
}
|
|
|
22792 |
});
|
|
|
22793 |
|
|
|
22794 |
// the actual .el_ is removed here, or replaced if
|
|
|
22795 |
super.dispose({
|
|
|
22796 |
restoreEl: this.options_.restoreEl
|
|
|
22797 |
});
|
|
|
22798 |
}
|
|
|
22799 |
|
|
|
22800 |
/**
|
|
|
22801 |
* Create the `Player`'s DOM element.
|
|
|
22802 |
*
|
|
|
22803 |
* @return {Element}
|
|
|
22804 |
* The DOM element that gets created.
|
|
|
22805 |
*/
|
|
|
22806 |
createEl() {
|
|
|
22807 |
let tag = this.tag;
|
|
|
22808 |
let el;
|
|
|
22809 |
let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
|
|
|
22810 |
const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
|
|
|
22811 |
if (playerElIngest) {
|
|
|
22812 |
el = this.el_ = tag.parentNode;
|
|
|
22813 |
} else if (!divEmbed) {
|
|
|
22814 |
el = this.el_ = super.createEl('div');
|
|
|
22815 |
}
|
|
|
22816 |
|
|
|
22817 |
// Copy over all the attributes from the tag, including ID and class
|
|
|
22818 |
// ID will now reference player box, not the video tag
|
|
|
22819 |
const attrs = getAttributes(tag);
|
|
|
22820 |
if (divEmbed) {
|
|
|
22821 |
el = this.el_ = tag;
|
|
|
22822 |
tag = this.tag = document.createElement('video');
|
|
|
22823 |
while (el.children.length) {
|
|
|
22824 |
tag.appendChild(el.firstChild);
|
|
|
22825 |
}
|
|
|
22826 |
if (!hasClass(el, 'video-js')) {
|
|
|
22827 |
addClass(el, 'video-js');
|
|
|
22828 |
}
|
|
|
22829 |
el.appendChild(tag);
|
|
|
22830 |
playerElIngest = this.playerElIngest_ = el;
|
|
|
22831 |
// move properties over from our custom `video-js` element
|
|
|
22832 |
// to our new `video` element. This will move things like
|
|
|
22833 |
// `src` or `controls` that were set via js before the player
|
|
|
22834 |
// was initialized.
|
|
|
22835 |
Object.keys(el).forEach(k => {
|
|
|
22836 |
try {
|
|
|
22837 |
tag[k] = el[k];
|
|
|
22838 |
} catch (e) {
|
|
|
22839 |
// we got a a property like outerHTML which we can't actually copy, ignore it
|
|
|
22840 |
}
|
|
|
22841 |
});
|
|
|
22842 |
}
|
|
|
22843 |
|
|
|
22844 |
// set tabindex to -1 to remove the video element from the focus order
|
|
|
22845 |
tag.setAttribute('tabindex', '-1');
|
|
|
22846 |
attrs.tabindex = '-1';
|
|
|
22847 |
|
|
|
22848 |
// Workaround for #4583 on Chrome (on Windows) with JAWS.
|
|
|
22849 |
// See https://github.com/FreedomScientific/VFO-standards-support/issues/78
|
|
|
22850 |
// Note that we can't detect if JAWS is being used, but this ARIA attribute
|
|
|
22851 |
// doesn't change behavior of Chrome if JAWS is not being used
|
|
|
22852 |
if (IS_CHROME && IS_WINDOWS) {
|
|
|
22853 |
tag.setAttribute('role', 'application');
|
|
|
22854 |
attrs.role = 'application';
|
|
|
22855 |
}
|
|
|
22856 |
|
|
|
22857 |
// Remove width/height attrs from tag so CSS can make it 100% width/height
|
|
|
22858 |
tag.removeAttribute('width');
|
|
|
22859 |
tag.removeAttribute('height');
|
|
|
22860 |
if ('width' in attrs) {
|
|
|
22861 |
delete attrs.width;
|
|
|
22862 |
}
|
|
|
22863 |
if ('height' in attrs) {
|
|
|
22864 |
delete attrs.height;
|
|
|
22865 |
}
|
|
|
22866 |
Object.getOwnPropertyNames(attrs).forEach(function (attr) {
|
|
|
22867 |
// don't copy over the class attribute to the player element when we're in a div embed
|
|
|
22868 |
// the class is already set up properly in the divEmbed case
|
|
|
22869 |
// and we want to make sure that the `video-js` class doesn't get lost
|
|
|
22870 |
if (!(divEmbed && attr === 'class')) {
|
|
|
22871 |
el.setAttribute(attr, attrs[attr]);
|
|
|
22872 |
}
|
|
|
22873 |
if (divEmbed) {
|
|
|
22874 |
tag.setAttribute(attr, attrs[attr]);
|
|
|
22875 |
}
|
|
|
22876 |
});
|
|
|
22877 |
|
|
|
22878 |
// Update tag id/class for use as HTML5 playback tech
|
|
|
22879 |
// Might think we should do this after embedding in container so .vjs-tech class
|
|
|
22880 |
// doesn't flash 100% width/height, but class only applies with .video-js parent
|
|
|
22881 |
tag.playerId = tag.id;
|
|
|
22882 |
tag.id += '_html5_api';
|
|
|
22883 |
tag.className = 'vjs-tech';
|
|
|
22884 |
|
|
|
22885 |
// Make player findable on elements
|
|
|
22886 |
tag.player = el.player = this;
|
|
|
22887 |
// Default state of video is paused
|
|
|
22888 |
this.addClass('vjs-paused');
|
|
|
22889 |
|
|
|
22890 |
// Add a style element in the player that we'll use to set the width/height
|
|
|
22891 |
// of the player in a way that's still overridable by CSS, just like the
|
|
|
22892 |
// video element
|
|
|
22893 |
if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
|
|
|
22894 |
this.styleEl_ = createStyleElement('vjs-styles-dimensions');
|
|
|
22895 |
const defaultsStyleEl = $('.vjs-styles-defaults');
|
|
|
22896 |
const head = $('head');
|
|
|
22897 |
head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
|
|
|
22898 |
}
|
|
|
22899 |
this.fill_ = false;
|
|
|
22900 |
this.fluid_ = false;
|
|
|
22901 |
|
|
|
22902 |
// Pass in the width/height/aspectRatio options which will update the style el
|
|
|
22903 |
this.width(this.options_.width);
|
|
|
22904 |
this.height(this.options_.height);
|
|
|
22905 |
this.fill(this.options_.fill);
|
|
|
22906 |
this.fluid(this.options_.fluid);
|
|
|
22907 |
this.aspectRatio(this.options_.aspectRatio);
|
|
|
22908 |
// support both crossOrigin and crossorigin to reduce confusion and issues around the name
|
|
|
22909 |
this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
|
|
|
22910 |
|
|
|
22911 |
// Hide any links within the video/audio tag,
|
|
|
22912 |
// because IE doesn't hide them completely from screen readers.
|
|
|
22913 |
const links = tag.getElementsByTagName('a');
|
|
|
22914 |
for (let i = 0; i < links.length; i++) {
|
|
|
22915 |
const linkEl = links.item(i);
|
|
|
22916 |
addClass(linkEl, 'vjs-hidden');
|
|
|
22917 |
linkEl.setAttribute('hidden', 'hidden');
|
|
|
22918 |
}
|
|
|
22919 |
|
|
|
22920 |
// insertElFirst seems to cause the networkState to flicker from 3 to 2, so
|
|
|
22921 |
// keep track of the original for later so we can know if the source originally failed
|
|
|
22922 |
tag.initNetworkState_ = tag.networkState;
|
|
|
22923 |
|
|
|
22924 |
// Wrap video tag in div (el/box) container
|
|
|
22925 |
if (tag.parentNode && !playerElIngest) {
|
|
|
22926 |
tag.parentNode.insertBefore(el, tag);
|
|
|
22927 |
}
|
|
|
22928 |
|
|
|
22929 |
// insert the tag as the first child of the player element
|
|
|
22930 |
// then manually add it to the children array so that this.addChild
|
|
|
22931 |
// will work properly for other components
|
|
|
22932 |
//
|
|
|
22933 |
// Breaks iPhone, fixed in HTML5 setup.
|
|
|
22934 |
prependTo(tag, el);
|
|
|
22935 |
this.children_.unshift(tag);
|
|
|
22936 |
|
|
|
22937 |
// Set lang attr on player to ensure CSS :lang() in consistent with player
|
|
|
22938 |
// if it's been set to something different to the doc
|
|
|
22939 |
this.el_.setAttribute('lang', this.language_);
|
|
|
22940 |
this.el_.setAttribute('translate', 'no');
|
|
|
22941 |
this.el_ = el;
|
|
|
22942 |
return el;
|
|
|
22943 |
}
|
|
|
22944 |
|
|
|
22945 |
/**
|
|
|
22946 |
* Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
|
|
|
22947 |
* sets the `crossOrigin` property on the `<video>` tag to control the CORS
|
|
|
22948 |
* behavior.
|
|
|
22949 |
*
|
|
|
22950 |
* @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
|
|
|
22951 |
*
|
|
|
22952 |
* @param {string|null} [value]
|
|
|
22953 |
* The value to set the `Player`'s crossOrigin to. If an argument is
|
|
|
22954 |
* given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
|
|
|
22955 |
*
|
|
|
22956 |
* @return {string|null|undefined}
|
|
|
22957 |
* - The current crossOrigin value of the `Player` when getting.
|
|
|
22958 |
* - undefined when setting
|
|
|
22959 |
*/
|
|
|
22960 |
crossOrigin(value) {
|
|
|
22961 |
// `null` can be set to unset a value
|
|
|
22962 |
if (typeof value === 'undefined') {
|
|
|
22963 |
return this.techGet_('crossOrigin');
|
|
|
22964 |
}
|
|
|
22965 |
if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
|
|
|
22966 |
log$1.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
|
|
|
22967 |
return;
|
|
|
22968 |
}
|
|
|
22969 |
this.techCall_('setCrossOrigin', value);
|
|
|
22970 |
if (this.posterImage) {
|
|
|
22971 |
this.posterImage.crossOrigin(value);
|
|
|
22972 |
}
|
|
|
22973 |
return;
|
|
|
22974 |
}
|
|
|
22975 |
|
|
|
22976 |
/**
|
|
|
22977 |
* A getter/setter for the `Player`'s width. Returns the player's configured value.
|
|
|
22978 |
* To get the current width use `currentWidth()`.
|
|
|
22979 |
*
|
|
|
22980 |
* @param {number|string} [value]
|
|
|
22981 |
* CSS value to set the `Player`'s width to.
|
|
|
22982 |
*
|
|
|
22983 |
* @return {number|undefined}
|
|
|
22984 |
* - The current width of the `Player` when getting.
|
|
|
22985 |
* - Nothing when setting
|
|
|
22986 |
*/
|
|
|
22987 |
width(value) {
|
|
|
22988 |
return this.dimension('width', value);
|
|
|
22989 |
}
|
|
|
22990 |
|
|
|
22991 |
/**
|
|
|
22992 |
* A getter/setter for the `Player`'s height. Returns the player's configured value.
|
|
|
22993 |
* To get the current height use `currentheight()`.
|
|
|
22994 |
*
|
|
|
22995 |
* @param {number|string} [value]
|
|
|
22996 |
* CSS value to set the `Player`'s height to.
|
|
|
22997 |
*
|
|
|
22998 |
* @return {number|undefined}
|
|
|
22999 |
* - The current height of the `Player` when getting.
|
|
|
23000 |
* - Nothing when setting
|
|
|
23001 |
*/
|
|
|
23002 |
height(value) {
|
|
|
23003 |
return this.dimension('height', value);
|
|
|
23004 |
}
|
|
|
23005 |
|
|
|
23006 |
/**
|
|
|
23007 |
* A getter/setter for the `Player`'s width & height.
|
|
|
23008 |
*
|
|
|
23009 |
* @param {string} dimension
|
|
|
23010 |
* This string can be:
|
|
|
23011 |
* - 'width'
|
|
|
23012 |
* - 'height'
|
|
|
23013 |
*
|
|
|
23014 |
* @param {number|string} [value]
|
|
|
23015 |
* Value for dimension specified in the first argument.
|
|
|
23016 |
*
|
|
|
23017 |
* @return {number}
|
|
|
23018 |
* The dimension arguments value when getting (width/height).
|
|
|
23019 |
*/
|
|
|
23020 |
dimension(dimension, value) {
|
|
|
23021 |
const privDimension = dimension + '_';
|
|
|
23022 |
if (value === undefined) {
|
|
|
23023 |
return this[privDimension] || 0;
|
|
|
23024 |
}
|
|
|
23025 |
if (value === '' || value === 'auto') {
|
|
|
23026 |
// If an empty string is given, reset the dimension to be automatic
|
|
|
23027 |
this[privDimension] = undefined;
|
|
|
23028 |
this.updateStyleEl_();
|
|
|
23029 |
return;
|
|
|
23030 |
}
|
|
|
23031 |
const parsedVal = parseFloat(value);
|
|
|
23032 |
if (isNaN(parsedVal)) {
|
|
|
23033 |
log$1.error(`Improper value "${value}" supplied for for ${dimension}`);
|
|
|
23034 |
return;
|
|
|
23035 |
}
|
|
|
23036 |
this[privDimension] = parsedVal;
|
|
|
23037 |
this.updateStyleEl_();
|
|
|
23038 |
}
|
|
|
23039 |
|
|
|
23040 |
/**
|
|
|
23041 |
* A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
|
|
|
23042 |
*
|
|
|
23043 |
* Turning this on will turn off fill mode.
|
|
|
23044 |
*
|
|
|
23045 |
* @param {boolean} [bool]
|
|
|
23046 |
* - A value of true adds the class.
|
|
|
23047 |
* - A value of false removes the class.
|
|
|
23048 |
* - No value will be a getter.
|
|
|
23049 |
*
|
|
|
23050 |
* @return {boolean|undefined}
|
|
|
23051 |
* - The value of fluid when getting.
|
|
|
23052 |
* - `undefined` when setting.
|
|
|
23053 |
*/
|
|
|
23054 |
fluid(bool) {
|
|
|
23055 |
if (bool === undefined) {
|
|
|
23056 |
return !!this.fluid_;
|
|
|
23057 |
}
|
|
|
23058 |
this.fluid_ = !!bool;
|
|
|
23059 |
if (isEvented(this)) {
|
|
|
23060 |
this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
|
|
|
23061 |
}
|
|
|
23062 |
if (bool) {
|
|
|
23063 |
this.addClass('vjs-fluid');
|
|
|
23064 |
this.fill(false);
|
|
|
23065 |
addEventedCallback(this, () => {
|
|
|
23066 |
this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
|
|
|
23067 |
});
|
|
|
23068 |
} else {
|
|
|
23069 |
this.removeClass('vjs-fluid');
|
|
|
23070 |
}
|
|
|
23071 |
this.updateStyleEl_();
|
|
|
23072 |
}
|
|
|
23073 |
|
|
|
23074 |
/**
|
|
|
23075 |
* A getter/setter/toggler for the vjs-fill `className` on the `Player`.
|
|
|
23076 |
*
|
|
|
23077 |
* Turning this on will turn off fluid mode.
|
|
|
23078 |
*
|
|
|
23079 |
* @param {boolean} [bool]
|
|
|
23080 |
* - A value of true adds the class.
|
|
|
23081 |
* - A value of false removes the class.
|
|
|
23082 |
* - No value will be a getter.
|
|
|
23083 |
*
|
|
|
23084 |
* @return {boolean|undefined}
|
|
|
23085 |
* - The value of fluid when getting.
|
|
|
23086 |
* - `undefined` when setting.
|
|
|
23087 |
*/
|
|
|
23088 |
fill(bool) {
|
|
|
23089 |
if (bool === undefined) {
|
|
|
23090 |
return !!this.fill_;
|
|
|
23091 |
}
|
|
|
23092 |
this.fill_ = !!bool;
|
|
|
23093 |
if (bool) {
|
|
|
23094 |
this.addClass('vjs-fill');
|
|
|
23095 |
this.fluid(false);
|
|
|
23096 |
} else {
|
|
|
23097 |
this.removeClass('vjs-fill');
|
|
|
23098 |
}
|
|
|
23099 |
}
|
|
|
23100 |
|
|
|
23101 |
/**
|
|
|
23102 |
* Get/Set the aspect ratio
|
|
|
23103 |
*
|
|
|
23104 |
* @param {string} [ratio]
|
|
|
23105 |
* Aspect ratio for player
|
|
|
23106 |
*
|
|
|
23107 |
* @return {string|undefined}
|
|
|
23108 |
* returns the current aspect ratio when getting
|
|
|
23109 |
*/
|
|
|
23110 |
|
|
|
23111 |
/**
|
|
|
23112 |
* A getter/setter for the `Player`'s aspect ratio.
|
|
|
23113 |
*
|
|
|
23114 |
* @param {string} [ratio]
|
|
|
23115 |
* The value to set the `Player`'s aspect ratio to.
|
|
|
23116 |
*
|
|
|
23117 |
* @return {string|undefined}
|
|
|
23118 |
* - The current aspect ratio of the `Player` when getting.
|
|
|
23119 |
* - undefined when setting
|
|
|
23120 |
*/
|
|
|
23121 |
aspectRatio(ratio) {
|
|
|
23122 |
if (ratio === undefined) {
|
|
|
23123 |
return this.aspectRatio_;
|
|
|
23124 |
}
|
|
|
23125 |
|
|
|
23126 |
// Check for width:height format
|
|
|
23127 |
if (!/^\d+\:\d+$/.test(ratio)) {
|
|
|
23128 |
throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
|
|
|
23129 |
}
|
|
|
23130 |
this.aspectRatio_ = ratio;
|
|
|
23131 |
|
|
|
23132 |
// We're assuming if you set an aspect ratio you want fluid mode,
|
|
|
23133 |
// because in fixed mode you could calculate width and height yourself.
|
|
|
23134 |
this.fluid(true);
|
|
|
23135 |
this.updateStyleEl_();
|
|
|
23136 |
}
|
|
|
23137 |
|
|
|
23138 |
/**
|
|
|
23139 |
* Update styles of the `Player` element (height, width and aspect ratio).
|
|
|
23140 |
*
|
|
|
23141 |
* @private
|
|
|
23142 |
* @listens Tech#loadedmetadata
|
|
|
23143 |
*/
|
|
|
23144 |
updateStyleEl_() {
|
|
|
23145 |
if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) {
|
|
|
23146 |
const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
|
|
|
23147 |
const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
|
|
|
23148 |
const techEl = this.tech_ && this.tech_.el();
|
|
|
23149 |
if (techEl) {
|
|
|
23150 |
if (width >= 0) {
|
|
|
23151 |
techEl.width = width;
|
|
|
23152 |
}
|
|
|
23153 |
if (height >= 0) {
|
|
|
23154 |
techEl.height = height;
|
|
|
23155 |
}
|
|
|
23156 |
}
|
|
|
23157 |
return;
|
|
|
23158 |
}
|
|
|
23159 |
let width;
|
|
|
23160 |
let height;
|
|
|
23161 |
let aspectRatio;
|
|
|
23162 |
let idClass;
|
|
|
23163 |
|
|
|
23164 |
// The aspect ratio is either used directly or to calculate width and height.
|
|
|
23165 |
if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
|
|
|
23166 |
// Use any aspectRatio that's been specifically set
|
|
|
23167 |
aspectRatio = this.aspectRatio_;
|
|
|
23168 |
} else if (this.videoWidth() > 0) {
|
|
|
23169 |
// Otherwise try to get the aspect ratio from the video metadata
|
|
|
23170 |
aspectRatio = this.videoWidth() + ':' + this.videoHeight();
|
|
|
23171 |
} else {
|
|
|
23172 |
// Or use a default. The video element's is 2:1, but 16:9 is more common.
|
|
|
23173 |
aspectRatio = '16:9';
|
|
|
23174 |
}
|
|
|
23175 |
|
|
|
23176 |
// Get the ratio as a decimal we can use to calculate dimensions
|
|
|
23177 |
const ratioParts = aspectRatio.split(':');
|
|
|
23178 |
const ratioMultiplier = ratioParts[1] / ratioParts[0];
|
|
|
23179 |
if (this.width_ !== undefined) {
|
|
|
23180 |
// Use any width that's been specifically set
|
|
|
23181 |
width = this.width_;
|
|
|
23182 |
} else if (this.height_ !== undefined) {
|
|
|
23183 |
// Or calculate the width from the aspect ratio if a height has been set
|
|
|
23184 |
width = this.height_ / ratioMultiplier;
|
|
|
23185 |
} else {
|
|
|
23186 |
// Or use the video's metadata, or use the video el's default of 300
|
|
|
23187 |
width = this.videoWidth() || 300;
|
|
|
23188 |
}
|
|
|
23189 |
if (this.height_ !== undefined) {
|
|
|
23190 |
// Use any height that's been specifically set
|
|
|
23191 |
height = this.height_;
|
|
|
23192 |
} else {
|
|
|
23193 |
// Otherwise calculate the height from the ratio and the width
|
|
|
23194 |
height = width * ratioMultiplier;
|
|
|
23195 |
}
|
|
|
23196 |
|
|
|
23197 |
// Ensure the CSS class is valid by starting with an alpha character
|
|
|
23198 |
if (/^[^a-zA-Z]/.test(this.id())) {
|
|
|
23199 |
idClass = 'dimensions-' + this.id();
|
|
|
23200 |
} else {
|
|
|
23201 |
idClass = this.id() + '-dimensions';
|
|
|
23202 |
}
|
|
|
23203 |
|
|
|
23204 |
// Ensure the right class is still on the player for the style element
|
|
|
23205 |
this.addClass(idClass);
|
|
|
23206 |
setTextContent(this.styleEl_, `
|
|
|
23207 |
.${idClass} {
|
|
|
23208 |
width: ${width}px;
|
|
|
23209 |
height: ${height}px;
|
|
|
23210 |
}
|
|
|
23211 |
|
|
|
23212 |
.${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
|
|
|
23213 |
padding-top: ${ratioMultiplier * 100}%;
|
|
|
23214 |
}
|
|
|
23215 |
`);
|
|
|
23216 |
}
|
|
|
23217 |
|
|
|
23218 |
/**
|
|
|
23219 |
* Load/Create an instance of playback {@link Tech} including element
|
|
|
23220 |
* and API methods. Then append the `Tech` element in `Player` as a child.
|
|
|
23221 |
*
|
|
|
23222 |
* @param {string} techName
|
|
|
23223 |
* name of the playback technology
|
|
|
23224 |
*
|
|
|
23225 |
* @param {string} source
|
|
|
23226 |
* video source
|
|
|
23227 |
*
|
|
|
23228 |
* @private
|
|
|
23229 |
*/
|
|
|
23230 |
loadTech_(techName, source) {
|
|
|
23231 |
// Pause and remove current playback technology
|
|
|
23232 |
if (this.tech_) {
|
|
|
23233 |
this.unloadTech_();
|
|
|
23234 |
}
|
|
|
23235 |
const titleTechName = toTitleCase$1(techName);
|
|
|
23236 |
const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
|
|
|
23237 |
|
|
|
23238 |
// get rid of the HTML5 video tag as soon as we are using another tech
|
|
|
23239 |
if (titleTechName !== 'Html5' && this.tag) {
|
|
|
23240 |
Tech.getTech('Html5').disposeMediaElement(this.tag);
|
|
|
23241 |
this.tag.player = null;
|
|
|
23242 |
this.tag = null;
|
|
|
23243 |
}
|
|
|
23244 |
this.techName_ = titleTechName;
|
|
|
23245 |
|
|
|
23246 |
// Turn off API access because we're loading a new tech that might load asynchronously
|
|
|
23247 |
this.isReady_ = false;
|
|
|
23248 |
let autoplay = this.autoplay();
|
|
|
23249 |
|
|
|
23250 |
// if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
|
|
|
23251 |
// because the player is going to handle autoplay on `loadstart`
|
|
|
23252 |
if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
|
|
|
23253 |
autoplay = false;
|
|
|
23254 |
}
|
|
|
23255 |
|
|
|
23256 |
// Grab tech-specific options from player options and add source and parent element to use.
|
|
|
23257 |
const techOptions = {
|
|
|
23258 |
source,
|
|
|
23259 |
autoplay,
|
|
|
23260 |
'nativeControlsForTouch': this.options_.nativeControlsForTouch,
|
|
|
23261 |
'playerId': this.id(),
|
|
|
23262 |
'techId': `${this.id()}_${camelTechName}_api`,
|
|
|
23263 |
'playsinline': this.options_.playsinline,
|
|
|
23264 |
'preload': this.options_.preload,
|
|
|
23265 |
'loop': this.options_.loop,
|
|
|
23266 |
'disablePictureInPicture': this.options_.disablePictureInPicture,
|
|
|
23267 |
'muted': this.options_.muted,
|
|
|
23268 |
'poster': this.poster(),
|
|
|
23269 |
'language': this.language(),
|
|
|
23270 |
'playerElIngest': this.playerElIngest_ || false,
|
|
|
23271 |
'vtt.js': this.options_['vtt.js'],
|
|
|
23272 |
'canOverridePoster': !!this.options_.techCanOverridePoster,
|
|
|
23273 |
'enableSourceset': this.options_.enableSourceset
|
|
|
23274 |
};
|
|
|
23275 |
ALL.names.forEach(name => {
|
|
|
23276 |
const props = ALL[name];
|
|
|
23277 |
techOptions[props.getterName] = this[props.privateName];
|
|
|
23278 |
});
|
|
|
23279 |
Object.assign(techOptions, this.options_[titleTechName]);
|
|
|
23280 |
Object.assign(techOptions, this.options_[camelTechName]);
|
|
|
23281 |
Object.assign(techOptions, this.options_[techName.toLowerCase()]);
|
|
|
23282 |
if (this.tag) {
|
|
|
23283 |
techOptions.tag = this.tag;
|
|
|
23284 |
}
|
|
|
23285 |
if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
|
|
|
23286 |
techOptions.startTime = this.cache_.currentTime;
|
|
|
23287 |
}
|
|
|
23288 |
|
|
|
23289 |
// Initialize tech instance
|
|
|
23290 |
const TechClass = Tech.getTech(techName);
|
|
|
23291 |
if (!TechClass) {
|
|
|
23292 |
throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
|
|
|
23293 |
}
|
|
|
23294 |
this.tech_ = new TechClass(techOptions);
|
|
|
23295 |
|
|
|
23296 |
// player.triggerReady is always async, so don't need this to be async
|
|
|
23297 |
this.tech_.ready(bind_(this, this.handleTechReady_), true);
|
|
|
23298 |
textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
|
|
|
23299 |
|
|
|
23300 |
// Listen to all HTML5-defined events and trigger them on the player
|
|
|
23301 |
TECH_EVENTS_RETRIGGER.forEach(event => {
|
|
|
23302 |
this.on(this.tech_, event, e => this[`handleTech${toTitleCase$1(event)}_`](e));
|
|
|
23303 |
});
|
|
|
23304 |
Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
|
|
|
23305 |
this.on(this.tech_, event, eventObj => {
|
|
|
23306 |
if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
|
|
|
23307 |
this.queuedCallbacks_.push({
|
|
|
23308 |
callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
|
|
|
23309 |
event: eventObj
|
|
|
23310 |
});
|
|
|
23311 |
return;
|
|
|
23312 |
}
|
|
|
23313 |
this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
|
|
|
23314 |
});
|
|
|
23315 |
});
|
|
|
23316 |
this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
|
|
|
23317 |
this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
|
|
|
23318 |
this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
|
|
|
23319 |
this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
|
|
|
23320 |
this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
|
|
|
23321 |
this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
|
|
|
23322 |
this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
|
|
|
23323 |
this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
|
|
|
23324 |
this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
|
|
|
23325 |
this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
|
|
|
23326 |
this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
|
|
|
23327 |
this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
|
|
|
23328 |
this.on(this.tech_, 'error', e => this.handleTechError_(e));
|
|
|
23329 |
this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
|
|
|
23330 |
this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
|
|
|
23331 |
this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
|
|
|
23332 |
this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
|
|
|
23333 |
this.usingNativeControls(this.techGet_('controls'));
|
|
|
23334 |
if (this.controls() && !this.usingNativeControls()) {
|
|
|
23335 |
this.addTechControlsListeners_();
|
|
|
23336 |
}
|
|
|
23337 |
|
|
|
23338 |
// Add the tech element in the DOM if it was not already there
|
|
|
23339 |
// Make sure to not insert the original video element if using Html5
|
|
|
23340 |
if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
|
|
|
23341 |
prependTo(this.tech_.el(), this.el());
|
|
|
23342 |
}
|
|
|
23343 |
|
|
|
23344 |
// Get rid of the original video tag reference after the first tech is loaded
|
|
|
23345 |
if (this.tag) {
|
|
|
23346 |
this.tag.player = null;
|
|
|
23347 |
this.tag = null;
|
|
|
23348 |
}
|
|
|
23349 |
}
|
|
|
23350 |
|
|
|
23351 |
/**
|
|
|
23352 |
* Unload and dispose of the current playback {@link Tech}.
|
|
|
23353 |
*
|
|
|
23354 |
* @private
|
|
|
23355 |
*/
|
|
|
23356 |
unloadTech_() {
|
|
|
23357 |
// Save the current text tracks so that we can reuse the same text tracks with the next tech
|
|
|
23358 |
ALL.names.forEach(name => {
|
|
|
23359 |
const props = ALL[name];
|
|
|
23360 |
this[props.privateName] = this[props.getterName]();
|
|
|
23361 |
});
|
|
|
23362 |
this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
|
|
|
23363 |
this.isReady_ = false;
|
|
|
23364 |
this.tech_.dispose();
|
|
|
23365 |
this.tech_ = false;
|
|
|
23366 |
if (this.isPosterFromTech_) {
|
|
|
23367 |
this.poster_ = '';
|
|
|
23368 |
this.trigger('posterchange');
|
|
|
23369 |
}
|
|
|
23370 |
this.isPosterFromTech_ = false;
|
|
|
23371 |
}
|
|
|
23372 |
|
|
|
23373 |
/**
|
|
|
23374 |
* Return a reference to the current {@link Tech}.
|
|
|
23375 |
* It will print a warning by default about the danger of using the tech directly
|
|
|
23376 |
* but any argument that is passed in will silence the warning.
|
|
|
23377 |
*
|
|
|
23378 |
* @param {*} [safety]
|
|
|
23379 |
* Anything passed in to silence the warning
|
|
|
23380 |
*
|
|
|
23381 |
* @return {Tech}
|
|
|
23382 |
* The Tech
|
|
|
23383 |
*/
|
|
|
23384 |
tech(safety) {
|
|
|
23385 |
if (safety === undefined) {
|
|
|
23386 |
log$1.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
|
|
|
23387 |
}
|
|
|
23388 |
return this.tech_;
|
|
|
23389 |
}
|
|
|
23390 |
|
|
|
23391 |
/**
|
|
|
23392 |
* An object that contains Video.js version.
|
|
|
23393 |
*
|
|
|
23394 |
* @typedef {Object} PlayerVersion
|
|
|
23395 |
*
|
|
|
23396 |
* @property {string} 'video.js' - Video.js version
|
|
|
23397 |
*/
|
|
|
23398 |
|
|
|
23399 |
/**
|
|
|
23400 |
* Returns an object with Video.js version.
|
|
|
23401 |
*
|
|
|
23402 |
* @return {PlayerVersion}
|
|
|
23403 |
* An object with Video.js version.
|
|
|
23404 |
*/
|
|
|
23405 |
version() {
|
|
|
23406 |
return {
|
|
|
23407 |
'video.js': version$5
|
|
|
23408 |
};
|
|
|
23409 |
}
|
|
|
23410 |
|
|
|
23411 |
/**
|
|
|
23412 |
* Set up click and touch listeners for the playback element
|
|
|
23413 |
*
|
|
|
23414 |
* - On desktops: a click on the video itself will toggle playback
|
|
|
23415 |
* - On mobile devices: a click on the video toggles controls
|
|
|
23416 |
* which is done by toggling the user state between active and
|
|
|
23417 |
* inactive
|
|
|
23418 |
* - A tap can signal that a user has become active or has become inactive
|
|
|
23419 |
* e.g. a quick tap on an iPhone movie should reveal the controls. Another
|
|
|
23420 |
* quick tap should hide them again (signaling the user is in an inactive
|
|
|
23421 |
* viewing state)
|
|
|
23422 |
* - In addition to this, we still want the user to be considered inactive after
|
|
|
23423 |
* a few seconds of inactivity.
|
|
|
23424 |
*
|
|
|
23425 |
* > Note: the only part of iOS interaction we can't mimic with this setup
|
|
|
23426 |
* is a touch and hold on the video element counting as activity in order to
|
|
|
23427 |
* keep the controls showing, but that shouldn't be an issue. A touch and hold
|
|
|
23428 |
* on any controls will still keep the user active
|
|
|
23429 |
*
|
|
|
23430 |
* @private
|
|
|
23431 |
*/
|
|
|
23432 |
addTechControlsListeners_() {
|
|
|
23433 |
// Make sure to remove all the previous listeners in case we are called multiple times.
|
|
|
23434 |
this.removeTechControlsListeners_();
|
|
|
23435 |
this.on(this.tech_, 'click', this.boundHandleTechClick_);
|
|
|
23436 |
this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
|
|
|
23437 |
|
|
|
23438 |
// If the controls were hidden we don't want that to change without a tap event
|
|
|
23439 |
// so we'll check if the controls were already showing before reporting user
|
|
|
23440 |
// activity
|
|
|
23441 |
this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
|
|
|
23442 |
this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
|
|
|
23443 |
this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
|
|
|
23444 |
|
|
|
23445 |
// The tap listener needs to come after the touchend listener because the tap
|
|
|
23446 |
// listener cancels out any reportedUserActivity when setting userActive(false)
|
|
|
23447 |
this.on(this.tech_, 'tap', this.boundHandleTechTap_);
|
|
|
23448 |
}
|
|
|
23449 |
|
|
|
23450 |
/**
|
|
|
23451 |
* Remove the listeners used for click and tap controls. This is needed for
|
|
|
23452 |
* toggling to controls disabled, where a tap/touch should do nothing.
|
|
|
23453 |
*
|
|
|
23454 |
* @private
|
|
|
23455 |
*/
|
|
|
23456 |
removeTechControlsListeners_() {
|
|
|
23457 |
// We don't want to just use `this.off()` because there might be other needed
|
|
|
23458 |
// listeners added by techs that extend this.
|
|
|
23459 |
this.off(this.tech_, 'tap', this.boundHandleTechTap_);
|
|
|
23460 |
this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
|
|
|
23461 |
this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
|
|
|
23462 |
this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
|
|
|
23463 |
this.off(this.tech_, 'click', this.boundHandleTechClick_);
|
|
|
23464 |
this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
|
|
|
23465 |
}
|
|
|
23466 |
|
|
|
23467 |
/**
|
|
|
23468 |
* Player waits for the tech to be ready
|
|
|
23469 |
*
|
|
|
23470 |
* @private
|
|
|
23471 |
*/
|
|
|
23472 |
handleTechReady_() {
|
|
|
23473 |
this.triggerReady();
|
|
|
23474 |
|
|
|
23475 |
// Keep the same volume as before
|
|
|
23476 |
if (this.cache_.volume) {
|
|
|
23477 |
this.techCall_('setVolume', this.cache_.volume);
|
|
|
23478 |
}
|
|
|
23479 |
|
|
|
23480 |
// Look if the tech found a higher resolution poster while loading
|
|
|
23481 |
this.handleTechPosterChange_();
|
|
|
23482 |
|
|
|
23483 |
// Update the duration if available
|
|
|
23484 |
this.handleTechDurationChange_();
|
|
|
23485 |
}
|
|
|
23486 |
|
|
|
23487 |
/**
|
|
|
23488 |
* Retrigger the `loadstart` event that was triggered by the {@link Tech}.
|
|
|
23489 |
*
|
|
|
23490 |
* @fires Player#loadstart
|
|
|
23491 |
* @listens Tech#loadstart
|
|
|
23492 |
* @private
|
|
|
23493 |
*/
|
|
|
23494 |
handleTechLoadStart_() {
|
|
|
23495 |
// TODO: Update to use `emptied` event instead. See #1277.
|
|
|
23496 |
|
|
|
23497 |
this.removeClass('vjs-ended', 'vjs-seeking');
|
|
|
23498 |
|
|
|
23499 |
// reset the error state
|
|
|
23500 |
this.error(null);
|
|
|
23501 |
|
|
|
23502 |
// Update the duration
|
|
|
23503 |
this.handleTechDurationChange_();
|
|
|
23504 |
if (!this.paused()) {
|
|
|
23505 |
/**
|
|
|
23506 |
* Fired when the user agent begins looking for media data
|
|
|
23507 |
*
|
|
|
23508 |
* @event Player#loadstart
|
|
|
23509 |
* @type {Event}
|
|
|
23510 |
*/
|
|
|
23511 |
this.trigger('loadstart');
|
|
|
23512 |
} else {
|
|
|
23513 |
// reset the hasStarted state
|
|
|
23514 |
this.hasStarted(false);
|
|
|
23515 |
this.trigger('loadstart');
|
|
|
23516 |
}
|
|
|
23517 |
|
|
|
23518 |
// autoplay happens after loadstart for the browser,
|
|
|
23519 |
// so we mimic that behavior
|
|
|
23520 |
this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
|
|
|
23521 |
}
|
|
|
23522 |
|
|
|
23523 |
/**
|
|
|
23524 |
* Handle autoplay string values, rather than the typical boolean
|
|
|
23525 |
* values that should be handled by the tech. Note that this is not
|
|
|
23526 |
* part of any specification. Valid values and what they do can be
|
|
|
23527 |
* found on the autoplay getter at Player#autoplay()
|
|
|
23528 |
*/
|
|
|
23529 |
manualAutoplay_(type) {
|
|
|
23530 |
if (!this.tech_ || typeof type !== 'string') {
|
|
|
23531 |
return;
|
|
|
23532 |
}
|
|
|
23533 |
|
|
|
23534 |
// Save original muted() value, set muted to true, and attempt to play().
|
|
|
23535 |
// On promise rejection, restore muted from saved value
|
|
|
23536 |
const resolveMuted = () => {
|
|
|
23537 |
const previouslyMuted = this.muted();
|
|
|
23538 |
this.muted(true);
|
|
|
23539 |
const restoreMuted = () => {
|
|
|
23540 |
this.muted(previouslyMuted);
|
|
|
23541 |
};
|
|
|
23542 |
|
|
|
23543 |
// restore muted on play terminatation
|
|
|
23544 |
this.playTerminatedQueue_.push(restoreMuted);
|
|
|
23545 |
const mutedPromise = this.play();
|
|
|
23546 |
if (!isPromise(mutedPromise)) {
|
|
|
23547 |
return;
|
|
|
23548 |
}
|
|
|
23549 |
return mutedPromise.catch(err => {
|
|
|
23550 |
restoreMuted();
|
|
|
23551 |
throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
|
|
|
23552 |
});
|
|
|
23553 |
};
|
|
|
23554 |
let promise;
|
|
|
23555 |
|
|
|
23556 |
// if muted defaults to true
|
|
|
23557 |
// the only thing we can do is call play
|
|
|
23558 |
if (type === 'any' && !this.muted()) {
|
|
|
23559 |
promise = this.play();
|
|
|
23560 |
if (isPromise(promise)) {
|
|
|
23561 |
promise = promise.catch(resolveMuted);
|
|
|
23562 |
}
|
|
|
23563 |
} else if (type === 'muted' && !this.muted()) {
|
|
|
23564 |
promise = resolveMuted();
|
|
|
23565 |
} else {
|
|
|
23566 |
promise = this.play();
|
|
|
23567 |
}
|
|
|
23568 |
if (!isPromise(promise)) {
|
|
|
23569 |
return;
|
|
|
23570 |
}
|
|
|
23571 |
return promise.then(() => {
|
|
|
23572 |
this.trigger({
|
|
|
23573 |
type: 'autoplay-success',
|
|
|
23574 |
autoplay: type
|
|
|
23575 |
});
|
|
|
23576 |
}).catch(() => {
|
|
|
23577 |
this.trigger({
|
|
|
23578 |
type: 'autoplay-failure',
|
|
|
23579 |
autoplay: type
|
|
|
23580 |
});
|
|
|
23581 |
});
|
|
|
23582 |
}
|
|
|
23583 |
|
|
|
23584 |
/**
|
|
|
23585 |
* Update the internal source caches so that we return the correct source from
|
|
|
23586 |
* `src()`, `currentSource()`, and `currentSources()`.
|
|
|
23587 |
*
|
|
|
23588 |
* > Note: `currentSources` will not be updated if the source that is passed in exists
|
|
|
23589 |
* in the current `currentSources` cache.
|
|
|
23590 |
*
|
|
|
23591 |
*
|
|
|
23592 |
* @param {Tech~SourceObject} srcObj
|
|
|
23593 |
* A string or object source to update our caches to.
|
|
|
23594 |
*/
|
|
|
23595 |
updateSourceCaches_(srcObj = '') {
|
|
|
23596 |
let src = srcObj;
|
|
|
23597 |
let type = '';
|
|
|
23598 |
if (typeof src !== 'string') {
|
|
|
23599 |
src = srcObj.src;
|
|
|
23600 |
type = srcObj.type;
|
|
|
23601 |
}
|
|
|
23602 |
|
|
|
23603 |
// make sure all the caches are set to default values
|
|
|
23604 |
// to prevent null checking
|
|
|
23605 |
this.cache_.source = this.cache_.source || {};
|
|
|
23606 |
this.cache_.sources = this.cache_.sources || [];
|
|
|
23607 |
|
|
|
23608 |
// try to get the type of the src that was passed in
|
|
|
23609 |
if (src && !type) {
|
|
|
23610 |
type = findMimetype(this, src);
|
|
|
23611 |
}
|
|
|
23612 |
|
|
|
23613 |
// update `currentSource` cache always
|
|
|
23614 |
this.cache_.source = merge$2({}, srcObj, {
|
|
|
23615 |
src,
|
|
|
23616 |
type
|
|
|
23617 |
});
|
|
|
23618 |
const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
|
|
|
23619 |
const sourceElSources = [];
|
|
|
23620 |
const sourceEls = this.$$('source');
|
|
|
23621 |
const matchingSourceEls = [];
|
|
|
23622 |
for (let i = 0; i < sourceEls.length; i++) {
|
|
|
23623 |
const sourceObj = getAttributes(sourceEls[i]);
|
|
|
23624 |
sourceElSources.push(sourceObj);
|
|
|
23625 |
if (sourceObj.src && sourceObj.src === src) {
|
|
|
23626 |
matchingSourceEls.push(sourceObj.src);
|
|
|
23627 |
}
|
|
|
23628 |
}
|
|
|
23629 |
|
|
|
23630 |
// if we have matching source els but not matching sources
|
|
|
23631 |
// the current source cache is not up to date
|
|
|
23632 |
if (matchingSourceEls.length && !matchingSources.length) {
|
|
|
23633 |
this.cache_.sources = sourceElSources;
|
|
|
23634 |
// if we don't have matching source or source els set the
|
|
|
23635 |
// sources cache to the `currentSource` cache
|
|
|
23636 |
} else if (!matchingSources.length) {
|
|
|
23637 |
this.cache_.sources = [this.cache_.source];
|
|
|
23638 |
}
|
|
|
23639 |
|
|
|
23640 |
// update the tech `src` cache
|
|
|
23641 |
this.cache_.src = src;
|
|
|
23642 |
}
|
|
|
23643 |
|
|
|
23644 |
/**
|
|
|
23645 |
* *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
|
|
|
23646 |
* causing the media element to reload.
|
|
|
23647 |
*
|
|
|
23648 |
* It will fire for the initial source and each subsequent source.
|
|
|
23649 |
* This event is a custom event from Video.js and is triggered by the {@link Tech}.
|
|
|
23650 |
*
|
|
|
23651 |
* The event object for this event contains a `src` property that will contain the source
|
|
|
23652 |
* that was available when the event was triggered. This is generally only necessary if Video.js
|
|
|
23653 |
* is switching techs while the source was being changed.
|
|
|
23654 |
*
|
|
|
23655 |
* It is also fired when `load` is called on the player (or media element)
|
|
|
23656 |
* because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
|
|
|
23657 |
* says that the resource selection algorithm needs to be aborted and restarted.
|
|
|
23658 |
* In this case, it is very likely that the `src` property will be set to the
|
|
|
23659 |
* empty string `""` to indicate we do not know what the source will be but
|
|
|
23660 |
* that it is changing.
|
|
|
23661 |
*
|
|
|
23662 |
* *This event is currently still experimental and may change in minor releases.*
|
|
|
23663 |
* __To use this, pass `enableSourceset` option to the player.__
|
|
|
23664 |
*
|
|
|
23665 |
* @event Player#sourceset
|
|
|
23666 |
* @type {Event}
|
|
|
23667 |
* @prop {string} src
|
|
|
23668 |
* The source url available when the `sourceset` was triggered.
|
|
|
23669 |
* It will be an empty string if we cannot know what the source is
|
|
|
23670 |
* but know that the source will change.
|
|
|
23671 |
*/
|
|
|
23672 |
/**
|
|
|
23673 |
* Retrigger the `sourceset` event that was triggered by the {@link Tech}.
|
|
|
23674 |
*
|
|
|
23675 |
* @fires Player#sourceset
|
|
|
23676 |
* @listens Tech#sourceset
|
|
|
23677 |
* @private
|
|
|
23678 |
*/
|
|
|
23679 |
handleTechSourceset_(event) {
|
|
|
23680 |
// only update the source cache when the source
|
|
|
23681 |
// was not updated using the player api
|
|
|
23682 |
if (!this.changingSrc_) {
|
|
|
23683 |
let updateSourceCaches = src => this.updateSourceCaches_(src);
|
|
|
23684 |
const playerSrc = this.currentSource().src;
|
|
|
23685 |
const eventSrc = event.src;
|
|
|
23686 |
|
|
|
23687 |
// if we have a playerSrc that is not a blob, and a tech src that is a blob
|
|
|
23688 |
if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
|
|
|
23689 |
// if both the tech source and the player source were updated we assume
|
|
|
23690 |
// something like @videojs/http-streaming did the sourceset and skip updating the source cache.
|
|
|
23691 |
if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
|
|
|
23692 |
updateSourceCaches = () => {};
|
|
|
23693 |
}
|
|
|
23694 |
}
|
|
|
23695 |
|
|
|
23696 |
// update the source to the initial source right away
|
|
|
23697 |
// in some cases this will be empty string
|
|
|
23698 |
updateSourceCaches(eventSrc);
|
|
|
23699 |
|
|
|
23700 |
// if the `sourceset` `src` was an empty string
|
|
|
23701 |
// wait for a `loadstart` to update the cache to `currentSrc`.
|
|
|
23702 |
// If a sourceset happens before a `loadstart`, we reset the state
|
|
|
23703 |
if (!event.src) {
|
|
|
23704 |
this.tech_.any(['sourceset', 'loadstart'], e => {
|
|
|
23705 |
// if a sourceset happens before a `loadstart` there
|
|
|
23706 |
// is nothing to do as this `handleTechSourceset_`
|
|
|
23707 |
// will be called again and this will be handled there.
|
|
|
23708 |
if (e.type === 'sourceset') {
|
|
|
23709 |
return;
|
|
|
23710 |
}
|
|
|
23711 |
const techSrc = this.techGet_('currentSrc');
|
|
|
23712 |
this.lastSource_.tech = techSrc;
|
|
|
23713 |
this.updateSourceCaches_(techSrc);
|
|
|
23714 |
});
|
|
|
23715 |
}
|
|
|
23716 |
}
|
|
|
23717 |
this.lastSource_ = {
|
|
|
23718 |
player: this.currentSource().src,
|
|
|
23719 |
tech: event.src
|
|
|
23720 |
};
|
|
|
23721 |
this.trigger({
|
|
|
23722 |
src: event.src,
|
|
|
23723 |
type: 'sourceset'
|
|
|
23724 |
});
|
|
|
23725 |
}
|
|
|
23726 |
|
|
|
23727 |
/**
|
|
|
23728 |
* Add/remove the vjs-has-started class
|
|
|
23729 |
*
|
|
|
23730 |
*
|
|
|
23731 |
* @param {boolean} request
|
|
|
23732 |
* - true: adds the class
|
|
|
23733 |
* - false: remove the class
|
|
|
23734 |
*
|
|
|
23735 |
* @return {boolean}
|
|
|
23736 |
* the boolean value of hasStarted_
|
|
|
23737 |
*/
|
|
|
23738 |
hasStarted(request) {
|
|
|
23739 |
if (request === undefined) {
|
|
|
23740 |
// act as getter, if we have no request to change
|
|
|
23741 |
return this.hasStarted_;
|
|
|
23742 |
}
|
|
|
23743 |
if (request === this.hasStarted_) {
|
|
|
23744 |
return;
|
|
|
23745 |
}
|
|
|
23746 |
this.hasStarted_ = request;
|
|
|
23747 |
if (this.hasStarted_) {
|
|
|
23748 |
this.addClass('vjs-has-started');
|
|
|
23749 |
} else {
|
|
|
23750 |
this.removeClass('vjs-has-started');
|
|
|
23751 |
}
|
|
|
23752 |
}
|
|
|
23753 |
|
|
|
23754 |
/**
|
|
|
23755 |
* Fired whenever the media begins or resumes playback
|
|
|
23756 |
*
|
|
|
23757 |
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
|
|
|
23758 |
* @fires Player#play
|
|
|
23759 |
* @listens Tech#play
|
|
|
23760 |
* @private
|
|
|
23761 |
*/
|
|
|
23762 |
handleTechPlay_() {
|
|
|
23763 |
this.removeClass('vjs-ended', 'vjs-paused');
|
|
|
23764 |
this.addClass('vjs-playing');
|
|
|
23765 |
|
|
|
23766 |
// hide the poster when the user hits play
|
|
|
23767 |
this.hasStarted(true);
|
|
|
23768 |
/**
|
|
|
23769 |
* Triggered whenever an {@link Tech#play} event happens. Indicates that
|
|
|
23770 |
* playback has started or resumed.
|
|
|
23771 |
*
|
|
|
23772 |
* @event Player#play
|
|
|
23773 |
* @type {Event}
|
|
|
23774 |
*/
|
|
|
23775 |
this.trigger('play');
|
|
|
23776 |
}
|
|
|
23777 |
|
|
|
23778 |
/**
|
|
|
23779 |
* Retrigger the `ratechange` event that was triggered by the {@link Tech}.
|
|
|
23780 |
*
|
|
|
23781 |
* If there were any events queued while the playback rate was zero, fire
|
|
|
23782 |
* those events now.
|
|
|
23783 |
*
|
|
|
23784 |
* @private
|
|
|
23785 |
* @method Player#handleTechRateChange_
|
|
|
23786 |
* @fires Player#ratechange
|
|
|
23787 |
* @listens Tech#ratechange
|
|
|
23788 |
*/
|
|
|
23789 |
handleTechRateChange_() {
|
|
|
23790 |
if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
|
|
|
23791 |
this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
|
|
|
23792 |
this.queuedCallbacks_ = [];
|
|
|
23793 |
}
|
|
|
23794 |
this.cache_.lastPlaybackRate = this.tech_.playbackRate();
|
|
|
23795 |
/**
|
|
|
23796 |
* Fires when the playing speed of the audio/video is changed
|
|
|
23797 |
*
|
|
|
23798 |
* @event Player#ratechange
|
|
|
23799 |
* @type {event}
|
|
|
23800 |
*/
|
|
|
23801 |
this.trigger('ratechange');
|
|
|
23802 |
}
|
|
|
23803 |
|
|
|
23804 |
/**
|
|
|
23805 |
* Retrigger the `waiting` event that was triggered by the {@link Tech}.
|
|
|
23806 |
*
|
|
|
23807 |
* @fires Player#waiting
|
|
|
23808 |
* @listens Tech#waiting
|
|
|
23809 |
* @private
|
|
|
23810 |
*/
|
|
|
23811 |
handleTechWaiting_() {
|
|
|
23812 |
this.addClass('vjs-waiting');
|
|
|
23813 |
/**
|
|
|
23814 |
* A readyState change on the DOM element has caused playback to stop.
|
|
|
23815 |
*
|
|
|
23816 |
* @event Player#waiting
|
|
|
23817 |
* @type {Event}
|
|
|
23818 |
*/
|
|
|
23819 |
this.trigger('waiting');
|
|
|
23820 |
|
|
|
23821 |
// Browsers may emit a timeupdate event after a waiting event. In order to prevent
|
|
|
23822 |
// premature removal of the waiting class, wait for the time to change.
|
|
|
23823 |
const timeWhenWaiting = this.currentTime();
|
|
|
23824 |
const timeUpdateListener = () => {
|
|
|
23825 |
if (timeWhenWaiting !== this.currentTime()) {
|
|
|
23826 |
this.removeClass('vjs-waiting');
|
|
|
23827 |
this.off('timeupdate', timeUpdateListener);
|
|
|
23828 |
}
|
|
|
23829 |
};
|
|
|
23830 |
this.on('timeupdate', timeUpdateListener);
|
|
|
23831 |
}
|
|
|
23832 |
|
|
|
23833 |
/**
|
|
|
23834 |
* Retrigger the `canplay` event that was triggered by the {@link Tech}.
|
|
|
23835 |
* > Note: This is not consistent between browsers. See #1351
|
|
|
23836 |
*
|
|
|
23837 |
* @fires Player#canplay
|
|
|
23838 |
* @listens Tech#canplay
|
|
|
23839 |
* @private
|
|
|
23840 |
*/
|
|
|
23841 |
handleTechCanPlay_() {
|
|
|
23842 |
this.removeClass('vjs-waiting');
|
|
|
23843 |
/**
|
|
|
23844 |
* The media has a readyState of HAVE_FUTURE_DATA or greater.
|
|
|
23845 |
*
|
|
|
23846 |
* @event Player#canplay
|
|
|
23847 |
* @type {Event}
|
|
|
23848 |
*/
|
|
|
23849 |
this.trigger('canplay');
|
|
|
23850 |
}
|
|
|
23851 |
|
|
|
23852 |
/**
|
|
|
23853 |
* Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
|
|
|
23854 |
*
|
|
|
23855 |
* @fires Player#canplaythrough
|
|
|
23856 |
* @listens Tech#canplaythrough
|
|
|
23857 |
* @private
|
|
|
23858 |
*/
|
|
|
23859 |
handleTechCanPlayThrough_() {
|
|
|
23860 |
this.removeClass('vjs-waiting');
|
|
|
23861 |
/**
|
|
|
23862 |
* The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
|
|
|
23863 |
* entire media file can be played without buffering.
|
|
|
23864 |
*
|
|
|
23865 |
* @event Player#canplaythrough
|
|
|
23866 |
* @type {Event}
|
|
|
23867 |
*/
|
|
|
23868 |
this.trigger('canplaythrough');
|
|
|
23869 |
}
|
|
|
23870 |
|
|
|
23871 |
/**
|
|
|
23872 |
* Retrigger the `playing` event that was triggered by the {@link Tech}.
|
|
|
23873 |
*
|
|
|
23874 |
* @fires Player#playing
|
|
|
23875 |
* @listens Tech#playing
|
|
|
23876 |
* @private
|
|
|
23877 |
*/
|
|
|
23878 |
handleTechPlaying_() {
|
|
|
23879 |
this.removeClass('vjs-waiting');
|
|
|
23880 |
/**
|
|
|
23881 |
* The media is no longer blocked from playback, and has started playing.
|
|
|
23882 |
*
|
|
|
23883 |
* @event Player#playing
|
|
|
23884 |
* @type {Event}
|
|
|
23885 |
*/
|
|
|
23886 |
this.trigger('playing');
|
|
|
23887 |
}
|
|
|
23888 |
|
|
|
23889 |
/**
|
|
|
23890 |
* Retrigger the `seeking` event that was triggered by the {@link Tech}.
|
|
|
23891 |
*
|
|
|
23892 |
* @fires Player#seeking
|
|
|
23893 |
* @listens Tech#seeking
|
|
|
23894 |
* @private
|
|
|
23895 |
*/
|
|
|
23896 |
handleTechSeeking_() {
|
|
|
23897 |
this.addClass('vjs-seeking');
|
|
|
23898 |
/**
|
|
|
23899 |
* Fired whenever the player is jumping to a new time
|
|
|
23900 |
*
|
|
|
23901 |
* @event Player#seeking
|
|
|
23902 |
* @type {Event}
|
|
|
23903 |
*/
|
|
|
23904 |
this.trigger('seeking');
|
|
|
23905 |
}
|
|
|
23906 |
|
|
|
23907 |
/**
|
|
|
23908 |
* Retrigger the `seeked` event that was triggered by the {@link Tech}.
|
|
|
23909 |
*
|
|
|
23910 |
* @fires Player#seeked
|
|
|
23911 |
* @listens Tech#seeked
|
|
|
23912 |
* @private
|
|
|
23913 |
*/
|
|
|
23914 |
handleTechSeeked_() {
|
|
|
23915 |
this.removeClass('vjs-seeking', 'vjs-ended');
|
|
|
23916 |
/**
|
|
|
23917 |
* Fired when the player has finished jumping to a new time
|
|
|
23918 |
*
|
|
|
23919 |
* @event Player#seeked
|
|
|
23920 |
* @type {Event}
|
|
|
23921 |
*/
|
|
|
23922 |
this.trigger('seeked');
|
|
|
23923 |
}
|
|
|
23924 |
|
|
|
23925 |
/**
|
|
|
23926 |
* Retrigger the `pause` event that was triggered by the {@link Tech}.
|
|
|
23927 |
*
|
|
|
23928 |
* @fires Player#pause
|
|
|
23929 |
* @listens Tech#pause
|
|
|
23930 |
* @private
|
|
|
23931 |
*/
|
|
|
23932 |
handleTechPause_() {
|
|
|
23933 |
this.removeClass('vjs-playing');
|
|
|
23934 |
this.addClass('vjs-paused');
|
|
|
23935 |
/**
|
|
|
23936 |
* Fired whenever the media has been paused
|
|
|
23937 |
*
|
|
|
23938 |
* @event Player#pause
|
|
|
23939 |
* @type {Event}
|
|
|
23940 |
*/
|
|
|
23941 |
this.trigger('pause');
|
|
|
23942 |
}
|
|
|
23943 |
|
|
|
23944 |
/**
|
|
|
23945 |
* Retrigger the `ended` event that was triggered by the {@link Tech}.
|
|
|
23946 |
*
|
|
|
23947 |
* @fires Player#ended
|
|
|
23948 |
* @listens Tech#ended
|
|
|
23949 |
* @private
|
|
|
23950 |
*/
|
|
|
23951 |
handleTechEnded_() {
|
|
|
23952 |
this.addClass('vjs-ended');
|
|
|
23953 |
this.removeClass('vjs-waiting');
|
|
|
23954 |
if (this.options_.loop) {
|
|
|
23955 |
this.currentTime(0);
|
|
|
23956 |
this.play();
|
|
|
23957 |
} else if (!this.paused()) {
|
|
|
23958 |
this.pause();
|
|
|
23959 |
}
|
|
|
23960 |
|
|
|
23961 |
/**
|
|
|
23962 |
* Fired when the end of the media resource is reached (currentTime == duration)
|
|
|
23963 |
*
|
|
|
23964 |
* @event Player#ended
|
|
|
23965 |
* @type {Event}
|
|
|
23966 |
*/
|
|
|
23967 |
this.trigger('ended');
|
|
|
23968 |
}
|
|
|
23969 |
|
|
|
23970 |
/**
|
|
|
23971 |
* Fired when the duration of the media resource is first known or changed
|
|
|
23972 |
*
|
|
|
23973 |
* @listens Tech#durationchange
|
|
|
23974 |
* @private
|
|
|
23975 |
*/
|
|
|
23976 |
handleTechDurationChange_() {
|
|
|
23977 |
this.duration(this.techGet_('duration'));
|
|
|
23978 |
}
|
|
|
23979 |
|
|
|
23980 |
/**
|
|
|
23981 |
* Handle a click on the media element to play/pause
|
|
|
23982 |
*
|
|
|
23983 |
* @param {Event} event
|
|
|
23984 |
* the event that caused this function to trigger
|
|
|
23985 |
*
|
|
|
23986 |
* @listens Tech#click
|
|
|
23987 |
* @private
|
|
|
23988 |
*/
|
|
|
23989 |
handleTechClick_(event) {
|
|
|
23990 |
// When controls are disabled a click should not toggle playback because
|
|
|
23991 |
// the click is considered a control
|
|
|
23992 |
if (!this.controls_) {
|
|
|
23993 |
return;
|
|
|
23994 |
}
|
|
|
23995 |
if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
|
|
|
23996 |
if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
|
|
|
23997 |
this.options_.userActions.click.call(this, event);
|
|
|
23998 |
} else if (this.paused()) {
|
|
|
23999 |
silencePromise(this.play());
|
|
|
24000 |
} else {
|
|
|
24001 |
this.pause();
|
|
|
24002 |
}
|
|
|
24003 |
}
|
|
|
24004 |
}
|
|
|
24005 |
|
|
|
24006 |
/**
|
|
|
24007 |
* Handle a double-click on the media element to enter/exit fullscreen
|
|
|
24008 |
*
|
|
|
24009 |
* @param {Event} event
|
|
|
24010 |
* the event that caused this function to trigger
|
|
|
24011 |
*
|
|
|
24012 |
* @listens Tech#dblclick
|
|
|
24013 |
* @private
|
|
|
24014 |
*/
|
|
|
24015 |
handleTechDoubleClick_(event) {
|
|
|
24016 |
if (!this.controls_) {
|
|
|
24017 |
return;
|
|
|
24018 |
}
|
|
|
24019 |
|
|
|
24020 |
// we do not want to toggle fullscreen state
|
|
|
24021 |
// when double-clicking inside a control bar or a modal
|
|
|
24022 |
const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
|
|
|
24023 |
if (!inAllowedEls) {
|
|
|
24024 |
/*
|
|
|
24025 |
* options.userActions.doubleClick
|
|
|
24026 |
*
|
|
|
24027 |
* If `undefined` or `true`, double-click toggles fullscreen if controls are present
|
|
|
24028 |
* Set to `false` to disable double-click handling
|
|
|
24029 |
* Set to a function to substitute an external double-click handler
|
|
|
24030 |
*/
|
|
|
24031 |
if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
|
|
|
24032 |
if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
|
|
|
24033 |
this.options_.userActions.doubleClick.call(this, event);
|
|
|
24034 |
} else if (this.isFullscreen()) {
|
|
|
24035 |
this.exitFullscreen();
|
|
|
24036 |
} else {
|
|
|
24037 |
this.requestFullscreen();
|
|
|
24038 |
}
|
|
|
24039 |
}
|
|
|
24040 |
}
|
|
|
24041 |
}
|
|
|
24042 |
|
|
|
24043 |
/**
|
|
|
24044 |
* Handle a tap on the media element. It will toggle the user
|
|
|
24045 |
* activity state, which hides and shows the controls.
|
|
|
24046 |
*
|
|
|
24047 |
* @listens Tech#tap
|
|
|
24048 |
* @private
|
|
|
24049 |
*/
|
|
|
24050 |
handleTechTap_() {
|
|
|
24051 |
this.userActive(!this.userActive());
|
|
|
24052 |
}
|
|
|
24053 |
|
|
|
24054 |
/**
|
|
|
24055 |
* Handle touch to start
|
|
|
24056 |
*
|
|
|
24057 |
* @listens Tech#touchstart
|
|
|
24058 |
* @private
|
|
|
24059 |
*/
|
|
|
24060 |
handleTechTouchStart_() {
|
|
|
24061 |
this.userWasActive = this.userActive();
|
|
|
24062 |
}
|
|
|
24063 |
|
|
|
24064 |
/**
|
|
|
24065 |
* Handle touch to move
|
|
|
24066 |
*
|
|
|
24067 |
* @listens Tech#touchmove
|
|
|
24068 |
* @private
|
|
|
24069 |
*/
|
|
|
24070 |
handleTechTouchMove_() {
|
|
|
24071 |
if (this.userWasActive) {
|
|
|
24072 |
this.reportUserActivity();
|
|
|
24073 |
}
|
|
|
24074 |
}
|
|
|
24075 |
|
|
|
24076 |
/**
|
|
|
24077 |
* Handle touch to end
|
|
|
24078 |
*
|
|
|
24079 |
* @param {Event} event
|
|
|
24080 |
* the touchend event that triggered
|
|
|
24081 |
* this function
|
|
|
24082 |
*
|
|
|
24083 |
* @listens Tech#touchend
|
|
|
24084 |
* @private
|
|
|
24085 |
*/
|
|
|
24086 |
handleTechTouchEnd_(event) {
|
|
|
24087 |
// Stop the mouse events from also happening
|
|
|
24088 |
if (event.cancelable) {
|
|
|
24089 |
event.preventDefault();
|
|
|
24090 |
}
|
|
|
24091 |
}
|
|
|
24092 |
|
|
|
24093 |
/**
|
|
|
24094 |
* @private
|
|
|
24095 |
*/
|
|
|
24096 |
toggleFullscreenClass_() {
|
|
|
24097 |
if (this.isFullscreen()) {
|
|
|
24098 |
this.addClass('vjs-fullscreen');
|
|
|
24099 |
} else {
|
|
|
24100 |
this.removeClass('vjs-fullscreen');
|
|
|
24101 |
}
|
|
|
24102 |
}
|
|
|
24103 |
|
|
|
24104 |
/**
|
|
|
24105 |
* when the document fschange event triggers it calls this
|
|
|
24106 |
*/
|
|
|
24107 |
documentFullscreenChange_(e) {
|
|
|
24108 |
const targetPlayer = e.target.player;
|
|
|
24109 |
|
|
|
24110 |
// if another player was fullscreen
|
|
|
24111 |
// do a null check for targetPlayer because older firefox's would put document as e.target
|
|
|
24112 |
if (targetPlayer && targetPlayer !== this) {
|
|
|
24113 |
return;
|
|
|
24114 |
}
|
|
|
24115 |
const el = this.el();
|
|
|
24116 |
let isFs = document[this.fsApi_.fullscreenElement] === el;
|
|
|
24117 |
if (!isFs && el.matches) {
|
|
|
24118 |
isFs = el.matches(':' + this.fsApi_.fullscreen);
|
|
|
24119 |
}
|
|
|
24120 |
this.isFullscreen(isFs);
|
|
|
24121 |
}
|
|
|
24122 |
|
|
|
24123 |
/**
|
|
|
24124 |
* Handle Tech Fullscreen Change
|
|
|
24125 |
*
|
|
|
24126 |
* @param {Event} event
|
|
|
24127 |
* the fullscreenchange event that triggered this function
|
|
|
24128 |
*
|
|
|
24129 |
* @param {Object} data
|
|
|
24130 |
* the data that was sent with the event
|
|
|
24131 |
*
|
|
|
24132 |
* @private
|
|
|
24133 |
* @listens Tech#fullscreenchange
|
|
|
24134 |
* @fires Player#fullscreenchange
|
|
|
24135 |
*/
|
|
|
24136 |
handleTechFullscreenChange_(event, data) {
|
|
|
24137 |
if (data) {
|
|
|
24138 |
if (data.nativeIOSFullscreen) {
|
|
|
24139 |
this.addClass('vjs-ios-native-fs');
|
|
|
24140 |
this.tech_.one('webkitendfullscreen', () => {
|
|
|
24141 |
this.removeClass('vjs-ios-native-fs');
|
|
|
24142 |
});
|
|
|
24143 |
}
|
|
|
24144 |
this.isFullscreen(data.isFullscreen);
|
|
|
24145 |
}
|
|
|
24146 |
}
|
|
|
24147 |
handleTechFullscreenError_(event, err) {
|
|
|
24148 |
this.trigger('fullscreenerror', err);
|
|
|
24149 |
}
|
|
|
24150 |
|
|
|
24151 |
/**
|
|
|
24152 |
* @private
|
|
|
24153 |
*/
|
|
|
24154 |
togglePictureInPictureClass_() {
|
|
|
24155 |
if (this.isInPictureInPicture()) {
|
|
|
24156 |
this.addClass('vjs-picture-in-picture');
|
|
|
24157 |
} else {
|
|
|
24158 |
this.removeClass('vjs-picture-in-picture');
|
|
|
24159 |
}
|
|
|
24160 |
}
|
|
|
24161 |
|
|
|
24162 |
/**
|
|
|
24163 |
* Handle Tech Enter Picture-in-Picture.
|
|
|
24164 |
*
|
|
|
24165 |
* @param {Event} event
|
|
|
24166 |
* the enterpictureinpicture event that triggered this function
|
|
|
24167 |
*
|
|
|
24168 |
* @private
|
|
|
24169 |
* @listens Tech#enterpictureinpicture
|
|
|
24170 |
*/
|
|
|
24171 |
handleTechEnterPictureInPicture_(event) {
|
|
|
24172 |
this.isInPictureInPicture(true);
|
|
|
24173 |
}
|
|
|
24174 |
|
|
|
24175 |
/**
|
|
|
24176 |
* Handle Tech Leave Picture-in-Picture.
|
|
|
24177 |
*
|
|
|
24178 |
* @param {Event} event
|
|
|
24179 |
* the leavepictureinpicture event that triggered this function
|
|
|
24180 |
*
|
|
|
24181 |
* @private
|
|
|
24182 |
* @listens Tech#leavepictureinpicture
|
|
|
24183 |
*/
|
|
|
24184 |
handleTechLeavePictureInPicture_(event) {
|
|
|
24185 |
this.isInPictureInPicture(false);
|
|
|
24186 |
}
|
|
|
24187 |
|
|
|
24188 |
/**
|
|
|
24189 |
* Fires when an error occurred during the loading of an audio/video.
|
|
|
24190 |
*
|
|
|
24191 |
* @private
|
|
|
24192 |
* @listens Tech#error
|
|
|
24193 |
*/
|
|
|
24194 |
handleTechError_() {
|
|
|
24195 |
const error = this.tech_.error();
|
|
|
24196 |
if (error) {
|
|
|
24197 |
this.error(error);
|
|
|
24198 |
}
|
|
|
24199 |
}
|
|
|
24200 |
|
|
|
24201 |
/**
|
|
|
24202 |
* Retrigger the `textdata` event that was triggered by the {@link Tech}.
|
|
|
24203 |
*
|
|
|
24204 |
* @fires Player#textdata
|
|
|
24205 |
* @listens Tech#textdata
|
|
|
24206 |
* @private
|
|
|
24207 |
*/
|
|
|
24208 |
handleTechTextData_() {
|
|
|
24209 |
let data = null;
|
|
|
24210 |
if (arguments.length > 1) {
|
|
|
24211 |
data = arguments[1];
|
|
|
24212 |
}
|
|
|
24213 |
|
|
|
24214 |
/**
|
|
|
24215 |
* Fires when we get a textdata event from tech
|
|
|
24216 |
*
|
|
|
24217 |
* @event Player#textdata
|
|
|
24218 |
* @type {Event}
|
|
|
24219 |
*/
|
|
|
24220 |
this.trigger('textdata', data);
|
|
|
24221 |
}
|
|
|
24222 |
|
|
|
24223 |
/**
|
|
|
24224 |
* Get object for cached values.
|
|
|
24225 |
*
|
|
|
24226 |
* @return {Object}
|
|
|
24227 |
* get the current object cache
|
|
|
24228 |
*/
|
|
|
24229 |
getCache() {
|
|
|
24230 |
return this.cache_;
|
|
|
24231 |
}
|
|
|
24232 |
|
|
|
24233 |
/**
|
|
|
24234 |
* Resets the internal cache object.
|
|
|
24235 |
*
|
|
|
24236 |
* Using this function outside the player constructor or reset method may
|
|
|
24237 |
* have unintended side-effects.
|
|
|
24238 |
*
|
|
|
24239 |
* @private
|
|
|
24240 |
*/
|
|
|
24241 |
resetCache_() {
|
|
|
24242 |
this.cache_ = {
|
|
|
24243 |
// Right now, the currentTime is not _really_ cached because it is always
|
|
|
24244 |
// retrieved from the tech (see: currentTime). However, for completeness,
|
|
|
24245 |
// we set it to zero here to ensure that if we do start actually caching
|
|
|
24246 |
// it, we reset it along with everything else.
|
|
|
24247 |
currentTime: 0,
|
|
|
24248 |
initTime: 0,
|
|
|
24249 |
inactivityTimeout: this.options_.inactivityTimeout,
|
|
|
24250 |
duration: NaN,
|
|
|
24251 |
lastVolume: 1,
|
|
|
24252 |
lastPlaybackRate: this.defaultPlaybackRate(),
|
|
|
24253 |
media: null,
|
|
|
24254 |
src: '',
|
|
|
24255 |
source: {},
|
|
|
24256 |
sources: [],
|
|
|
24257 |
playbackRates: [],
|
|
|
24258 |
volume: 1
|
|
|
24259 |
};
|
|
|
24260 |
}
|
|
|
24261 |
|
|
|
24262 |
/**
|
|
|
24263 |
* Pass values to the playback tech
|
|
|
24264 |
*
|
|
|
24265 |
* @param {string} [method]
|
|
|
24266 |
* the method to call
|
|
|
24267 |
*
|
|
|
24268 |
* @param {Object} [arg]
|
|
|
24269 |
* the argument to pass
|
|
|
24270 |
*
|
|
|
24271 |
* @private
|
|
|
24272 |
*/
|
|
|
24273 |
techCall_(method, arg) {
|
|
|
24274 |
// If it's not ready yet, call method when it is
|
|
|
24275 |
|
|
|
24276 |
this.ready(function () {
|
|
|
24277 |
if (method in allowedSetters) {
|
|
|
24278 |
return set(this.middleware_, this.tech_, method, arg);
|
|
|
24279 |
} else if (method in allowedMediators) {
|
|
|
24280 |
return mediate(this.middleware_, this.tech_, method, arg);
|
|
|
24281 |
}
|
|
|
24282 |
try {
|
|
|
24283 |
if (this.tech_) {
|
|
|
24284 |
this.tech_[method](arg);
|
|
|
24285 |
}
|
|
|
24286 |
} catch (e) {
|
|
|
24287 |
log$1(e);
|
|
|
24288 |
throw e;
|
|
|
24289 |
}
|
|
|
24290 |
}, true);
|
|
|
24291 |
}
|
|
|
24292 |
|
|
|
24293 |
/**
|
|
|
24294 |
* Mediate attempt to call playback tech method
|
|
|
24295 |
* and return the value of the method called.
|
|
|
24296 |
*
|
|
|
24297 |
* @param {string} method
|
|
|
24298 |
* Tech method
|
|
|
24299 |
*
|
|
|
24300 |
* @return {*}
|
|
|
24301 |
* Value returned by the tech method called, undefined if tech
|
|
|
24302 |
* is not ready or tech method is not present
|
|
|
24303 |
*
|
|
|
24304 |
* @private
|
|
|
24305 |
*/
|
|
|
24306 |
techGet_(method) {
|
|
|
24307 |
if (!this.tech_ || !this.tech_.isReady_) {
|
|
|
24308 |
return;
|
|
|
24309 |
}
|
|
|
24310 |
if (method in allowedGetters) {
|
|
|
24311 |
return get(this.middleware_, this.tech_, method);
|
|
|
24312 |
} else if (method in allowedMediators) {
|
|
|
24313 |
return mediate(this.middleware_, this.tech_, method);
|
|
|
24314 |
}
|
|
|
24315 |
|
|
|
24316 |
// Log error when playback tech object is present but method
|
|
|
24317 |
// is undefined or unavailable
|
|
|
24318 |
try {
|
|
|
24319 |
return this.tech_[method]();
|
|
|
24320 |
} catch (e) {
|
|
|
24321 |
// When building additional tech libs, an expected method may not be defined yet
|
|
|
24322 |
if (this.tech_[method] === undefined) {
|
|
|
24323 |
log$1(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
|
|
|
24324 |
throw e;
|
|
|
24325 |
}
|
|
|
24326 |
|
|
|
24327 |
// When a method isn't available on the object it throws a TypeError
|
|
|
24328 |
if (e.name === 'TypeError') {
|
|
|
24329 |
log$1(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
|
|
|
24330 |
this.tech_.isReady_ = false;
|
|
|
24331 |
throw e;
|
|
|
24332 |
}
|
|
|
24333 |
|
|
|
24334 |
// If error unknown, just log and throw
|
|
|
24335 |
log$1(e);
|
|
|
24336 |
throw e;
|
|
|
24337 |
}
|
|
|
24338 |
}
|
|
|
24339 |
|
|
|
24340 |
/**
|
|
|
24341 |
* Attempt to begin playback at the first opportunity.
|
|
|
24342 |
*
|
|
|
24343 |
* @return {Promise|undefined}
|
|
|
24344 |
* Returns a promise if the browser supports Promises (or one
|
|
|
24345 |
* was passed in as an option). This promise will be resolved on
|
|
|
24346 |
* the return value of play. If this is undefined it will fulfill the
|
|
|
24347 |
* promise chain otherwise the promise chain will be fulfilled when
|
|
|
24348 |
* the promise from play is fulfilled.
|
|
|
24349 |
*/
|
|
|
24350 |
play() {
|
|
|
24351 |
return new Promise(resolve => {
|
|
|
24352 |
this.play_(resolve);
|
|
|
24353 |
});
|
|
|
24354 |
}
|
|
|
24355 |
|
|
|
24356 |
/**
|
|
|
24357 |
* The actual logic for play, takes a callback that will be resolved on the
|
|
|
24358 |
* return value of play. This allows us to resolve to the play promise if there
|
|
|
24359 |
* is one on modern browsers.
|
|
|
24360 |
*
|
|
|
24361 |
* @private
|
|
|
24362 |
* @param {Function} [callback]
|
|
|
24363 |
* The callback that should be called when the techs play is actually called
|
|
|
24364 |
*/
|
|
|
24365 |
play_(callback = silencePromise) {
|
|
|
24366 |
this.playCallbacks_.push(callback);
|
|
|
24367 |
const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
|
|
|
24368 |
const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
|
|
|
24369 |
|
|
|
24370 |
// treat calls to play_ somewhat like the `one` event function
|
|
|
24371 |
if (this.waitToPlay_) {
|
|
|
24372 |
this.off(['ready', 'loadstart'], this.waitToPlay_);
|
|
|
24373 |
this.waitToPlay_ = null;
|
|
|
24374 |
}
|
|
|
24375 |
|
|
|
24376 |
// if the player/tech is not ready or the src itself is not ready
|
|
|
24377 |
// queue up a call to play on `ready` or `loadstart`
|
|
|
24378 |
if (!this.isReady_ || !isSrcReady) {
|
|
|
24379 |
this.waitToPlay_ = e => {
|
|
|
24380 |
this.play_();
|
|
|
24381 |
};
|
|
|
24382 |
this.one(['ready', 'loadstart'], this.waitToPlay_);
|
|
|
24383 |
|
|
|
24384 |
// if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
|
|
|
24385 |
// in that case, we need to prime the video element by calling load so it'll be ready in time
|
|
|
24386 |
if (!isSrcReady && isSafariOrIOS) {
|
|
|
24387 |
this.load();
|
|
|
24388 |
}
|
|
|
24389 |
return;
|
|
|
24390 |
}
|
|
|
24391 |
|
|
|
24392 |
// If the player/tech is ready and we have a source, we can attempt playback.
|
|
|
24393 |
const val = this.techGet_('play');
|
|
|
24394 |
|
|
|
24395 |
// For native playback, reset the progress bar if we get a play call from a replay.
|
|
|
24396 |
const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
|
|
|
24397 |
if (isNativeReplay) {
|
|
|
24398 |
this.resetProgressBar_();
|
|
|
24399 |
}
|
|
|
24400 |
// play was terminated if the returned value is null
|
|
|
24401 |
if (val === null) {
|
|
|
24402 |
this.runPlayTerminatedQueue_();
|
|
|
24403 |
} else {
|
|
|
24404 |
this.runPlayCallbacks_(val);
|
|
|
24405 |
}
|
|
|
24406 |
}
|
|
|
24407 |
|
|
|
24408 |
/**
|
|
|
24409 |
* These functions will be run when if play is terminated. If play
|
|
|
24410 |
* runPlayCallbacks_ is run these function will not be run. This allows us
|
|
|
24411 |
* to differentiate between a terminated play and an actual call to play.
|
|
|
24412 |
*/
|
|
|
24413 |
runPlayTerminatedQueue_() {
|
|
|
24414 |
const queue = this.playTerminatedQueue_.slice(0);
|
|
|
24415 |
this.playTerminatedQueue_ = [];
|
|
|
24416 |
queue.forEach(function (q) {
|
|
|
24417 |
q();
|
|
|
24418 |
});
|
|
|
24419 |
}
|
|
|
24420 |
|
|
|
24421 |
/**
|
|
|
24422 |
* When a callback to play is delayed we have to run these
|
|
|
24423 |
* callbacks when play is actually called on the tech. This function
|
|
|
24424 |
* runs the callbacks that were delayed and accepts the return value
|
|
|
24425 |
* from the tech.
|
|
|
24426 |
*
|
|
|
24427 |
* @param {undefined|Promise} val
|
|
|
24428 |
* The return value from the tech.
|
|
|
24429 |
*/
|
|
|
24430 |
runPlayCallbacks_(val) {
|
|
|
24431 |
const callbacks = this.playCallbacks_.slice(0);
|
|
|
24432 |
this.playCallbacks_ = [];
|
|
|
24433 |
// clear play terminatedQueue since we finished a real play
|
|
|
24434 |
this.playTerminatedQueue_ = [];
|
|
|
24435 |
callbacks.forEach(function (cb) {
|
|
|
24436 |
cb(val);
|
|
|
24437 |
});
|
|
|
24438 |
}
|
|
|
24439 |
|
|
|
24440 |
/**
|
|
|
24441 |
* Pause the video playback
|
|
|
24442 |
*/
|
|
|
24443 |
pause() {
|
|
|
24444 |
this.techCall_('pause');
|
|
|
24445 |
}
|
|
|
24446 |
|
|
|
24447 |
/**
|
|
|
24448 |
* Check if the player is paused or has yet to play
|
|
|
24449 |
*
|
|
|
24450 |
* @return {boolean}
|
|
|
24451 |
* - false: if the media is currently playing
|
|
|
24452 |
* - true: if media is not currently playing
|
|
|
24453 |
*/
|
|
|
24454 |
paused() {
|
|
|
24455 |
// The initial state of paused should be true (in Safari it's actually false)
|
|
|
24456 |
return this.techGet_('paused') === false ? false : true;
|
|
|
24457 |
}
|
|
|
24458 |
|
|
|
24459 |
/**
|
|
|
24460 |
* Get a TimeRange object representing the current ranges of time that the user
|
|
|
24461 |
* has played.
|
|
|
24462 |
*
|
|
|
24463 |
* @return { import('./utils/time').TimeRange }
|
|
|
24464 |
* A time range object that represents all the increments of time that have
|
|
|
24465 |
* been played.
|
|
|
24466 |
*/
|
|
|
24467 |
played() {
|
|
|
24468 |
return this.techGet_('played') || createTimeRanges$1(0, 0);
|
|
|
24469 |
}
|
|
|
24470 |
|
|
|
24471 |
/**
|
|
|
24472 |
* Sets or returns whether or not the user is "scrubbing". Scrubbing is
|
|
|
24473 |
* when the user has clicked the progress bar handle and is
|
|
|
24474 |
* dragging it along the progress bar.
|
|
|
24475 |
*
|
|
|
24476 |
* @param {boolean} [isScrubbing]
|
|
|
24477 |
* whether the user is or is not scrubbing
|
|
|
24478 |
*
|
|
|
24479 |
* @return {boolean|undefined}
|
|
|
24480 |
* - The value of scrubbing when getting
|
|
|
24481 |
* - Nothing when setting
|
|
|
24482 |
*/
|
|
|
24483 |
scrubbing(isScrubbing) {
|
|
|
24484 |
if (typeof isScrubbing === 'undefined') {
|
|
|
24485 |
return this.scrubbing_;
|
|
|
24486 |
}
|
|
|
24487 |
this.scrubbing_ = !!isScrubbing;
|
|
|
24488 |
this.techCall_('setScrubbing', this.scrubbing_);
|
|
|
24489 |
if (isScrubbing) {
|
|
|
24490 |
this.addClass('vjs-scrubbing');
|
|
|
24491 |
} else {
|
|
|
24492 |
this.removeClass('vjs-scrubbing');
|
|
|
24493 |
}
|
|
|
24494 |
}
|
|
|
24495 |
|
|
|
24496 |
/**
|
|
|
24497 |
* Get or set the current time (in seconds)
|
|
|
24498 |
*
|
|
|
24499 |
* @param {number|string} [seconds]
|
|
|
24500 |
* The time to seek to in seconds
|
|
|
24501 |
*
|
|
|
24502 |
* @return {number|undefined}
|
|
|
24503 |
* - the current time in seconds when getting
|
|
|
24504 |
* - Nothing when setting
|
|
|
24505 |
*/
|
|
|
24506 |
currentTime(seconds) {
|
|
|
24507 |
if (seconds === undefined) {
|
|
|
24508 |
// cache last currentTime and return. default to 0 seconds
|
|
|
24509 |
//
|
|
|
24510 |
// Caching the currentTime is meant to prevent a massive amount of reads on the tech's
|
|
|
24511 |
// currentTime when scrubbing, but may not provide much performance benefit after all.
|
|
|
24512 |
// Should be tested. Also something has to read the actual current time or the cache will
|
|
|
24513 |
// never get updated.
|
|
|
24514 |
this.cache_.currentTime = this.techGet_('currentTime') || 0;
|
|
|
24515 |
return this.cache_.currentTime;
|
|
|
24516 |
}
|
|
|
24517 |
if (seconds < 0) {
|
|
|
24518 |
seconds = 0;
|
|
|
24519 |
}
|
|
|
24520 |
if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
|
|
|
24521 |
this.cache_.initTime = seconds;
|
|
|
24522 |
this.off('canplay', this.boundApplyInitTime_);
|
|
|
24523 |
this.one('canplay', this.boundApplyInitTime_);
|
|
|
24524 |
return;
|
|
|
24525 |
}
|
|
|
24526 |
this.techCall_('setCurrentTime', seconds);
|
|
|
24527 |
this.cache_.initTime = 0;
|
|
|
24528 |
if (isFinite(seconds)) {
|
|
|
24529 |
this.cache_.currentTime = Number(seconds);
|
|
|
24530 |
}
|
|
|
24531 |
}
|
|
|
24532 |
|
|
|
24533 |
/**
|
|
|
24534 |
* Apply the value of initTime stored in cache as currentTime.
|
|
|
24535 |
*
|
|
|
24536 |
* @private
|
|
|
24537 |
*/
|
|
|
24538 |
applyInitTime_() {
|
|
|
24539 |
this.currentTime(this.cache_.initTime);
|
|
|
24540 |
}
|
|
|
24541 |
|
|
|
24542 |
/**
|
|
|
24543 |
* Normally gets the length in time of the video in seconds;
|
|
|
24544 |
* in all but the rarest use cases an argument will NOT be passed to the method
|
|
|
24545 |
*
|
|
|
24546 |
* > **NOTE**: The video must have started loading before the duration can be
|
|
|
24547 |
* known, and depending on preload behaviour may not be known until the video starts
|
|
|
24548 |
* playing.
|
|
|
24549 |
*
|
|
|
24550 |
* @fires Player#durationchange
|
|
|
24551 |
*
|
|
|
24552 |
* @param {number} [seconds]
|
|
|
24553 |
* The duration of the video to set in seconds
|
|
|
24554 |
*
|
|
|
24555 |
* @return {number|undefined}
|
|
|
24556 |
* - The duration of the video in seconds when getting
|
|
|
24557 |
* - Nothing when setting
|
|
|
24558 |
*/
|
|
|
24559 |
duration(seconds) {
|
|
|
24560 |
if (seconds === undefined) {
|
|
|
24561 |
// return NaN if the duration is not known
|
|
|
24562 |
return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
|
|
|
24563 |
}
|
|
|
24564 |
seconds = parseFloat(seconds);
|
|
|
24565 |
|
|
|
24566 |
// Standardize on Infinity for signaling video is live
|
|
|
24567 |
if (seconds < 0) {
|
|
|
24568 |
seconds = Infinity;
|
|
|
24569 |
}
|
|
|
24570 |
if (seconds !== this.cache_.duration) {
|
|
|
24571 |
// Cache the last set value for optimized scrubbing
|
|
|
24572 |
this.cache_.duration = seconds;
|
|
|
24573 |
if (seconds === Infinity) {
|
|
|
24574 |
this.addClass('vjs-live');
|
|
|
24575 |
} else {
|
|
|
24576 |
this.removeClass('vjs-live');
|
|
|
24577 |
}
|
|
|
24578 |
if (!isNaN(seconds)) {
|
|
|
24579 |
// Do not fire durationchange unless the duration value is known.
|
|
|
24580 |
// @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
|
|
|
24581 |
|
|
|
24582 |
/**
|
|
|
24583 |
* @event Player#durationchange
|
|
|
24584 |
* @type {Event}
|
|
|
24585 |
*/
|
|
|
24586 |
this.trigger('durationchange');
|
|
|
24587 |
}
|
|
|
24588 |
}
|
|
|
24589 |
}
|
|
|
24590 |
|
|
|
24591 |
/**
|
|
|
24592 |
* Calculates how much time is left in the video. Not part
|
|
|
24593 |
* of the native video API.
|
|
|
24594 |
*
|
|
|
24595 |
* @return {number}
|
|
|
24596 |
* The time remaining in seconds
|
|
|
24597 |
*/
|
|
|
24598 |
remainingTime() {
|
|
|
24599 |
return this.duration() - this.currentTime();
|
|
|
24600 |
}
|
|
|
24601 |
|
|
|
24602 |
/**
|
|
|
24603 |
* A remaining time function that is intended to be used when
|
|
|
24604 |
* the time is to be displayed directly to the user.
|
|
|
24605 |
*
|
|
|
24606 |
* @return {number}
|
|
|
24607 |
* The rounded time remaining in seconds
|
|
|
24608 |
*/
|
|
|
24609 |
remainingTimeDisplay() {
|
|
|
24610 |
return Math.floor(this.duration()) - Math.floor(this.currentTime());
|
|
|
24611 |
}
|
|
|
24612 |
|
|
|
24613 |
//
|
|
|
24614 |
// Kind of like an array of portions of the video that have been downloaded.
|
|
|
24615 |
|
|
|
24616 |
/**
|
|
|
24617 |
* Get a TimeRange object with an array of the times of the video
|
|
|
24618 |
* that have been downloaded. If you just want the percent of the
|
|
|
24619 |
* video that's been downloaded, use bufferedPercent.
|
|
|
24620 |
*
|
|
|
24621 |
* @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
|
|
|
24622 |
*
|
|
|
24623 |
* @return { import('./utils/time').TimeRange }
|
|
|
24624 |
* A mock {@link TimeRanges} object (following HTML spec)
|
|
|
24625 |
*/
|
|
|
24626 |
buffered() {
|
|
|
24627 |
let buffered = this.techGet_('buffered');
|
|
|
24628 |
if (!buffered || !buffered.length) {
|
|
|
24629 |
buffered = createTimeRanges$1(0, 0);
|
|
|
24630 |
}
|
|
|
24631 |
return buffered;
|
|
|
24632 |
}
|
|
|
24633 |
|
|
|
24634 |
/**
|
|
|
24635 |
* Get the TimeRanges of the media that are currently available
|
|
|
24636 |
* for seeking to.
|
|
|
24637 |
*
|
|
|
24638 |
* @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable}
|
|
|
24639 |
*
|
|
|
24640 |
* @return { import('./utils/time').TimeRange }
|
|
|
24641 |
* A mock {@link TimeRanges} object (following HTML spec)
|
|
|
24642 |
*/
|
|
|
24643 |
seekable() {
|
|
|
24644 |
let seekable = this.techGet_('seekable');
|
|
|
24645 |
if (!seekable || !seekable.length) {
|
|
|
24646 |
seekable = createTimeRanges$1(0, 0);
|
|
|
24647 |
}
|
|
|
24648 |
return seekable;
|
|
|
24649 |
}
|
|
|
24650 |
|
|
|
24651 |
/**
|
|
|
24652 |
* Returns whether the player is in the "seeking" state.
|
|
|
24653 |
*
|
|
|
24654 |
* @return {boolean} True if the player is in the seeking state, false if not.
|
|
|
24655 |
*/
|
|
|
24656 |
seeking() {
|
|
|
24657 |
return this.techGet_('seeking');
|
|
|
24658 |
}
|
|
|
24659 |
|
|
|
24660 |
/**
|
|
|
24661 |
* Returns whether the player is in the "ended" state.
|
|
|
24662 |
*
|
|
|
24663 |
* @return {boolean} True if the player is in the ended state, false if not.
|
|
|
24664 |
*/
|
|
|
24665 |
ended() {
|
|
|
24666 |
return this.techGet_('ended');
|
|
|
24667 |
}
|
|
|
24668 |
|
|
|
24669 |
/**
|
|
|
24670 |
* Returns the current state of network activity for the element, from
|
|
|
24671 |
* the codes in the list below.
|
|
|
24672 |
* - NETWORK_EMPTY (numeric value 0)
|
|
|
24673 |
* The element has not yet been initialised. All attributes are in
|
|
|
24674 |
* their initial states.
|
|
|
24675 |
* - NETWORK_IDLE (numeric value 1)
|
|
|
24676 |
* The element's resource selection algorithm is active and has
|
|
|
24677 |
* selected a resource, but it is not actually using the network at
|
|
|
24678 |
* this time.
|
|
|
24679 |
* - NETWORK_LOADING (numeric value 2)
|
|
|
24680 |
* The user agent is actively trying to download data.
|
|
|
24681 |
* - NETWORK_NO_SOURCE (numeric value 3)
|
|
|
24682 |
* The element's resource selection algorithm is active, but it has
|
|
|
24683 |
* not yet found a resource to use.
|
|
|
24684 |
*
|
|
|
24685 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
|
|
|
24686 |
* @return {number} the current network activity state
|
|
|
24687 |
*/
|
|
|
24688 |
networkState() {
|
|
|
24689 |
return this.techGet_('networkState');
|
|
|
24690 |
}
|
|
|
24691 |
|
|
|
24692 |
/**
|
|
|
24693 |
* Returns a value that expresses the current state of the element
|
|
|
24694 |
* with respect to rendering the current playback position, from the
|
|
|
24695 |
* codes in the list below.
|
|
|
24696 |
* - HAVE_NOTHING (numeric value 0)
|
|
|
24697 |
* No information regarding the media resource is available.
|
|
|
24698 |
* - HAVE_METADATA (numeric value 1)
|
|
|
24699 |
* Enough of the resource has been obtained that the duration of the
|
|
|
24700 |
* resource is available.
|
|
|
24701 |
* - HAVE_CURRENT_DATA (numeric value 2)
|
|
|
24702 |
* Data for the immediate current playback position is available.
|
|
|
24703 |
* - HAVE_FUTURE_DATA (numeric value 3)
|
|
|
24704 |
* Data for the immediate current playback position is available, as
|
|
|
24705 |
* well as enough data for the user agent to advance the current
|
|
|
24706 |
* playback position in the direction of playback.
|
|
|
24707 |
* - HAVE_ENOUGH_DATA (numeric value 4)
|
|
|
24708 |
* The user agent estimates that enough data is available for
|
|
|
24709 |
* playback to proceed uninterrupted.
|
|
|
24710 |
*
|
|
|
24711 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
|
|
|
24712 |
* @return {number} the current playback rendering state
|
|
|
24713 |
*/
|
|
|
24714 |
readyState() {
|
|
|
24715 |
return this.techGet_('readyState');
|
|
|
24716 |
}
|
|
|
24717 |
|
|
|
24718 |
/**
|
|
|
24719 |
* Get the percent (as a decimal) of the video that's been downloaded.
|
|
|
24720 |
* This method is not a part of the native HTML video API.
|
|
|
24721 |
*
|
|
|
24722 |
* @return {number}
|
|
|
24723 |
* A decimal between 0 and 1 representing the percent
|
|
|
24724 |
* that is buffered 0 being 0% and 1 being 100%
|
|
|
24725 |
*/
|
|
|
24726 |
bufferedPercent() {
|
|
|
24727 |
return bufferedPercent(this.buffered(), this.duration());
|
|
|
24728 |
}
|
|
|
24729 |
|
|
|
24730 |
/**
|
|
|
24731 |
* Get the ending time of the last buffered time range
|
|
|
24732 |
* This is used in the progress bar to encapsulate all time ranges.
|
|
|
24733 |
*
|
|
|
24734 |
* @return {number}
|
|
|
24735 |
* The end of the last buffered time range
|
|
|
24736 |
*/
|
|
|
24737 |
bufferedEnd() {
|
|
|
24738 |
const buffered = this.buffered();
|
|
|
24739 |
const duration = this.duration();
|
|
|
24740 |
let end = buffered.end(buffered.length - 1);
|
|
|
24741 |
if (end > duration) {
|
|
|
24742 |
end = duration;
|
|
|
24743 |
}
|
|
|
24744 |
return end;
|
|
|
24745 |
}
|
|
|
24746 |
|
|
|
24747 |
/**
|
|
|
24748 |
* Get or set the current volume of the media
|
|
|
24749 |
*
|
|
|
24750 |
* @param {number} [percentAsDecimal]
|
|
|
24751 |
* The new volume as a decimal percent:
|
|
|
24752 |
* - 0 is muted/0%/off
|
|
|
24753 |
* - 1.0 is 100%/full
|
|
|
24754 |
* - 0.5 is half volume or 50%
|
|
|
24755 |
*
|
|
|
24756 |
* @return {number|undefined}
|
|
|
24757 |
* The current volume as a percent when getting
|
|
|
24758 |
*/
|
|
|
24759 |
volume(percentAsDecimal) {
|
|
|
24760 |
let vol;
|
|
|
24761 |
if (percentAsDecimal !== undefined) {
|
|
|
24762 |
// Force value to between 0 and 1
|
|
|
24763 |
vol = Math.max(0, Math.min(1, percentAsDecimal));
|
|
|
24764 |
this.cache_.volume = vol;
|
|
|
24765 |
this.techCall_('setVolume', vol);
|
|
|
24766 |
if (vol > 0) {
|
|
|
24767 |
this.lastVolume_(vol);
|
|
|
24768 |
}
|
|
|
24769 |
return;
|
|
|
24770 |
}
|
|
|
24771 |
|
|
|
24772 |
// Default to 1 when returning current volume.
|
|
|
24773 |
vol = parseFloat(this.techGet_('volume'));
|
|
|
24774 |
return isNaN(vol) ? 1 : vol;
|
|
|
24775 |
}
|
|
|
24776 |
|
|
|
24777 |
/**
|
|
|
24778 |
* Get the current muted state, or turn mute on or off
|
|
|
24779 |
*
|
|
|
24780 |
* @param {boolean} [muted]
|
|
|
24781 |
* - true to mute
|
|
|
24782 |
* - false to unmute
|
|
|
24783 |
*
|
|
|
24784 |
* @return {boolean|undefined}
|
|
|
24785 |
* - true if mute is on and getting
|
|
|
24786 |
* - false if mute is off and getting
|
|
|
24787 |
* - nothing if setting
|
|
|
24788 |
*/
|
|
|
24789 |
muted(muted) {
|
|
|
24790 |
if (muted !== undefined) {
|
|
|
24791 |
this.techCall_('setMuted', muted);
|
|
|
24792 |
return;
|
|
|
24793 |
}
|
|
|
24794 |
return this.techGet_('muted') || false;
|
|
|
24795 |
}
|
|
|
24796 |
|
|
|
24797 |
/**
|
|
|
24798 |
* Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
|
|
|
24799 |
* indicates the state of muted on initial playback.
|
|
|
24800 |
*
|
|
|
24801 |
* ```js
|
|
|
24802 |
* var myPlayer = videojs('some-player-id');
|
|
|
24803 |
*
|
|
|
24804 |
* myPlayer.src("http://www.example.com/path/to/video.mp4");
|
|
|
24805 |
*
|
|
|
24806 |
* // get, should be false
|
|
|
24807 |
* console.log(myPlayer.defaultMuted());
|
|
|
24808 |
* // set to true
|
|
|
24809 |
* myPlayer.defaultMuted(true);
|
|
|
24810 |
* // get should be true
|
|
|
24811 |
* console.log(myPlayer.defaultMuted());
|
|
|
24812 |
* ```
|
|
|
24813 |
*
|
|
|
24814 |
* @param {boolean} [defaultMuted]
|
|
|
24815 |
* - true to mute
|
|
|
24816 |
* - false to unmute
|
|
|
24817 |
*
|
|
|
24818 |
* @return {boolean|undefined}
|
|
|
24819 |
* - true if defaultMuted is on and getting
|
|
|
24820 |
* - false if defaultMuted is off and getting
|
|
|
24821 |
* - Nothing when setting
|
|
|
24822 |
*/
|
|
|
24823 |
defaultMuted(defaultMuted) {
|
|
|
24824 |
if (defaultMuted !== undefined) {
|
|
|
24825 |
this.techCall_('setDefaultMuted', defaultMuted);
|
|
|
24826 |
}
|
|
|
24827 |
return this.techGet_('defaultMuted') || false;
|
|
|
24828 |
}
|
|
|
24829 |
|
|
|
24830 |
/**
|
|
|
24831 |
* Get the last volume, or set it
|
|
|
24832 |
*
|
|
|
24833 |
* @param {number} [percentAsDecimal]
|
|
|
24834 |
* The new last volume as a decimal percent:
|
|
|
24835 |
* - 0 is muted/0%/off
|
|
|
24836 |
* - 1.0 is 100%/full
|
|
|
24837 |
* - 0.5 is half volume or 50%
|
|
|
24838 |
*
|
|
|
24839 |
* @return {number|undefined}
|
|
|
24840 |
* - The current value of lastVolume as a percent when getting
|
|
|
24841 |
* - Nothing when setting
|
|
|
24842 |
*
|
|
|
24843 |
* @private
|
|
|
24844 |
*/
|
|
|
24845 |
lastVolume_(percentAsDecimal) {
|
|
|
24846 |
if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
|
|
|
24847 |
this.cache_.lastVolume = percentAsDecimal;
|
|
|
24848 |
return;
|
|
|
24849 |
}
|
|
|
24850 |
return this.cache_.lastVolume;
|
|
|
24851 |
}
|
|
|
24852 |
|
|
|
24853 |
/**
|
|
|
24854 |
* Check if current tech can support native fullscreen
|
|
|
24855 |
* (e.g. with built in controls like iOS)
|
|
|
24856 |
*
|
|
|
24857 |
* @return {boolean}
|
|
|
24858 |
* if native fullscreen is supported
|
|
|
24859 |
*/
|
|
|
24860 |
supportsFullScreen() {
|
|
|
24861 |
return this.techGet_('supportsFullScreen') || false;
|
|
|
24862 |
}
|
|
|
24863 |
|
|
|
24864 |
/**
|
|
|
24865 |
* Check if the player is in fullscreen mode or tell the player that it
|
|
|
24866 |
* is or is not in fullscreen mode.
|
|
|
24867 |
*
|
|
|
24868 |
* > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
|
|
|
24869 |
* property and instead document.fullscreenElement is used. But isFullscreen is
|
|
|
24870 |
* still a valuable property for internal player workings.
|
|
|
24871 |
*
|
|
|
24872 |
* @param {boolean} [isFS]
|
|
|
24873 |
* Set the players current fullscreen state
|
|
|
24874 |
*
|
|
|
24875 |
* @return {boolean|undefined}
|
|
|
24876 |
* - true if fullscreen is on and getting
|
|
|
24877 |
* - false if fullscreen is off and getting
|
|
|
24878 |
* - Nothing when setting
|
|
|
24879 |
*/
|
|
|
24880 |
isFullscreen(isFS) {
|
|
|
24881 |
if (isFS !== undefined) {
|
|
|
24882 |
const oldValue = this.isFullscreen_;
|
|
|
24883 |
this.isFullscreen_ = Boolean(isFS);
|
|
|
24884 |
|
|
|
24885 |
// if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
|
|
|
24886 |
// this is the only place where we trigger fullscreenchange events for older browsers
|
|
|
24887 |
// fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
|
|
|
24888 |
if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
|
|
|
24889 |
/**
|
|
|
24890 |
* @event Player#fullscreenchange
|
|
|
24891 |
* @type {Event}
|
|
|
24892 |
*/
|
|
|
24893 |
this.trigger('fullscreenchange');
|
|
|
24894 |
}
|
|
|
24895 |
this.toggleFullscreenClass_();
|
|
|
24896 |
return;
|
|
|
24897 |
}
|
|
|
24898 |
return this.isFullscreen_;
|
|
|
24899 |
}
|
|
|
24900 |
|
|
|
24901 |
/**
|
|
|
24902 |
* Increase the size of the video to full screen
|
|
|
24903 |
* In some browsers, full screen is not supported natively, so it enters
|
|
|
24904 |
* "full window mode", where the video fills the browser window.
|
|
|
24905 |
* In browsers and devices that support native full screen, sometimes the
|
|
|
24906 |
* browser's default controls will be shown, and not the Video.js custom skin.
|
|
|
24907 |
* This includes most mobile devices (iOS, Android) and older versions of
|
|
|
24908 |
* Safari.
|
|
|
24909 |
*
|
|
|
24910 |
* @param {Object} [fullscreenOptions]
|
|
|
24911 |
* Override the player fullscreen options
|
|
|
24912 |
*
|
|
|
24913 |
* @fires Player#fullscreenchange
|
|
|
24914 |
*/
|
|
|
24915 |
requestFullscreen(fullscreenOptions) {
|
|
|
24916 |
if (this.isInPictureInPicture()) {
|
|
|
24917 |
this.exitPictureInPicture();
|
|
|
24918 |
}
|
|
|
24919 |
const self = this;
|
|
|
24920 |
return new Promise((resolve, reject) => {
|
|
|
24921 |
function offHandler() {
|
|
|
24922 |
self.off('fullscreenerror', errorHandler);
|
|
|
24923 |
self.off('fullscreenchange', changeHandler);
|
|
|
24924 |
}
|
|
|
24925 |
function changeHandler() {
|
|
|
24926 |
offHandler();
|
|
|
24927 |
resolve();
|
|
|
24928 |
}
|
|
|
24929 |
function errorHandler(e, err) {
|
|
|
24930 |
offHandler();
|
|
|
24931 |
reject(err);
|
|
|
24932 |
}
|
|
|
24933 |
self.one('fullscreenchange', changeHandler);
|
|
|
24934 |
self.one('fullscreenerror', errorHandler);
|
|
|
24935 |
const promise = self.requestFullscreenHelper_(fullscreenOptions);
|
|
|
24936 |
if (promise) {
|
|
|
24937 |
promise.then(offHandler, offHandler);
|
|
|
24938 |
promise.then(resolve, reject);
|
|
|
24939 |
}
|
|
|
24940 |
});
|
|
|
24941 |
}
|
|
|
24942 |
requestFullscreenHelper_(fullscreenOptions) {
|
|
|
24943 |
let fsOptions;
|
|
|
24944 |
|
|
|
24945 |
// Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
|
|
|
24946 |
// Use defaults or player configured option unless passed directly to this method.
|
|
|
24947 |
if (!this.fsApi_.prefixed) {
|
|
|
24948 |
fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
|
|
|
24949 |
if (fullscreenOptions !== undefined) {
|
|
|
24950 |
fsOptions = fullscreenOptions;
|
|
|
24951 |
}
|
|
|
24952 |
}
|
|
|
24953 |
|
|
|
24954 |
// This method works as follows:
|
|
|
24955 |
// 1. if a fullscreen api is available, use it
|
|
|
24956 |
// 1. call requestFullscreen with potential options
|
|
|
24957 |
// 2. if we got a promise from above, use it to update isFullscreen()
|
|
|
24958 |
// 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
|
|
|
24959 |
// This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
|
|
|
24960 |
// 3. otherwise, use "fullWindow" mode
|
|
|
24961 |
if (this.fsApi_.requestFullscreen) {
|
|
|
24962 |
const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
|
|
|
24963 |
|
|
|
24964 |
// Even on browsers with promise support this may not return a promise
|
|
|
24965 |
if (promise) {
|
|
|
24966 |
promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
|
|
|
24967 |
}
|
|
|
24968 |
return promise;
|
|
|
24969 |
} else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
|
|
|
24970 |
// we can't take the video.js controls fullscreen but we can go fullscreen
|
|
|
24971 |
// with native controls
|
|
|
24972 |
this.techCall_('enterFullScreen');
|
|
|
24973 |
} else {
|
|
|
24974 |
// fullscreen isn't supported so we'll just stretch the video element to
|
|
|
24975 |
// fill the viewport
|
|
|
24976 |
this.enterFullWindow();
|
|
|
24977 |
}
|
|
|
24978 |
}
|
|
|
24979 |
|
|
|
24980 |
/**
|
|
|
24981 |
* Return the video to its normal size after having been in full screen mode
|
|
|
24982 |
*
|
|
|
24983 |
* @fires Player#fullscreenchange
|
|
|
24984 |
*/
|
|
|
24985 |
exitFullscreen() {
|
|
|
24986 |
const self = this;
|
|
|
24987 |
return new Promise((resolve, reject) => {
|
|
|
24988 |
function offHandler() {
|
|
|
24989 |
self.off('fullscreenerror', errorHandler);
|
|
|
24990 |
self.off('fullscreenchange', changeHandler);
|
|
|
24991 |
}
|
|
|
24992 |
function changeHandler() {
|
|
|
24993 |
offHandler();
|
|
|
24994 |
resolve();
|
|
|
24995 |
}
|
|
|
24996 |
function errorHandler(e, err) {
|
|
|
24997 |
offHandler();
|
|
|
24998 |
reject(err);
|
|
|
24999 |
}
|
|
|
25000 |
self.one('fullscreenchange', changeHandler);
|
|
|
25001 |
self.one('fullscreenerror', errorHandler);
|
|
|
25002 |
const promise = self.exitFullscreenHelper_();
|
|
|
25003 |
if (promise) {
|
|
|
25004 |
promise.then(offHandler, offHandler);
|
|
|
25005 |
// map the promise to our resolve/reject methods
|
|
|
25006 |
promise.then(resolve, reject);
|
|
|
25007 |
}
|
|
|
25008 |
});
|
|
|
25009 |
}
|
|
|
25010 |
exitFullscreenHelper_() {
|
|
|
25011 |
if (this.fsApi_.requestFullscreen) {
|
|
|
25012 |
const promise = document[this.fsApi_.exitFullscreen]();
|
|
|
25013 |
|
|
|
25014 |
// Even on browsers with promise support this may not return a promise
|
|
|
25015 |
if (promise) {
|
|
|
25016 |
// we're splitting the promise here, so, we want to catch the
|
|
|
25017 |
// potential error so that this chain doesn't have unhandled errors
|
|
|
25018 |
silencePromise(promise.then(() => this.isFullscreen(false)));
|
|
|
25019 |
}
|
|
|
25020 |
return promise;
|
|
|
25021 |
} else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
|
|
|
25022 |
this.techCall_('exitFullScreen');
|
|
|
25023 |
} else {
|
|
|
25024 |
this.exitFullWindow();
|
|
|
25025 |
}
|
|
|
25026 |
}
|
|
|
25027 |
|
|
|
25028 |
/**
|
|
|
25029 |
* When fullscreen isn't supported we can stretch the
|
|
|
25030 |
* video container to as wide as the browser will let us.
|
|
|
25031 |
*
|
|
|
25032 |
* @fires Player#enterFullWindow
|
|
|
25033 |
*/
|
|
|
25034 |
enterFullWindow() {
|
|
|
25035 |
this.isFullscreen(true);
|
|
|
25036 |
this.isFullWindow = true;
|
|
|
25037 |
|
|
|
25038 |
// Storing original doc overflow value to return to when fullscreen is off
|
|
|
25039 |
this.docOrigOverflow = document.documentElement.style.overflow;
|
|
|
25040 |
|
|
|
25041 |
// Add listener for esc key to exit fullscreen
|
|
|
25042 |
on(document, 'keydown', this.boundFullWindowOnEscKey_);
|
|
|
25043 |
|
|
|
25044 |
// Hide any scroll bars
|
|
|
25045 |
document.documentElement.style.overflow = 'hidden';
|
|
|
25046 |
|
|
|
25047 |
// Apply fullscreen styles
|
|
|
25048 |
addClass(document.body, 'vjs-full-window');
|
|
|
25049 |
|
|
|
25050 |
/**
|
|
|
25051 |
* @event Player#enterFullWindow
|
|
|
25052 |
* @type {Event}
|
|
|
25053 |
*/
|
|
|
25054 |
this.trigger('enterFullWindow');
|
|
|
25055 |
}
|
|
|
25056 |
|
|
|
25057 |
/**
|
|
|
25058 |
* Check for call to either exit full window or
|
|
|
25059 |
* full screen on ESC key
|
|
|
25060 |
*
|
|
|
25061 |
* @param {string} event
|
|
|
25062 |
* Event to check for key press
|
|
|
25063 |
*/
|
|
|
25064 |
fullWindowOnEscKey(event) {
|
|
|
25065 |
if (keycode.isEventKey(event, 'Esc')) {
|
|
|
25066 |
if (this.isFullscreen() === true) {
|
|
|
25067 |
if (!this.isFullWindow) {
|
|
|
25068 |
this.exitFullscreen();
|
|
|
25069 |
} else {
|
|
|
25070 |
this.exitFullWindow();
|
|
|
25071 |
}
|
|
|
25072 |
}
|
|
|
25073 |
}
|
|
|
25074 |
}
|
|
|
25075 |
|
|
|
25076 |
/**
|
|
|
25077 |
* Exit full window
|
|
|
25078 |
*
|
|
|
25079 |
* @fires Player#exitFullWindow
|
|
|
25080 |
*/
|
|
|
25081 |
exitFullWindow() {
|
|
|
25082 |
this.isFullscreen(false);
|
|
|
25083 |
this.isFullWindow = false;
|
|
|
25084 |
off(document, 'keydown', this.boundFullWindowOnEscKey_);
|
|
|
25085 |
|
|
|
25086 |
// Unhide scroll bars.
|
|
|
25087 |
document.documentElement.style.overflow = this.docOrigOverflow;
|
|
|
25088 |
|
|
|
25089 |
// Remove fullscreen styles
|
|
|
25090 |
removeClass(document.body, 'vjs-full-window');
|
|
|
25091 |
|
|
|
25092 |
// Resize the box, controller, and poster to original sizes
|
|
|
25093 |
// this.positionAll();
|
|
|
25094 |
/**
|
|
|
25095 |
* @event Player#exitFullWindow
|
|
|
25096 |
* @type {Event}
|
|
|
25097 |
*/
|
|
|
25098 |
this.trigger('exitFullWindow');
|
|
|
25099 |
}
|
|
|
25100 |
|
|
|
25101 |
/**
|
|
|
25102 |
* Get or set disable Picture-in-Picture mode.
|
|
|
25103 |
*
|
|
|
25104 |
* @param {boolean} [value]
|
|
|
25105 |
* - true will disable Picture-in-Picture mode
|
|
|
25106 |
* - false will enable Picture-in-Picture mode
|
|
|
25107 |
*/
|
|
|
25108 |
disablePictureInPicture(value) {
|
|
|
25109 |
if (value === undefined) {
|
|
|
25110 |
return this.techGet_('disablePictureInPicture');
|
|
|
25111 |
}
|
|
|
25112 |
this.techCall_('setDisablePictureInPicture', value);
|
|
|
25113 |
this.options_.disablePictureInPicture = value;
|
|
|
25114 |
this.trigger('disablepictureinpicturechanged');
|
|
|
25115 |
}
|
|
|
25116 |
|
|
|
25117 |
/**
|
|
|
25118 |
* Check if the player is in Picture-in-Picture mode or tell the player that it
|
|
|
25119 |
* is or is not in Picture-in-Picture mode.
|
|
|
25120 |
*
|
|
|
25121 |
* @param {boolean} [isPiP]
|
|
|
25122 |
* Set the players current Picture-in-Picture state
|
|
|
25123 |
*
|
|
|
25124 |
* @return {boolean|undefined}
|
|
|
25125 |
* - true if Picture-in-Picture is on and getting
|
|
|
25126 |
* - false if Picture-in-Picture is off and getting
|
|
|
25127 |
* - nothing if setting
|
|
|
25128 |
*/
|
|
|
25129 |
isInPictureInPicture(isPiP) {
|
|
|
25130 |
if (isPiP !== undefined) {
|
|
|
25131 |
this.isInPictureInPicture_ = !!isPiP;
|
|
|
25132 |
this.togglePictureInPictureClass_();
|
|
|
25133 |
return;
|
|
|
25134 |
}
|
|
|
25135 |
return !!this.isInPictureInPicture_;
|
|
|
25136 |
}
|
|
|
25137 |
|
|
|
25138 |
/**
|
|
|
25139 |
* Create a floating video window always on top of other windows so that users may
|
|
|
25140 |
* continue consuming media while they interact with other content sites, or
|
|
|
25141 |
* applications on their device.
|
|
|
25142 |
*
|
|
|
25143 |
* This can use document picture-in-picture or element picture in picture
|
|
|
25144 |
*
|
|
|
25145 |
* Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
|
|
|
25146 |
* Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
|
|
|
25147 |
*
|
|
|
25148 |
*
|
|
|
25149 |
* @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
|
|
|
25150 |
* @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
|
|
|
25151 |
*
|
|
|
25152 |
* @fires Player#enterpictureinpicture
|
|
|
25153 |
*
|
|
|
25154 |
* @return {Promise}
|
|
|
25155 |
* A promise with a Picture-in-Picture window.
|
|
|
25156 |
*/
|
|
|
25157 |
requestPictureInPicture() {
|
|
|
25158 |
if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) {
|
|
|
25159 |
const pipContainer = document.createElement(this.el().tagName);
|
|
|
25160 |
pipContainer.classList = this.el().classList;
|
|
|
25161 |
pipContainer.classList.add('vjs-pip-container');
|
|
|
25162 |
if (this.posterImage) {
|
|
|
25163 |
pipContainer.appendChild(this.posterImage.el().cloneNode(true));
|
|
|
25164 |
}
|
|
|
25165 |
if (this.titleBar) {
|
|
|
25166 |
pipContainer.appendChild(this.titleBar.el().cloneNode(true));
|
|
|
25167 |
}
|
|
|
25168 |
pipContainer.appendChild(createEl('p', {
|
|
|
25169 |
className: 'vjs-pip-text'
|
|
|
25170 |
}, {}, this.localize('Playing in picture-in-picture')));
|
|
|
25171 |
return window.documentPictureInPicture.requestWindow({
|
|
|
25172 |
// The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
|
|
|
25173 |
width: this.videoWidth(),
|
|
|
25174 |
height: this.videoHeight()
|
|
|
25175 |
}).then(pipWindow => {
|
|
|
25176 |
copyStyleSheetsToWindow(pipWindow);
|
|
|
25177 |
this.el_.parentNode.insertBefore(pipContainer, this.el_);
|
|
|
25178 |
pipWindow.document.body.appendChild(this.el_);
|
|
|
25179 |
pipWindow.document.body.classList.add('vjs-pip-window');
|
|
|
25180 |
this.player_.isInPictureInPicture(true);
|
|
|
25181 |
this.player_.trigger('enterpictureinpicture');
|
|
|
25182 |
|
|
|
25183 |
// Listen for the PiP closing event to move the video back.
|
|
|
25184 |
pipWindow.addEventListener('pagehide', event => {
|
|
|
25185 |
const pipVideo = event.target.querySelector('.video-js');
|
|
|
25186 |
pipContainer.parentNode.replaceChild(pipVideo, pipContainer);
|
|
|
25187 |
this.player_.isInPictureInPicture(false);
|
|
|
25188 |
this.player_.trigger('leavepictureinpicture');
|
|
|
25189 |
});
|
|
|
25190 |
return pipWindow;
|
|
|
25191 |
});
|
|
|
25192 |
}
|
|
|
25193 |
if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
|
|
|
25194 |
/**
|
|
|
25195 |
* This event fires when the player enters picture in picture mode
|
|
|
25196 |
*
|
|
|
25197 |
* @event Player#enterpictureinpicture
|
|
|
25198 |
* @type {Event}
|
|
|
25199 |
*/
|
|
|
25200 |
return this.techGet_('requestPictureInPicture');
|
|
|
25201 |
}
|
|
|
25202 |
return Promise.reject('No PiP mode is available');
|
|
|
25203 |
}
|
|
|
25204 |
|
|
|
25205 |
/**
|
|
|
25206 |
* Exit Picture-in-Picture mode.
|
|
|
25207 |
*
|
|
|
25208 |
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
|
|
|
25209 |
*
|
|
|
25210 |
* @fires Player#leavepictureinpicture
|
|
|
25211 |
*
|
|
|
25212 |
* @return {Promise}
|
|
|
25213 |
* A promise.
|
|
|
25214 |
*/
|
|
|
25215 |
exitPictureInPicture() {
|
|
|
25216 |
if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
|
|
|
25217 |
// With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler
|
|
|
25218 |
window.documentPictureInPicture.window.close();
|
|
|
25219 |
return Promise.resolve();
|
|
|
25220 |
}
|
|
|
25221 |
if ('pictureInPictureEnabled' in document) {
|
|
|
25222 |
/**
|
|
|
25223 |
* This event fires when the player leaves picture in picture mode
|
|
|
25224 |
*
|
|
|
25225 |
* @event Player#leavepictureinpicture
|
|
|
25226 |
* @type {Event}
|
|
|
25227 |
*/
|
|
|
25228 |
return document.exitPictureInPicture();
|
|
|
25229 |
}
|
|
|
25230 |
}
|
|
|
25231 |
|
|
|
25232 |
/**
|
|
|
25233 |
* Called when this Player has focus and a key gets pressed down, or when
|
|
|
25234 |
* any Component of this player receives a key press that it doesn't handle.
|
|
|
25235 |
* This allows player-wide hotkeys (either as defined below, or optionally
|
|
|
25236 |
* by an external function).
|
|
|
25237 |
*
|
|
|
25238 |
* @param {KeyboardEvent} event
|
|
|
25239 |
* The `keydown` event that caused this function to be called.
|
|
|
25240 |
*
|
|
|
25241 |
* @listens keydown
|
|
|
25242 |
*/
|
|
|
25243 |
handleKeyDown(event) {
|
|
|
25244 |
const {
|
|
|
25245 |
userActions
|
|
|
25246 |
} = this.options_;
|
|
|
25247 |
|
|
|
25248 |
// Bail out if hotkeys are not configured.
|
|
|
25249 |
if (!userActions || !userActions.hotkeys) {
|
|
|
25250 |
return;
|
|
|
25251 |
}
|
|
|
25252 |
|
|
|
25253 |
// Function that determines whether or not to exclude an element from
|
|
|
25254 |
// hotkeys handling.
|
|
|
25255 |
const excludeElement = el => {
|
|
|
25256 |
const tagName = el.tagName.toLowerCase();
|
|
|
25257 |
|
|
|
25258 |
// The first and easiest test is for `contenteditable` elements.
|
|
|
25259 |
if (el.isContentEditable) {
|
|
|
25260 |
return true;
|
|
|
25261 |
}
|
|
|
25262 |
|
|
|
25263 |
// Inputs matching these types will still trigger hotkey handling as
|
|
|
25264 |
// they are not text inputs.
|
|
|
25265 |
const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
|
|
|
25266 |
if (tagName === 'input') {
|
|
|
25267 |
return allowedInputTypes.indexOf(el.type) === -1;
|
|
|
25268 |
}
|
|
|
25269 |
|
|
|
25270 |
// The final test is by tag name. These tags will be excluded entirely.
|
|
|
25271 |
const excludedTags = ['textarea'];
|
|
|
25272 |
return excludedTags.indexOf(tagName) !== -1;
|
|
|
25273 |
};
|
|
|
25274 |
|
|
|
25275 |
// Bail out if the user is focused on an interactive form element.
|
|
|
25276 |
if (excludeElement(this.el_.ownerDocument.activeElement)) {
|
|
|
25277 |
return;
|
|
|
25278 |
}
|
|
|
25279 |
if (typeof userActions.hotkeys === 'function') {
|
|
|
25280 |
userActions.hotkeys.call(this, event);
|
|
|
25281 |
} else {
|
|
|
25282 |
this.handleHotkeys(event);
|
|
|
25283 |
}
|
|
|
25284 |
}
|
|
|
25285 |
|
|
|
25286 |
/**
|
|
|
25287 |
* Called when this Player receives a hotkey keydown event.
|
|
|
25288 |
* Supported player-wide hotkeys are:
|
|
|
25289 |
*
|
|
|
25290 |
* f - toggle fullscreen
|
|
|
25291 |
* m - toggle mute
|
|
|
25292 |
* k or Space - toggle play/pause
|
|
|
25293 |
*
|
|
|
25294 |
* @param {Event} event
|
|
|
25295 |
* The `keydown` event that caused this function to be called.
|
|
|
25296 |
*/
|
|
|
25297 |
handleHotkeys(event) {
|
|
|
25298 |
const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
|
|
|
25299 |
|
|
|
25300 |
// set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
|
|
|
25301 |
const {
|
|
|
25302 |
fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
|
|
|
25303 |
muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
|
|
|
25304 |
playPauseKey = keydownEvent => keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space')
|
|
|
25305 |
} = hotkeys;
|
|
|
25306 |
if (fullscreenKey.call(this, event)) {
|
|
|
25307 |
event.preventDefault();
|
|
|
25308 |
event.stopPropagation();
|
|
|
25309 |
const FSToggle = Component$1.getComponent('FullscreenToggle');
|
|
|
25310 |
if (document[this.fsApi_.fullscreenEnabled] !== false) {
|
|
|
25311 |
FSToggle.prototype.handleClick.call(this, event);
|
|
|
25312 |
}
|
|
|
25313 |
} else if (muteKey.call(this, event)) {
|
|
|
25314 |
event.preventDefault();
|
|
|
25315 |
event.stopPropagation();
|
|
|
25316 |
const MuteToggle = Component$1.getComponent('MuteToggle');
|
|
|
25317 |
MuteToggle.prototype.handleClick.call(this, event);
|
|
|
25318 |
} else if (playPauseKey.call(this, event)) {
|
|
|
25319 |
event.preventDefault();
|
|
|
25320 |
event.stopPropagation();
|
|
|
25321 |
const PlayToggle = Component$1.getComponent('PlayToggle');
|
|
|
25322 |
PlayToggle.prototype.handleClick.call(this, event);
|
|
|
25323 |
}
|
|
|
25324 |
}
|
|
|
25325 |
|
|
|
25326 |
/**
|
|
|
25327 |
* Check whether the player can play a given mimetype
|
|
|
25328 |
*
|
|
|
25329 |
* @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
|
|
|
25330 |
*
|
|
|
25331 |
* @param {string} type
|
|
|
25332 |
* The mimetype to check
|
|
|
25333 |
*
|
|
|
25334 |
* @return {string}
|
|
|
25335 |
* 'probably', 'maybe', or '' (empty string)
|
|
|
25336 |
*/
|
|
|
25337 |
canPlayType(type) {
|
|
|
25338 |
let can;
|
|
|
25339 |
|
|
|
25340 |
// Loop through each playback technology in the options order
|
|
|
25341 |
for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
|
|
|
25342 |
const techName = j[i];
|
|
|
25343 |
let tech = Tech.getTech(techName);
|
|
|
25344 |
|
|
|
25345 |
// Support old behavior of techs being registered as components.
|
|
|
25346 |
// Remove once that deprecated behavior is removed.
|
|
|
25347 |
if (!tech) {
|
|
|
25348 |
tech = Component$1.getComponent(techName);
|
|
|
25349 |
}
|
|
|
25350 |
|
|
|
25351 |
// Check if the current tech is defined before continuing
|
|
|
25352 |
if (!tech) {
|
|
|
25353 |
log$1.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
|
|
|
25354 |
continue;
|
|
|
25355 |
}
|
|
|
25356 |
|
|
|
25357 |
// Check if the browser supports this technology
|
|
|
25358 |
if (tech.isSupported()) {
|
|
|
25359 |
can = tech.canPlayType(type);
|
|
|
25360 |
if (can) {
|
|
|
25361 |
return can;
|
|
|
25362 |
}
|
|
|
25363 |
}
|
|
|
25364 |
}
|
|
|
25365 |
return '';
|
|
|
25366 |
}
|
|
|
25367 |
|
|
|
25368 |
/**
|
|
|
25369 |
* Select source based on tech-order or source-order
|
|
|
25370 |
* Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
|
|
|
25371 |
* defaults to tech-order selection
|
|
|
25372 |
*
|
|
|
25373 |
* @param {Array} sources
|
|
|
25374 |
* The sources for a media asset
|
|
|
25375 |
*
|
|
|
25376 |
* @return {Object|boolean}
|
|
|
25377 |
* Object of source and tech order or false
|
|
|
25378 |
*/
|
|
|
25379 |
selectSource(sources) {
|
|
|
25380 |
// Get only the techs specified in `techOrder` that exist and are supported by the
|
|
|
25381 |
// current platform
|
|
|
25382 |
const techs = this.options_.techOrder.map(techName => {
|
|
|
25383 |
return [techName, Tech.getTech(techName)];
|
|
|
25384 |
}).filter(([techName, tech]) => {
|
|
|
25385 |
// Check if the current tech is defined before continuing
|
|
|
25386 |
if (tech) {
|
|
|
25387 |
// Check if the browser supports this technology
|
|
|
25388 |
return tech.isSupported();
|
|
|
25389 |
}
|
|
|
25390 |
log$1.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
|
|
|
25391 |
return false;
|
|
|
25392 |
});
|
|
|
25393 |
|
|
|
25394 |
// Iterate over each `innerArray` element once per `outerArray` element and execute
|
|
|
25395 |
// `tester` with both. If `tester` returns a non-falsy value, exit early and return
|
|
|
25396 |
// that value.
|
|
|
25397 |
const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
|
|
|
25398 |
let found;
|
|
|
25399 |
outerArray.some(outerChoice => {
|
|
|
25400 |
return innerArray.some(innerChoice => {
|
|
|
25401 |
found = tester(outerChoice, innerChoice);
|
|
|
25402 |
if (found) {
|
|
|
25403 |
return true;
|
|
|
25404 |
}
|
|
|
25405 |
});
|
|
|
25406 |
});
|
|
|
25407 |
return found;
|
|
|
25408 |
};
|
|
|
25409 |
let foundSourceAndTech;
|
|
|
25410 |
const flip = fn => (a, b) => fn(b, a);
|
|
|
25411 |
const finder = ([techName, tech], source) => {
|
|
|
25412 |
if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
|
|
|
25413 |
return {
|
|
|
25414 |
source,
|
|
|
25415 |
tech: techName
|
|
|
25416 |
};
|
|
|
25417 |
}
|
|
|
25418 |
};
|
|
|
25419 |
|
|
|
25420 |
// Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
|
|
|
25421 |
// to select from them based on their priority.
|
|
|
25422 |
if (this.options_.sourceOrder) {
|
|
|
25423 |
// Source-first ordering
|
|
|
25424 |
foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
|
|
|
25425 |
} else {
|
|
|
25426 |
// Tech-first ordering
|
|
|
25427 |
foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
|
|
|
25428 |
}
|
|
|
25429 |
return foundSourceAndTech || false;
|
|
|
25430 |
}
|
|
|
25431 |
|
|
|
25432 |
/**
|
|
|
25433 |
* Executes source setting and getting logic
|
|
|
25434 |
*
|
|
|
25435 |
* @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
|
|
|
25436 |
* A SourceObject, an array of SourceObjects, or a string referencing
|
|
|
25437 |
* a URL to a media source. It is _highly recommended_ that an object
|
|
|
25438 |
* or array of objects is used here, so that source selection
|
|
|
25439 |
* algorithms can take the `type` into account.
|
|
|
25440 |
*
|
|
|
25441 |
* If not provided, this method acts as a getter.
|
|
|
25442 |
* @param {boolean} [isRetry]
|
|
|
25443 |
* Indicates whether this is being called internally as a result of a retry
|
|
|
25444 |
*
|
|
|
25445 |
* @return {string|undefined}
|
|
|
25446 |
* If the `source` argument is missing, returns the current source
|
|
|
25447 |
* URL. Otherwise, returns nothing/undefined.
|
|
|
25448 |
*/
|
|
|
25449 |
handleSrc_(source, isRetry) {
|
|
|
25450 |
// getter usage
|
|
|
25451 |
if (typeof source === 'undefined') {
|
|
|
25452 |
return this.cache_.src || '';
|
|
|
25453 |
}
|
|
|
25454 |
|
|
|
25455 |
// Reset retry behavior for new source
|
|
|
25456 |
if (this.resetRetryOnError_) {
|
|
|
25457 |
this.resetRetryOnError_();
|
|
|
25458 |
}
|
|
|
25459 |
|
|
|
25460 |
// filter out invalid sources and turn our source into
|
|
|
25461 |
// an array of source objects
|
|
|
25462 |
const sources = filterSource(source);
|
|
|
25463 |
|
|
|
25464 |
// if a source was passed in then it is invalid because
|
|
|
25465 |
// it was filtered to a zero length Array. So we have to
|
|
|
25466 |
// show an error
|
|
|
25467 |
if (!sources.length) {
|
|
|
25468 |
this.setTimeout(function () {
|
|
|
25469 |
this.error({
|
|
|
25470 |
code: 4,
|
|
|
25471 |
message: this.options_.notSupportedMessage
|
|
|
25472 |
});
|
|
|
25473 |
}, 0);
|
|
|
25474 |
return;
|
|
|
25475 |
}
|
|
|
25476 |
|
|
|
25477 |
// initial sources
|
|
|
25478 |
this.changingSrc_ = true;
|
|
|
25479 |
|
|
|
25480 |
// Only update the cached source list if we are not retrying a new source after error,
|
|
|
25481 |
// since in that case we want to include the failed source(s) in the cache
|
|
|
25482 |
if (!isRetry) {
|
|
|
25483 |
this.cache_.sources = sources;
|
|
|
25484 |
}
|
|
|
25485 |
this.updateSourceCaches_(sources[0]);
|
|
|
25486 |
|
|
|
25487 |
// middlewareSource is the source after it has been changed by middleware
|
|
|
25488 |
setSource(this, sources[0], (middlewareSource, mws) => {
|
|
|
25489 |
this.middleware_ = mws;
|
|
|
25490 |
|
|
|
25491 |
// since sourceSet is async we have to update the cache again after we select a source since
|
|
|
25492 |
// the source that is selected could be out of order from the cache update above this callback.
|
|
|
25493 |
if (!isRetry) {
|
|
|
25494 |
this.cache_.sources = sources;
|
|
|
25495 |
}
|
|
|
25496 |
this.updateSourceCaches_(middlewareSource);
|
|
|
25497 |
const err = this.src_(middlewareSource);
|
|
|
25498 |
if (err) {
|
|
|
25499 |
if (sources.length > 1) {
|
|
|
25500 |
return this.handleSrc_(sources.slice(1));
|
|
|
25501 |
}
|
|
|
25502 |
this.changingSrc_ = false;
|
|
|
25503 |
|
|
|
25504 |
// We need to wrap this in a timeout to give folks a chance to add error event handlers
|
|
|
25505 |
this.setTimeout(function () {
|
|
|
25506 |
this.error({
|
|
|
25507 |
code: 4,
|
|
|
25508 |
message: this.options_.notSupportedMessage
|
|
|
25509 |
});
|
|
|
25510 |
}, 0);
|
|
|
25511 |
|
|
|
25512 |
// we could not find an appropriate tech, but let's still notify the delegate that this is it
|
|
|
25513 |
// this needs a better comment about why this is needed
|
|
|
25514 |
this.triggerReady();
|
|
|
25515 |
return;
|
|
|
25516 |
}
|
|
|
25517 |
setTech(mws, this.tech_);
|
|
|
25518 |
});
|
|
|
25519 |
|
|
|
25520 |
// Try another available source if this one fails before playback.
|
|
|
25521 |
if (sources.length > 1) {
|
|
|
25522 |
const retry = () => {
|
|
|
25523 |
// Remove the error modal
|
|
|
25524 |
this.error(null);
|
|
|
25525 |
this.handleSrc_(sources.slice(1), true);
|
|
|
25526 |
};
|
|
|
25527 |
const stopListeningForErrors = () => {
|
|
|
25528 |
this.off('error', retry);
|
|
|
25529 |
};
|
|
|
25530 |
this.one('error', retry);
|
|
|
25531 |
this.one('playing', stopListeningForErrors);
|
|
|
25532 |
this.resetRetryOnError_ = () => {
|
|
|
25533 |
this.off('error', retry);
|
|
|
25534 |
this.off('playing', stopListeningForErrors);
|
|
|
25535 |
};
|
|
|
25536 |
}
|
|
|
25537 |
}
|
|
|
25538 |
|
|
|
25539 |
/**
|
|
|
25540 |
* Get or set the video source.
|
|
|
25541 |
*
|
|
|
25542 |
* @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
|
|
|
25543 |
* A SourceObject, an array of SourceObjects, or a string referencing
|
|
|
25544 |
* a URL to a media source. It is _highly recommended_ that an object
|
|
|
25545 |
* or array of objects is used here, so that source selection
|
|
|
25546 |
* algorithms can take the `type` into account.
|
|
|
25547 |
*
|
|
|
25548 |
* If not provided, this method acts as a getter.
|
|
|
25549 |
*
|
|
|
25550 |
* @return {string|undefined}
|
|
|
25551 |
* If the `source` argument is missing, returns the current source
|
|
|
25552 |
* URL. Otherwise, returns nothing/undefined.
|
|
|
25553 |
*/
|
|
|
25554 |
src(source) {
|
|
|
25555 |
return this.handleSrc_(source, false);
|
|
|
25556 |
}
|
|
|
25557 |
|
|
|
25558 |
/**
|
|
|
25559 |
* Set the source object on the tech, returns a boolean that indicates whether
|
|
|
25560 |
* there is a tech that can play the source or not
|
|
|
25561 |
*
|
|
|
25562 |
* @param {Tech~SourceObject} source
|
|
|
25563 |
* The source object to set on the Tech
|
|
|
25564 |
*
|
|
|
25565 |
* @return {boolean}
|
|
|
25566 |
* - True if there is no Tech to playback this source
|
|
|
25567 |
* - False otherwise
|
|
|
25568 |
*
|
|
|
25569 |
* @private
|
|
|
25570 |
*/
|
|
|
25571 |
src_(source) {
|
|
|
25572 |
const sourceTech = this.selectSource([source]);
|
|
|
25573 |
if (!sourceTech) {
|
|
|
25574 |
return true;
|
|
|
25575 |
}
|
|
|
25576 |
if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
|
|
|
25577 |
this.changingSrc_ = true;
|
|
|
25578 |
// load this technology with the chosen source
|
|
|
25579 |
this.loadTech_(sourceTech.tech, sourceTech.source);
|
|
|
25580 |
this.tech_.ready(() => {
|
|
|
25581 |
this.changingSrc_ = false;
|
|
|
25582 |
});
|
|
|
25583 |
return false;
|
|
|
25584 |
}
|
|
|
25585 |
|
|
|
25586 |
// wait until the tech is ready to set the source
|
|
|
25587 |
// and set it synchronously if possible (#2326)
|
|
|
25588 |
this.ready(function () {
|
|
|
25589 |
// The setSource tech method was added with source handlers
|
|
|
25590 |
// so older techs won't support it
|
|
|
25591 |
// We need to check the direct prototype for the case where subclasses
|
|
|
25592 |
// of the tech do not support source handlers
|
|
|
25593 |
if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
|
|
|
25594 |
this.techCall_('setSource', source);
|
|
|
25595 |
} else {
|
|
|
25596 |
this.techCall_('src', source.src);
|
|
|
25597 |
}
|
|
|
25598 |
this.changingSrc_ = false;
|
|
|
25599 |
}, true);
|
|
|
25600 |
return false;
|
|
|
25601 |
}
|
|
|
25602 |
|
|
|
25603 |
/**
|
|
|
25604 |
* Begin loading the src data.
|
|
|
25605 |
*/
|
|
|
25606 |
load() {
|
|
|
25607 |
// Workaround to use the load method with the VHS.
|
|
|
25608 |
// Does not cover the case when the load method is called directly from the mediaElement.
|
|
|
25609 |
if (this.tech_ && this.tech_.vhs) {
|
|
|
25610 |
this.src(this.currentSource());
|
|
|
25611 |
return;
|
|
|
25612 |
}
|
|
|
25613 |
this.techCall_('load');
|
|
|
25614 |
}
|
|
|
25615 |
|
|
|
25616 |
/**
|
|
|
25617 |
* Reset the player. Loads the first tech in the techOrder,
|
|
|
25618 |
* removes all the text tracks in the existing `tech`,
|
|
|
25619 |
* and calls `reset` on the `tech`.
|
|
|
25620 |
*/
|
|
|
25621 |
reset() {
|
|
|
25622 |
if (this.paused()) {
|
|
|
25623 |
this.doReset_();
|
|
|
25624 |
} else {
|
|
|
25625 |
const playPromise = this.play();
|
|
|
25626 |
silencePromise(playPromise.then(() => this.doReset_()));
|
|
|
25627 |
}
|
|
|
25628 |
}
|
|
|
25629 |
doReset_() {
|
|
|
25630 |
if (this.tech_) {
|
|
|
25631 |
this.tech_.clearTracks('text');
|
|
|
25632 |
}
|
|
|
25633 |
this.removeClass('vjs-playing');
|
|
|
25634 |
this.addClass('vjs-paused');
|
|
|
25635 |
this.resetCache_();
|
|
|
25636 |
this.poster('');
|
|
|
25637 |
this.loadTech_(this.options_.techOrder[0], null);
|
|
|
25638 |
this.techCall_('reset');
|
|
|
25639 |
this.resetControlBarUI_();
|
|
|
25640 |
this.error(null);
|
|
|
25641 |
if (this.titleBar) {
|
|
|
25642 |
this.titleBar.update({
|
|
|
25643 |
title: undefined,
|
|
|
25644 |
description: undefined
|
|
|
25645 |
});
|
|
|
25646 |
}
|
|
|
25647 |
if (isEvented(this)) {
|
|
|
25648 |
this.trigger('playerreset');
|
|
|
25649 |
}
|
|
|
25650 |
}
|
|
|
25651 |
|
|
|
25652 |
/**
|
|
|
25653 |
* Reset Control Bar's UI by calling sub-methods that reset
|
|
|
25654 |
* all of Control Bar's components
|
|
|
25655 |
*/
|
|
|
25656 |
resetControlBarUI_() {
|
|
|
25657 |
this.resetProgressBar_();
|
|
|
25658 |
this.resetPlaybackRate_();
|
|
|
25659 |
this.resetVolumeBar_();
|
|
|
25660 |
}
|
|
|
25661 |
|
|
|
25662 |
/**
|
|
|
25663 |
* Reset tech's progress so progress bar is reset in the UI
|
|
|
25664 |
*/
|
|
|
25665 |
resetProgressBar_() {
|
|
|
25666 |
this.currentTime(0);
|
|
|
25667 |
const {
|
|
|
25668 |
currentTimeDisplay,
|
|
|
25669 |
durationDisplay,
|
|
|
25670 |
progressControl,
|
|
|
25671 |
remainingTimeDisplay
|
|
|
25672 |
} = this.controlBar || {};
|
|
|
25673 |
const {
|
|
|
25674 |
seekBar
|
|
|
25675 |
} = progressControl || {};
|
|
|
25676 |
if (currentTimeDisplay) {
|
|
|
25677 |
currentTimeDisplay.updateContent();
|
|
|
25678 |
}
|
|
|
25679 |
if (durationDisplay) {
|
|
|
25680 |
durationDisplay.updateContent();
|
|
|
25681 |
}
|
|
|
25682 |
if (remainingTimeDisplay) {
|
|
|
25683 |
remainingTimeDisplay.updateContent();
|
|
|
25684 |
}
|
|
|
25685 |
if (seekBar) {
|
|
|
25686 |
seekBar.update();
|
|
|
25687 |
if (seekBar.loadProgressBar) {
|
|
|
25688 |
seekBar.loadProgressBar.update();
|
|
|
25689 |
}
|
|
|
25690 |
}
|
|
|
25691 |
}
|
|
|
25692 |
|
|
|
25693 |
/**
|
|
|
25694 |
* Reset Playback ratio
|
|
|
25695 |
*/
|
|
|
25696 |
resetPlaybackRate_() {
|
|
|
25697 |
this.playbackRate(this.defaultPlaybackRate());
|
|
|
25698 |
this.handleTechRateChange_();
|
|
|
25699 |
}
|
|
|
25700 |
|
|
|
25701 |
/**
|
|
|
25702 |
* Reset Volume bar
|
|
|
25703 |
*/
|
|
|
25704 |
resetVolumeBar_() {
|
|
|
25705 |
this.volume(1.0);
|
|
|
25706 |
this.trigger('volumechange');
|
|
|
25707 |
}
|
|
|
25708 |
|
|
|
25709 |
/**
|
|
|
25710 |
* Returns all of the current source objects.
|
|
|
25711 |
*
|
|
|
25712 |
* @return {Tech~SourceObject[]}
|
|
|
25713 |
* The current source objects
|
|
|
25714 |
*/
|
|
|
25715 |
currentSources() {
|
|
|
25716 |
const source = this.currentSource();
|
|
|
25717 |
const sources = [];
|
|
|
25718 |
|
|
|
25719 |
// assume `{}` or `{ src }`
|
|
|
25720 |
if (Object.keys(source).length !== 0) {
|
|
|
25721 |
sources.push(source);
|
|
|
25722 |
}
|
|
|
25723 |
return this.cache_.sources || sources;
|
|
|
25724 |
}
|
|
|
25725 |
|
|
|
25726 |
/**
|
|
|
25727 |
* Returns the current source object.
|
|
|
25728 |
*
|
|
|
25729 |
* @return {Tech~SourceObject}
|
|
|
25730 |
* The current source object
|
|
|
25731 |
*/
|
|
|
25732 |
currentSource() {
|
|
|
25733 |
return this.cache_.source || {};
|
|
|
25734 |
}
|
|
|
25735 |
|
|
|
25736 |
/**
|
|
|
25737 |
* Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
|
|
|
25738 |
* Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
|
|
|
25739 |
*
|
|
|
25740 |
* @return {string}
|
|
|
25741 |
* The current source
|
|
|
25742 |
*/
|
|
|
25743 |
currentSrc() {
|
|
|
25744 |
return this.currentSource() && this.currentSource().src || '';
|
|
|
25745 |
}
|
|
|
25746 |
|
|
|
25747 |
/**
|
|
|
25748 |
* Get the current source type e.g. video/mp4
|
|
|
25749 |
* This can allow you rebuild the current source object so that you could load the same
|
|
|
25750 |
* source and tech later
|
|
|
25751 |
*
|
|
|
25752 |
* @return {string}
|
|
|
25753 |
* The source MIME type
|
|
|
25754 |
*/
|
|
|
25755 |
currentType() {
|
|
|
25756 |
return this.currentSource() && this.currentSource().type || '';
|
|
|
25757 |
}
|
|
|
25758 |
|
|
|
25759 |
/**
|
|
|
25760 |
* Get or set the preload attribute
|
|
|
25761 |
*
|
|
|
25762 |
* @param {'none'|'auto'|'metadata'} [value]
|
|
|
25763 |
* Preload mode to pass to tech
|
|
|
25764 |
*
|
|
|
25765 |
* @return {string|undefined}
|
|
|
25766 |
* - The preload attribute value when getting
|
|
|
25767 |
* - Nothing when setting
|
|
|
25768 |
*/
|
|
|
25769 |
preload(value) {
|
|
|
25770 |
if (value !== undefined) {
|
|
|
25771 |
this.techCall_('setPreload', value);
|
|
|
25772 |
this.options_.preload = value;
|
|
|
25773 |
return;
|
|
|
25774 |
}
|
|
|
25775 |
return this.techGet_('preload');
|
|
|
25776 |
}
|
|
|
25777 |
|
|
|
25778 |
/**
|
|
|
25779 |
* Get or set the autoplay option. When this is a boolean it will
|
|
|
25780 |
* modify the attribute on the tech. When this is a string the attribute on
|
|
|
25781 |
* the tech will be removed and `Player` will handle autoplay on loadstarts.
|
|
|
25782 |
*
|
|
|
25783 |
* @param {boolean|'play'|'muted'|'any'} [value]
|
|
|
25784 |
* - true: autoplay using the browser behavior
|
|
|
25785 |
* - false: do not autoplay
|
|
|
25786 |
* - 'play': call play() on every loadstart
|
|
|
25787 |
* - 'muted': call muted() then play() on every loadstart
|
|
|
25788 |
* - 'any': call play() on every loadstart. if that fails call muted() then play().
|
|
|
25789 |
* - *: values other than those listed here will be set `autoplay` to true
|
|
|
25790 |
*
|
|
|
25791 |
* @return {boolean|string|undefined}
|
|
|
25792 |
* - The current value of autoplay when getting
|
|
|
25793 |
* - Nothing when setting
|
|
|
25794 |
*/
|
|
|
25795 |
autoplay(value) {
|
|
|
25796 |
// getter usage
|
|
|
25797 |
if (value === undefined) {
|
|
|
25798 |
return this.options_.autoplay || false;
|
|
|
25799 |
}
|
|
|
25800 |
let techAutoplay;
|
|
|
25801 |
|
|
|
25802 |
// if the value is a valid string set it to that, or normalize `true` to 'play', if need be
|
|
|
25803 |
if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
|
|
|
25804 |
this.options_.autoplay = value;
|
|
|
25805 |
this.manualAutoplay_(typeof value === 'string' ? value : 'play');
|
|
|
25806 |
techAutoplay = false;
|
|
|
25807 |
|
|
|
25808 |
// any falsy value sets autoplay to false in the browser,
|
|
|
25809 |
// lets do the same
|
|
|
25810 |
} else if (!value) {
|
|
|
25811 |
this.options_.autoplay = false;
|
|
|
25812 |
|
|
|
25813 |
// any other value (ie truthy) sets autoplay to true
|
|
|
25814 |
} else {
|
|
|
25815 |
this.options_.autoplay = true;
|
|
|
25816 |
}
|
|
|
25817 |
techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
|
|
|
25818 |
|
|
|
25819 |
// if we don't have a tech then we do not queue up
|
|
|
25820 |
// a setAutoplay call on tech ready. We do this because the
|
|
|
25821 |
// autoplay option will be passed in the constructor and we
|
|
|
25822 |
// do not need to set it twice
|
|
|
25823 |
if (this.tech_) {
|
|
|
25824 |
this.techCall_('setAutoplay', techAutoplay);
|
|
|
25825 |
}
|
|
|
25826 |
}
|
|
|
25827 |
|
|
|
25828 |
/**
|
|
|
25829 |
* Set or unset the playsinline attribute.
|
|
|
25830 |
* Playsinline tells the browser that non-fullscreen playback is preferred.
|
|
|
25831 |
*
|
|
|
25832 |
* @param {boolean} [value]
|
|
|
25833 |
* - true means that we should try to play inline by default
|
|
|
25834 |
* - false means that we should use the browser's default playback mode,
|
|
|
25835 |
* which in most cases is inline. iOS Safari is a notable exception
|
|
|
25836 |
* and plays fullscreen by default.
|
|
|
25837 |
*
|
|
|
25838 |
* @return {string|undefined}
|
|
|
25839 |
* - the current value of playsinline
|
|
|
25840 |
* - Nothing when setting
|
|
|
25841 |
*
|
|
|
25842 |
* @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
|
|
|
25843 |
*/
|
|
|
25844 |
playsinline(value) {
|
|
|
25845 |
if (value !== undefined) {
|
|
|
25846 |
this.techCall_('setPlaysinline', value);
|
|
|
25847 |
this.options_.playsinline = value;
|
|
|
25848 |
}
|
|
|
25849 |
return this.techGet_('playsinline');
|
|
|
25850 |
}
|
|
|
25851 |
|
|
|
25852 |
/**
|
|
|
25853 |
* Get or set the loop attribute on the video element.
|
|
|
25854 |
*
|
|
|
25855 |
* @param {boolean} [value]
|
|
|
25856 |
* - true means that we should loop the video
|
|
|
25857 |
* - false means that we should not loop the video
|
|
|
25858 |
*
|
|
|
25859 |
* @return {boolean|undefined}
|
|
|
25860 |
* - The current value of loop when getting
|
|
|
25861 |
* - Nothing when setting
|
|
|
25862 |
*/
|
|
|
25863 |
loop(value) {
|
|
|
25864 |
if (value !== undefined) {
|
|
|
25865 |
this.techCall_('setLoop', value);
|
|
|
25866 |
this.options_.loop = value;
|
|
|
25867 |
return;
|
|
|
25868 |
}
|
|
|
25869 |
return this.techGet_('loop');
|
|
|
25870 |
}
|
|
|
25871 |
|
|
|
25872 |
/**
|
|
|
25873 |
* Get or set the poster image source url
|
|
|
25874 |
*
|
|
|
25875 |
* @fires Player#posterchange
|
|
|
25876 |
*
|
|
|
25877 |
* @param {string} [src]
|
|
|
25878 |
* Poster image source URL
|
|
|
25879 |
*
|
|
|
25880 |
* @return {string|undefined}
|
|
|
25881 |
* - The current value of poster when getting
|
|
|
25882 |
* - Nothing when setting
|
|
|
25883 |
*/
|
|
|
25884 |
poster(src) {
|
|
|
25885 |
if (src === undefined) {
|
|
|
25886 |
return this.poster_;
|
|
|
25887 |
}
|
|
|
25888 |
|
|
|
25889 |
// The correct way to remove a poster is to set as an empty string
|
|
|
25890 |
// other falsey values will throw errors
|
|
|
25891 |
if (!src) {
|
|
|
25892 |
src = '';
|
|
|
25893 |
}
|
|
|
25894 |
if (src === this.poster_) {
|
|
|
25895 |
return;
|
|
|
25896 |
}
|
|
|
25897 |
|
|
|
25898 |
// update the internal poster variable
|
|
|
25899 |
this.poster_ = src;
|
|
|
25900 |
|
|
|
25901 |
// update the tech's poster
|
|
|
25902 |
this.techCall_('setPoster', src);
|
|
|
25903 |
this.isPosterFromTech_ = false;
|
|
|
25904 |
|
|
|
25905 |
// alert components that the poster has been set
|
|
|
25906 |
/**
|
|
|
25907 |
* This event fires when the poster image is changed on the player.
|
|
|
25908 |
*
|
|
|
25909 |
* @event Player#posterchange
|
|
|
25910 |
* @type {Event}
|
|
|
25911 |
*/
|
|
|
25912 |
this.trigger('posterchange');
|
|
|
25913 |
}
|
|
|
25914 |
|
|
|
25915 |
/**
|
|
|
25916 |
* Some techs (e.g. YouTube) can provide a poster source in an
|
|
|
25917 |
* asynchronous way. We want the poster component to use this
|
|
|
25918 |
* poster source so that it covers up the tech's controls.
|
|
|
25919 |
* (YouTube's play button). However we only want to use this
|
|
|
25920 |
* source if the player user hasn't set a poster through
|
|
|
25921 |
* the normal APIs.
|
|
|
25922 |
*
|
|
|
25923 |
* @fires Player#posterchange
|
|
|
25924 |
* @listens Tech#posterchange
|
|
|
25925 |
* @private
|
|
|
25926 |
*/
|
|
|
25927 |
handleTechPosterChange_() {
|
|
|
25928 |
if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
|
|
|
25929 |
const newPoster = this.tech_.poster() || '';
|
|
|
25930 |
if (newPoster !== this.poster_) {
|
|
|
25931 |
this.poster_ = newPoster;
|
|
|
25932 |
this.isPosterFromTech_ = true;
|
|
|
25933 |
|
|
|
25934 |
// Let components know the poster has changed
|
|
|
25935 |
this.trigger('posterchange');
|
|
|
25936 |
}
|
|
|
25937 |
}
|
|
|
25938 |
}
|
|
|
25939 |
|
|
|
25940 |
/**
|
|
|
25941 |
* Get or set whether or not the controls are showing.
|
|
|
25942 |
*
|
|
|
25943 |
* @fires Player#controlsenabled
|
|
|
25944 |
*
|
|
|
25945 |
* @param {boolean} [bool]
|
|
|
25946 |
* - true to turn controls on
|
|
|
25947 |
* - false to turn controls off
|
|
|
25948 |
*
|
|
|
25949 |
* @return {boolean|undefined}
|
|
|
25950 |
* - The current value of controls when getting
|
|
|
25951 |
* - Nothing when setting
|
|
|
25952 |
*/
|
|
|
25953 |
controls(bool) {
|
|
|
25954 |
if (bool === undefined) {
|
|
|
25955 |
return !!this.controls_;
|
|
|
25956 |
}
|
|
|
25957 |
bool = !!bool;
|
|
|
25958 |
|
|
|
25959 |
// Don't trigger a change event unless it actually changed
|
|
|
25960 |
if (this.controls_ === bool) {
|
|
|
25961 |
return;
|
|
|
25962 |
}
|
|
|
25963 |
this.controls_ = bool;
|
|
|
25964 |
if (this.usingNativeControls()) {
|
|
|
25965 |
this.techCall_('setControls', bool);
|
|
|
25966 |
}
|
|
|
25967 |
if (this.controls_) {
|
|
|
25968 |
this.removeClass('vjs-controls-disabled');
|
|
|
25969 |
this.addClass('vjs-controls-enabled');
|
|
|
25970 |
/**
|
|
|
25971 |
* @event Player#controlsenabled
|
|
|
25972 |
* @type {Event}
|
|
|
25973 |
*/
|
|
|
25974 |
this.trigger('controlsenabled');
|
|
|
25975 |
if (!this.usingNativeControls()) {
|
|
|
25976 |
this.addTechControlsListeners_();
|
|
|
25977 |
}
|
|
|
25978 |
} else {
|
|
|
25979 |
this.removeClass('vjs-controls-enabled');
|
|
|
25980 |
this.addClass('vjs-controls-disabled');
|
|
|
25981 |
/**
|
|
|
25982 |
* @event Player#controlsdisabled
|
|
|
25983 |
* @type {Event}
|
|
|
25984 |
*/
|
|
|
25985 |
this.trigger('controlsdisabled');
|
|
|
25986 |
if (!this.usingNativeControls()) {
|
|
|
25987 |
this.removeTechControlsListeners_();
|
|
|
25988 |
}
|
|
|
25989 |
}
|
|
|
25990 |
}
|
|
|
25991 |
|
|
|
25992 |
/**
|
|
|
25993 |
* Toggle native controls on/off. Native controls are the controls built into
|
|
|
25994 |
* devices (e.g. default iPhone controls) or other techs
|
|
|
25995 |
* (e.g. Vimeo Controls)
|
|
|
25996 |
* **This should only be set by the current tech, because only the tech knows
|
|
|
25997 |
* if it can support native controls**
|
|
|
25998 |
*
|
|
|
25999 |
* @fires Player#usingnativecontrols
|
|
|
26000 |
* @fires Player#usingcustomcontrols
|
|
|
26001 |
*
|
|
|
26002 |
* @param {boolean} [bool]
|
|
|
26003 |
* - true to turn native controls on
|
|
|
26004 |
* - false to turn native controls off
|
|
|
26005 |
*
|
|
|
26006 |
* @return {boolean|undefined}
|
|
|
26007 |
* - The current value of native controls when getting
|
|
|
26008 |
* - Nothing when setting
|
|
|
26009 |
*/
|
|
|
26010 |
usingNativeControls(bool) {
|
|
|
26011 |
if (bool === undefined) {
|
|
|
26012 |
return !!this.usingNativeControls_;
|
|
|
26013 |
}
|
|
|
26014 |
bool = !!bool;
|
|
|
26015 |
|
|
|
26016 |
// Don't trigger a change event unless it actually changed
|
|
|
26017 |
if (this.usingNativeControls_ === bool) {
|
|
|
26018 |
return;
|
|
|
26019 |
}
|
|
|
26020 |
this.usingNativeControls_ = bool;
|
|
|
26021 |
if (this.usingNativeControls_) {
|
|
|
26022 |
this.addClass('vjs-using-native-controls');
|
|
|
26023 |
|
|
|
26024 |
/**
|
|
|
26025 |
* player is using the native device controls
|
|
|
26026 |
*
|
|
|
26027 |
* @event Player#usingnativecontrols
|
|
|
26028 |
* @type {Event}
|
|
|
26029 |
*/
|
|
|
26030 |
this.trigger('usingnativecontrols');
|
|
|
26031 |
} else {
|
|
|
26032 |
this.removeClass('vjs-using-native-controls');
|
|
|
26033 |
|
|
|
26034 |
/**
|
|
|
26035 |
* player is using the custom HTML controls
|
|
|
26036 |
*
|
|
|
26037 |
* @event Player#usingcustomcontrols
|
|
|
26038 |
* @type {Event}
|
|
|
26039 |
*/
|
|
|
26040 |
this.trigger('usingcustomcontrols');
|
|
|
26041 |
}
|
|
|
26042 |
}
|
|
|
26043 |
|
|
|
26044 |
/**
|
|
|
26045 |
* Set or get the current MediaError
|
|
|
26046 |
*
|
|
|
26047 |
* @fires Player#error
|
|
|
26048 |
*
|
|
|
26049 |
* @param {MediaError|string|number} [err]
|
|
|
26050 |
* A MediaError or a string/number to be turned
|
|
|
26051 |
* into a MediaError
|
|
|
26052 |
*
|
|
|
26053 |
* @return {MediaError|null|undefined}
|
|
|
26054 |
* - The current MediaError when getting (or null)
|
|
|
26055 |
* - Nothing when setting
|
|
|
26056 |
*/
|
|
|
26057 |
error(err) {
|
|
|
26058 |
if (err === undefined) {
|
|
|
26059 |
return this.error_ || null;
|
|
|
26060 |
}
|
|
|
26061 |
|
|
|
26062 |
// allow hooks to modify error object
|
|
|
26063 |
hooks('beforeerror').forEach(hookFunction => {
|
|
|
26064 |
const newErr = hookFunction(this, err);
|
|
|
26065 |
if (!(isObject$1(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
|
|
|
26066 |
this.log.error('please return a value that MediaError expects in beforeerror hooks');
|
|
|
26067 |
return;
|
|
|
26068 |
}
|
|
|
26069 |
err = newErr;
|
|
|
26070 |
});
|
|
|
26071 |
|
|
|
26072 |
// Suppress the first error message for no compatible source until
|
|
|
26073 |
// user interaction
|
|
|
26074 |
if (this.options_.suppressNotSupportedError && err && err.code === 4) {
|
|
|
26075 |
const triggerSuppressedError = function () {
|
|
|
26076 |
this.error(err);
|
|
|
26077 |
};
|
|
|
26078 |
this.options_.suppressNotSupportedError = false;
|
|
|
26079 |
this.any(['click', 'touchstart'], triggerSuppressedError);
|
|
|
26080 |
this.one('loadstart', function () {
|
|
|
26081 |
this.off(['click', 'touchstart'], triggerSuppressedError);
|
|
|
26082 |
});
|
|
|
26083 |
return;
|
|
|
26084 |
}
|
|
|
26085 |
|
|
|
26086 |
// restoring to default
|
|
|
26087 |
if (err === null) {
|
|
|
26088 |
this.error_ = null;
|
|
|
26089 |
this.removeClass('vjs-error');
|
|
|
26090 |
if (this.errorDisplay) {
|
|
|
26091 |
this.errorDisplay.close();
|
|
|
26092 |
}
|
|
|
26093 |
return;
|
|
|
26094 |
}
|
|
|
26095 |
this.error_ = new MediaError(err);
|
|
|
26096 |
|
|
|
26097 |
// add the vjs-error classname to the player
|
|
|
26098 |
this.addClass('vjs-error');
|
|
|
26099 |
|
|
|
26100 |
// log the name of the error type and any message
|
|
|
26101 |
// IE11 logs "[object object]" and required you to expand message to see error object
|
|
|
26102 |
log$1.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
|
|
|
26103 |
|
|
|
26104 |
/**
|
|
|
26105 |
* @event Player#error
|
|
|
26106 |
* @type {Event}
|
|
|
26107 |
*/
|
|
|
26108 |
this.trigger('error');
|
|
|
26109 |
|
|
|
26110 |
// notify hooks of the per player error
|
|
|
26111 |
hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
|
|
|
26112 |
return;
|
|
|
26113 |
}
|
|
|
26114 |
|
|
|
26115 |
/**
|
|
|
26116 |
* Report user activity
|
|
|
26117 |
*
|
|
|
26118 |
* @param {Object} event
|
|
|
26119 |
* Event object
|
|
|
26120 |
*/
|
|
|
26121 |
reportUserActivity(event) {
|
|
|
26122 |
this.userActivity_ = true;
|
|
|
26123 |
}
|
|
|
26124 |
|
|
|
26125 |
/**
|
|
|
26126 |
* Get/set if user is active
|
|
|
26127 |
*
|
|
|
26128 |
* @fires Player#useractive
|
|
|
26129 |
* @fires Player#userinactive
|
|
|
26130 |
*
|
|
|
26131 |
* @param {boolean} [bool]
|
|
|
26132 |
* - true if the user is active
|
|
|
26133 |
* - false if the user is inactive
|
|
|
26134 |
*
|
|
|
26135 |
* @return {boolean|undefined}
|
|
|
26136 |
* - The current value of userActive when getting
|
|
|
26137 |
* - Nothing when setting
|
|
|
26138 |
*/
|
|
|
26139 |
userActive(bool) {
|
|
|
26140 |
if (bool === undefined) {
|
|
|
26141 |
return this.userActive_;
|
|
|
26142 |
}
|
|
|
26143 |
bool = !!bool;
|
|
|
26144 |
if (bool === this.userActive_) {
|
|
|
26145 |
return;
|
|
|
26146 |
}
|
|
|
26147 |
this.userActive_ = bool;
|
|
|
26148 |
if (this.userActive_) {
|
|
|
26149 |
this.userActivity_ = true;
|
|
|
26150 |
this.removeClass('vjs-user-inactive');
|
|
|
26151 |
this.addClass('vjs-user-active');
|
|
|
26152 |
/**
|
|
|
26153 |
* @event Player#useractive
|
|
|
26154 |
* @type {Event}
|
|
|
26155 |
*/
|
|
|
26156 |
this.trigger('useractive');
|
|
|
26157 |
return;
|
|
|
26158 |
}
|
|
|
26159 |
|
|
|
26160 |
// Chrome/Safari/IE have bugs where when you change the cursor it can
|
|
|
26161 |
// trigger a mousemove event. This causes an issue when you're hiding
|
|
|
26162 |
// the cursor when the user is inactive, and a mousemove signals user
|
|
|
26163 |
// activity. Making it impossible to go into inactive mode. Specifically
|
|
|
26164 |
// this happens in fullscreen when we really need to hide the cursor.
|
|
|
26165 |
//
|
|
|
26166 |
// When this gets resolved in ALL browsers it can be removed
|
|
|
26167 |
// https://code.google.com/p/chromium/issues/detail?id=103041
|
|
|
26168 |
if (this.tech_) {
|
|
|
26169 |
this.tech_.one('mousemove', function (e) {
|
|
|
26170 |
e.stopPropagation();
|
|
|
26171 |
e.preventDefault();
|
|
|
26172 |
});
|
|
|
26173 |
}
|
|
|
26174 |
this.userActivity_ = false;
|
|
|
26175 |
this.removeClass('vjs-user-active');
|
|
|
26176 |
this.addClass('vjs-user-inactive');
|
|
|
26177 |
/**
|
|
|
26178 |
* @event Player#userinactive
|
|
|
26179 |
* @type {Event}
|
|
|
26180 |
*/
|
|
|
26181 |
this.trigger('userinactive');
|
|
|
26182 |
}
|
|
|
26183 |
|
|
|
26184 |
/**
|
|
|
26185 |
* Listen for user activity based on timeout value
|
|
|
26186 |
*
|
|
|
26187 |
* @private
|
|
|
26188 |
*/
|
|
|
26189 |
listenForUserActivity_() {
|
|
|
26190 |
let mouseInProgress;
|
|
|
26191 |
let lastMoveX;
|
|
|
26192 |
let lastMoveY;
|
|
|
26193 |
const handleActivity = bind_(this, this.reportUserActivity);
|
|
|
26194 |
const handleMouseMove = function (e) {
|
|
|
26195 |
// #1068 - Prevent mousemove spamming
|
|
|
26196 |
// Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
|
|
|
26197 |
if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
|
|
|
26198 |
lastMoveX = e.screenX;
|
|
|
26199 |
lastMoveY = e.screenY;
|
|
|
26200 |
handleActivity();
|
|
|
26201 |
}
|
|
|
26202 |
};
|
|
|
26203 |
const handleMouseDown = function () {
|
|
|
26204 |
handleActivity();
|
|
|
26205 |
// For as long as the they are touching the device or have their mouse down,
|
|
|
26206 |
// we consider them active even if they're not moving their finger or mouse.
|
|
|
26207 |
// So we want to continue to update that they are active
|
|
|
26208 |
this.clearInterval(mouseInProgress);
|
|
|
26209 |
// Setting userActivity=true now and setting the interval to the same time
|
|
|
26210 |
// as the activityCheck interval (250) should ensure we never miss the
|
|
|
26211 |
// next activityCheck
|
|
|
26212 |
mouseInProgress = this.setInterval(handleActivity, 250);
|
|
|
26213 |
};
|
|
|
26214 |
const handleMouseUpAndMouseLeave = function (event) {
|
|
|
26215 |
handleActivity();
|
|
|
26216 |
// Stop the interval that maintains activity if the mouse/touch is down
|
|
|
26217 |
this.clearInterval(mouseInProgress);
|
|
|
26218 |
};
|
|
|
26219 |
|
|
|
26220 |
// Any mouse movement will be considered user activity
|
|
|
26221 |
this.on('mousedown', handleMouseDown);
|
|
|
26222 |
this.on('mousemove', handleMouseMove);
|
|
|
26223 |
this.on('mouseup', handleMouseUpAndMouseLeave);
|
|
|
26224 |
this.on('mouseleave', handleMouseUpAndMouseLeave);
|
|
|
26225 |
const controlBar = this.getChild('controlBar');
|
|
|
26226 |
|
|
|
26227 |
// Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
|
|
|
26228 |
// controlBar would no longer be hidden by default timeout.
|
|
|
26229 |
if (controlBar && !IS_IOS && !IS_ANDROID) {
|
|
|
26230 |
controlBar.on('mouseenter', function (event) {
|
|
|
26231 |
if (this.player().options_.inactivityTimeout !== 0) {
|
|
|
26232 |
this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
|
|
|
26233 |
}
|
|
|
26234 |
this.player().options_.inactivityTimeout = 0;
|
|
|
26235 |
});
|
|
|
26236 |
controlBar.on('mouseleave', function (event) {
|
|
|
26237 |
this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
|
|
|
26238 |
});
|
|
|
26239 |
}
|
|
|
26240 |
|
|
|
26241 |
// Listen for keyboard navigation
|
|
|
26242 |
// Shouldn't need to use inProgress interval because of key repeat
|
|
|
26243 |
this.on('keydown', handleActivity);
|
|
|
26244 |
this.on('keyup', handleActivity);
|
|
|
26245 |
|
|
|
26246 |
// Run an interval every 250 milliseconds instead of stuffing everything into
|
|
|
26247 |
// the mousemove/touchmove function itself, to prevent performance degradation.
|
|
|
26248 |
// `this.reportUserActivity` simply sets this.userActivity_ to true, which
|
|
|
26249 |
// then gets picked up by this loop
|
|
|
26250 |
// http://ejohn.org/blog/learning-from-twitter/
|
|
|
26251 |
let inactivityTimeout;
|
|
|
26252 |
|
|
|
26253 |
/** @this Player */
|
|
|
26254 |
const activityCheck = function () {
|
|
|
26255 |
// Check to see if mouse/touch activity has happened
|
|
|
26256 |
if (!this.userActivity_) {
|
|
|
26257 |
return;
|
|
|
26258 |
}
|
|
|
26259 |
|
|
|
26260 |
// Reset the activity tracker
|
|
|
26261 |
this.userActivity_ = false;
|
|
|
26262 |
|
|
|
26263 |
// If the user state was inactive, set the state to active
|
|
|
26264 |
this.userActive(true);
|
|
|
26265 |
|
|
|
26266 |
// Clear any existing inactivity timeout to start the timer over
|
|
|
26267 |
this.clearTimeout(inactivityTimeout);
|
|
|
26268 |
const timeout = this.options_.inactivityTimeout;
|
|
|
26269 |
if (timeout <= 0) {
|
|
|
26270 |
return;
|
|
|
26271 |
}
|
|
|
26272 |
|
|
|
26273 |
// In <timeout> milliseconds, if no more activity has occurred the
|
|
|
26274 |
// user will be considered inactive
|
|
|
26275 |
inactivityTimeout = this.setTimeout(function () {
|
|
|
26276 |
// Protect against the case where the inactivityTimeout can trigger just
|
|
|
26277 |
// before the next user activity is picked up by the activity check loop
|
|
|
26278 |
// causing a flicker
|
|
|
26279 |
if (!this.userActivity_) {
|
|
|
26280 |
this.userActive(false);
|
|
|
26281 |
}
|
|
|
26282 |
}, timeout);
|
|
|
26283 |
};
|
|
|
26284 |
this.setInterval(activityCheck, 250);
|
|
|
26285 |
}
|
|
|
26286 |
|
|
|
26287 |
/**
|
|
|
26288 |
* Gets or sets the current playback rate. A playback rate of
|
|
|
26289 |
* 1.0 represents normal speed and 0.5 would indicate half-speed
|
|
|
26290 |
* playback, for instance.
|
|
|
26291 |
*
|
|
|
26292 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
|
|
|
26293 |
*
|
|
|
26294 |
* @param {number} [rate]
|
|
|
26295 |
* New playback rate to set.
|
|
|
26296 |
*
|
|
|
26297 |
* @return {number|undefined}
|
|
|
26298 |
* - The current playback rate when getting or 1.0
|
|
|
26299 |
* - Nothing when setting
|
|
|
26300 |
*/
|
|
|
26301 |
playbackRate(rate) {
|
|
|
26302 |
if (rate !== undefined) {
|
|
|
26303 |
// NOTE: this.cache_.lastPlaybackRate is set from the tech handler
|
|
|
26304 |
// that is registered above
|
|
|
26305 |
this.techCall_('setPlaybackRate', rate);
|
|
|
26306 |
return;
|
|
|
26307 |
}
|
|
|
26308 |
if (this.tech_ && this.tech_.featuresPlaybackRate) {
|
|
|
26309 |
return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
|
|
|
26310 |
}
|
|
|
26311 |
return 1.0;
|
|
|
26312 |
}
|
|
|
26313 |
|
|
|
26314 |
/**
|
|
|
26315 |
* Gets or sets the current default playback rate. A default playback rate of
|
|
|
26316 |
* 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
|
|
|
26317 |
* defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
|
|
|
26318 |
* not the current playbackRate.
|
|
|
26319 |
*
|
|
|
26320 |
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
|
|
|
26321 |
*
|
|
|
26322 |
* @param {number} [rate]
|
|
|
26323 |
* New default playback rate to set.
|
|
|
26324 |
*
|
|
|
26325 |
* @return {number|undefined}
|
|
|
26326 |
* - The default playback rate when getting or 1.0
|
|
|
26327 |
* - Nothing when setting
|
|
|
26328 |
*/
|
|
|
26329 |
defaultPlaybackRate(rate) {
|
|
|
26330 |
if (rate !== undefined) {
|
|
|
26331 |
return this.techCall_('setDefaultPlaybackRate', rate);
|
|
|
26332 |
}
|
|
|
26333 |
if (this.tech_ && this.tech_.featuresPlaybackRate) {
|
|
|
26334 |
return this.techGet_('defaultPlaybackRate');
|
|
|
26335 |
}
|
|
|
26336 |
return 1.0;
|
|
|
26337 |
}
|
|
|
26338 |
|
|
|
26339 |
/**
|
|
|
26340 |
* Gets or sets the audio flag
|
|
|
26341 |
*
|
|
|
26342 |
* @param {boolean} [bool]
|
|
|
26343 |
* - true signals that this is an audio player
|
|
|
26344 |
* - false signals that this is not an audio player
|
|
|
26345 |
*
|
|
|
26346 |
* @return {boolean|undefined}
|
|
|
26347 |
* - The current value of isAudio when getting
|
|
|
26348 |
* - Nothing when setting
|
|
|
26349 |
*/
|
|
|
26350 |
isAudio(bool) {
|
|
|
26351 |
if (bool !== undefined) {
|
|
|
26352 |
this.isAudio_ = !!bool;
|
|
|
26353 |
return;
|
|
|
26354 |
}
|
|
|
26355 |
return !!this.isAudio_;
|
|
|
26356 |
}
|
|
|
26357 |
enableAudioOnlyUI_() {
|
|
|
26358 |
// Update styling immediately to show the control bar so we can get its height
|
|
|
26359 |
this.addClass('vjs-audio-only-mode');
|
|
|
26360 |
const playerChildren = this.children();
|
|
|
26361 |
const controlBar = this.getChild('ControlBar');
|
|
|
26362 |
const controlBarHeight = controlBar && controlBar.currentHeight();
|
|
|
26363 |
|
|
|
26364 |
// Hide all player components except the control bar. Control bar components
|
|
|
26365 |
// needed only for video are hidden with CSS
|
|
|
26366 |
playerChildren.forEach(child => {
|
|
|
26367 |
if (child === controlBar) {
|
|
|
26368 |
return;
|
|
|
26369 |
}
|
|
|
26370 |
if (child.el_ && !child.hasClass('vjs-hidden')) {
|
|
|
26371 |
child.hide();
|
|
|
26372 |
this.audioOnlyCache_.hiddenChildren.push(child);
|
|
|
26373 |
}
|
|
|
26374 |
});
|
|
|
26375 |
this.audioOnlyCache_.playerHeight = this.currentHeight();
|
|
|
26376 |
|
|
|
26377 |
// Set the player height the same as the control bar
|
|
|
26378 |
this.height(controlBarHeight);
|
|
|
26379 |
this.trigger('audioonlymodechange');
|
|
|
26380 |
}
|
|
|
26381 |
disableAudioOnlyUI_() {
|
|
|
26382 |
this.removeClass('vjs-audio-only-mode');
|
|
|
26383 |
|
|
|
26384 |
// Show player components that were previously hidden
|
|
|
26385 |
this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
|
|
|
26386 |
|
|
|
26387 |
// Reset player height
|
|
|
26388 |
this.height(this.audioOnlyCache_.playerHeight);
|
|
|
26389 |
this.trigger('audioonlymodechange');
|
|
|
26390 |
}
|
|
|
26391 |
|
|
|
26392 |
/**
|
|
|
26393 |
* Get the current audioOnlyMode state or set audioOnlyMode to true or false.
|
|
|
26394 |
*
|
|
|
26395 |
* Setting this to `true` will hide all player components except the control bar,
|
|
|
26396 |
* as well as control bar components needed only for video.
|
|
|
26397 |
*
|
|
|
26398 |
* @param {boolean} [value]
|
|
|
26399 |
* The value to set audioOnlyMode to.
|
|
|
26400 |
*
|
|
|
26401 |
* @return {Promise|boolean}
|
|
|
26402 |
* A Promise is returned when setting the state, and a boolean when getting
|
|
|
26403 |
* the present state
|
|
|
26404 |
*/
|
|
|
26405 |
audioOnlyMode(value) {
|
|
|
26406 |
if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
|
|
|
26407 |
return this.audioOnlyMode_;
|
|
|
26408 |
}
|
|
|
26409 |
this.audioOnlyMode_ = value;
|
|
|
26410 |
|
|
|
26411 |
// Enable Audio Only Mode
|
|
|
26412 |
if (value) {
|
|
|
26413 |
const exitPromises = [];
|
|
|
26414 |
|
|
|
26415 |
// Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
|
|
|
26416 |
if (this.isInPictureInPicture()) {
|
|
|
26417 |
exitPromises.push(this.exitPictureInPicture());
|
|
|
26418 |
}
|
|
|
26419 |
if (this.isFullscreen()) {
|
|
|
26420 |
exitPromises.push(this.exitFullscreen());
|
|
|
26421 |
}
|
|
|
26422 |
if (this.audioPosterMode()) {
|
|
|
26423 |
exitPromises.push(this.audioPosterMode(false));
|
|
|
26424 |
}
|
|
|
26425 |
return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
|
|
|
26426 |
}
|
|
|
26427 |
|
|
|
26428 |
// Disable Audio Only Mode
|
|
|
26429 |
return Promise.resolve().then(() => this.disableAudioOnlyUI_());
|
|
|
26430 |
}
|
|
|
26431 |
enablePosterModeUI_() {
|
|
|
26432 |
// Hide the video element and show the poster image to enable posterModeUI
|
|
|
26433 |
const tech = this.tech_ && this.tech_;
|
|
|
26434 |
tech.hide();
|
|
|
26435 |
this.addClass('vjs-audio-poster-mode');
|
|
|
26436 |
this.trigger('audiopostermodechange');
|
|
|
26437 |
}
|
|
|
26438 |
disablePosterModeUI_() {
|
|
|
26439 |
// Show the video element and hide the poster image to disable posterModeUI
|
|
|
26440 |
const tech = this.tech_ && this.tech_;
|
|
|
26441 |
tech.show();
|
|
|
26442 |
this.removeClass('vjs-audio-poster-mode');
|
|
|
26443 |
this.trigger('audiopostermodechange');
|
|
|
26444 |
}
|
|
|
26445 |
|
|
|
26446 |
/**
|
|
|
26447 |
* Get the current audioPosterMode state or set audioPosterMode to true or false
|
|
|
26448 |
*
|
|
|
26449 |
* @param {boolean} [value]
|
|
|
26450 |
* The value to set audioPosterMode to.
|
|
|
26451 |
*
|
|
|
26452 |
* @return {Promise|boolean}
|
|
|
26453 |
* A Promise is returned when setting the state, and a boolean when getting
|
|
|
26454 |
* the present state
|
|
|
26455 |
*/
|
|
|
26456 |
audioPosterMode(value) {
|
|
|
26457 |
if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
|
|
|
26458 |
return this.audioPosterMode_;
|
|
|
26459 |
}
|
|
|
26460 |
this.audioPosterMode_ = value;
|
|
|
26461 |
if (value) {
|
|
|
26462 |
if (this.audioOnlyMode()) {
|
|
|
26463 |
const audioOnlyModePromise = this.audioOnlyMode(false);
|
|
|
26464 |
return audioOnlyModePromise.then(() => {
|
|
|
26465 |
// enable audio poster mode after audio only mode is disabled
|
|
|
26466 |
this.enablePosterModeUI_();
|
|
|
26467 |
});
|
|
|
26468 |
}
|
|
|
26469 |
return Promise.resolve().then(() => {
|
|
|
26470 |
// enable audio poster mode
|
|
|
26471 |
this.enablePosterModeUI_();
|
|
|
26472 |
});
|
|
|
26473 |
}
|
|
|
26474 |
return Promise.resolve().then(() => {
|
|
|
26475 |
// disable audio poster mode
|
|
|
26476 |
this.disablePosterModeUI_();
|
|
|
26477 |
});
|
|
|
26478 |
}
|
|
|
26479 |
|
|
|
26480 |
/**
|
|
|
26481 |
* A helper method for adding a {@link TextTrack} to our
|
|
|
26482 |
* {@link TextTrackList}.
|
|
|
26483 |
*
|
|
|
26484 |
* In addition to the W3C settings we allow adding additional info through options.
|
|
|
26485 |
*
|
|
|
26486 |
* @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
|
|
|
26487 |
*
|
|
|
26488 |
* @param {string} [kind]
|
|
|
26489 |
* the kind of TextTrack you are adding
|
|
|
26490 |
*
|
|
|
26491 |
* @param {string} [label]
|
|
|
26492 |
* the label to give the TextTrack label
|
|
|
26493 |
*
|
|
|
26494 |
* @param {string} [language]
|
|
|
26495 |
* the language to set on the TextTrack
|
|
|
26496 |
*
|
|
|
26497 |
* @return {TextTrack|undefined}
|
|
|
26498 |
* the TextTrack that was added or undefined
|
|
|
26499 |
* if there is no tech
|
|
|
26500 |
*/
|
|
|
26501 |
addTextTrack(kind, label, language) {
|
|
|
26502 |
if (this.tech_) {
|
|
|
26503 |
return this.tech_.addTextTrack(kind, label, language);
|
|
|
26504 |
}
|
|
|
26505 |
}
|
|
|
26506 |
|
|
|
26507 |
/**
|
|
|
26508 |
* Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
|
|
|
26509 |
*
|
|
|
26510 |
* @param {Object} options
|
|
|
26511 |
* Options to pass to {@link HTMLTrackElement} during creation. See
|
|
|
26512 |
* {@link HTMLTrackElement} for object properties that you should use.
|
|
|
26513 |
*
|
|
|
26514 |
* @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
|
|
|
26515 |
* from the TextTrackList and HtmlTrackElementList
|
|
|
26516 |
* after a source change
|
|
|
26517 |
*
|
|
|
26518 |
* @return { import('./tracks/html-track-element').default }
|
|
|
26519 |
* the HTMLTrackElement that was created and added
|
|
|
26520 |
* to the HtmlTrackElementList and the remote
|
|
|
26521 |
* TextTrackList
|
|
|
26522 |
*
|
|
|
26523 |
*/
|
|
|
26524 |
addRemoteTextTrack(options, manualCleanup) {
|
|
|
26525 |
if (this.tech_) {
|
|
|
26526 |
return this.tech_.addRemoteTextTrack(options, manualCleanup);
|
|
|
26527 |
}
|
|
|
26528 |
}
|
|
|
26529 |
|
|
|
26530 |
/**
|
|
|
26531 |
* Remove a remote {@link TextTrack} from the respective
|
|
|
26532 |
* {@link TextTrackList} and {@link HtmlTrackElementList}.
|
|
|
26533 |
*
|
|
|
26534 |
* @param {Object} track
|
|
|
26535 |
* Remote {@link TextTrack} to remove
|
|
|
26536 |
*
|
|
|
26537 |
* @return {undefined}
|
|
|
26538 |
* does not return anything
|
|
|
26539 |
*/
|
|
|
26540 |
removeRemoteTextTrack(obj = {}) {
|
|
|
26541 |
let {
|
|
|
26542 |
track
|
|
|
26543 |
} = obj;
|
|
|
26544 |
if (!track) {
|
|
|
26545 |
track = obj;
|
|
|
26546 |
}
|
|
|
26547 |
|
|
|
26548 |
// destructure the input into an object with a track argument, defaulting to arguments[0]
|
|
|
26549 |
// default the whole argument to an empty object if nothing was passed in
|
|
|
26550 |
|
|
|
26551 |
if (this.tech_) {
|
|
|
26552 |
return this.tech_.removeRemoteTextTrack(track);
|
|
|
26553 |
}
|
|
|
26554 |
}
|
|
|
26555 |
|
|
|
26556 |
/**
|
|
|
26557 |
* Gets available media playback quality metrics as specified by the W3C's Media
|
|
|
26558 |
* Playback Quality API.
|
|
|
26559 |
*
|
|
|
26560 |
* @see [Spec]{@link https://wicg.github.io/media-playback-quality}
|
|
|
26561 |
*
|
|
|
26562 |
* @return {Object|undefined}
|
|
|
26563 |
* An object with supported media playback quality metrics or undefined if there
|
|
|
26564 |
* is no tech or the tech does not support it.
|
|
|
26565 |
*/
|
|
|
26566 |
getVideoPlaybackQuality() {
|
|
|
26567 |
return this.techGet_('getVideoPlaybackQuality');
|
|
|
26568 |
}
|
|
|
26569 |
|
|
|
26570 |
/**
|
|
|
26571 |
* Get video width
|
|
|
26572 |
*
|
|
|
26573 |
* @return {number}
|
|
|
26574 |
* current video width
|
|
|
26575 |
*/
|
|
|
26576 |
videoWidth() {
|
|
|
26577 |
return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
|
|
|
26578 |
}
|
|
|
26579 |
|
|
|
26580 |
/**
|
|
|
26581 |
* Get video height
|
|
|
26582 |
*
|
|
|
26583 |
* @return {number}
|
|
|
26584 |
* current video height
|
|
|
26585 |
*/
|
|
|
26586 |
videoHeight() {
|
|
|
26587 |
return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
|
|
|
26588 |
}
|
|
|
26589 |
|
|
|
26590 |
/**
|
|
|
26591 |
* Set or get the player's language code.
|
|
|
26592 |
*
|
|
|
26593 |
* Changing the language will trigger
|
|
|
26594 |
* [languagechange]{@link Player#event:languagechange}
|
|
|
26595 |
* which Components can use to update control text.
|
|
|
26596 |
* ClickableComponent will update its control text by default on
|
|
|
26597 |
* [languagechange]{@link Player#event:languagechange}.
|
|
|
26598 |
*
|
|
|
26599 |
* @fires Player#languagechange
|
|
|
26600 |
*
|
|
|
26601 |
* @param {string} [code]
|
|
|
26602 |
* the language code to set the player to
|
|
|
26603 |
*
|
|
|
26604 |
* @return {string|undefined}
|
|
|
26605 |
* - The current language code when getting
|
|
|
26606 |
* - Nothing when setting
|
|
|
26607 |
*/
|
|
|
26608 |
language(code) {
|
|
|
26609 |
if (code === undefined) {
|
|
|
26610 |
return this.language_;
|
|
|
26611 |
}
|
|
|
26612 |
if (this.language_ !== String(code).toLowerCase()) {
|
|
|
26613 |
this.language_ = String(code).toLowerCase();
|
|
|
26614 |
|
|
|
26615 |
// during first init, it's possible some things won't be evented
|
|
|
26616 |
if (isEvented(this)) {
|
|
|
26617 |
/**
|
|
|
26618 |
* fires when the player language change
|
|
|
26619 |
*
|
|
|
26620 |
* @event Player#languagechange
|
|
|
26621 |
* @type {Event}
|
|
|
26622 |
*/
|
|
|
26623 |
this.trigger('languagechange');
|
|
|
26624 |
}
|
|
|
26625 |
}
|
|
|
26626 |
}
|
|
|
26627 |
|
|
|
26628 |
/**
|
|
|
26629 |
* Get the player's language dictionary
|
|
|
26630 |
* Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
|
|
|
26631 |
* Languages specified directly in the player options have precedence
|
|
|
26632 |
*
|
|
|
26633 |
* @return {Array}
|
|
|
26634 |
* An array of of supported languages
|
|
|
26635 |
*/
|
|
|
26636 |
languages() {
|
|
|
26637 |
return merge$2(Player.prototype.options_.languages, this.languages_);
|
|
|
26638 |
}
|
|
|
26639 |
|
|
|
26640 |
/**
|
|
|
26641 |
* returns a JavaScript object representing the current track
|
|
|
26642 |
* information. **DOES not return it as JSON**
|
|
|
26643 |
*
|
|
|
26644 |
* @return {Object}
|
|
|
26645 |
* Object representing the current of track info
|
|
|
26646 |
*/
|
|
|
26647 |
toJSON() {
|
|
|
26648 |
const options = merge$2(this.options_);
|
|
|
26649 |
const tracks = options.tracks;
|
|
|
26650 |
options.tracks = [];
|
|
|
26651 |
for (let i = 0; i < tracks.length; i++) {
|
|
|
26652 |
let track = tracks[i];
|
|
|
26653 |
|
|
|
26654 |
// deep merge tracks and null out player so no circular references
|
|
|
26655 |
track = merge$2(track);
|
|
|
26656 |
track.player = undefined;
|
|
|
26657 |
options.tracks[i] = track;
|
|
|
26658 |
}
|
|
|
26659 |
return options;
|
|
|
26660 |
}
|
|
|
26661 |
|
|
|
26662 |
/**
|
|
|
26663 |
* Creates a simple modal dialog (an instance of the {@link ModalDialog}
|
|
|
26664 |
* component) that immediately overlays the player with arbitrary
|
|
|
26665 |
* content and removes itself when closed.
|
|
|
26666 |
*
|
|
|
26667 |
* @param {string|Function|Element|Array|null} content
|
|
|
26668 |
* Same as {@link ModalDialog#content}'s param of the same name.
|
|
|
26669 |
* The most straight-forward usage is to provide a string or DOM
|
|
|
26670 |
* element.
|
|
|
26671 |
*
|
|
|
26672 |
* @param {Object} [options]
|
|
|
26673 |
* Extra options which will be passed on to the {@link ModalDialog}.
|
|
|
26674 |
*
|
|
|
26675 |
* @return {ModalDialog}
|
|
|
26676 |
* the {@link ModalDialog} that was created
|
|
|
26677 |
*/
|
|
|
26678 |
createModal(content, options) {
|
|
|
26679 |
options = options || {};
|
|
|
26680 |
options.content = content || '';
|
|
|
26681 |
const modal = new ModalDialog(this, options);
|
|
|
26682 |
this.addChild(modal);
|
|
|
26683 |
modal.on('dispose', () => {
|
|
|
26684 |
this.removeChild(modal);
|
|
|
26685 |
});
|
|
|
26686 |
modal.open();
|
|
|
26687 |
return modal;
|
|
|
26688 |
}
|
|
|
26689 |
|
|
|
26690 |
/**
|
|
|
26691 |
* Change breakpoint classes when the player resizes.
|
|
|
26692 |
*
|
|
|
26693 |
* @private
|
|
|
26694 |
*/
|
|
|
26695 |
updateCurrentBreakpoint_() {
|
|
|
26696 |
if (!this.responsive()) {
|
|
|
26697 |
return;
|
|
|
26698 |
}
|
|
|
26699 |
const currentBreakpoint = this.currentBreakpoint();
|
|
|
26700 |
const currentWidth = this.currentWidth();
|
|
|
26701 |
for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
|
|
|
26702 |
const candidateBreakpoint = BREAKPOINT_ORDER[i];
|
|
|
26703 |
const maxWidth = this.breakpoints_[candidateBreakpoint];
|
|
|
26704 |
if (currentWidth <= maxWidth) {
|
|
|
26705 |
// The current breakpoint did not change, nothing to do.
|
|
|
26706 |
if (currentBreakpoint === candidateBreakpoint) {
|
|
|
26707 |
return;
|
|
|
26708 |
}
|
|
|
26709 |
|
|
|
26710 |
// Only remove a class if there is a current breakpoint.
|
|
|
26711 |
if (currentBreakpoint) {
|
|
|
26712 |
this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
|
|
|
26713 |
}
|
|
|
26714 |
this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
|
|
|
26715 |
this.breakpoint_ = candidateBreakpoint;
|
|
|
26716 |
break;
|
|
|
26717 |
}
|
|
|
26718 |
}
|
|
|
26719 |
}
|
|
|
26720 |
|
|
|
26721 |
/**
|
|
|
26722 |
* Removes the current breakpoint.
|
|
|
26723 |
*
|
|
|
26724 |
* @private
|
|
|
26725 |
*/
|
|
|
26726 |
removeCurrentBreakpoint_() {
|
|
|
26727 |
const className = this.currentBreakpointClass();
|
|
|
26728 |
this.breakpoint_ = '';
|
|
|
26729 |
if (className) {
|
|
|
26730 |
this.removeClass(className);
|
|
|
26731 |
}
|
|
|
26732 |
}
|
|
|
26733 |
|
|
|
26734 |
/**
|
|
|
26735 |
* Get or set breakpoints on the player.
|
|
|
26736 |
*
|
|
|
26737 |
* Calling this method with an object or `true` will remove any previous
|
|
|
26738 |
* custom breakpoints and start from the defaults again.
|
|
|
26739 |
*
|
|
|
26740 |
* @param {Object|boolean} [breakpoints]
|
|
|
26741 |
* If an object is given, it can be used to provide custom
|
|
|
26742 |
* breakpoints. If `true` is given, will set default breakpoints.
|
|
|
26743 |
* If this argument is not given, will simply return the current
|
|
|
26744 |
* breakpoints.
|
|
|
26745 |
*
|
|
|
26746 |
* @param {number} [breakpoints.tiny]
|
|
|
26747 |
* The maximum width for the "vjs-layout-tiny" class.
|
|
|
26748 |
*
|
|
|
26749 |
* @param {number} [breakpoints.xsmall]
|
|
|
26750 |
* The maximum width for the "vjs-layout-x-small" class.
|
|
|
26751 |
*
|
|
|
26752 |
* @param {number} [breakpoints.small]
|
|
|
26753 |
* The maximum width for the "vjs-layout-small" class.
|
|
|
26754 |
*
|
|
|
26755 |
* @param {number} [breakpoints.medium]
|
|
|
26756 |
* The maximum width for the "vjs-layout-medium" class.
|
|
|
26757 |
*
|
|
|
26758 |
* @param {number} [breakpoints.large]
|
|
|
26759 |
* The maximum width for the "vjs-layout-large" class.
|
|
|
26760 |
*
|
|
|
26761 |
* @param {number} [breakpoints.xlarge]
|
|
|
26762 |
* The maximum width for the "vjs-layout-x-large" class.
|
|
|
26763 |
*
|
|
|
26764 |
* @param {number} [breakpoints.huge]
|
|
|
26765 |
* The maximum width for the "vjs-layout-huge" class.
|
|
|
26766 |
*
|
|
|
26767 |
* @return {Object}
|
|
|
26768 |
* An object mapping breakpoint names to maximum width values.
|
|
|
26769 |
*/
|
|
|
26770 |
breakpoints(breakpoints) {
|
|
|
26771 |
// Used as a getter.
|
|
|
26772 |
if (breakpoints === undefined) {
|
|
|
26773 |
return Object.assign(this.breakpoints_);
|
|
|
26774 |
}
|
|
|
26775 |
this.breakpoint_ = '';
|
|
|
26776 |
this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
|
|
|
26777 |
|
|
|
26778 |
// When breakpoint definitions change, we need to update the currently
|
|
|
26779 |
// selected breakpoint.
|
|
|
26780 |
this.updateCurrentBreakpoint_();
|
|
|
26781 |
|
|
|
26782 |
// Clone the breakpoints before returning.
|
|
|
26783 |
return Object.assign(this.breakpoints_);
|
|
|
26784 |
}
|
|
|
26785 |
|
|
|
26786 |
/**
|
|
|
26787 |
* Get or set a flag indicating whether or not this player should adjust
|
|
|
26788 |
* its UI based on its dimensions.
|
|
|
26789 |
*
|
|
|
26790 |
* @param {boolean} [value]
|
|
|
26791 |
* Should be `true` if the player should adjust its UI based on its
|
|
|
26792 |
* dimensions; otherwise, should be `false`.
|
|
|
26793 |
*
|
|
|
26794 |
* @return {boolean|undefined}
|
|
|
26795 |
* Will be `true` if this player should adjust its UI based on its
|
|
|
26796 |
* dimensions; otherwise, will be `false`.
|
|
|
26797 |
* Nothing if setting
|
|
|
26798 |
*/
|
|
|
26799 |
responsive(value) {
|
|
|
26800 |
// Used as a getter.
|
|
|
26801 |
if (value === undefined) {
|
|
|
26802 |
return this.responsive_;
|
|
|
26803 |
}
|
|
|
26804 |
value = Boolean(value);
|
|
|
26805 |
const current = this.responsive_;
|
|
|
26806 |
|
|
|
26807 |
// Nothing changed.
|
|
|
26808 |
if (value === current) {
|
|
|
26809 |
return;
|
|
|
26810 |
}
|
|
|
26811 |
|
|
|
26812 |
// The value actually changed, set it.
|
|
|
26813 |
this.responsive_ = value;
|
|
|
26814 |
|
|
|
26815 |
// Start listening for breakpoints and set the initial breakpoint if the
|
|
|
26816 |
// player is now responsive.
|
|
|
26817 |
if (value) {
|
|
|
26818 |
this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
|
|
|
26819 |
this.updateCurrentBreakpoint_();
|
|
|
26820 |
|
|
|
26821 |
// Stop listening for breakpoints if the player is no longer responsive.
|
|
|
26822 |
} else {
|
|
|
26823 |
this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
|
|
|
26824 |
this.removeCurrentBreakpoint_();
|
|
|
26825 |
}
|
|
|
26826 |
return value;
|
|
|
26827 |
}
|
|
|
26828 |
|
|
|
26829 |
/**
|
|
|
26830 |
* Get current breakpoint name, if any.
|
|
|
26831 |
*
|
|
|
26832 |
* @return {string}
|
|
|
26833 |
* If there is currently a breakpoint set, returns a the key from the
|
|
|
26834 |
* breakpoints object matching it. Otherwise, returns an empty string.
|
|
|
26835 |
*/
|
|
|
26836 |
currentBreakpoint() {
|
|
|
26837 |
return this.breakpoint_;
|
|
|
26838 |
}
|
|
|
26839 |
|
|
|
26840 |
/**
|
|
|
26841 |
* Get the current breakpoint class name.
|
|
|
26842 |
*
|
|
|
26843 |
* @return {string}
|
|
|
26844 |
* The matching class name (e.g. `"vjs-layout-tiny"` or
|
|
|
26845 |
* `"vjs-layout-large"`) for the current breakpoint. Empty string if
|
|
|
26846 |
* there is no current breakpoint.
|
|
|
26847 |
*/
|
|
|
26848 |
currentBreakpointClass() {
|
|
|
26849 |
return BREAKPOINT_CLASSES[this.breakpoint_] || '';
|
|
|
26850 |
}
|
|
|
26851 |
|
|
|
26852 |
/**
|
|
|
26853 |
* An object that describes a single piece of media.
|
|
|
26854 |
*
|
|
|
26855 |
* Properties that are not part of this type description will be retained; so,
|
|
|
26856 |
* this can be viewed as a generic metadata storage mechanism as well.
|
|
|
26857 |
*
|
|
|
26858 |
* @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
|
|
|
26859 |
* @typedef {Object} Player~MediaObject
|
|
|
26860 |
*
|
|
|
26861 |
* @property {string} [album]
|
|
|
26862 |
* Unused, except if this object is passed to the `MediaSession`
|
|
|
26863 |
* API.
|
|
|
26864 |
*
|
|
|
26865 |
* @property {string} [artist]
|
|
|
26866 |
* Unused, except if this object is passed to the `MediaSession`
|
|
|
26867 |
* API.
|
|
|
26868 |
*
|
|
|
26869 |
* @property {Object[]} [artwork]
|
|
|
26870 |
* Unused, except if this object is passed to the `MediaSession`
|
|
|
26871 |
* API. If not specified, will be populated via the `poster`, if
|
|
|
26872 |
* available.
|
|
|
26873 |
*
|
|
|
26874 |
* @property {string} [poster]
|
|
|
26875 |
* URL to an image that will display before playback.
|
|
|
26876 |
*
|
|
|
26877 |
* @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
|
|
|
26878 |
* A single source object, an array of source objects, or a string
|
|
|
26879 |
* referencing a URL to a media source. It is _highly recommended_
|
|
|
26880 |
* that an object or array of objects is used here, so that source
|
|
|
26881 |
* selection algorithms can take the `type` into account.
|
|
|
26882 |
*
|
|
|
26883 |
* @property {string} [title]
|
|
|
26884 |
* Unused, except if this object is passed to the `MediaSession`
|
|
|
26885 |
* API.
|
|
|
26886 |
*
|
|
|
26887 |
* @property {Object[]} [textTracks]
|
|
|
26888 |
* An array of objects to be used to create text tracks, following
|
|
|
26889 |
* the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
|
|
|
26890 |
* For ease of removal, these will be created as "remote" text
|
|
|
26891 |
* tracks and set to automatically clean up on source changes.
|
|
|
26892 |
*
|
|
|
26893 |
* These objects may have properties like `src`, `kind`, `label`,
|
|
|
26894 |
* and `language`, see {@link Tech#createRemoteTextTrack}.
|
|
|
26895 |
*/
|
|
|
26896 |
|
|
|
26897 |
/**
|
|
|
26898 |
* Populate the player using a {@link Player~MediaObject|MediaObject}.
|
|
|
26899 |
*
|
|
|
26900 |
* @param {Player~MediaObject} media
|
|
|
26901 |
* A media object.
|
|
|
26902 |
*
|
|
|
26903 |
* @param {Function} ready
|
|
|
26904 |
* A callback to be called when the player is ready.
|
|
|
26905 |
*/
|
|
|
26906 |
loadMedia(media, ready) {
|
|
|
26907 |
if (!media || typeof media !== 'object') {
|
|
|
26908 |
return;
|
|
|
26909 |
}
|
|
|
26910 |
const crossOrigin = this.crossOrigin();
|
|
|
26911 |
this.reset();
|
|
|
26912 |
|
|
|
26913 |
// Clone the media object so it cannot be mutated from outside.
|
|
|
26914 |
this.cache_.media = merge$2(media);
|
|
|
26915 |
const {
|
|
|
26916 |
artist,
|
|
|
26917 |
artwork,
|
|
|
26918 |
description,
|
|
|
26919 |
poster,
|
|
|
26920 |
src,
|
|
|
26921 |
textTracks,
|
|
|
26922 |
title
|
|
|
26923 |
} = this.cache_.media;
|
|
|
26924 |
|
|
|
26925 |
// If `artwork` is not given, create it using `poster`.
|
|
|
26926 |
if (!artwork && poster) {
|
|
|
26927 |
this.cache_.media.artwork = [{
|
|
|
26928 |
src: poster,
|
|
|
26929 |
type: getMimetype(poster)
|
|
|
26930 |
}];
|
|
|
26931 |
}
|
|
|
26932 |
if (crossOrigin) {
|
|
|
26933 |
this.crossOrigin(crossOrigin);
|
|
|
26934 |
}
|
|
|
26935 |
if (src) {
|
|
|
26936 |
this.src(src);
|
|
|
26937 |
}
|
|
|
26938 |
if (poster) {
|
|
|
26939 |
this.poster(poster);
|
|
|
26940 |
}
|
|
|
26941 |
if (Array.isArray(textTracks)) {
|
|
|
26942 |
textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
|
|
|
26943 |
}
|
|
|
26944 |
if (this.titleBar) {
|
|
|
26945 |
this.titleBar.update({
|
|
|
26946 |
title,
|
|
|
26947 |
description: description || artist || ''
|
|
|
26948 |
});
|
|
|
26949 |
}
|
|
|
26950 |
this.ready(ready);
|
|
|
26951 |
}
|
|
|
26952 |
|
|
|
26953 |
/**
|
|
|
26954 |
* Get a clone of the current {@link Player~MediaObject} for this player.
|
|
|
26955 |
*
|
|
|
26956 |
* If the `loadMedia` method has not been used, will attempt to return a
|
|
|
26957 |
* {@link Player~MediaObject} based on the current state of the player.
|
|
|
26958 |
*
|
|
|
26959 |
* @return {Player~MediaObject}
|
|
|
26960 |
*/
|
|
|
26961 |
getMedia() {
|
|
|
26962 |
if (!this.cache_.media) {
|
|
|
26963 |
const poster = this.poster();
|
|
|
26964 |
const src = this.currentSources();
|
|
|
26965 |
const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
|
|
|
26966 |
kind: tt.kind,
|
|
|
26967 |
label: tt.label,
|
|
|
26968 |
language: tt.language,
|
|
|
26969 |
src: tt.src
|
|
|
26970 |
}));
|
|
|
26971 |
const media = {
|
|
|
26972 |
src,
|
|
|
26973 |
textTracks
|
|
|
26974 |
};
|
|
|
26975 |
if (poster) {
|
|
|
26976 |
media.poster = poster;
|
|
|
26977 |
media.artwork = [{
|
|
|
26978 |
src: media.poster,
|
|
|
26979 |
type: getMimetype(media.poster)
|
|
|
26980 |
}];
|
|
|
26981 |
}
|
|
|
26982 |
return media;
|
|
|
26983 |
}
|
|
|
26984 |
return merge$2(this.cache_.media);
|
|
|
26985 |
}
|
|
|
26986 |
|
|
|
26987 |
/**
|
|
|
26988 |
* Gets tag settings
|
|
|
26989 |
*
|
|
|
26990 |
* @param {Element} tag
|
|
|
26991 |
* The player tag
|
|
|
26992 |
*
|
|
|
26993 |
* @return {Object}
|
|
|
26994 |
* An object containing all of the settings
|
|
|
26995 |
* for a player tag
|
|
|
26996 |
*/
|
|
|
26997 |
static getTagSettings(tag) {
|
|
|
26998 |
const baseOptions = {
|
|
|
26999 |
sources: [],
|
|
|
27000 |
tracks: []
|
|
|
27001 |
};
|
|
|
27002 |
const tagOptions = getAttributes(tag);
|
|
|
27003 |
const dataSetup = tagOptions['data-setup'];
|
|
|
27004 |
if (hasClass(tag, 'vjs-fill')) {
|
|
|
27005 |
tagOptions.fill = true;
|
|
|
27006 |
}
|
|
|
27007 |
if (hasClass(tag, 'vjs-fluid')) {
|
|
|
27008 |
tagOptions.fluid = true;
|
|
|
27009 |
}
|
|
|
27010 |
|
|
|
27011 |
// Check if data-setup attr exists.
|
|
|
27012 |
if (dataSetup !== null) {
|
|
|
27013 |
// Parse options JSON
|
|
|
27014 |
// If empty string, make it a parsable json object.
|
|
|
27015 |
const [err, data] = tuple(dataSetup || '{}');
|
|
|
27016 |
if (err) {
|
|
|
27017 |
log$1.error(err);
|
|
|
27018 |
}
|
|
|
27019 |
Object.assign(tagOptions, data);
|
|
|
27020 |
}
|
|
|
27021 |
Object.assign(baseOptions, tagOptions);
|
|
|
27022 |
|
|
|
27023 |
// Get tag children settings
|
|
|
27024 |
if (tag.hasChildNodes()) {
|
|
|
27025 |
const children = tag.childNodes;
|
|
|
27026 |
for (let i = 0, j = children.length; i < j; i++) {
|
|
|
27027 |
const child = children[i];
|
|
|
27028 |
// Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
|
|
|
27029 |
const childName = child.nodeName.toLowerCase();
|
|
|
27030 |
if (childName === 'source') {
|
|
|
27031 |
baseOptions.sources.push(getAttributes(child));
|
|
|
27032 |
} else if (childName === 'track') {
|
|
|
27033 |
baseOptions.tracks.push(getAttributes(child));
|
|
|
27034 |
}
|
|
|
27035 |
}
|
|
|
27036 |
}
|
|
|
27037 |
return baseOptions;
|
|
|
27038 |
}
|
|
|
27039 |
|
|
|
27040 |
/**
|
|
|
27041 |
* Set debug mode to enable/disable logs at info level.
|
|
|
27042 |
*
|
|
|
27043 |
* @param {boolean} enabled
|
|
|
27044 |
* @fires Player#debugon
|
|
|
27045 |
* @fires Player#debugoff
|
|
|
27046 |
* @return {boolean|undefined}
|
|
|
27047 |
*/
|
|
|
27048 |
debug(enabled) {
|
|
|
27049 |
if (enabled === undefined) {
|
|
|
27050 |
return this.debugEnabled_;
|
|
|
27051 |
}
|
|
|
27052 |
if (enabled) {
|
|
|
27053 |
this.trigger('debugon');
|
|
|
27054 |
this.previousLogLevel_ = this.log.level;
|
|
|
27055 |
this.log.level('debug');
|
|
|
27056 |
this.debugEnabled_ = true;
|
|
|
27057 |
} else {
|
|
|
27058 |
this.trigger('debugoff');
|
|
|
27059 |
this.log.level(this.previousLogLevel_);
|
|
|
27060 |
this.previousLogLevel_ = undefined;
|
|
|
27061 |
this.debugEnabled_ = false;
|
|
|
27062 |
}
|
|
|
27063 |
}
|
|
|
27064 |
|
|
|
27065 |
/**
|
|
|
27066 |
* Set or get current playback rates.
|
|
|
27067 |
* Takes an array and updates the playback rates menu with the new items.
|
|
|
27068 |
* Pass in an empty array to hide the menu.
|
|
|
27069 |
* Values other than arrays are ignored.
|
|
|
27070 |
*
|
|
|
27071 |
* @fires Player#playbackrateschange
|
|
|
27072 |
* @param {number[]} newRates
|
|
|
27073 |
* The new rates that the playback rates menu should update to.
|
|
|
27074 |
* An empty array will hide the menu
|
|
|
27075 |
* @return {number[]} When used as a getter will return the current playback rates
|
|
|
27076 |
*/
|
|
|
27077 |
playbackRates(newRates) {
|
|
|
27078 |
if (newRates === undefined) {
|
|
|
27079 |
return this.cache_.playbackRates;
|
|
|
27080 |
}
|
|
|
27081 |
|
|
|
27082 |
// ignore any value that isn't an array
|
|
|
27083 |
if (!Array.isArray(newRates)) {
|
|
|
27084 |
return;
|
|
|
27085 |
}
|
|
|
27086 |
|
|
|
27087 |
// ignore any arrays that don't only contain numbers
|
|
|
27088 |
if (!newRates.every(rate => typeof rate === 'number')) {
|
|
|
27089 |
return;
|
|
|
27090 |
}
|
|
|
27091 |
this.cache_.playbackRates = newRates;
|
|
|
27092 |
|
|
|
27093 |
/**
|
|
|
27094 |
* fires when the playback rates in a player are changed
|
|
|
27095 |
*
|
|
|
27096 |
* @event Player#playbackrateschange
|
|
|
27097 |
* @type {Event}
|
|
|
27098 |
*/
|
|
|
27099 |
this.trigger('playbackrateschange');
|
|
|
27100 |
}
|
|
|
27101 |
}
|
|
|
27102 |
|
|
|
27103 |
/**
|
|
|
27104 |
* Get the {@link VideoTrackList}
|
|
|
27105 |
*
|
|
|
27106 |
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
|
|
|
27107 |
*
|
|
|
27108 |
* @return {VideoTrackList}
|
|
|
27109 |
* the current video track list
|
|
|
27110 |
*
|
|
|
27111 |
* @method Player.prototype.videoTracks
|
|
|
27112 |
*/
|
|
|
27113 |
|
|
|
27114 |
/**
|
|
|
27115 |
* Get the {@link AudioTrackList}
|
|
|
27116 |
*
|
|
|
27117 |
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
|
|
|
27118 |
*
|
|
|
27119 |
* @return {AudioTrackList}
|
|
|
27120 |
* the current audio track list
|
|
|
27121 |
*
|
|
|
27122 |
* @method Player.prototype.audioTracks
|
|
|
27123 |
*/
|
|
|
27124 |
|
|
|
27125 |
/**
|
|
|
27126 |
* Get the {@link TextTrackList}
|
|
|
27127 |
*
|
|
|
27128 |
* @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
|
|
|
27129 |
*
|
|
|
27130 |
* @return {TextTrackList}
|
|
|
27131 |
* the current text track list
|
|
|
27132 |
*
|
|
|
27133 |
* @method Player.prototype.textTracks
|
|
|
27134 |
*/
|
|
|
27135 |
|
|
|
27136 |
/**
|
|
|
27137 |
* Get the remote {@link TextTrackList}
|
|
|
27138 |
*
|
|
|
27139 |
* @return {TextTrackList}
|
|
|
27140 |
* The current remote text track list
|
|
|
27141 |
*
|
|
|
27142 |
* @method Player.prototype.remoteTextTracks
|
|
|
27143 |
*/
|
|
|
27144 |
|
|
|
27145 |
/**
|
|
|
27146 |
* Get the remote {@link HtmlTrackElementList} tracks.
|
|
|
27147 |
*
|
|
|
27148 |
* @return {HtmlTrackElementList}
|
|
|
27149 |
* The current remote text track element list
|
|
|
27150 |
*
|
|
|
27151 |
* @method Player.prototype.remoteTextTrackEls
|
|
|
27152 |
*/
|
|
|
27153 |
|
|
|
27154 |
ALL.names.forEach(function (name) {
|
|
|
27155 |
const props = ALL[name];
|
|
|
27156 |
Player.prototype[props.getterName] = function () {
|
|
|
27157 |
if (this.tech_) {
|
|
|
27158 |
return this.tech_[props.getterName]();
|
|
|
27159 |
}
|
|
|
27160 |
|
|
|
27161 |
// if we have not yet loadTech_, we create {video,audio,text}Tracks_
|
|
|
27162 |
// these will be passed to the tech during loading
|
|
|
27163 |
this[props.privateName] = this[props.privateName] || new props.ListClass();
|
|
|
27164 |
return this[props.privateName];
|
|
|
27165 |
};
|
|
|
27166 |
});
|
|
|
27167 |
|
|
|
27168 |
/**
|
|
|
27169 |
* Get or set the `Player`'s crossorigin option. For the HTML5 player, this
|
|
|
27170 |
* sets the `crossOrigin` property on the `<video>` tag to control the CORS
|
|
|
27171 |
* behavior.
|
|
|
27172 |
*
|
|
|
27173 |
* @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
|
|
|
27174 |
*
|
|
|
27175 |
* @param {string} [value]
|
|
|
27176 |
* The value to set the `Player`'s crossorigin to. If an argument is
|
|
|
27177 |
* given, must be one of `anonymous` or `use-credentials`.
|
|
|
27178 |
*
|
|
|
27179 |
* @return {string|undefined}
|
|
|
27180 |
* - The current crossorigin value of the `Player` when getting.
|
|
|
27181 |
* - undefined when setting
|
|
|
27182 |
*/
|
|
|
27183 |
Player.prototype.crossorigin = Player.prototype.crossOrigin;
|
|
|
27184 |
|
|
|
27185 |
/**
|
|
|
27186 |
* Global enumeration of players.
|
|
|
27187 |
*
|
|
|
27188 |
* The keys are the player IDs and the values are either the {@link Player}
|
|
|
27189 |
* instance or `null` for disposed players.
|
|
|
27190 |
*
|
|
|
27191 |
* @type {Object}
|
|
|
27192 |
*/
|
|
|
27193 |
Player.players = {};
|
|
|
27194 |
const navigator = window.navigator;
|
|
|
27195 |
|
|
|
27196 |
/*
|
|
|
27197 |
* Player instance options, surfaced using options
|
|
|
27198 |
* options = Player.prototype.options_
|
|
|
27199 |
* Make changes in options, not here.
|
|
|
27200 |
*
|
|
|
27201 |
* @type {Object}
|
|
|
27202 |
* @private
|
|
|
27203 |
*/
|
|
|
27204 |
Player.prototype.options_ = {
|
|
|
27205 |
// Default order of fallback technology
|
|
|
27206 |
techOrder: Tech.defaultTechOrder_,
|
|
|
27207 |
html5: {},
|
|
|
27208 |
// enable sourceset by default
|
|
|
27209 |
enableSourceset: true,
|
|
|
27210 |
// default inactivity timeout
|
|
|
27211 |
inactivityTimeout: 2000,
|
|
|
27212 |
// default playback rates
|
|
|
27213 |
playbackRates: [],
|
|
|
27214 |
// Add playback rate selection by adding rates
|
|
|
27215 |
// 'playbackRates': [0.5, 1, 1.5, 2],
|
|
|
27216 |
liveui: false,
|
|
|
27217 |
// Included control sets
|
|
|
27218 |
children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
|
|
|
27219 |
language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
|
|
|
27220 |
// locales and their language translations
|
|
|
27221 |
languages: {},
|
|
|
27222 |
// Default message to show when a video cannot be played.
|
|
|
27223 |
notSupportedMessage: 'No compatible source was found for this media.',
|
|
|
27224 |
normalizeAutoplay: false,
|
|
|
27225 |
fullscreen: {
|
|
|
27226 |
options: {
|
|
|
27227 |
navigationUI: 'hide'
|
|
|
27228 |
}
|
|
|
27229 |
},
|
|
|
27230 |
breakpoints: {},
|
|
|
27231 |
responsive: false,
|
|
|
27232 |
audioOnlyMode: false,
|
|
|
27233 |
audioPosterMode: false,
|
|
|
27234 |
// Default smooth seeking to false
|
|
|
27235 |
enableSmoothSeeking: false
|
|
|
27236 |
};
|
|
|
27237 |
TECH_EVENTS_RETRIGGER.forEach(function (event) {
|
|
|
27238 |
Player.prototype[`handleTech${toTitleCase$1(event)}_`] = function () {
|
|
|
27239 |
return this.trigger(event);
|
|
|
27240 |
};
|
|
|
27241 |
});
|
|
|
27242 |
|
|
|
27243 |
/**
|
|
|
27244 |
* Fired when the player has initial duration and dimension information
|
|
|
27245 |
*
|
|
|
27246 |
* @event Player#loadedmetadata
|
|
|
27247 |
* @type {Event}
|
|
|
27248 |
*/
|
|
|
27249 |
|
|
|
27250 |
/**
|
|
|
27251 |
* Fired when the player has downloaded data at the current playback position
|
|
|
27252 |
*
|
|
|
27253 |
* @event Player#loadeddata
|
|
|
27254 |
* @type {Event}
|
|
|
27255 |
*/
|
|
|
27256 |
|
|
|
27257 |
/**
|
|
|
27258 |
* Fired when the current playback position has changed *
|
|
|
27259 |
* During playback this is fired every 15-250 milliseconds, depending on the
|
|
|
27260 |
* playback technology in use.
|
|
|
27261 |
*
|
|
|
27262 |
* @event Player#timeupdate
|
|
|
27263 |
* @type {Event}
|
|
|
27264 |
*/
|
|
|
27265 |
|
|
|
27266 |
/**
|
|
|
27267 |
* Fired when the volume changes
|
|
|
27268 |
*
|
|
|
27269 |
* @event Player#volumechange
|
|
|
27270 |
* @type {Event}
|
|
|
27271 |
*/
|
|
|
27272 |
|
|
|
27273 |
/**
|
|
|
27274 |
* Reports whether or not a player has a plugin available.
|
|
|
27275 |
*
|
|
|
27276 |
* This does not report whether or not the plugin has ever been initialized
|
|
|
27277 |
* on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
|
|
|
27278 |
*
|
|
|
27279 |
* @method Player#hasPlugin
|
|
|
27280 |
* @param {string} name
|
|
|
27281 |
* The name of a plugin.
|
|
|
27282 |
*
|
|
|
27283 |
* @return {boolean}
|
|
|
27284 |
* Whether or not this player has the requested plugin available.
|
|
|
27285 |
*/
|
|
|
27286 |
|
|
|
27287 |
/**
|
|
|
27288 |
* Reports whether or not a player is using a plugin by name.
|
|
|
27289 |
*
|
|
|
27290 |
* For basic plugins, this only reports whether the plugin has _ever_ been
|
|
|
27291 |
* initialized on this player.
|
|
|
27292 |
*
|
|
|
27293 |
* @method Player#usingPlugin
|
|
|
27294 |
* @param {string} name
|
|
|
27295 |
* The name of a plugin.
|
|
|
27296 |
*
|
|
|
27297 |
* @return {boolean}
|
|
|
27298 |
* Whether or not this player is using the requested plugin.
|
|
|
27299 |
*/
|
|
|
27300 |
|
|
|
27301 |
Component$1.registerComponent('Player', Player);
|
|
|
27302 |
|
|
|
27303 |
/**
|
|
|
27304 |
* @file plugin.js
|
|
|
27305 |
*/
|
|
|
27306 |
|
|
|
27307 |
/**
|
|
|
27308 |
* The base plugin name.
|
|
|
27309 |
*
|
|
|
27310 |
* @private
|
|
|
27311 |
* @constant
|
|
|
27312 |
* @type {string}
|
|
|
27313 |
*/
|
|
|
27314 |
const BASE_PLUGIN_NAME = 'plugin';
|
|
|
27315 |
|
|
|
27316 |
/**
|
|
|
27317 |
* The key on which a player's active plugins cache is stored.
|
|
|
27318 |
*
|
|
|
27319 |
* @private
|
|
|
27320 |
* @constant
|
|
|
27321 |
* @type {string}
|
|
|
27322 |
*/
|
|
|
27323 |
const PLUGIN_CACHE_KEY = 'activePlugins_';
|
|
|
27324 |
|
|
|
27325 |
/**
|
|
|
27326 |
* Stores registered plugins in a private space.
|
|
|
27327 |
*
|
|
|
27328 |
* @private
|
|
|
27329 |
* @type {Object}
|
|
|
27330 |
*/
|
|
|
27331 |
const pluginStorage = {};
|
|
|
27332 |
|
|
|
27333 |
/**
|
|
|
27334 |
* Reports whether or not a plugin has been registered.
|
|
|
27335 |
*
|
|
|
27336 |
* @private
|
|
|
27337 |
* @param {string} name
|
|
|
27338 |
* The name of a plugin.
|
|
|
27339 |
*
|
|
|
27340 |
* @return {boolean}
|
|
|
27341 |
* Whether or not the plugin has been registered.
|
|
|
27342 |
*/
|
|
|
27343 |
const pluginExists = name => pluginStorage.hasOwnProperty(name);
|
|
|
27344 |
|
|
|
27345 |
/**
|
|
|
27346 |
* Get a single registered plugin by name.
|
|
|
27347 |
*
|
|
|
27348 |
* @private
|
|
|
27349 |
* @param {string} name
|
|
|
27350 |
* The name of a plugin.
|
|
|
27351 |
*
|
|
|
27352 |
* @return {typeof Plugin|Function|undefined}
|
|
|
27353 |
* The plugin (or undefined).
|
|
|
27354 |
*/
|
|
|
27355 |
const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
|
|
|
27356 |
|
|
|
27357 |
/**
|
|
|
27358 |
* Marks a plugin as "active" on a player.
|
|
|
27359 |
*
|
|
|
27360 |
* Also, ensures that the player has an object for tracking active plugins.
|
|
|
27361 |
*
|
|
|
27362 |
* @private
|
|
|
27363 |
* @param {Player} player
|
|
|
27364 |
* A Video.js player instance.
|
|
|
27365 |
*
|
|
|
27366 |
* @param {string} name
|
|
|
27367 |
* The name of a plugin.
|
|
|
27368 |
*/
|
|
|
27369 |
const markPluginAsActive = (player, name) => {
|
|
|
27370 |
player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
|
|
|
27371 |
player[PLUGIN_CACHE_KEY][name] = true;
|
|
|
27372 |
};
|
|
|
27373 |
|
|
|
27374 |
/**
|
|
|
27375 |
* Triggers a pair of plugin setup events.
|
|
|
27376 |
*
|
|
|
27377 |
* @private
|
|
|
27378 |
* @param {Player} player
|
|
|
27379 |
* A Video.js player instance.
|
|
|
27380 |
*
|
|
|
27381 |
* @param {PluginEventHash} hash
|
|
|
27382 |
* A plugin event hash.
|
|
|
27383 |
*
|
|
|
27384 |
* @param {boolean} [before]
|
|
|
27385 |
* If true, prefixes the event name with "before". In other words,
|
|
|
27386 |
* use this to trigger "beforepluginsetup" instead of "pluginsetup".
|
|
|
27387 |
*/
|
|
|
27388 |
const triggerSetupEvent = (player, hash, before) => {
|
|
|
27389 |
const eventName = (before ? 'before' : '') + 'pluginsetup';
|
|
|
27390 |
player.trigger(eventName, hash);
|
|
|
27391 |
player.trigger(eventName + ':' + hash.name, hash);
|
|
|
27392 |
};
|
|
|
27393 |
|
|
|
27394 |
/**
|
|
|
27395 |
* Takes a basic plugin function and returns a wrapper function which marks
|
|
|
27396 |
* on the player that the plugin has been activated.
|
|
|
27397 |
*
|
|
|
27398 |
* @private
|
|
|
27399 |
* @param {string} name
|
|
|
27400 |
* The name of the plugin.
|
|
|
27401 |
*
|
|
|
27402 |
* @param {Function} plugin
|
|
|
27403 |
* The basic plugin.
|
|
|
27404 |
*
|
|
|
27405 |
* @return {Function}
|
|
|
27406 |
* A wrapper function for the given plugin.
|
|
|
27407 |
*/
|
|
|
27408 |
const createBasicPlugin = function (name, plugin) {
|
|
|
27409 |
const basicPluginWrapper = function () {
|
|
|
27410 |
// We trigger the "beforepluginsetup" and "pluginsetup" events on the player
|
|
|
27411 |
// regardless, but we want the hash to be consistent with the hash provided
|
|
|
27412 |
// for advanced plugins.
|
|
|
27413 |
//
|
|
|
27414 |
// The only potentially counter-intuitive thing here is the `instance` in
|
|
|
27415 |
// the "pluginsetup" event is the value returned by the `plugin` function.
|
|
|
27416 |
triggerSetupEvent(this, {
|
|
|
27417 |
name,
|
|
|
27418 |
plugin,
|
|
|
27419 |
instance: null
|
|
|
27420 |
}, true);
|
|
|
27421 |
const instance = plugin.apply(this, arguments);
|
|
|
27422 |
markPluginAsActive(this, name);
|
|
|
27423 |
triggerSetupEvent(this, {
|
|
|
27424 |
name,
|
|
|
27425 |
plugin,
|
|
|
27426 |
instance
|
|
|
27427 |
});
|
|
|
27428 |
return instance;
|
|
|
27429 |
};
|
|
|
27430 |
Object.keys(plugin).forEach(function (prop) {
|
|
|
27431 |
basicPluginWrapper[prop] = plugin[prop];
|
|
|
27432 |
});
|
|
|
27433 |
return basicPluginWrapper;
|
|
|
27434 |
};
|
|
|
27435 |
|
|
|
27436 |
/**
|
|
|
27437 |
* Takes a plugin sub-class and returns a factory function for generating
|
|
|
27438 |
* instances of it.
|
|
|
27439 |
*
|
|
|
27440 |
* This factory function will replace itself with an instance of the requested
|
|
|
27441 |
* sub-class of Plugin.
|
|
|
27442 |
*
|
|
|
27443 |
* @private
|
|
|
27444 |
* @param {string} name
|
|
|
27445 |
* The name of the plugin.
|
|
|
27446 |
*
|
|
|
27447 |
* @param {Plugin} PluginSubClass
|
|
|
27448 |
* The advanced plugin.
|
|
|
27449 |
*
|
|
|
27450 |
* @return {Function}
|
|
|
27451 |
*/
|
|
|
27452 |
const createPluginFactory = (name, PluginSubClass) => {
|
|
|
27453 |
// Add a `name` property to the plugin prototype so that each plugin can
|
|
|
27454 |
// refer to itself by name.
|
|
|
27455 |
PluginSubClass.prototype.name = name;
|
|
|
27456 |
return function (...args) {
|
|
|
27457 |
triggerSetupEvent(this, {
|
|
|
27458 |
name,
|
|
|
27459 |
plugin: PluginSubClass,
|
|
|
27460 |
instance: null
|
|
|
27461 |
}, true);
|
|
|
27462 |
const instance = new PluginSubClass(...[this, ...args]);
|
|
|
27463 |
|
|
|
27464 |
// The plugin is replaced by a function that returns the current instance.
|
|
|
27465 |
this[name] = () => instance;
|
|
|
27466 |
triggerSetupEvent(this, instance.getEventHash());
|
|
|
27467 |
return instance;
|
|
|
27468 |
};
|
|
|
27469 |
};
|
|
|
27470 |
|
|
|
27471 |
/**
|
|
|
27472 |
* Parent class for all advanced plugins.
|
|
|
27473 |
*
|
|
|
27474 |
* @mixes module:evented~EventedMixin
|
|
|
27475 |
* @mixes module:stateful~StatefulMixin
|
|
|
27476 |
* @fires Player#beforepluginsetup
|
|
|
27477 |
* @fires Player#beforepluginsetup:$name
|
|
|
27478 |
* @fires Player#pluginsetup
|
|
|
27479 |
* @fires Player#pluginsetup:$name
|
|
|
27480 |
* @listens Player#dispose
|
|
|
27481 |
* @throws {Error}
|
|
|
27482 |
* If attempting to instantiate the base {@link Plugin} class
|
|
|
27483 |
* directly instead of via a sub-class.
|
|
|
27484 |
*/
|
|
|
27485 |
class Plugin {
|
|
|
27486 |
/**
|
|
|
27487 |
* Creates an instance of this class.
|
|
|
27488 |
*
|
|
|
27489 |
* Sub-classes should call `super` to ensure plugins are properly initialized.
|
|
|
27490 |
*
|
|
|
27491 |
* @param {Player} player
|
|
|
27492 |
* A Video.js player instance.
|
|
|
27493 |
*/
|
|
|
27494 |
constructor(player) {
|
|
|
27495 |
if (this.constructor === Plugin) {
|
|
|
27496 |
throw new Error('Plugin must be sub-classed; not directly instantiated.');
|
|
|
27497 |
}
|
|
|
27498 |
this.player = player;
|
|
|
27499 |
if (!this.log) {
|
|
|
27500 |
this.log = this.player.log.createLogger(this.name);
|
|
|
27501 |
}
|
|
|
27502 |
|
|
|
27503 |
// Make this object evented, but remove the added `trigger` method so we
|
|
|
27504 |
// use the prototype version instead.
|
|
|
27505 |
evented(this);
|
|
|
27506 |
delete this.trigger;
|
|
|
27507 |
stateful(this, this.constructor.defaultState);
|
|
|
27508 |
markPluginAsActive(player, this.name);
|
|
|
27509 |
|
|
|
27510 |
// Auto-bind the dispose method so we can use it as a listener and unbind
|
|
|
27511 |
// it later easily.
|
|
|
27512 |
this.dispose = this.dispose.bind(this);
|
|
|
27513 |
|
|
|
27514 |
// If the player is disposed, dispose the plugin.
|
|
|
27515 |
player.on('dispose', this.dispose);
|
|
|
27516 |
}
|
|
|
27517 |
|
|
|
27518 |
/**
|
|
|
27519 |
* Get the version of the plugin that was set on <pluginName>.VERSION
|
|
|
27520 |
*/
|
|
|
27521 |
version() {
|
|
|
27522 |
return this.constructor.VERSION;
|
|
|
27523 |
}
|
|
|
27524 |
|
|
|
27525 |
/**
|
|
|
27526 |
* Each event triggered by plugins includes a hash of additional data with
|
|
|
27527 |
* conventional properties.
|
|
|
27528 |
*
|
|
|
27529 |
* This returns that object or mutates an existing hash.
|
|
|
27530 |
*
|
|
|
27531 |
* @param {Object} [hash={}]
|
|
|
27532 |
* An object to be used as event an event hash.
|
|
|
27533 |
*
|
|
|
27534 |
* @return {PluginEventHash}
|
|
|
27535 |
* An event hash object with provided properties mixed-in.
|
|
|
27536 |
*/
|
|
|
27537 |
getEventHash(hash = {}) {
|
|
|
27538 |
hash.name = this.name;
|
|
|
27539 |
hash.plugin = this.constructor;
|
|
|
27540 |
hash.instance = this;
|
|
|
27541 |
return hash;
|
|
|
27542 |
}
|
|
|
27543 |
|
|
|
27544 |
/**
|
|
|
27545 |
* Triggers an event on the plugin object and overrides
|
|
|
27546 |
* {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
|
|
|
27547 |
*
|
|
|
27548 |
* @param {string|Object} event
|
|
|
27549 |
* An event type or an object with a type property.
|
|
|
27550 |
*
|
|
|
27551 |
* @param {Object} [hash={}]
|
|
|
27552 |
* Additional data hash to merge with a
|
|
|
27553 |
* {@link PluginEventHash|PluginEventHash}.
|
|
|
27554 |
*
|
|
|
27555 |
* @return {boolean}
|
|
|
27556 |
* Whether or not default was prevented.
|
|
|
27557 |
*/
|
|
|
27558 |
trigger(event, hash = {}) {
|
|
|
27559 |
return trigger(this.eventBusEl_, event, this.getEventHash(hash));
|
|
|
27560 |
}
|
|
|
27561 |
|
|
|
27562 |
/**
|
|
|
27563 |
* Handles "statechanged" events on the plugin. No-op by default, override by
|
|
|
27564 |
* subclassing.
|
|
|
27565 |
*
|
|
|
27566 |
* @abstract
|
|
|
27567 |
* @param {Event} e
|
|
|
27568 |
* An event object provided by a "statechanged" event.
|
|
|
27569 |
*
|
|
|
27570 |
* @param {Object} e.changes
|
|
|
27571 |
* An object describing changes that occurred with the "statechanged"
|
|
|
27572 |
* event.
|
|
|
27573 |
*/
|
|
|
27574 |
handleStateChanged(e) {}
|
|
|
27575 |
|
|
|
27576 |
/**
|
|
|
27577 |
* Disposes a plugin.
|
|
|
27578 |
*
|
|
|
27579 |
* Subclasses can override this if they want, but for the sake of safety,
|
|
|
27580 |
* it's probably best to subscribe the "dispose" event.
|
|
|
27581 |
*
|
|
|
27582 |
* @fires Plugin#dispose
|
|
|
27583 |
*/
|
|
|
27584 |
dispose() {
|
|
|
27585 |
const {
|
|
|
27586 |
name,
|
|
|
27587 |
player
|
|
|
27588 |
} = this;
|
|
|
27589 |
|
|
|
27590 |
/**
|
|
|
27591 |
* Signals that a advanced plugin is about to be disposed.
|
|
|
27592 |
*
|
|
|
27593 |
* @event Plugin#dispose
|
|
|
27594 |
* @type {Event}
|
|
|
27595 |
*/
|
|
|
27596 |
this.trigger('dispose');
|
|
|
27597 |
this.off();
|
|
|
27598 |
player.off('dispose', this.dispose);
|
|
|
27599 |
|
|
|
27600 |
// Eliminate any possible sources of leaking memory by clearing up
|
|
|
27601 |
// references between the player and the plugin instance and nulling out
|
|
|
27602 |
// the plugin's state and replacing methods with a function that throws.
|
|
|
27603 |
player[PLUGIN_CACHE_KEY][name] = false;
|
|
|
27604 |
this.player = this.state = null;
|
|
|
27605 |
|
|
|
27606 |
// Finally, replace the plugin name on the player with a new factory
|
|
|
27607 |
// function, so that the plugin is ready to be set up again.
|
|
|
27608 |
player[name] = createPluginFactory(name, pluginStorage[name]);
|
|
|
27609 |
}
|
|
|
27610 |
|
|
|
27611 |
/**
|
|
|
27612 |
* Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
|
|
|
27613 |
*
|
|
|
27614 |
* @param {string|Function} plugin
|
|
|
27615 |
* If a string, matches the name of a plugin. If a function, will be
|
|
|
27616 |
* tested directly.
|
|
|
27617 |
*
|
|
|
27618 |
* @return {boolean}
|
|
|
27619 |
* Whether or not a plugin is a basic plugin.
|
|
|
27620 |
*/
|
|
|
27621 |
static isBasic(plugin) {
|
|
|
27622 |
const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
|
|
|
27623 |
return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
|
|
|
27624 |
}
|
|
|
27625 |
|
|
|
27626 |
/**
|
|
|
27627 |
* Register a Video.js plugin.
|
|
|
27628 |
*
|
|
|
27629 |
* @param {string} name
|
|
|
27630 |
* The name of the plugin to be registered. Must be a string and
|
|
|
27631 |
* must not match an existing plugin or a method on the `Player`
|
|
|
27632 |
* prototype.
|
|
|
27633 |
*
|
|
|
27634 |
* @param {typeof Plugin|Function} plugin
|
|
|
27635 |
* A sub-class of `Plugin` or a function for basic plugins.
|
|
|
27636 |
*
|
|
|
27637 |
* @return {typeof Plugin|Function}
|
|
|
27638 |
* For advanced plugins, a factory function for that plugin. For
|
|
|
27639 |
* basic plugins, a wrapper function that initializes the plugin.
|
|
|
27640 |
*/
|
|
|
27641 |
static registerPlugin(name, plugin) {
|
|
|
27642 |
if (typeof name !== 'string') {
|
|
|
27643 |
throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
|
|
|
27644 |
}
|
|
|
27645 |
if (pluginExists(name)) {
|
|
|
27646 |
log$1.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
|
|
|
27647 |
} else if (Player.prototype.hasOwnProperty(name)) {
|
|
|
27648 |
throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
|
|
|
27649 |
}
|
|
|
27650 |
if (typeof plugin !== 'function') {
|
|
|
27651 |
throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
|
|
|
27652 |
}
|
|
|
27653 |
pluginStorage[name] = plugin;
|
|
|
27654 |
|
|
|
27655 |
// Add a player prototype method for all sub-classed plugins (but not for
|
|
|
27656 |
// the base Plugin class).
|
|
|
27657 |
if (name !== BASE_PLUGIN_NAME) {
|
|
|
27658 |
if (Plugin.isBasic(plugin)) {
|
|
|
27659 |
Player.prototype[name] = createBasicPlugin(name, plugin);
|
|
|
27660 |
} else {
|
|
|
27661 |
Player.prototype[name] = createPluginFactory(name, plugin);
|
|
|
27662 |
}
|
|
|
27663 |
}
|
|
|
27664 |
return plugin;
|
|
|
27665 |
}
|
|
|
27666 |
|
|
|
27667 |
/**
|
|
|
27668 |
* De-register a Video.js plugin.
|
|
|
27669 |
*
|
|
|
27670 |
* @param {string} name
|
|
|
27671 |
* The name of the plugin to be de-registered. Must be a string that
|
|
|
27672 |
* matches an existing plugin.
|
|
|
27673 |
*
|
|
|
27674 |
* @throws {Error}
|
|
|
27675 |
* If an attempt is made to de-register the base plugin.
|
|
|
27676 |
*/
|
|
|
27677 |
static deregisterPlugin(name) {
|
|
|
27678 |
if (name === BASE_PLUGIN_NAME) {
|
|
|
27679 |
throw new Error('Cannot de-register base plugin.');
|
|
|
27680 |
}
|
|
|
27681 |
if (pluginExists(name)) {
|
|
|
27682 |
delete pluginStorage[name];
|
|
|
27683 |
delete Player.prototype[name];
|
|
|
27684 |
}
|
|
|
27685 |
}
|
|
|
27686 |
|
|
|
27687 |
/**
|
|
|
27688 |
* Gets an object containing multiple Video.js plugins.
|
|
|
27689 |
*
|
|
|
27690 |
* @param {Array} [names]
|
|
|
27691 |
* If provided, should be an array of plugin names. Defaults to _all_
|
|
|
27692 |
* plugin names.
|
|
|
27693 |
*
|
|
|
27694 |
* @return {Object|undefined}
|
|
|
27695 |
* An object containing plugin(s) associated with their name(s) or
|
|
|
27696 |
* `undefined` if no matching plugins exist).
|
|
|
27697 |
*/
|
|
|
27698 |
static getPlugins(names = Object.keys(pluginStorage)) {
|
|
|
27699 |
let result;
|
|
|
27700 |
names.forEach(name => {
|
|
|
27701 |
const plugin = getPlugin(name);
|
|
|
27702 |
if (plugin) {
|
|
|
27703 |
result = result || {};
|
|
|
27704 |
result[name] = plugin;
|
|
|
27705 |
}
|
|
|
27706 |
});
|
|
|
27707 |
return result;
|
|
|
27708 |
}
|
|
|
27709 |
|
|
|
27710 |
/**
|
|
|
27711 |
* Gets a plugin's version, if available
|
|
|
27712 |
*
|
|
|
27713 |
* @param {string} name
|
|
|
27714 |
* The name of a plugin.
|
|
|
27715 |
*
|
|
|
27716 |
* @return {string}
|
|
|
27717 |
* The plugin's version or an empty string.
|
|
|
27718 |
*/
|
|
|
27719 |
static getPluginVersion(name) {
|
|
|
27720 |
const plugin = getPlugin(name);
|
|
|
27721 |
return plugin && plugin.VERSION || '';
|
|
|
27722 |
}
|
|
|
27723 |
}
|
|
|
27724 |
|
|
|
27725 |
/**
|
|
|
27726 |
* Gets a plugin by name if it exists.
|
|
|
27727 |
*
|
|
|
27728 |
* @static
|
|
|
27729 |
* @method getPlugin
|
|
|
27730 |
* @memberOf Plugin
|
|
|
27731 |
* @param {string} name
|
|
|
27732 |
* The name of a plugin.
|
|
|
27733 |
*
|
|
|
27734 |
* @returns {typeof Plugin|Function|undefined}
|
|
|
27735 |
* The plugin (or `undefined`).
|
|
|
27736 |
*/
|
|
|
27737 |
Plugin.getPlugin = getPlugin;
|
|
|
27738 |
|
|
|
27739 |
/**
|
|
|
27740 |
* The name of the base plugin class as it is registered.
|
|
|
27741 |
*
|
|
|
27742 |
* @type {string}
|
|
|
27743 |
*/
|
|
|
27744 |
Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
|
|
|
27745 |
Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
|
|
|
27746 |
|
|
|
27747 |
/**
|
|
|
27748 |
* Documented in player.js
|
|
|
27749 |
*
|
|
|
27750 |
* @ignore
|
|
|
27751 |
*/
|
|
|
27752 |
Player.prototype.usingPlugin = function (name) {
|
|
|
27753 |
return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
|
|
|
27754 |
};
|
|
|
27755 |
|
|
|
27756 |
/**
|
|
|
27757 |
* Documented in player.js
|
|
|
27758 |
*
|
|
|
27759 |
* @ignore
|
|
|
27760 |
*/
|
|
|
27761 |
Player.prototype.hasPlugin = function (name) {
|
|
|
27762 |
return !!pluginExists(name);
|
|
|
27763 |
};
|
|
|
27764 |
|
|
|
27765 |
/**
|
|
|
27766 |
* Signals that a plugin is about to be set up on a player.
|
|
|
27767 |
*
|
|
|
27768 |
* @event Player#beforepluginsetup
|
|
|
27769 |
* @type {PluginEventHash}
|
|
|
27770 |
*/
|
|
|
27771 |
|
|
|
27772 |
/**
|
|
|
27773 |
* Signals that a plugin is about to be set up on a player - by name. The name
|
|
|
27774 |
* is the name of the plugin.
|
|
|
27775 |
*
|
|
|
27776 |
* @event Player#beforepluginsetup:$name
|
|
|
27777 |
* @type {PluginEventHash}
|
|
|
27778 |
*/
|
|
|
27779 |
|
|
|
27780 |
/**
|
|
|
27781 |
* Signals that a plugin has just been set up on a player.
|
|
|
27782 |
*
|
|
|
27783 |
* @event Player#pluginsetup
|
|
|
27784 |
* @type {PluginEventHash}
|
|
|
27785 |
*/
|
|
|
27786 |
|
|
|
27787 |
/**
|
|
|
27788 |
* Signals that a plugin has just been set up on a player - by name. The name
|
|
|
27789 |
* is the name of the plugin.
|
|
|
27790 |
*
|
|
|
27791 |
* @event Player#pluginsetup:$name
|
|
|
27792 |
* @type {PluginEventHash}
|
|
|
27793 |
*/
|
|
|
27794 |
|
|
|
27795 |
/**
|
|
|
27796 |
* @typedef {Object} PluginEventHash
|
|
|
27797 |
*
|
|
|
27798 |
* @property {string} instance
|
|
|
27799 |
* For basic plugins, the return value of the plugin function. For
|
|
|
27800 |
* advanced plugins, the plugin instance on which the event is fired.
|
|
|
27801 |
*
|
|
|
27802 |
* @property {string} name
|
|
|
27803 |
* The name of the plugin.
|
|
|
27804 |
*
|
|
|
27805 |
* @property {string} plugin
|
|
|
27806 |
* For basic plugins, the plugin function. For advanced plugins, the
|
|
|
27807 |
* plugin class/constructor.
|
|
|
27808 |
*/
|
|
|
27809 |
|
|
|
27810 |
/**
|
|
|
27811 |
* @file deprecate.js
|
|
|
27812 |
* @module deprecate
|
|
|
27813 |
*/
|
|
|
27814 |
|
|
|
27815 |
/**
|
|
|
27816 |
* Decorate a function with a deprecation message the first time it is called.
|
|
|
27817 |
*
|
|
|
27818 |
* @param {string} message
|
|
|
27819 |
* A deprecation message to log the first time the returned function
|
|
|
27820 |
* is called.
|
|
|
27821 |
*
|
|
|
27822 |
* @param {Function} fn
|
|
|
27823 |
* The function to be deprecated.
|
|
|
27824 |
*
|
|
|
27825 |
* @return {Function}
|
|
|
27826 |
* A wrapper function that will log a deprecation warning the first
|
|
|
27827 |
* time it is called. The return value will be the return value of
|
|
|
27828 |
* the wrapped function.
|
|
|
27829 |
*/
|
|
|
27830 |
function deprecate(message, fn) {
|
|
|
27831 |
let warned = false;
|
|
|
27832 |
return function (...args) {
|
|
|
27833 |
if (!warned) {
|
|
|
27834 |
log$1.warn(message);
|
|
|
27835 |
}
|
|
|
27836 |
warned = true;
|
|
|
27837 |
return fn.apply(this, args);
|
|
|
27838 |
};
|
|
|
27839 |
}
|
|
|
27840 |
|
|
|
27841 |
/**
|
|
|
27842 |
* Internal function used to mark a function as deprecated in the next major
|
|
|
27843 |
* version with consistent messaging.
|
|
|
27844 |
*
|
|
|
27845 |
* @param {number} major The major version where it will be removed
|
|
|
27846 |
* @param {string} oldName The old function name
|
|
|
27847 |
* @param {string} newName The new function name
|
|
|
27848 |
* @param {Function} fn The function to deprecate
|
|
|
27849 |
* @return {Function} The decorated function
|
|
|
27850 |
*/
|
|
|
27851 |
function deprecateForMajor(major, oldName, newName, fn) {
|
|
|
27852 |
return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
|
|
|
27853 |
}
|
|
|
27854 |
|
|
|
27855 |
/**
|
|
|
27856 |
* @file video.js
|
|
|
27857 |
* @module videojs
|
|
|
27858 |
*/
|
|
|
27859 |
|
|
|
27860 |
/**
|
|
|
27861 |
* Normalize an `id` value by trimming off a leading `#`
|
|
|
27862 |
*
|
|
|
27863 |
* @private
|
|
|
27864 |
* @param {string} id
|
|
|
27865 |
* A string, maybe with a leading `#`.
|
|
|
27866 |
*
|
|
|
27867 |
* @return {string}
|
|
|
27868 |
* The string, without any leading `#`.
|
|
|
27869 |
*/
|
|
|
27870 |
const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
|
|
|
27871 |
|
|
|
27872 |
/**
|
|
|
27873 |
* A callback that is called when a component is ready. Does not have any
|
|
|
27874 |
* parameters and any callback value will be ignored. See: {@link Component~ReadyCallback}
|
|
|
27875 |
*
|
|
|
27876 |
* @callback ReadyCallback
|
|
|
27877 |
*/
|
|
|
27878 |
|
|
|
27879 |
/**
|
|
|
27880 |
* The `videojs()` function doubles as the main function for users to create a
|
|
|
27881 |
* {@link Player} instance as well as the main library namespace.
|
|
|
27882 |
*
|
|
|
27883 |
* It can also be used as a getter for a pre-existing {@link Player} instance.
|
|
|
27884 |
* However, we _strongly_ recommend using `videojs.getPlayer()` for this
|
|
|
27885 |
* purpose because it avoids any potential for unintended initialization.
|
|
|
27886 |
*
|
|
|
27887 |
* Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
|
|
|
27888 |
* of our JSDoc template, we cannot properly document this as both a function
|
|
|
27889 |
* and a namespace, so its function signature is documented here.
|
|
|
27890 |
*
|
|
|
27891 |
* #### Arguments
|
|
|
27892 |
* ##### id
|
|
|
27893 |
* string|Element, **required**
|
|
|
27894 |
*
|
|
|
27895 |
* Video element or video element ID.
|
|
|
27896 |
*
|
|
|
27897 |
* ##### options
|
|
|
27898 |
* Object, optional
|
|
|
27899 |
*
|
|
|
27900 |
* Options object for providing settings.
|
|
|
27901 |
* See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
|
|
|
27902 |
*
|
|
|
27903 |
* ##### ready
|
|
|
27904 |
* {@link Component~ReadyCallback}, optional
|
|
|
27905 |
*
|
|
|
27906 |
* A function to be called when the {@link Player} and {@link Tech} are ready.
|
|
|
27907 |
*
|
|
|
27908 |
* #### Return Value
|
|
|
27909 |
*
|
|
|
27910 |
* The `videojs()` function returns a {@link Player} instance.
|
|
|
27911 |
*
|
|
|
27912 |
* @namespace
|
|
|
27913 |
*
|
|
|
27914 |
* @borrows AudioTrack as AudioTrack
|
|
|
27915 |
* @borrows Component.getComponent as getComponent
|
|
|
27916 |
* @borrows module:events.on as on
|
|
|
27917 |
* @borrows module:events.one as one
|
|
|
27918 |
* @borrows module:events.off as off
|
|
|
27919 |
* @borrows module:events.trigger as trigger
|
|
|
27920 |
* @borrows EventTarget as EventTarget
|
|
|
27921 |
* @borrows module:middleware.use as use
|
|
|
27922 |
* @borrows Player.players as players
|
|
|
27923 |
* @borrows Plugin.registerPlugin as registerPlugin
|
|
|
27924 |
* @borrows Plugin.deregisterPlugin as deregisterPlugin
|
|
|
27925 |
* @borrows Plugin.getPlugins as getPlugins
|
|
|
27926 |
* @borrows Plugin.getPlugin as getPlugin
|
|
|
27927 |
* @borrows Plugin.getPluginVersion as getPluginVersion
|
|
|
27928 |
* @borrows Tech.getTech as getTech
|
|
|
27929 |
* @borrows Tech.registerTech as registerTech
|
|
|
27930 |
* @borrows TextTrack as TextTrack
|
|
|
27931 |
* @borrows VideoTrack as VideoTrack
|
|
|
27932 |
*
|
|
|
27933 |
* @param {string|Element} id
|
|
|
27934 |
* Video element or video element ID.
|
|
|
27935 |
*
|
|
|
27936 |
* @param {Object} [options]
|
|
|
27937 |
* Options object for providing settings.
|
|
|
27938 |
* See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
|
|
|
27939 |
*
|
|
|
27940 |
* @param {ReadyCallback} [ready]
|
|
|
27941 |
* A function to be called when the {@link Player} and {@link Tech} are
|
|
|
27942 |
* ready.
|
|
|
27943 |
*
|
|
|
27944 |
* @return {Player}
|
|
|
27945 |
* The `videojs()` function returns a {@link Player|Player} instance.
|
|
|
27946 |
*/
|
|
|
27947 |
function videojs(id, options, ready) {
|
|
|
27948 |
let player = videojs.getPlayer(id);
|
|
|
27949 |
if (player) {
|
|
|
27950 |
if (options) {
|
|
|
27951 |
log$1.warn(`Player "${id}" is already initialised. Options will not be applied.`);
|
|
|
27952 |
}
|
|
|
27953 |
if (ready) {
|
|
|
27954 |
player.ready(ready);
|
|
|
27955 |
}
|
|
|
27956 |
return player;
|
|
|
27957 |
}
|
|
|
27958 |
const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
|
|
|
27959 |
if (!isEl(el)) {
|
|
|
27960 |
throw new TypeError('The element or ID supplied is not valid. (videojs)');
|
|
|
27961 |
}
|
|
|
27962 |
|
|
|
27963 |
// document.body.contains(el) will only check if el is contained within that one document.
|
|
|
27964 |
// This causes problems for elements in iframes.
|
|
|
27965 |
// Instead, use the element's ownerDocument instead of the global document.
|
|
|
27966 |
// This will make sure that the element is indeed in the dom of that document.
|
|
|
27967 |
// Additionally, check that the document in question has a default view.
|
|
|
27968 |
// If the document is no longer attached to the dom, the defaultView of the document will be null.
|
|
|
27969 |
// If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body
|
|
|
27970 |
// always returns false. Instead, use the Shadow DOM root.
|
|
|
27971 |
const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window.ShadowRoot : false;
|
|
|
27972 |
const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body;
|
|
|
27973 |
if (!el.ownerDocument.defaultView || !rootNode.contains(el)) {
|
|
|
27974 |
log$1.warn('The element supplied is not included in the DOM');
|
|
|
27975 |
}
|
|
|
27976 |
options = options || {};
|
|
|
27977 |
|
|
|
27978 |
// Store a copy of the el before modification, if it is to be restored in destroy()
|
|
|
27979 |
// If div ingest, store the parent div
|
|
|
27980 |
if (options.restoreEl === true) {
|
|
|
27981 |
options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
|
|
|
27982 |
}
|
|
|
27983 |
hooks('beforesetup').forEach(hookFunction => {
|
|
|
27984 |
const opts = hookFunction(el, merge$2(options));
|
|
|
27985 |
if (!isObject$1(opts) || Array.isArray(opts)) {
|
|
|
27986 |
log$1.error('please return an object in beforesetup hooks');
|
|
|
27987 |
return;
|
|
|
27988 |
}
|
|
|
27989 |
options = merge$2(options, opts);
|
|
|
27990 |
});
|
|
|
27991 |
|
|
|
27992 |
// We get the current "Player" component here in case an integration has
|
|
|
27993 |
// replaced it with a custom player.
|
|
|
27994 |
const PlayerComponent = Component$1.getComponent('Player');
|
|
|
27995 |
player = new PlayerComponent(el, options, ready);
|
|
|
27996 |
hooks('setup').forEach(hookFunction => hookFunction(player));
|
|
|
27997 |
return player;
|
|
|
27998 |
}
|
|
|
27999 |
videojs.hooks_ = hooks_;
|
|
|
28000 |
videojs.hooks = hooks;
|
|
|
28001 |
videojs.hook = hook;
|
|
|
28002 |
videojs.hookOnce = hookOnce;
|
|
|
28003 |
videojs.removeHook = removeHook;
|
|
|
28004 |
|
|
|
28005 |
// Add default styles
|
|
|
28006 |
if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
|
|
|
28007 |
let style = $('.vjs-styles-defaults');
|
|
|
28008 |
if (!style) {
|
|
|
28009 |
style = createStyleElement('vjs-styles-defaults');
|
|
|
28010 |
const head = $('head');
|
|
|
28011 |
if (head) {
|
|
|
28012 |
head.insertBefore(style, head.firstChild);
|
|
|
28013 |
}
|
|
|
28014 |
setTextContent(style, `
|
|
|
28015 |
.video-js {
|
|
|
28016 |
width: 300px;
|
|
|
28017 |
height: 150px;
|
|
|
28018 |
}
|
|
|
28019 |
|
|
|
28020 |
.vjs-fluid:not(.vjs-audio-only-mode) {
|
|
|
28021 |
padding-top: 56.25%
|
|
|
28022 |
}
|
|
|
28023 |
`);
|
|
|
28024 |
}
|
|
|
28025 |
}
|
|
|
28026 |
|
|
|
28027 |
// Run Auto-load players
|
|
|
28028 |
// You have to wait at least once in case this script is loaded after your
|
|
|
28029 |
// video in the DOM (weird behavior only with minified version)
|
|
|
28030 |
autoSetupTimeout(1, videojs);
|
|
|
28031 |
|
|
|
28032 |
/**
|
|
|
28033 |
* Current Video.js version. Follows [semantic versioning](https://semver.org/).
|
|
|
28034 |
*
|
|
|
28035 |
* @type {string}
|
|
|
28036 |
*/
|
|
|
28037 |
videojs.VERSION = version$5;
|
|
|
28038 |
|
|
|
28039 |
/**
|
|
|
28040 |
* The global options object. These are the settings that take effect
|
|
|
28041 |
* if no overrides are specified when the player is created.
|
|
|
28042 |
*
|
|
|
28043 |
* @type {Object}
|
|
|
28044 |
*/
|
|
|
28045 |
videojs.options = Player.prototype.options_;
|
|
|
28046 |
|
|
|
28047 |
/**
|
|
|
28048 |
* Get an object with the currently created players, keyed by player ID
|
|
|
28049 |
*
|
|
|
28050 |
* @return {Object}
|
|
|
28051 |
* The created players
|
|
|
28052 |
*/
|
|
|
28053 |
videojs.getPlayers = () => Player.players;
|
|
|
28054 |
|
|
|
28055 |
/**
|
|
|
28056 |
* Get a single player based on an ID or DOM element.
|
|
|
28057 |
*
|
|
|
28058 |
* This is useful if you want to check if an element or ID has an associated
|
|
|
28059 |
* Video.js player, but not create one if it doesn't.
|
|
|
28060 |
*
|
|
|
28061 |
* @param {string|Element} id
|
|
|
28062 |
* An HTML element - `<video>`, `<audio>`, or `<video-js>` -
|
|
|
28063 |
* or a string matching the `id` of such an element.
|
|
|
28064 |
*
|
|
|
28065 |
* @return {Player|undefined}
|
|
|
28066 |
* A player instance or `undefined` if there is no player instance
|
|
|
28067 |
* matching the argument.
|
|
|
28068 |
*/
|
|
|
28069 |
videojs.getPlayer = id => {
|
|
|
28070 |
const players = Player.players;
|
|
|
28071 |
let tag;
|
|
|
28072 |
if (typeof id === 'string') {
|
|
|
28073 |
const nId = normalizeId(id);
|
|
|
28074 |
const player = players[nId];
|
|
|
28075 |
if (player) {
|
|
|
28076 |
return player;
|
|
|
28077 |
}
|
|
|
28078 |
tag = $('#' + nId);
|
|
|
28079 |
} else {
|
|
|
28080 |
tag = id;
|
|
|
28081 |
}
|
|
|
28082 |
if (isEl(tag)) {
|
|
|
28083 |
const {
|
|
|
28084 |
player,
|
|
|
28085 |
playerId
|
|
|
28086 |
} = tag;
|
|
|
28087 |
|
|
|
28088 |
// Element may have a `player` property referring to an already created
|
|
|
28089 |
// player instance. If so, return that.
|
|
|
28090 |
if (player || players[playerId]) {
|
|
|
28091 |
return player || players[playerId];
|
|
|
28092 |
}
|
|
|
28093 |
}
|
|
|
28094 |
};
|
|
|
28095 |
|
|
|
28096 |
/**
|
|
|
28097 |
* Returns an array of all current players.
|
|
|
28098 |
*
|
|
|
28099 |
* @return {Array}
|
|
|
28100 |
* An array of all players. The array will be in the order that
|
|
|
28101 |
* `Object.keys` provides, which could potentially vary between
|
|
|
28102 |
* JavaScript engines.
|
|
|
28103 |
*
|
|
|
28104 |
*/
|
|
|
28105 |
videojs.getAllPlayers = () =>
|
|
|
28106 |
// Disposed players leave a key with a `null` value, so we need to make sure
|
|
|
28107 |
// we filter those out.
|
|
|
28108 |
Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
|
|
|
28109 |
videojs.players = Player.players;
|
|
|
28110 |
videojs.getComponent = Component$1.getComponent;
|
|
|
28111 |
|
|
|
28112 |
/**
|
|
|
28113 |
* Register a component so it can referred to by name. Used when adding to other
|
|
|
28114 |
* components, either through addChild `component.addChild('myComponent')` or through
|
|
|
28115 |
* default children options `{ children: ['myComponent'] }`.
|
|
|
28116 |
*
|
|
|
28117 |
* > NOTE: You could also just initialize the component before adding.
|
|
|
28118 |
* `component.addChild(new MyComponent());`
|
|
|
28119 |
*
|
|
|
28120 |
* @param {string} name
|
|
|
28121 |
* The class name of the component
|
|
|
28122 |
*
|
|
|
28123 |
* @param {typeof Component} comp
|
|
|
28124 |
* The component class
|
|
|
28125 |
*
|
|
|
28126 |
* @return {typeof Component}
|
|
|
28127 |
* The newly registered component
|
|
|
28128 |
*/
|
|
|
28129 |
videojs.registerComponent = (name, comp) => {
|
|
|
28130 |
if (Tech.isTech(comp)) {
|
|
|
28131 |
log$1.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
|
|
|
28132 |
}
|
|
|
28133 |
return Component$1.registerComponent.call(Component$1, name, comp);
|
|
|
28134 |
};
|
|
|
28135 |
videojs.getTech = Tech.getTech;
|
|
|
28136 |
videojs.registerTech = Tech.registerTech;
|
|
|
28137 |
videojs.use = use;
|
|
|
28138 |
|
|
|
28139 |
/**
|
|
|
28140 |
* An object that can be returned by a middleware to signify
|
|
|
28141 |
* that the middleware is being terminated.
|
|
|
28142 |
*
|
|
|
28143 |
* @type {object}
|
|
|
28144 |
* @property {object} middleware.TERMINATOR
|
|
|
28145 |
*/
|
|
|
28146 |
Object.defineProperty(videojs, 'middleware', {
|
|
|
28147 |
value: {},
|
|
|
28148 |
writeable: false,
|
|
|
28149 |
enumerable: true
|
|
|
28150 |
});
|
|
|
28151 |
Object.defineProperty(videojs.middleware, 'TERMINATOR', {
|
|
|
28152 |
value: TERMINATOR,
|
|
|
28153 |
writeable: false,
|
|
|
28154 |
enumerable: true
|
|
|
28155 |
});
|
|
|
28156 |
|
|
|
28157 |
/**
|
|
|
28158 |
* A reference to the {@link module:browser|browser utility module} as an object.
|
|
|
28159 |
*
|
|
|
28160 |
* @type {Object}
|
|
|
28161 |
* @see {@link module:browser|browser}
|
|
|
28162 |
*/
|
|
|
28163 |
videojs.browser = browser;
|
|
|
28164 |
|
|
|
28165 |
/**
|
|
|
28166 |
* A reference to the {@link module:obj|obj utility module} as an object.
|
|
|
28167 |
*
|
|
|
28168 |
* @type {Object}
|
|
|
28169 |
* @see {@link module:obj|obj}
|
|
|
28170 |
*/
|
|
|
28171 |
videojs.obj = Obj;
|
|
|
28172 |
|
|
|
28173 |
/**
|
|
|
28174 |
* Deprecated reference to the {@link module:obj.merge|merge function}
|
|
|
28175 |
*
|
|
|
28176 |
* @type {Function}
|
|
|
28177 |
* @see {@link module:obj.merge|merge}
|
|
|
28178 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
|
|
|
28179 |
*/
|
|
|
28180 |
videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge$2);
|
|
|
28181 |
|
|
|
28182 |
/**
|
|
|
28183 |
* Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
|
|
|
28184 |
*
|
|
|
28185 |
* @type {Function}
|
|
|
28186 |
* @see {@link module:obj.defineLazyProperty|defineLazyProperty}
|
|
|
28187 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
|
|
|
28188 |
*/
|
|
|
28189 |
videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
|
|
|
28190 |
|
|
|
28191 |
/**
|
|
|
28192 |
* Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
|
|
|
28193 |
*
|
|
|
28194 |
* @type {Function}
|
|
|
28195 |
* @see {@link module:fn.bind_|fn.bind_}
|
|
|
28196 |
* @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
|
|
|
28197 |
*/
|
|
|
28198 |
videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
|
|
|
28199 |
videojs.registerPlugin = Plugin.registerPlugin;
|
|
|
28200 |
videojs.deregisterPlugin = Plugin.deregisterPlugin;
|
|
|
28201 |
|
|
|
28202 |
/**
|
|
|
28203 |
* Deprecated method to register a plugin with Video.js
|
|
|
28204 |
*
|
|
|
28205 |
* @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
|
|
|
28206 |
*
|
|
|
28207 |
* @param {string} name
|
|
|
28208 |
* The plugin name
|
|
|
28209 |
*
|
|
|
28210 |
* @param {typeof Plugin|Function} plugin
|
|
|
28211 |
* The plugin sub-class or function
|
|
|
28212 |
*
|
|
|
28213 |
* @return {typeof Plugin|Function}
|
|
|
28214 |
*/
|
|
|
28215 |
videojs.plugin = (name, plugin) => {
|
|
|
28216 |
log$1.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
|
|
|
28217 |
return Plugin.registerPlugin(name, plugin);
|
|
|
28218 |
};
|
|
|
28219 |
videojs.getPlugins = Plugin.getPlugins;
|
|
|
28220 |
videojs.getPlugin = Plugin.getPlugin;
|
|
|
28221 |
videojs.getPluginVersion = Plugin.getPluginVersion;
|
|
|
28222 |
|
|
|
28223 |
/**
|
|
|
28224 |
* Adding languages so that they're available to all players.
|
|
|
28225 |
* Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
|
|
|
28226 |
*
|
|
|
28227 |
* @param {string} code
|
|
|
28228 |
* The language code or dictionary property
|
|
|
28229 |
*
|
|
|
28230 |
* @param {Object} data
|
|
|
28231 |
* The data values to be translated
|
|
|
28232 |
*
|
|
|
28233 |
* @return {Object}
|
|
|
28234 |
* The resulting language dictionary object
|
|
|
28235 |
*/
|
|
|
28236 |
videojs.addLanguage = function (code, data) {
|
|
|
28237 |
code = ('' + code).toLowerCase();
|
|
|
28238 |
videojs.options.languages = merge$2(videojs.options.languages, {
|
|
|
28239 |
[code]: data
|
|
|
28240 |
});
|
|
|
28241 |
return videojs.options.languages[code];
|
|
|
28242 |
};
|
|
|
28243 |
|
|
|
28244 |
/**
|
|
|
28245 |
* A reference to the {@link module:log|log utility module} as an object.
|
|
|
28246 |
*
|
|
|
28247 |
* @type {Function}
|
|
|
28248 |
* @see {@link module:log|log}
|
|
|
28249 |
*/
|
|
|
28250 |
videojs.log = log$1;
|
|
|
28251 |
videojs.createLogger = createLogger;
|
|
|
28252 |
|
|
|
28253 |
/**
|
|
|
28254 |
* A reference to the {@link module:time|time utility module} as an object.
|
|
|
28255 |
*
|
|
|
28256 |
* @type {Object}
|
|
|
28257 |
* @see {@link module:time|time}
|
|
|
28258 |
*/
|
|
|
28259 |
videojs.time = Time;
|
|
|
28260 |
|
|
|
28261 |
/**
|
|
|
28262 |
* Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
|
|
|
28263 |
*
|
|
|
28264 |
* @type {Function}
|
|
|
28265 |
* @see {@link module:time.createTimeRanges|createTimeRanges}
|
|
|
28266 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
|
|
|
28267 |
*/
|
|
|
28268 |
videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges$1);
|
|
|
28269 |
|
|
|
28270 |
/**
|
|
|
28271 |
* Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
|
|
|
28272 |
*
|
|
|
28273 |
* @type {Function}
|
|
|
28274 |
* @see {@link module:time.createTimeRanges|createTimeRanges}
|
|
|
28275 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
|
|
|
28276 |
*/
|
|
|
28277 |
videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges$1);
|
|
|
28278 |
|
|
|
28279 |
/**
|
|
|
28280 |
* Deprecated reference to the {@link module:time.formatTime|formatTime function}
|
|
|
28281 |
*
|
|
|
28282 |
* @type {Function}
|
|
|
28283 |
* @see {@link module:time.formatTime|formatTime}
|
|
|
28284 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
|
|
|
28285 |
*/
|
|
|
28286 |
videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
|
|
|
28287 |
|
|
|
28288 |
/**
|
|
|
28289 |
* Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
|
|
|
28290 |
*
|
|
|
28291 |
* @type {Function}
|
|
|
28292 |
* @see {@link module:time.setFormatTime|setFormatTime}
|
|
|
28293 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
|
|
|
28294 |
*/
|
|
|
28295 |
videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
|
|
|
28296 |
|
|
|
28297 |
/**
|
|
|
28298 |
* Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
|
|
|
28299 |
*
|
|
|
28300 |
* @type {Function}
|
|
|
28301 |
* @see {@link module:time.resetFormatTime|resetFormatTime}
|
|
|
28302 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
|
|
|
28303 |
*/
|
|
|
28304 |
videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
|
|
|
28305 |
|
|
|
28306 |
/**
|
|
|
28307 |
* Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
|
|
|
28308 |
*
|
|
|
28309 |
* @type {Function}
|
|
|
28310 |
* @see {@link module:url.parseUrl|parseUrl}
|
|
|
28311 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
|
|
|
28312 |
*/
|
|
|
28313 |
videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
|
|
|
28314 |
|
|
|
28315 |
/**
|
|
|
28316 |
* Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
|
|
|
28317 |
*
|
|
|
28318 |
* @type {Function}
|
|
|
28319 |
* @see {@link module:url.isCrossOrigin|isCrossOrigin}
|
|
|
28320 |
* @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
|
|
|
28321 |
*/
|
|
|
28322 |
videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
|
|
|
28323 |
videojs.EventTarget = EventTarget$2;
|
|
|
28324 |
videojs.any = any;
|
|
|
28325 |
videojs.on = on;
|
|
|
28326 |
videojs.one = one;
|
|
|
28327 |
videojs.off = off;
|
|
|
28328 |
videojs.trigger = trigger;
|
|
|
28329 |
|
|
|
28330 |
/**
|
|
|
28331 |
* A cross-browser XMLHttpRequest wrapper.
|
|
|
28332 |
*
|
|
|
28333 |
* @function
|
|
|
28334 |
* @param {Object} options
|
|
|
28335 |
* Settings for the request.
|
|
|
28336 |
*
|
|
|
28337 |
* @return {XMLHttpRequest|XDomainRequest}
|
|
|
28338 |
* The request object.
|
|
|
28339 |
*
|
|
|
28340 |
* @see https://github.com/Raynos/xhr
|
|
|
28341 |
*/
|
|
|
28342 |
videojs.xhr = lib;
|
|
|
28343 |
videojs.TextTrack = TextTrack;
|
|
|
28344 |
videojs.AudioTrack = AudioTrack;
|
|
|
28345 |
videojs.VideoTrack = VideoTrack;
|
|
|
28346 |
['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
|
|
|
28347 |
videojs[k] = function () {
|
|
|
28348 |
log$1.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
|
|
|
28349 |
return Dom[k].apply(null, arguments);
|
|
|
28350 |
};
|
|
|
28351 |
});
|
|
|
28352 |
videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
|
|
|
28353 |
|
|
|
28354 |
/**
|
|
|
28355 |
* A reference to the {@link module:dom|DOM utility module} as an object.
|
|
|
28356 |
*
|
|
|
28357 |
* @type {Object}
|
|
|
28358 |
* @see {@link module:dom|dom}
|
|
|
28359 |
*/
|
|
|
28360 |
videojs.dom = Dom;
|
|
|
28361 |
|
|
|
28362 |
/**
|
|
|
28363 |
* A reference to the {@link module:fn|fn utility module} as an object.
|
|
|
28364 |
*
|
|
|
28365 |
* @type {Object}
|
|
|
28366 |
* @see {@link module:fn|fn}
|
|
|
28367 |
*/
|
|
|
28368 |
videojs.fn = Fn;
|
|
|
28369 |
|
|
|
28370 |
/**
|
|
|
28371 |
* A reference to the {@link module:num|num utility module} as an object.
|
|
|
28372 |
*
|
|
|
28373 |
* @type {Object}
|
|
|
28374 |
* @see {@link module:num|num}
|
|
|
28375 |
*/
|
|
|
28376 |
videojs.num = Num;
|
|
|
28377 |
|
|
|
28378 |
/**
|
|
|
28379 |
* A reference to the {@link module:str|str utility module} as an object.
|
|
|
28380 |
*
|
|
|
28381 |
* @type {Object}
|
|
|
28382 |
* @see {@link module:str|str}
|
|
|
28383 |
*/
|
|
|
28384 |
videojs.str = Str;
|
|
|
28385 |
|
|
|
28386 |
/**
|
|
|
28387 |
* A reference to the {@link module:url|URL utility module} as an object.
|
|
|
28388 |
*
|
|
|
28389 |
* @type {Object}
|
|
|
28390 |
* @see {@link module:url|url}
|
|
|
28391 |
*/
|
|
|
28392 |
videojs.url = Url;
|
|
|
28393 |
|
|
|
28394 |
createCommonjsModule(function (module, exports) {
|
|
|
28395 |
/*! @name videojs-contrib-quality-levels @version 4.0.0 @license Apache-2.0 */
|
|
|
28396 |
(function (global, factory) {
|
|
|
28397 |
module.exports = factory(videojs) ;
|
|
|
28398 |
})(commonjsGlobal, function (videojs) {
|
|
|
28399 |
|
|
|
28400 |
function _interopDefaultLegacy(e) {
|
|
|
28401 |
return e && typeof e === 'object' && 'default' in e ? e : {
|
|
|
28402 |
'default': e
|
|
|
28403 |
};
|
|
|
28404 |
}
|
|
|
28405 |
var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
|
|
|
28406 |
|
|
|
28407 |
/**
|
|
|
28408 |
* A single QualityLevel.
|
|
|
28409 |
*
|
|
|
28410 |
* interface QualityLevel {
|
|
|
28411 |
* readonly attribute DOMString id;
|
|
|
28412 |
* attribute DOMString label;
|
|
|
28413 |
* readonly attribute long width;
|
|
|
28414 |
* readonly attribute long height;
|
|
|
28415 |
* readonly attribute long bitrate;
|
|
|
28416 |
* attribute boolean enabled;
|
|
|
28417 |
* };
|
|
|
28418 |
*
|
|
|
28419 |
* @class QualityLevel
|
|
|
28420 |
*/
|
|
|
28421 |
class QualityLevel {
|
|
|
28422 |
/**
|
|
|
28423 |
* Creates a QualityLevel
|
|
|
28424 |
*
|
|
|
28425 |
* @param {Representation|Object} representation The representation of the quality level
|
|
|
28426 |
* @param {string} representation.id Unique id of the QualityLevel
|
|
|
28427 |
* @param {number=} representation.width Resolution width of the QualityLevel
|
|
|
28428 |
* @param {number=} representation.height Resolution height of the QualityLevel
|
|
|
28429 |
* @param {number} representation.bandwidth Bitrate of the QualityLevel
|
|
|
28430 |
* @param {number=} representation.frameRate Frame-rate of the QualityLevel
|
|
|
28431 |
* @param {Function} representation.enabled Callback to enable/disable QualityLevel
|
|
|
28432 |
*/
|
|
|
28433 |
constructor(representation) {
|
|
|
28434 |
let level = this; // eslint-disable-line
|
|
|
28435 |
|
|
|
28436 |
level.id = representation.id;
|
|
|
28437 |
level.label = level.id;
|
|
|
28438 |
level.width = representation.width;
|
|
|
28439 |
level.height = representation.height;
|
|
|
28440 |
level.bitrate = representation.bandwidth;
|
|
|
28441 |
level.frameRate = representation.frameRate;
|
|
|
28442 |
level.enabled_ = representation.enabled;
|
|
|
28443 |
Object.defineProperty(level, 'enabled', {
|
|
|
28444 |
/**
|
|
|
28445 |
* Get whether the QualityLevel is enabled.
|
|
|
28446 |
*
|
|
|
28447 |
* @return {boolean} True if the QualityLevel is enabled.
|
|
|
28448 |
*/
|
|
|
28449 |
get() {
|
|
|
28450 |
return level.enabled_();
|
|
|
28451 |
},
|
|
|
28452 |
/**
|
|
|
28453 |
* Enable or disable the QualityLevel.
|
|
|
28454 |
*
|
|
|
28455 |
* @param {boolean} enable true to enable QualityLevel, false to disable.
|
|
|
28456 |
*/
|
|
|
28457 |
set(enable) {
|
|
|
28458 |
level.enabled_(enable);
|
|
|
28459 |
}
|
|
|
28460 |
});
|
|
|
28461 |
return level;
|
|
|
28462 |
}
|
|
|
28463 |
}
|
|
|
28464 |
|
|
|
28465 |
/**
|
|
|
28466 |
* A list of QualityLevels.
|
|
|
28467 |
*
|
|
|
28468 |
* interface QualityLevelList : EventTarget {
|
|
|
28469 |
* getter QualityLevel (unsigned long index);
|
|
|
28470 |
* readonly attribute unsigned long length;
|
|
|
28471 |
* readonly attribute long selectedIndex;
|
|
|
28472 |
*
|
|
|
28473 |
* void addQualityLevel(QualityLevel qualityLevel)
|
|
|
28474 |
* void removeQualityLevel(QualityLevel remove)
|
|
|
28475 |
* QualityLevel? getQualityLevelById(DOMString id);
|
|
|
28476 |
*
|
|
|
28477 |
* attribute EventHandler onchange;
|
|
|
28478 |
* attribute EventHandler onaddqualitylevel;
|
|
|
28479 |
* attribute EventHandler onremovequalitylevel;
|
|
|
28480 |
* };
|
|
|
28481 |
*
|
|
|
28482 |
* @extends videojs.EventTarget
|
|
|
28483 |
* @class QualityLevelList
|
|
|
28484 |
*/
|
|
|
28485 |
|
|
|
28486 |
class QualityLevelList extends videojs__default['default'].EventTarget {
|
|
|
28487 |
/**
|
|
|
28488 |
* Creates a QualityLevelList.
|
|
|
28489 |
*/
|
|
|
28490 |
constructor() {
|
|
|
28491 |
super();
|
|
|
28492 |
let list = this; // eslint-disable-line
|
|
|
28493 |
|
|
|
28494 |
list.levels_ = [];
|
|
|
28495 |
list.selectedIndex_ = -1;
|
|
|
28496 |
/**
|
|
|
28497 |
* Get the index of the currently selected QualityLevel.
|
|
|
28498 |
*
|
|
|
28499 |
* @returns {number} The index of the selected QualityLevel. -1 if none selected.
|
|
|
28500 |
* @readonly
|
|
|
28501 |
*/
|
|
|
28502 |
|
|
|
28503 |
Object.defineProperty(list, 'selectedIndex', {
|
|
|
28504 |
get() {
|
|
|
28505 |
return list.selectedIndex_;
|
|
|
28506 |
}
|
|
|
28507 |
});
|
|
|
28508 |
/**
|
|
|
28509 |
* Get the length of the list of QualityLevels.
|
|
|
28510 |
*
|
|
|
28511 |
* @returns {number} The length of the list.
|
|
|
28512 |
* @readonly
|
|
|
28513 |
*/
|
|
|
28514 |
|
|
|
28515 |
Object.defineProperty(list, 'length', {
|
|
|
28516 |
get() {
|
|
|
28517 |
return list.levels_.length;
|
|
|
28518 |
}
|
|
|
28519 |
});
|
|
|
28520 |
list[Symbol.iterator] = () => list.levels_.values();
|
|
|
28521 |
return list;
|
|
|
28522 |
}
|
|
|
28523 |
/**
|
|
|
28524 |
* Adds a quality level to the list.
|
|
|
28525 |
*
|
|
|
28526 |
* @param {Representation|Object} representation The representation of the quality level
|
|
|
28527 |
* @param {string} representation.id Unique id of the QualityLevel
|
|
|
28528 |
* @param {number=} representation.width Resolution width of the QualityLevel
|
|
|
28529 |
* @param {number=} representation.height Resolution height of the QualityLevel
|
|
|
28530 |
* @param {number} representation.bandwidth Bitrate of the QualityLevel
|
|
|
28531 |
* @param {number=} representation.frameRate Frame-rate of the QualityLevel
|
|
|
28532 |
* @param {Function} representation.enabled Callback to enable/disable QualityLevel
|
|
|
28533 |
* @return {QualityLevel} the QualityLevel added to the list
|
|
|
28534 |
* @method addQualityLevel
|
|
|
28535 |
*/
|
|
|
28536 |
|
|
|
28537 |
addQualityLevel(representation) {
|
|
|
28538 |
let qualityLevel = this.getQualityLevelById(representation.id); // Do not add duplicate quality levels
|
|
|
28539 |
|
|
|
28540 |
if (qualityLevel) {
|
|
|
28541 |
return qualityLevel;
|
|
|
28542 |
}
|
|
|
28543 |
const index = this.levels_.length;
|
|
|
28544 |
qualityLevel = new QualityLevel(representation);
|
|
|
28545 |
if (!('' + index in this)) {
|
|
|
28546 |
Object.defineProperty(this, index, {
|
|
|
28547 |
get() {
|
|
|
28548 |
return this.levels_[index];
|
|
|
28549 |
}
|
|
|
28550 |
});
|
|
|
28551 |
}
|
|
|
28552 |
this.levels_.push(qualityLevel);
|
|
|
28553 |
this.trigger({
|
|
|
28554 |
qualityLevel,
|
|
|
28555 |
type: 'addqualitylevel'
|
|
|
28556 |
});
|
|
|
28557 |
return qualityLevel;
|
|
|
28558 |
}
|
|
|
28559 |
/**
|
|
|
28560 |
* Removes a quality level from the list.
|
|
|
28561 |
*
|
|
|
28562 |
* @param {QualityLevel} qualityLevel The QualityLevel to remove from the list.
|
|
|
28563 |
* @return {QualityLevel|null} the QualityLevel removed or null if nothing removed
|
|
|
28564 |
* @method removeQualityLevel
|
|
|
28565 |
*/
|
|
|
28566 |
|
|
|
28567 |
removeQualityLevel(qualityLevel) {
|
|
|
28568 |
let removed = null;
|
|
|
28569 |
for (let i = 0, l = this.length; i < l; i++) {
|
|
|
28570 |
if (this[i] === qualityLevel) {
|
|
|
28571 |
removed = this.levels_.splice(i, 1)[0];
|
|
|
28572 |
if (this.selectedIndex_ === i) {
|
|
|
28573 |
this.selectedIndex_ = -1;
|
|
|
28574 |
} else if (this.selectedIndex_ > i) {
|
|
|
28575 |
this.selectedIndex_--;
|
|
|
28576 |
}
|
|
|
28577 |
break;
|
|
|
28578 |
}
|
|
|
28579 |
}
|
|
|
28580 |
if (removed) {
|
|
|
28581 |
this.trigger({
|
|
|
28582 |
qualityLevel,
|
|
|
28583 |
type: 'removequalitylevel'
|
|
|
28584 |
});
|
|
|
28585 |
}
|
|
|
28586 |
return removed;
|
|
|
28587 |
}
|
|
|
28588 |
/**
|
|
|
28589 |
* Searches for a QualityLevel with the given id.
|
|
|
28590 |
*
|
|
|
28591 |
* @param {string} id The id of the QualityLevel to find.
|
|
|
28592 |
* @return {QualityLevel|null} The QualityLevel with id, or null if not found.
|
|
|
28593 |
* @method getQualityLevelById
|
|
|
28594 |
*/
|
|
|
28595 |
|
|
|
28596 |
getQualityLevelById(id) {
|
|
|
28597 |
for (let i = 0, l = this.length; i < l; i++) {
|
|
|
28598 |
const level = this[i];
|
|
|
28599 |
if (level.id === id) {
|
|
|
28600 |
return level;
|
|
|
28601 |
}
|
|
|
28602 |
}
|
|
|
28603 |
return null;
|
|
|
28604 |
}
|
|
|
28605 |
/**
|
|
|
28606 |
* Resets the list of QualityLevels to empty
|
|
|
28607 |
*
|
|
|
28608 |
* @method dispose
|
|
|
28609 |
*/
|
|
|
28610 |
|
|
|
28611 |
dispose() {
|
|
|
28612 |
this.selectedIndex_ = -1;
|
|
|
28613 |
this.levels_.length = 0;
|
|
|
28614 |
}
|
|
|
28615 |
}
|
|
|
28616 |
/**
|
|
|
28617 |
* change - The selected QualityLevel has changed.
|
|
|
28618 |
* addqualitylevel - A QualityLevel has been added to the QualityLevelList.
|
|
|
28619 |
* removequalitylevel - A QualityLevel has been removed from the QualityLevelList.
|
|
|
28620 |
*/
|
|
|
28621 |
|
|
|
28622 |
QualityLevelList.prototype.allowedEvents_ = {
|
|
|
28623 |
change: 'change',
|
|
|
28624 |
addqualitylevel: 'addqualitylevel',
|
|
|
28625 |
removequalitylevel: 'removequalitylevel'
|
|
|
28626 |
}; // emulate attribute EventHandler support to allow for feature detection
|
|
|
28627 |
|
|
|
28628 |
for (const event in QualityLevelList.prototype.allowedEvents_) {
|
|
|
28629 |
QualityLevelList.prototype['on' + event] = null;
|
|
|
28630 |
}
|
|
|
28631 |
var version = "4.0.0";
|
|
|
28632 |
|
|
|
28633 |
/**
|
|
|
28634 |
* Initialization function for the qualityLevels plugin. Sets up the QualityLevelList and
|
|
|
28635 |
* event handlers.
|
|
|
28636 |
*
|
|
|
28637 |
* @param {Player} player Player object.
|
|
|
28638 |
* @param {Object} options Plugin options object.
|
|
|
28639 |
* @return {QualityLevelList} a list of QualityLevels
|
|
|
28640 |
*/
|
|
|
28641 |
|
|
|
28642 |
const initPlugin = function (player, options) {
|
|
|
28643 |
const originalPluginFn = player.qualityLevels;
|
|
|
28644 |
const qualityLevelList = new QualityLevelList();
|
|
|
28645 |
const disposeHandler = function () {
|
|
|
28646 |
qualityLevelList.dispose();
|
|
|
28647 |
player.qualityLevels = originalPluginFn;
|
|
|
28648 |
player.off('dispose', disposeHandler);
|
|
|
28649 |
};
|
|
|
28650 |
player.on('dispose', disposeHandler);
|
|
|
28651 |
player.qualityLevels = () => qualityLevelList;
|
|
|
28652 |
player.qualityLevels.VERSION = version;
|
|
|
28653 |
return qualityLevelList;
|
|
|
28654 |
};
|
|
|
28655 |
/**
|
|
|
28656 |
* A video.js plugin.
|
|
|
28657 |
*
|
|
|
28658 |
* In the plugin function, the value of `this` is a video.js `Player`
|
|
|
28659 |
* instance. You cannot rely on the player being in a "ready" state here,
|
|
|
28660 |
* depending on how the plugin is invoked. This may or may not be important
|
|
|
28661 |
* to you; if not, remove the wait for "ready"!
|
|
|
28662 |
*
|
|
|
28663 |
* @param {Object} options Plugin options object
|
|
|
28664 |
* @return {QualityLevelList} a list of QualityLevels
|
|
|
28665 |
*/
|
|
|
28666 |
|
|
|
28667 |
const qualityLevels = function (options) {
|
|
|
28668 |
return initPlugin(this, videojs__default['default'].obj.merge({}, options));
|
|
|
28669 |
}; // Register the plugin with video.js.
|
|
|
28670 |
|
|
|
28671 |
videojs__default['default'].registerPlugin('qualityLevels', qualityLevels); // Include the version number.
|
|
|
28672 |
|
|
|
28673 |
qualityLevels.VERSION = version;
|
|
|
28674 |
return qualityLevels;
|
|
|
28675 |
});
|
|
|
28676 |
});
|
|
|
28677 |
|
|
|
28678 |
var urlToolkit = createCommonjsModule(function (module, exports) {
|
|
|
28679 |
// see https://tools.ietf.org/html/rfc1808
|
|
|
28680 |
|
|
|
28681 |
(function (root) {
|
|
|
28682 |
var URL_REGEX = /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/;
|
|
|
28683 |
var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/;
|
|
|
28684 |
var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
|
|
|
28685 |
var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g;
|
|
|
28686 |
var URLToolkit = {
|
|
|
28687 |
// If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
|
|
|
28688 |
// E.g
|
|
|
28689 |
// With opts.alwaysNormalize = false (default, spec compliant)
|
|
|
28690 |
// http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
|
|
|
28691 |
// With opts.alwaysNormalize = true (not spec compliant)
|
|
|
28692 |
// http://a.com/b/cd + /e/f/../g => http://a.com/e/g
|
|
|
28693 |
buildAbsoluteURL: function (baseURL, relativeURL, opts) {
|
|
|
28694 |
opts = opts || {};
|
|
|
28695 |
// remove any remaining space and CRLF
|
|
|
28696 |
baseURL = baseURL.trim();
|
|
|
28697 |
relativeURL = relativeURL.trim();
|
|
|
28698 |
if (!relativeURL) {
|
|
|
28699 |
// 2a) If the embedded URL is entirely empty, it inherits the
|
|
|
28700 |
// entire base URL (i.e., is set equal to the base URL)
|
|
|
28701 |
// and we are done.
|
|
|
28702 |
if (!opts.alwaysNormalize) {
|
|
|
28703 |
return baseURL;
|
|
|
28704 |
}
|
|
|
28705 |
var basePartsForNormalise = URLToolkit.parseURL(baseURL);
|
|
|
28706 |
if (!basePartsForNormalise) {
|
|
|
28707 |
throw new Error('Error trying to parse base URL.');
|
|
|
28708 |
}
|
|
|
28709 |
basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
|
|
|
28710 |
return URLToolkit.buildURLFromParts(basePartsForNormalise);
|
|
|
28711 |
}
|
|
|
28712 |
var relativeParts = URLToolkit.parseURL(relativeURL);
|
|
|
28713 |
if (!relativeParts) {
|
|
|
28714 |
throw new Error('Error trying to parse relative URL.');
|
|
|
28715 |
}
|
|
|
28716 |
if (relativeParts.scheme) {
|
|
|
28717 |
// 2b) If the embedded URL starts with a scheme name, it is
|
|
|
28718 |
// interpreted as an absolute URL and we are done.
|
|
|
28719 |
if (!opts.alwaysNormalize) {
|
|
|
28720 |
return relativeURL;
|
|
|
28721 |
}
|
|
|
28722 |
relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
|
|
|
28723 |
return URLToolkit.buildURLFromParts(relativeParts);
|
|
|
28724 |
}
|
|
|
28725 |
var baseParts = URLToolkit.parseURL(baseURL);
|
|
|
28726 |
if (!baseParts) {
|
|
|
28727 |
throw new Error('Error trying to parse base URL.');
|
|
|
28728 |
}
|
|
|
28729 |
if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
|
|
|
28730 |
// If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
|
|
|
28731 |
// This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
|
|
|
28732 |
var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
|
|
|
28733 |
baseParts.netLoc = pathParts[1];
|
|
|
28734 |
baseParts.path = pathParts[2];
|
|
|
28735 |
}
|
|
|
28736 |
if (baseParts.netLoc && !baseParts.path) {
|
|
|
28737 |
baseParts.path = '/';
|
|
|
28738 |
}
|
|
|
28739 |
var builtParts = {
|
|
|
28740 |
// 2c) Otherwise, the embedded URL inherits the scheme of
|
|
|
28741 |
// the base URL.
|
|
|
28742 |
scheme: baseParts.scheme,
|
|
|
28743 |
netLoc: relativeParts.netLoc,
|
|
|
28744 |
path: null,
|
|
|
28745 |
params: relativeParts.params,
|
|
|
28746 |
query: relativeParts.query,
|
|
|
28747 |
fragment: relativeParts.fragment
|
|
|
28748 |
};
|
|
|
28749 |
if (!relativeParts.netLoc) {
|
|
|
28750 |
// 3) If the embedded URL's <net_loc> is non-empty, we skip to
|
|
|
28751 |
// Step 7. Otherwise, the embedded URL inherits the <net_loc>
|
|
|
28752 |
// (if any) of the base URL.
|
|
|
28753 |
builtParts.netLoc = baseParts.netLoc;
|
|
|
28754 |
// 4) If the embedded URL path is preceded by a slash "/", the
|
|
|
28755 |
// path is not relative and we skip to Step 7.
|
|
|
28756 |
if (relativeParts.path[0] !== '/') {
|
|
|
28757 |
if (!relativeParts.path) {
|
|
|
28758 |
// 5) If the embedded URL path is empty (and not preceded by a
|
|
|
28759 |
// slash), then the embedded URL inherits the base URL path
|
|
|
28760 |
builtParts.path = baseParts.path;
|
|
|
28761 |
// 5a) if the embedded URL's <params> is non-empty, we skip to
|
|
|
28762 |
// step 7; otherwise, it inherits the <params> of the base
|
|
|
28763 |
// URL (if any) and
|
|
|
28764 |
if (!relativeParts.params) {
|
|
|
28765 |
builtParts.params = baseParts.params;
|
|
|
28766 |
// 5b) if the embedded URL's <query> is non-empty, we skip to
|
|
|
28767 |
// step 7; otherwise, it inherits the <query> of the base
|
|
|
28768 |
// URL (if any) and we skip to step 7.
|
|
|
28769 |
if (!relativeParts.query) {
|
|
|
28770 |
builtParts.query = baseParts.query;
|
|
|
28771 |
}
|
|
|
28772 |
}
|
|
|
28773 |
} else {
|
|
|
28774 |
// 6) The last segment of the base URL's path (anything
|
|
|
28775 |
// following the rightmost slash "/", or the entire path if no
|
|
|
28776 |
// slash is present) is removed and the embedded URL's path is
|
|
|
28777 |
// appended in its place.
|
|
|
28778 |
var baseURLPath = baseParts.path;
|
|
|
28779 |
var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
|
|
|
28780 |
builtParts.path = URLToolkit.normalizePath(newPath);
|
|
|
28781 |
}
|
|
|
28782 |
}
|
|
|
28783 |
}
|
|
|
28784 |
if (builtParts.path === null) {
|
|
|
28785 |
builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
|
|
|
28786 |
}
|
|
|
28787 |
return URLToolkit.buildURLFromParts(builtParts);
|
|
|
28788 |
},
|
|
|
28789 |
parseURL: function (url) {
|
|
|
28790 |
var parts = URL_REGEX.exec(url);
|
|
|
28791 |
if (!parts) {
|
|
|
28792 |
return null;
|
|
|
28793 |
}
|
|
|
28794 |
return {
|
|
|
28795 |
scheme: parts[1] || '',
|
|
|
28796 |
netLoc: parts[2] || '',
|
|
|
28797 |
path: parts[3] || '',
|
|
|
28798 |
params: parts[4] || '',
|
|
|
28799 |
query: parts[5] || '',
|
|
|
28800 |
fragment: parts[6] || ''
|
|
|
28801 |
};
|
|
|
28802 |
},
|
|
|
28803 |
normalizePath: function (path) {
|
|
|
28804 |
// The following operations are
|
|
|
28805 |
// then applied, in order, to the new path:
|
|
|
28806 |
// 6a) All occurrences of "./", where "." is a complete path
|
|
|
28807 |
// segment, are removed.
|
|
|
28808 |
// 6b) If the path ends with "." as a complete path segment,
|
|
|
28809 |
// that "." is removed.
|
|
|
28810 |
path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, '');
|
|
|
28811 |
// 6c) All occurrences of "<segment>/../", where <segment> is a
|
|
|
28812 |
// complete path segment not equal to "..", are removed.
|
|
|
28813 |
// Removal of these path segments is performed iteratively,
|
|
|
28814 |
// removing the leftmost matching pattern on each iteration,
|
|
|
28815 |
// until no matching pattern remains.
|
|
|
28816 |
// 6d) If the path ends with "<segment>/..", where <segment> is a
|
|
|
28817 |
// complete path segment not equal to "..", that
|
|
|
28818 |
// "<segment>/.." is removed.
|
|
|
28819 |
while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {}
|
|
|
28820 |
return path.split('').reverse().join('');
|
|
|
28821 |
},
|
|
|
28822 |
buildURLFromParts: function (parts) {
|
|
|
28823 |
return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
|
|
|
28824 |
}
|
|
|
28825 |
};
|
|
|
28826 |
module.exports = URLToolkit;
|
|
|
28827 |
})();
|
|
|
28828 |
});
|
|
|
28829 |
|
|
|
28830 |
var DEFAULT_LOCATION = 'http://example.com';
|
|
|
28831 |
var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) {
|
|
|
28832 |
// return early if we don't need to resolve
|
|
|
28833 |
if (/^[a-z]+:/i.test(relativeUrl)) {
|
|
|
28834 |
return relativeUrl;
|
|
|
28835 |
} // if baseUrl is a data URI, ignore it and resolve everything relative to window.location
|
|
|
28836 |
|
|
|
28837 |
if (/^data:/.test(baseUrl)) {
|
|
|
28838 |
baseUrl = window.location && window.location.href || '';
|
|
|
28839 |
} // IE11 supports URL but not the URL constructor
|
|
|
28840 |
// feature detect the behavior we want
|
|
|
28841 |
|
|
|
28842 |
var nativeURL = typeof window.URL === 'function';
|
|
|
28843 |
var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node)
|
|
|
28844 |
// and if baseUrl isn't an absolute url
|
|
|
28845 |
|
|
|
28846 |
var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location
|
|
|
28847 |
|
|
|
28848 |
if (nativeURL) {
|
|
|
28849 |
baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION);
|
|
|
28850 |
} else if (!/\/\//i.test(baseUrl)) {
|
|
|
28851 |
baseUrl = urlToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl);
|
|
|
28852 |
}
|
|
|
28853 |
if (nativeURL) {
|
|
|
28854 |
var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol
|
|
|
28855 |
// and if we're location-less, remove the location
|
|
|
28856 |
// otherwise, return the url unmodified
|
|
|
28857 |
|
|
|
28858 |
if (removeLocation) {
|
|
|
28859 |
return newUrl.href.slice(DEFAULT_LOCATION.length);
|
|
|
28860 |
} else if (protocolLess) {
|
|
|
28861 |
return newUrl.href.slice(newUrl.protocol.length);
|
|
|
28862 |
}
|
|
|
28863 |
return newUrl.href;
|
|
|
28864 |
}
|
|
|
28865 |
return urlToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
|
|
|
28866 |
};
|
|
|
28867 |
|
|
|
28868 |
/**
|
|
|
28869 |
* @file stream.js
|
|
|
28870 |
*/
|
|
|
28871 |
|
|
|
28872 |
/**
|
|
|
28873 |
* A lightweight readable stream implemention that handles event dispatching.
|
|
|
28874 |
*
|
|
|
28875 |
* @class Stream
|
|
|
28876 |
*/
|
|
|
28877 |
var Stream = /*#__PURE__*/function () {
|
|
|
28878 |
function Stream() {
|
|
|
28879 |
this.listeners = {};
|
|
|
28880 |
}
|
|
|
28881 |
/**
|
|
|
28882 |
* Add a listener for a specified event type.
|
|
|
28883 |
*
|
|
|
28884 |
* @param {string} type the event name
|
|
|
28885 |
* @param {Function} listener the callback to be invoked when an event of
|
|
|
28886 |
* the specified type occurs
|
|
|
28887 |
*/
|
|
|
28888 |
|
|
|
28889 |
var _proto = Stream.prototype;
|
|
|
28890 |
_proto.on = function on(type, listener) {
|
|
|
28891 |
if (!this.listeners[type]) {
|
|
|
28892 |
this.listeners[type] = [];
|
|
|
28893 |
}
|
|
|
28894 |
this.listeners[type].push(listener);
|
|
|
28895 |
}
|
|
|
28896 |
/**
|
|
|
28897 |
* Remove a listener for a specified event type.
|
|
|
28898 |
*
|
|
|
28899 |
* @param {string} type the event name
|
|
|
28900 |
* @param {Function} listener a function previously registered for this
|
|
|
28901 |
* type of event through `on`
|
|
|
28902 |
* @return {boolean} if we could turn it off or not
|
|
|
28903 |
*/;
|
|
|
28904 |
|
|
|
28905 |
_proto.off = function off(type, listener) {
|
|
|
28906 |
if (!this.listeners[type]) {
|
|
|
28907 |
return false;
|
|
|
28908 |
}
|
|
|
28909 |
var index = this.listeners[type].indexOf(listener); // TODO: which is better?
|
|
|
28910 |
// In Video.js we slice listener functions
|
|
|
28911 |
// on trigger so that it does not mess up the order
|
|
|
28912 |
// while we loop through.
|
|
|
28913 |
//
|
|
|
28914 |
// Here we slice on off so that the loop in trigger
|
|
|
28915 |
// can continue using it's old reference to loop without
|
|
|
28916 |
// messing up the order.
|
|
|
28917 |
|
|
|
28918 |
this.listeners[type] = this.listeners[type].slice(0);
|
|
|
28919 |
this.listeners[type].splice(index, 1);
|
|
|
28920 |
return index > -1;
|
|
|
28921 |
}
|
|
|
28922 |
/**
|
|
|
28923 |
* Trigger an event of the specified type on this stream. Any additional
|
|
|
28924 |
* arguments to this function are passed as parameters to event listeners.
|
|
|
28925 |
*
|
|
|
28926 |
* @param {string} type the event name
|
|
|
28927 |
*/;
|
|
|
28928 |
|
|
|
28929 |
_proto.trigger = function trigger(type) {
|
|
|
28930 |
var callbacks = this.listeners[type];
|
|
|
28931 |
if (!callbacks) {
|
|
|
28932 |
return;
|
|
|
28933 |
} // Slicing the arguments on every invocation of this method
|
|
|
28934 |
// can add a significant amount of overhead. Avoid the
|
|
|
28935 |
// intermediate object creation for the common case of a
|
|
|
28936 |
// single callback argument
|
|
|
28937 |
|
|
|
28938 |
if (arguments.length === 2) {
|
|
|
28939 |
var length = callbacks.length;
|
|
|
28940 |
for (var i = 0; i < length; ++i) {
|
|
|
28941 |
callbacks[i].call(this, arguments[1]);
|
|
|
28942 |
}
|
|
|
28943 |
} else {
|
|
|
28944 |
var args = Array.prototype.slice.call(arguments, 1);
|
|
|
28945 |
var _length = callbacks.length;
|
|
|
28946 |
for (var _i = 0; _i < _length; ++_i) {
|
|
|
28947 |
callbacks[_i].apply(this, args);
|
|
|
28948 |
}
|
|
|
28949 |
}
|
|
|
28950 |
}
|
|
|
28951 |
/**
|
|
|
28952 |
* Destroys the stream and cleans up.
|
|
|
28953 |
*/;
|
|
|
28954 |
|
|
|
28955 |
_proto.dispose = function dispose() {
|
|
|
28956 |
this.listeners = {};
|
|
|
28957 |
}
|
|
|
28958 |
/**
|
|
|
28959 |
* Forwards all `data` events on this stream to the destination stream. The
|
|
|
28960 |
* destination stream should provide a method `push` to receive the data
|
|
|
28961 |
* events as they arrive.
|
|
|
28962 |
*
|
|
|
28963 |
* @param {Stream} destination the stream that will receive all `data` events
|
|
|
28964 |
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
|
|
|
28965 |
*/;
|
|
|
28966 |
|
|
|
28967 |
_proto.pipe = function pipe(destination) {
|
|
|
28968 |
this.on('data', function (data) {
|
|
|
28969 |
destination.push(data);
|
|
|
28970 |
});
|
|
|
28971 |
};
|
|
|
28972 |
return Stream;
|
|
|
28973 |
}();
|
|
|
28974 |
|
|
|
28975 |
var atob$1 = function atob(s) {
|
|
|
28976 |
return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
|
|
|
28977 |
};
|
|
|
28978 |
function decodeB64ToUint8Array$1(b64Text) {
|
|
|
28979 |
var decodedString = atob$1(b64Text);
|
|
|
28980 |
var array = new Uint8Array(decodedString.length);
|
|
|
28981 |
for (var i = 0; i < decodedString.length; i++) {
|
|
|
28982 |
array[i] = decodedString.charCodeAt(i);
|
|
|
28983 |
}
|
|
|
28984 |
return array;
|
|
|
28985 |
}
|
|
|
28986 |
|
|
|
28987 |
/*! @name m3u8-parser @version 7.1.0 @license Apache-2.0 */
|
|
|
28988 |
|
|
|
28989 |
/**
|
|
|
28990 |
* @file m3u8/line-stream.js
|
|
|
28991 |
*/
|
|
|
28992 |
/**
|
|
|
28993 |
* A stream that buffers string input and generates a `data` event for each
|
|
|
28994 |
* line.
|
|
|
28995 |
*
|
|
|
28996 |
* @class LineStream
|
|
|
28997 |
* @extends Stream
|
|
|
28998 |
*/
|
|
|
28999 |
|
|
|
29000 |
class LineStream extends Stream {
|
|
|
29001 |
constructor() {
|
|
|
29002 |
super();
|
|
|
29003 |
this.buffer = '';
|
|
|
29004 |
}
|
|
|
29005 |
/**
|
|
|
29006 |
* Add new data to be parsed.
|
|
|
29007 |
*
|
|
|
29008 |
* @param {string} data the text to process
|
|
|
29009 |
*/
|
|
|
29010 |
|
|
|
29011 |
push(data) {
|
|
|
29012 |
let nextNewline;
|
|
|
29013 |
this.buffer += data;
|
|
|
29014 |
nextNewline = this.buffer.indexOf('\n');
|
|
|
29015 |
for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
|
|
|
29016 |
this.trigger('data', this.buffer.substring(0, nextNewline));
|
|
|
29017 |
this.buffer = this.buffer.substring(nextNewline + 1);
|
|
|
29018 |
}
|
|
|
29019 |
}
|
|
|
29020 |
}
|
|
|
29021 |
const TAB = String.fromCharCode(0x09);
|
|
|
29022 |
const parseByterange = function (byterangeString) {
|
|
|
29023 |
// optionally match and capture 0+ digits before `@`
|
|
|
29024 |
// optionally match and capture 0+ digits after `@`
|
|
|
29025 |
const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
|
|
|
29026 |
const result = {};
|
|
|
29027 |
if (match[1]) {
|
|
|
29028 |
result.length = parseInt(match[1], 10);
|
|
|
29029 |
}
|
|
|
29030 |
if (match[2]) {
|
|
|
29031 |
result.offset = parseInt(match[2], 10);
|
|
|
29032 |
}
|
|
|
29033 |
return result;
|
|
|
29034 |
};
|
|
|
29035 |
/**
|
|
|
29036 |
* "forgiving" attribute list psuedo-grammar:
|
|
|
29037 |
* attributes -> keyvalue (',' keyvalue)*
|
|
|
29038 |
* keyvalue -> key '=' value
|
|
|
29039 |
* key -> [^=]*
|
|
|
29040 |
* value -> '"' [^"]* '"' | [^,]*
|
|
|
29041 |
*/
|
|
|
29042 |
|
|
|
29043 |
const attributeSeparator = function () {
|
|
|
29044 |
const key = '[^=]*';
|
|
|
29045 |
const value = '"[^"]*"|[^,]*';
|
|
|
29046 |
const keyvalue = '(?:' + key + ')=(?:' + value + ')';
|
|
|
29047 |
return new RegExp('(?:^|,)(' + keyvalue + ')');
|
|
|
29048 |
};
|
|
|
29049 |
/**
|
|
|
29050 |
* Parse attributes from a line given the separator
|
|
|
29051 |
*
|
|
|
29052 |
* @param {string} attributes the attribute line to parse
|
|
|
29053 |
*/
|
|
|
29054 |
|
|
|
29055 |
const parseAttributes$1 = function (attributes) {
|
|
|
29056 |
const result = {};
|
|
|
29057 |
if (!attributes) {
|
|
|
29058 |
return result;
|
|
|
29059 |
} // split the string using attributes as the separator
|
|
|
29060 |
|
|
|
29061 |
const attrs = attributes.split(attributeSeparator());
|
|
|
29062 |
let i = attrs.length;
|
|
|
29063 |
let attr;
|
|
|
29064 |
while (i--) {
|
|
|
29065 |
// filter out unmatched portions of the string
|
|
|
29066 |
if (attrs[i] === '') {
|
|
|
29067 |
continue;
|
|
|
29068 |
} // split the key and value
|
|
|
29069 |
|
|
|
29070 |
attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value
|
|
|
29071 |
|
|
|
29072 |
attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
|
|
|
29073 |
attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
|
|
|
29074 |
attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
|
|
|
29075 |
result[attr[0]] = attr[1];
|
|
|
29076 |
}
|
|
|
29077 |
return result;
|
|
|
29078 |
};
|
|
|
29079 |
/**
|
|
|
29080 |
* A line-level M3U8 parser event stream. It expects to receive input one
|
|
|
29081 |
* line at a time and performs a context-free parse of its contents. A stream
|
|
|
29082 |
* interpretation of a manifest can be useful if the manifest is expected to
|
|
|
29083 |
* be too large to fit comfortably into memory or the entirety of the input
|
|
|
29084 |
* is not immediately available. Otherwise, it's probably much easier to work
|
|
|
29085 |
* with a regular `Parser` object.
|
|
|
29086 |
*
|
|
|
29087 |
* Produces `data` events with an object that captures the parser's
|
|
|
29088 |
* interpretation of the input. That object has a property `tag` that is one
|
|
|
29089 |
* of `uri`, `comment`, or `tag`. URIs only have a single additional
|
|
|
29090 |
* property, `line`, which captures the entirety of the input without
|
|
|
29091 |
* interpretation. Comments similarly have a single additional property
|
|
|
29092 |
* `text` which is the input without the leading `#`.
|
|
|
29093 |
*
|
|
|
29094 |
* Tags always have a property `tagType` which is the lower-cased version of
|
|
|
29095 |
* the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
|
|
|
29096 |
* `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
|
|
|
29097 |
* tags are given the tag type `unknown` and a single additional property
|
|
|
29098 |
* `data` with the remainder of the input.
|
|
|
29099 |
*
|
|
|
29100 |
* @class ParseStream
|
|
|
29101 |
* @extends Stream
|
|
|
29102 |
*/
|
|
|
29103 |
|
|
|
29104 |
class ParseStream extends Stream {
|
|
|
29105 |
constructor() {
|
|
|
29106 |
super();
|
|
|
29107 |
this.customParsers = [];
|
|
|
29108 |
this.tagMappers = [];
|
|
|
29109 |
}
|
|
|
29110 |
/**
|
|
|
29111 |
* Parses an additional line of input.
|
|
|
29112 |
*
|
|
|
29113 |
* @param {string} line a single line of an M3U8 file to parse
|
|
|
29114 |
*/
|
|
|
29115 |
|
|
|
29116 |
push(line) {
|
|
|
29117 |
let match;
|
|
|
29118 |
let event; // strip whitespace
|
|
|
29119 |
|
|
|
29120 |
line = line.trim();
|
|
|
29121 |
if (line.length === 0) {
|
|
|
29122 |
// ignore empty lines
|
|
|
29123 |
return;
|
|
|
29124 |
} // URIs
|
|
|
29125 |
|
|
|
29126 |
if (line[0] !== '#') {
|
|
|
29127 |
this.trigger('data', {
|
|
|
29128 |
type: 'uri',
|
|
|
29129 |
uri: line
|
|
|
29130 |
});
|
|
|
29131 |
return;
|
|
|
29132 |
} // map tags
|
|
|
29133 |
|
|
|
29134 |
const newLines = this.tagMappers.reduce((acc, mapper) => {
|
|
|
29135 |
const mappedLine = mapper(line); // skip if unchanged
|
|
|
29136 |
|
|
|
29137 |
if (mappedLine === line) {
|
|
|
29138 |
return acc;
|
|
|
29139 |
}
|
|
|
29140 |
return acc.concat([mappedLine]);
|
|
|
29141 |
}, [line]);
|
|
|
29142 |
newLines.forEach(newLine => {
|
|
|
29143 |
for (let i = 0; i < this.customParsers.length; i++) {
|
|
|
29144 |
if (this.customParsers[i].call(this, newLine)) {
|
|
|
29145 |
return;
|
|
|
29146 |
}
|
|
|
29147 |
} // Comments
|
|
|
29148 |
|
|
|
29149 |
if (newLine.indexOf('#EXT') !== 0) {
|
|
|
29150 |
this.trigger('data', {
|
|
|
29151 |
type: 'comment',
|
|
|
29152 |
text: newLine.slice(1)
|
|
|
29153 |
});
|
|
|
29154 |
return;
|
|
|
29155 |
} // strip off any carriage returns here so the regex matching
|
|
|
29156 |
// doesn't have to account for them.
|
|
|
29157 |
|
|
|
29158 |
newLine = newLine.replace('\r', ''); // Tags
|
|
|
29159 |
|
|
|
29160 |
match = /^#EXTM3U/.exec(newLine);
|
|
|
29161 |
if (match) {
|
|
|
29162 |
this.trigger('data', {
|
|
|
29163 |
type: 'tag',
|
|
|
29164 |
tagType: 'm3u'
|
|
|
29165 |
});
|
|
|
29166 |
return;
|
|
|
29167 |
}
|
|
|
29168 |
match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine);
|
|
|
29169 |
if (match) {
|
|
|
29170 |
event = {
|
|
|
29171 |
type: 'tag',
|
|
|
29172 |
tagType: 'inf'
|
|
|
29173 |
};
|
|
|
29174 |
if (match[1]) {
|
|
|
29175 |
event.duration = parseFloat(match[1]);
|
|
|
29176 |
}
|
|
|
29177 |
if (match[2]) {
|
|
|
29178 |
event.title = match[2];
|
|
|
29179 |
}
|
|
|
29180 |
this.trigger('data', event);
|
|
|
29181 |
return;
|
|
|
29182 |
}
|
|
|
29183 |
match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine);
|
|
|
29184 |
if (match) {
|
|
|
29185 |
event = {
|
|
|
29186 |
type: 'tag',
|
|
|
29187 |
tagType: 'targetduration'
|
|
|
29188 |
};
|
|
|
29189 |
if (match[1]) {
|
|
|
29190 |
event.duration = parseInt(match[1], 10);
|
|
|
29191 |
}
|
|
|
29192 |
this.trigger('data', event);
|
|
|
29193 |
return;
|
|
|
29194 |
}
|
|
|
29195 |
match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine);
|
|
|
29196 |
if (match) {
|
|
|
29197 |
event = {
|
|
|
29198 |
type: 'tag',
|
|
|
29199 |
tagType: 'version'
|
|
|
29200 |
};
|
|
|
29201 |
if (match[1]) {
|
|
|
29202 |
event.version = parseInt(match[1], 10);
|
|
|
29203 |
}
|
|
|
29204 |
this.trigger('data', event);
|
|
|
29205 |
return;
|
|
|
29206 |
}
|
|
|
29207 |
match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
|
|
|
29208 |
if (match) {
|
|
|
29209 |
event = {
|
|
|
29210 |
type: 'tag',
|
|
|
29211 |
tagType: 'media-sequence'
|
|
|
29212 |
};
|
|
|
29213 |
if (match[1]) {
|
|
|
29214 |
event.number = parseInt(match[1], 10);
|
|
|
29215 |
}
|
|
|
29216 |
this.trigger('data', event);
|
|
|
29217 |
return;
|
|
|
29218 |
}
|
|
|
29219 |
match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
|
|
|
29220 |
if (match) {
|
|
|
29221 |
event = {
|
|
|
29222 |
type: 'tag',
|
|
|
29223 |
tagType: 'discontinuity-sequence'
|
|
|
29224 |
};
|
|
|
29225 |
if (match[1]) {
|
|
|
29226 |
event.number = parseInt(match[1], 10);
|
|
|
29227 |
}
|
|
|
29228 |
this.trigger('data', event);
|
|
|
29229 |
return;
|
|
|
29230 |
}
|
|
|
29231 |
match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine);
|
|
|
29232 |
if (match) {
|
|
|
29233 |
event = {
|
|
|
29234 |
type: 'tag',
|
|
|
29235 |
tagType: 'playlist-type'
|
|
|
29236 |
};
|
|
|
29237 |
if (match[1]) {
|
|
|
29238 |
event.playlistType = match[1];
|
|
|
29239 |
}
|
|
|
29240 |
this.trigger('data', event);
|
|
|
29241 |
return;
|
|
|
29242 |
}
|
|
|
29243 |
match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine);
|
|
|
29244 |
if (match) {
|
|
|
29245 |
event = _extends$1(parseByterange(match[1]), {
|
|
|
29246 |
type: 'tag',
|
|
|
29247 |
tagType: 'byterange'
|
|
|
29248 |
});
|
|
|
29249 |
this.trigger('data', event);
|
|
|
29250 |
return;
|
|
|
29251 |
}
|
|
|
29252 |
match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine);
|
|
|
29253 |
if (match) {
|
|
|
29254 |
event = {
|
|
|
29255 |
type: 'tag',
|
|
|
29256 |
tagType: 'allow-cache'
|
|
|
29257 |
};
|
|
|
29258 |
if (match[1]) {
|
|
|
29259 |
event.allowed = !/NO/.test(match[1]);
|
|
|
29260 |
}
|
|
|
29261 |
this.trigger('data', event);
|
|
|
29262 |
return;
|
|
|
29263 |
}
|
|
|
29264 |
match = /^#EXT-X-MAP:(.*)$/.exec(newLine);
|
|
|
29265 |
if (match) {
|
|
|
29266 |
event = {
|
|
|
29267 |
type: 'tag',
|
|
|
29268 |
tagType: 'map'
|
|
|
29269 |
};
|
|
|
29270 |
if (match[1]) {
|
|
|
29271 |
const attributes = parseAttributes$1(match[1]);
|
|
|
29272 |
if (attributes.URI) {
|
|
|
29273 |
event.uri = attributes.URI;
|
|
|
29274 |
}
|
|
|
29275 |
if (attributes.BYTERANGE) {
|
|
|
29276 |
event.byterange = parseByterange(attributes.BYTERANGE);
|
|
|
29277 |
}
|
|
|
29278 |
}
|
|
|
29279 |
this.trigger('data', event);
|
|
|
29280 |
return;
|
|
|
29281 |
}
|
|
|
29282 |
match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine);
|
|
|
29283 |
if (match) {
|
|
|
29284 |
event = {
|
|
|
29285 |
type: 'tag',
|
|
|
29286 |
tagType: 'stream-inf'
|
|
|
29287 |
};
|
|
|
29288 |
if (match[1]) {
|
|
|
29289 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29290 |
if (event.attributes.RESOLUTION) {
|
|
|
29291 |
const split = event.attributes.RESOLUTION.split('x');
|
|
|
29292 |
const resolution = {};
|
|
|
29293 |
if (split[0]) {
|
|
|
29294 |
resolution.width = parseInt(split[0], 10);
|
|
|
29295 |
}
|
|
|
29296 |
if (split[1]) {
|
|
|
29297 |
resolution.height = parseInt(split[1], 10);
|
|
|
29298 |
}
|
|
|
29299 |
event.attributes.RESOLUTION = resolution;
|
|
|
29300 |
}
|
|
|
29301 |
if (event.attributes.BANDWIDTH) {
|
|
|
29302 |
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
|
|
|
29303 |
}
|
|
|
29304 |
if (event.attributes['FRAME-RATE']) {
|
|
|
29305 |
event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']);
|
|
|
29306 |
}
|
|
|
29307 |
if (event.attributes['PROGRAM-ID']) {
|
|
|
29308 |
event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
|
|
|
29309 |
}
|
|
|
29310 |
}
|
|
|
29311 |
this.trigger('data', event);
|
|
|
29312 |
return;
|
|
|
29313 |
}
|
|
|
29314 |
match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine);
|
|
|
29315 |
if (match) {
|
|
|
29316 |
event = {
|
|
|
29317 |
type: 'tag',
|
|
|
29318 |
tagType: 'media'
|
|
|
29319 |
};
|
|
|
29320 |
if (match[1]) {
|
|
|
29321 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29322 |
}
|
|
|
29323 |
this.trigger('data', event);
|
|
|
29324 |
return;
|
|
|
29325 |
}
|
|
|
29326 |
match = /^#EXT-X-ENDLIST/.exec(newLine);
|
|
|
29327 |
if (match) {
|
|
|
29328 |
this.trigger('data', {
|
|
|
29329 |
type: 'tag',
|
|
|
29330 |
tagType: 'endlist'
|
|
|
29331 |
});
|
|
|
29332 |
return;
|
|
|
29333 |
}
|
|
|
29334 |
match = /^#EXT-X-DISCONTINUITY/.exec(newLine);
|
|
|
29335 |
if (match) {
|
|
|
29336 |
this.trigger('data', {
|
|
|
29337 |
type: 'tag',
|
|
|
29338 |
tagType: 'discontinuity'
|
|
|
29339 |
});
|
|
|
29340 |
return;
|
|
|
29341 |
}
|
|
|
29342 |
match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine);
|
|
|
29343 |
if (match) {
|
|
|
29344 |
event = {
|
|
|
29345 |
type: 'tag',
|
|
|
29346 |
tagType: 'program-date-time'
|
|
|
29347 |
};
|
|
|
29348 |
if (match[1]) {
|
|
|
29349 |
event.dateTimeString = match[1];
|
|
|
29350 |
event.dateTimeObject = new Date(match[1]);
|
|
|
29351 |
}
|
|
|
29352 |
this.trigger('data', event);
|
|
|
29353 |
return;
|
|
|
29354 |
}
|
|
|
29355 |
match = /^#EXT-X-KEY:(.*)$/.exec(newLine);
|
|
|
29356 |
if (match) {
|
|
|
29357 |
event = {
|
|
|
29358 |
type: 'tag',
|
|
|
29359 |
tagType: 'key'
|
|
|
29360 |
};
|
|
|
29361 |
if (match[1]) {
|
|
|
29362 |
event.attributes = parseAttributes$1(match[1]); // parse the IV string into a Uint32Array
|
|
|
29363 |
|
|
|
29364 |
if (event.attributes.IV) {
|
|
|
29365 |
if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
|
|
|
29366 |
event.attributes.IV = event.attributes.IV.substring(2);
|
|
|
29367 |
}
|
|
|
29368 |
event.attributes.IV = event.attributes.IV.match(/.{8}/g);
|
|
|
29369 |
event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
|
|
|
29370 |
event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
|
|
|
29371 |
event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
|
|
|
29372 |
event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
|
|
|
29373 |
event.attributes.IV = new Uint32Array(event.attributes.IV);
|
|
|
29374 |
}
|
|
|
29375 |
}
|
|
|
29376 |
this.trigger('data', event);
|
|
|
29377 |
return;
|
|
|
29378 |
}
|
|
|
29379 |
match = /^#EXT-X-START:(.*)$/.exec(newLine);
|
|
|
29380 |
if (match) {
|
|
|
29381 |
event = {
|
|
|
29382 |
type: 'tag',
|
|
|
29383 |
tagType: 'start'
|
|
|
29384 |
};
|
|
|
29385 |
if (match[1]) {
|
|
|
29386 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29387 |
event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']);
|
|
|
29388 |
event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE);
|
|
|
29389 |
}
|
|
|
29390 |
this.trigger('data', event);
|
|
|
29391 |
return;
|
|
|
29392 |
}
|
|
|
29393 |
match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine);
|
|
|
29394 |
if (match) {
|
|
|
29395 |
event = {
|
|
|
29396 |
type: 'tag',
|
|
|
29397 |
tagType: 'cue-out-cont'
|
|
|
29398 |
};
|
|
|
29399 |
if (match[1]) {
|
|
|
29400 |
event.data = match[1];
|
|
|
29401 |
} else {
|
|
|
29402 |
event.data = '';
|
|
|
29403 |
}
|
|
|
29404 |
this.trigger('data', event);
|
|
|
29405 |
return;
|
|
|
29406 |
}
|
|
|
29407 |
match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine);
|
|
|
29408 |
if (match) {
|
|
|
29409 |
event = {
|
|
|
29410 |
type: 'tag',
|
|
|
29411 |
tagType: 'cue-out'
|
|
|
29412 |
};
|
|
|
29413 |
if (match[1]) {
|
|
|
29414 |
event.data = match[1];
|
|
|
29415 |
} else {
|
|
|
29416 |
event.data = '';
|
|
|
29417 |
}
|
|
|
29418 |
this.trigger('data', event);
|
|
|
29419 |
return;
|
|
|
29420 |
}
|
|
|
29421 |
match = /^#EXT-X-CUE-IN:(.*)?$/.exec(newLine);
|
|
|
29422 |
if (match) {
|
|
|
29423 |
event = {
|
|
|
29424 |
type: 'tag',
|
|
|
29425 |
tagType: 'cue-in'
|
|
|
29426 |
};
|
|
|
29427 |
if (match[1]) {
|
|
|
29428 |
event.data = match[1];
|
|
|
29429 |
} else {
|
|
|
29430 |
event.data = '';
|
|
|
29431 |
}
|
|
|
29432 |
this.trigger('data', event);
|
|
|
29433 |
return;
|
|
|
29434 |
}
|
|
|
29435 |
match = /^#EXT-X-SKIP:(.*)$/.exec(newLine);
|
|
|
29436 |
if (match && match[1]) {
|
|
|
29437 |
event = {
|
|
|
29438 |
type: 'tag',
|
|
|
29439 |
tagType: 'skip'
|
|
|
29440 |
};
|
|
|
29441 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29442 |
if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) {
|
|
|
29443 |
event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10);
|
|
|
29444 |
}
|
|
|
29445 |
if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) {
|
|
|
29446 |
event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB);
|
|
|
29447 |
}
|
|
|
29448 |
this.trigger('data', event);
|
|
|
29449 |
return;
|
|
|
29450 |
}
|
|
|
29451 |
match = /^#EXT-X-PART:(.*)$/.exec(newLine);
|
|
|
29452 |
if (match && match[1]) {
|
|
|
29453 |
event = {
|
|
|
29454 |
type: 'tag',
|
|
|
29455 |
tagType: 'part'
|
|
|
29456 |
};
|
|
|
29457 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29458 |
['DURATION'].forEach(function (key) {
|
|
|
29459 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29460 |
event.attributes[key] = parseFloat(event.attributes[key]);
|
|
|
29461 |
}
|
|
|
29462 |
});
|
|
|
29463 |
['INDEPENDENT', 'GAP'].forEach(function (key) {
|
|
|
29464 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29465 |
event.attributes[key] = /YES/.test(event.attributes[key]);
|
|
|
29466 |
}
|
|
|
29467 |
});
|
|
|
29468 |
if (event.attributes.hasOwnProperty('BYTERANGE')) {
|
|
|
29469 |
event.attributes.byterange = parseByterange(event.attributes.BYTERANGE);
|
|
|
29470 |
}
|
|
|
29471 |
this.trigger('data', event);
|
|
|
29472 |
return;
|
|
|
29473 |
}
|
|
|
29474 |
match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine);
|
|
|
29475 |
if (match && match[1]) {
|
|
|
29476 |
event = {
|
|
|
29477 |
type: 'tag',
|
|
|
29478 |
tagType: 'server-control'
|
|
|
29479 |
};
|
|
|
29480 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29481 |
['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) {
|
|
|
29482 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29483 |
event.attributes[key] = parseFloat(event.attributes[key]);
|
|
|
29484 |
}
|
|
|
29485 |
});
|
|
|
29486 |
['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) {
|
|
|
29487 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29488 |
event.attributes[key] = /YES/.test(event.attributes[key]);
|
|
|
29489 |
}
|
|
|
29490 |
});
|
|
|
29491 |
this.trigger('data', event);
|
|
|
29492 |
return;
|
|
|
29493 |
}
|
|
|
29494 |
match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine);
|
|
|
29495 |
if (match && match[1]) {
|
|
|
29496 |
event = {
|
|
|
29497 |
type: 'tag',
|
|
|
29498 |
tagType: 'part-inf'
|
|
|
29499 |
};
|
|
|
29500 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29501 |
['PART-TARGET'].forEach(function (key) {
|
|
|
29502 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29503 |
event.attributes[key] = parseFloat(event.attributes[key]);
|
|
|
29504 |
}
|
|
|
29505 |
});
|
|
|
29506 |
this.trigger('data', event);
|
|
|
29507 |
return;
|
|
|
29508 |
}
|
|
|
29509 |
match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine);
|
|
|
29510 |
if (match && match[1]) {
|
|
|
29511 |
event = {
|
|
|
29512 |
type: 'tag',
|
|
|
29513 |
tagType: 'preload-hint'
|
|
|
29514 |
};
|
|
|
29515 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29516 |
['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) {
|
|
|
29517 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29518 |
event.attributes[key] = parseInt(event.attributes[key], 10);
|
|
|
29519 |
const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset';
|
|
|
29520 |
event.attributes.byterange = event.attributes.byterange || {};
|
|
|
29521 |
event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object.
|
|
|
29522 |
|
|
|
29523 |
delete event.attributes[key];
|
|
|
29524 |
}
|
|
|
29525 |
});
|
|
|
29526 |
this.trigger('data', event);
|
|
|
29527 |
return;
|
|
|
29528 |
}
|
|
|
29529 |
match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine);
|
|
|
29530 |
if (match && match[1]) {
|
|
|
29531 |
event = {
|
|
|
29532 |
type: 'tag',
|
|
|
29533 |
tagType: 'rendition-report'
|
|
|
29534 |
};
|
|
|
29535 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29536 |
['LAST-MSN', 'LAST-PART'].forEach(function (key) {
|
|
|
29537 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29538 |
event.attributes[key] = parseInt(event.attributes[key], 10);
|
|
|
29539 |
}
|
|
|
29540 |
});
|
|
|
29541 |
this.trigger('data', event);
|
|
|
29542 |
return;
|
|
|
29543 |
}
|
|
|
29544 |
match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine);
|
|
|
29545 |
if (match && match[1]) {
|
|
|
29546 |
event = {
|
|
|
29547 |
type: 'tag',
|
|
|
29548 |
tagType: 'daterange'
|
|
|
29549 |
};
|
|
|
29550 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29551 |
['ID', 'CLASS'].forEach(function (key) {
|
|
|
29552 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29553 |
event.attributes[key] = String(event.attributes[key]);
|
|
|
29554 |
}
|
|
|
29555 |
});
|
|
|
29556 |
['START-DATE', 'END-DATE'].forEach(function (key) {
|
|
|
29557 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29558 |
event.attributes[key] = new Date(event.attributes[key]);
|
|
|
29559 |
}
|
|
|
29560 |
});
|
|
|
29561 |
['DURATION', 'PLANNED-DURATION'].forEach(function (key) {
|
|
|
29562 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29563 |
event.attributes[key] = parseFloat(event.attributes[key]);
|
|
|
29564 |
}
|
|
|
29565 |
});
|
|
|
29566 |
['END-ON-NEXT'].forEach(function (key) {
|
|
|
29567 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29568 |
event.attributes[key] = /YES/i.test(event.attributes[key]);
|
|
|
29569 |
}
|
|
|
29570 |
});
|
|
|
29571 |
['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) {
|
|
|
29572 |
if (event.attributes.hasOwnProperty(key)) {
|
|
|
29573 |
event.attributes[key] = event.attributes[key].toString(16);
|
|
|
29574 |
}
|
|
|
29575 |
});
|
|
|
29576 |
const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/;
|
|
|
29577 |
for (const key in event.attributes) {
|
|
|
29578 |
if (!clientAttributePattern.test(key)) {
|
|
|
29579 |
continue;
|
|
|
29580 |
}
|
|
|
29581 |
const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]);
|
|
|
29582 |
const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]);
|
|
|
29583 |
event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]);
|
|
|
29584 |
}
|
|
|
29585 |
this.trigger('data', event);
|
|
|
29586 |
return;
|
|
|
29587 |
}
|
|
|
29588 |
match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine);
|
|
|
29589 |
if (match) {
|
|
|
29590 |
this.trigger('data', {
|
|
|
29591 |
type: 'tag',
|
|
|
29592 |
tagType: 'independent-segments'
|
|
|
29593 |
});
|
|
|
29594 |
return;
|
|
|
29595 |
}
|
|
|
29596 |
match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine);
|
|
|
29597 |
if (match) {
|
|
|
29598 |
event = {
|
|
|
29599 |
type: 'tag',
|
|
|
29600 |
tagType: 'content-steering'
|
|
|
29601 |
};
|
|
|
29602 |
event.attributes = parseAttributes$1(match[1]);
|
|
|
29603 |
this.trigger('data', event);
|
|
|
29604 |
return;
|
|
|
29605 |
} // unknown tag type
|
|
|
29606 |
|
|
|
29607 |
this.trigger('data', {
|
|
|
29608 |
type: 'tag',
|
|
|
29609 |
data: newLine.slice(4)
|
|
|
29610 |
});
|
|
|
29611 |
});
|
|
|
29612 |
}
|
|
|
29613 |
/**
|
|
|
29614 |
* Add a parser for custom headers
|
|
|
29615 |
*
|
|
|
29616 |
* @param {Object} options a map of options for the added parser
|
|
|
29617 |
* @param {RegExp} options.expression a regular expression to match the custom header
|
|
|
29618 |
* @param {string} options.customType the custom type to register to the output
|
|
|
29619 |
* @param {Function} [options.dataParser] function to parse the line into an object
|
|
|
29620 |
* @param {boolean} [options.segment] should tag data be attached to the segment object
|
|
|
29621 |
*/
|
|
|
29622 |
|
|
|
29623 |
addParser({
|
|
|
29624 |
expression,
|
|
|
29625 |
customType,
|
|
|
29626 |
dataParser,
|
|
|
29627 |
segment
|
|
|
29628 |
}) {
|
|
|
29629 |
if (typeof dataParser !== 'function') {
|
|
|
29630 |
dataParser = line => line;
|
|
|
29631 |
}
|
|
|
29632 |
this.customParsers.push(line => {
|
|
|
29633 |
const match = expression.exec(line);
|
|
|
29634 |
if (match) {
|
|
|
29635 |
this.trigger('data', {
|
|
|
29636 |
type: 'custom',
|
|
|
29637 |
data: dataParser(line),
|
|
|
29638 |
customType,
|
|
|
29639 |
segment
|
|
|
29640 |
});
|
|
|
29641 |
return true;
|
|
|
29642 |
}
|
|
|
29643 |
});
|
|
|
29644 |
}
|
|
|
29645 |
/**
|
|
|
29646 |
* Add a custom header mapper
|
|
|
29647 |
*
|
|
|
29648 |
* @param {Object} options
|
|
|
29649 |
* @param {RegExp} options.expression a regular expression to match the custom header
|
|
|
29650 |
* @param {Function} options.map function to translate tag into a different tag
|
|
|
29651 |
*/
|
|
|
29652 |
|
|
|
29653 |
addTagMapper({
|
|
|
29654 |
expression,
|
|
|
29655 |
map
|
|
|
29656 |
}) {
|
|
|
29657 |
const mapFn = line => {
|
|
|
29658 |
if (expression.test(line)) {
|
|
|
29659 |
return map(line);
|
|
|
29660 |
}
|
|
|
29661 |
return line;
|
|
|
29662 |
};
|
|
|
29663 |
this.tagMappers.push(mapFn);
|
|
|
29664 |
}
|
|
|
29665 |
}
|
|
|
29666 |
const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase());
|
|
|
29667 |
const camelCaseKeys = function (attributes) {
|
|
|
29668 |
const result = {};
|
|
|
29669 |
Object.keys(attributes).forEach(function (key) {
|
|
|
29670 |
result[camelCase(key)] = attributes[key];
|
|
|
29671 |
});
|
|
|
29672 |
return result;
|
|
|
29673 |
}; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration
|
|
|
29674 |
// we need this helper because defaults are based upon targetDuration and
|
|
|
29675 |
// partTargetDuration being set, but they may not be if SERVER-CONTROL appears before
|
|
|
29676 |
// target durations are set.
|
|
|
29677 |
|
|
|
29678 |
const setHoldBack = function (manifest) {
|
|
|
29679 |
const {
|
|
|
29680 |
serverControl,
|
|
|
29681 |
targetDuration,
|
|
|
29682 |
partTargetDuration
|
|
|
29683 |
} = manifest;
|
|
|
29684 |
if (!serverControl) {
|
|
|
29685 |
return;
|
|
|
29686 |
}
|
|
|
29687 |
const tag = '#EXT-X-SERVER-CONTROL';
|
|
|
29688 |
const hb = 'holdBack';
|
|
|
29689 |
const phb = 'partHoldBack';
|
|
|
29690 |
const minTargetDuration = targetDuration && targetDuration * 3;
|
|
|
29691 |
const minPartDuration = partTargetDuration && partTargetDuration * 2;
|
|
|
29692 |
if (targetDuration && !serverControl.hasOwnProperty(hb)) {
|
|
|
29693 |
serverControl[hb] = minTargetDuration;
|
|
|
29694 |
this.trigger('info', {
|
|
|
29695 |
message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).`
|
|
|
29696 |
});
|
|
|
29697 |
}
|
|
|
29698 |
if (minTargetDuration && serverControl[hb] < minTargetDuration) {
|
|
|
29699 |
this.trigger('warn', {
|
|
|
29700 |
message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})`
|
|
|
29701 |
});
|
|
|
29702 |
serverControl[hb] = minTargetDuration;
|
|
|
29703 |
} // default no part hold back to part target duration * 3
|
|
|
29704 |
|
|
|
29705 |
if (partTargetDuration && !serverControl.hasOwnProperty(phb)) {
|
|
|
29706 |
serverControl[phb] = partTargetDuration * 3;
|
|
|
29707 |
this.trigger('info', {
|
|
|
29708 |
message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).`
|
|
|
29709 |
});
|
|
|
29710 |
} // if part hold back is too small default it to part target duration * 2
|
|
|
29711 |
|
|
|
29712 |
if (partTargetDuration && serverControl[phb] < minPartDuration) {
|
|
|
29713 |
this.trigger('warn', {
|
|
|
29714 |
message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).`
|
|
|
29715 |
});
|
|
|
29716 |
serverControl[phb] = minPartDuration;
|
|
|
29717 |
}
|
|
|
29718 |
};
|
|
|
29719 |
/**
|
|
|
29720 |
* A parser for M3U8 files. The current interpretation of the input is
|
|
|
29721 |
* exposed as a property `manifest` on parser objects. It's just two lines to
|
|
|
29722 |
* create and parse a manifest once you have the contents available as a string:
|
|
|
29723 |
*
|
|
|
29724 |
* ```js
|
|
|
29725 |
* var parser = new m3u8.Parser();
|
|
|
29726 |
* parser.push(xhr.responseText);
|
|
|
29727 |
* ```
|
|
|
29728 |
*
|
|
|
29729 |
* New input can later be applied to update the manifest object by calling
|
|
|
29730 |
* `push` again.
|
|
|
29731 |
*
|
|
|
29732 |
* The parser attempts to create a usable manifest object even if the
|
|
|
29733 |
* underlying input is somewhat nonsensical. It emits `info` and `warning`
|
|
|
29734 |
* events during the parse if it encounters input that seems invalid or
|
|
|
29735 |
* requires some property of the manifest object to be defaulted.
|
|
|
29736 |
*
|
|
|
29737 |
* @class Parser
|
|
|
29738 |
* @extends Stream
|
|
|
29739 |
*/
|
|
|
29740 |
|
|
|
29741 |
class Parser extends Stream {
|
|
|
29742 |
constructor() {
|
|
|
29743 |
super();
|
|
|
29744 |
this.lineStream = new LineStream();
|
|
|
29745 |
this.parseStream = new ParseStream();
|
|
|
29746 |
this.lineStream.pipe(this.parseStream);
|
|
|
29747 |
this.lastProgramDateTime = null;
|
|
|
29748 |
/* eslint-disable consistent-this */
|
|
|
29749 |
|
|
|
29750 |
const self = this;
|
|
|
29751 |
/* eslint-enable consistent-this */
|
|
|
29752 |
|
|
|
29753 |
const uris = [];
|
|
|
29754 |
let currentUri = {}; // if specified, the active EXT-X-MAP definition
|
|
|
29755 |
|
|
|
29756 |
let currentMap; // if specified, the active decryption key
|
|
|
29757 |
|
|
|
29758 |
let key;
|
|
|
29759 |
let hasParts = false;
|
|
|
29760 |
const noop = function () {};
|
|
|
29761 |
const defaultMediaGroups = {
|
|
|
29762 |
'AUDIO': {},
|
|
|
29763 |
'VIDEO': {},
|
|
|
29764 |
'CLOSED-CAPTIONS': {},
|
|
|
29765 |
'SUBTITLES': {}
|
|
|
29766 |
}; // This is the Widevine UUID from DASH IF IOP. The same exact string is
|
|
|
29767 |
// used in MPDs with Widevine encrypted streams.
|
|
|
29768 |
|
|
|
29769 |
const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities
|
|
|
29770 |
|
|
|
29771 |
let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data
|
|
|
29772 |
|
|
|
29773 |
this.manifest = {
|
|
|
29774 |
allowCache: true,
|
|
|
29775 |
discontinuityStarts: [],
|
|
|
29776 |
dateRanges: [],
|
|
|
29777 |
segments: []
|
|
|
29778 |
}; // keep track of the last seen segment's byte range end, as segments are not required
|
|
|
29779 |
// to provide the offset, in which case it defaults to the next byte after the
|
|
|
29780 |
// previous segment
|
|
|
29781 |
|
|
|
29782 |
let lastByterangeEnd = 0; // keep track of the last seen part's byte range end.
|
|
|
29783 |
|
|
|
29784 |
let lastPartByterangeEnd = 0;
|
|
|
29785 |
const dateRangeTags = {};
|
|
|
29786 |
this.on('end', () => {
|
|
|
29787 |
// only add preloadSegment if we don't yet have a uri for it.
|
|
|
29788 |
// and we actually have parts/preloadHints
|
|
|
29789 |
if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) {
|
|
|
29790 |
return;
|
|
|
29791 |
}
|
|
|
29792 |
if (!currentUri.map && currentMap) {
|
|
|
29793 |
currentUri.map = currentMap;
|
|
|
29794 |
}
|
|
|
29795 |
if (!currentUri.key && key) {
|
|
|
29796 |
currentUri.key = key;
|
|
|
29797 |
}
|
|
|
29798 |
if (!currentUri.timeline && typeof currentTimeline === 'number') {
|
|
|
29799 |
currentUri.timeline = currentTimeline;
|
|
|
29800 |
}
|
|
|
29801 |
this.manifest.preloadSegment = currentUri;
|
|
|
29802 |
}); // update the manifest with the m3u8 entry from the parse stream
|
|
|
29803 |
|
|
|
29804 |
this.parseStream.on('data', function (entry) {
|
|
|
29805 |
let mediaGroup;
|
|
|
29806 |
let rendition;
|
|
|
29807 |
({
|
|
|
29808 |
tag() {
|
|
|
29809 |
// switch based on the tag type
|
|
|
29810 |
(({
|
|
|
29811 |
version() {
|
|
|
29812 |
if (entry.version) {
|
|
|
29813 |
this.manifest.version = entry.version;
|
|
|
29814 |
}
|
|
|
29815 |
},
|
|
|
29816 |
'allow-cache'() {
|
|
|
29817 |
this.manifest.allowCache = entry.allowed;
|
|
|
29818 |
if (!('allowed' in entry)) {
|
|
|
29819 |
this.trigger('info', {
|
|
|
29820 |
message: 'defaulting allowCache to YES'
|
|
|
29821 |
});
|
|
|
29822 |
this.manifest.allowCache = true;
|
|
|
29823 |
}
|
|
|
29824 |
},
|
|
|
29825 |
byterange() {
|
|
|
29826 |
const byterange = {};
|
|
|
29827 |
if ('length' in entry) {
|
|
|
29828 |
currentUri.byterange = byterange;
|
|
|
29829 |
byterange.length = entry.length;
|
|
|
29830 |
if (!('offset' in entry)) {
|
|
|
29831 |
/*
|
|
|
29832 |
* From the latest spec (as of this writing):
|
|
|
29833 |
* https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2
|
|
|
29834 |
*
|
|
|
29835 |
* Same text since EXT-X-BYTERANGE's introduction in draft 7:
|
|
|
29836 |
* https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1)
|
|
|
29837 |
*
|
|
|
29838 |
* "If o [offset] is not present, the sub-range begins at the next byte
|
|
|
29839 |
* following the sub-range of the previous media segment."
|
|
|
29840 |
*/
|
|
|
29841 |
entry.offset = lastByterangeEnd;
|
|
|
29842 |
}
|
|
|
29843 |
}
|
|
|
29844 |
if ('offset' in entry) {
|
|
|
29845 |
currentUri.byterange = byterange;
|
|
|
29846 |
byterange.offset = entry.offset;
|
|
|
29847 |
}
|
|
|
29848 |
lastByterangeEnd = byterange.offset + byterange.length;
|
|
|
29849 |
},
|
|
|
29850 |
endlist() {
|
|
|
29851 |
this.manifest.endList = true;
|
|
|
29852 |
},
|
|
|
29853 |
inf() {
|
|
|
29854 |
if (!('mediaSequence' in this.manifest)) {
|
|
|
29855 |
this.manifest.mediaSequence = 0;
|
|
|
29856 |
this.trigger('info', {
|
|
|
29857 |
message: 'defaulting media sequence to zero'
|
|
|
29858 |
});
|
|
|
29859 |
}
|
|
|
29860 |
if (!('discontinuitySequence' in this.manifest)) {
|
|
|
29861 |
this.manifest.discontinuitySequence = 0;
|
|
|
29862 |
this.trigger('info', {
|
|
|
29863 |
message: 'defaulting discontinuity sequence to zero'
|
|
|
29864 |
});
|
|
|
29865 |
}
|
|
|
29866 |
if (entry.title) {
|
|
|
29867 |
currentUri.title = entry.title;
|
|
|
29868 |
}
|
|
|
29869 |
if (entry.duration > 0) {
|
|
|
29870 |
currentUri.duration = entry.duration;
|
|
|
29871 |
}
|
|
|
29872 |
if (entry.duration === 0) {
|
|
|
29873 |
currentUri.duration = 0.01;
|
|
|
29874 |
this.trigger('info', {
|
|
|
29875 |
message: 'updating zero segment duration to a small value'
|
|
|
29876 |
});
|
|
|
29877 |
}
|
|
|
29878 |
this.manifest.segments = uris;
|
|
|
29879 |
},
|
|
|
29880 |
key() {
|
|
|
29881 |
if (!entry.attributes) {
|
|
|
29882 |
this.trigger('warn', {
|
|
|
29883 |
message: 'ignoring key declaration without attribute list'
|
|
|
29884 |
});
|
|
|
29885 |
return;
|
|
|
29886 |
} // clear the active encryption key
|
|
|
29887 |
|
|
|
29888 |
if (entry.attributes.METHOD === 'NONE') {
|
|
|
29889 |
key = null;
|
|
|
29890 |
return;
|
|
|
29891 |
}
|
|
|
29892 |
if (!entry.attributes.URI) {
|
|
|
29893 |
this.trigger('warn', {
|
|
|
29894 |
message: 'ignoring key declaration without URI'
|
|
|
29895 |
});
|
|
|
29896 |
return;
|
|
|
29897 |
}
|
|
|
29898 |
if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') {
|
|
|
29899 |
this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
|
|
|
29900 |
|
|
|
29901 |
this.manifest.contentProtection['com.apple.fps.1_0'] = {
|
|
|
29902 |
attributes: entry.attributes
|
|
|
29903 |
};
|
|
|
29904 |
return;
|
|
|
29905 |
}
|
|
|
29906 |
if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') {
|
|
|
29907 |
this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
|
|
|
29908 |
|
|
|
29909 |
this.manifest.contentProtection['com.microsoft.playready'] = {
|
|
|
29910 |
uri: entry.attributes.URI
|
|
|
29911 |
};
|
|
|
29912 |
return;
|
|
|
29913 |
} // check if the content is encrypted for Widevine
|
|
|
29914 |
// Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
|
|
|
29915 |
|
|
|
29916 |
if (entry.attributes.KEYFORMAT === widevineUuid) {
|
|
|
29917 |
const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];
|
|
|
29918 |
if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) {
|
|
|
29919 |
this.trigger('warn', {
|
|
|
29920 |
message: 'invalid key method provided for Widevine'
|
|
|
29921 |
});
|
|
|
29922 |
return;
|
|
|
29923 |
}
|
|
|
29924 |
if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') {
|
|
|
29925 |
this.trigger('warn', {
|
|
|
29926 |
message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead'
|
|
|
29927 |
});
|
|
|
29928 |
}
|
|
|
29929 |
if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') {
|
|
|
29930 |
this.trigger('warn', {
|
|
|
29931 |
message: 'invalid key URI provided for Widevine'
|
|
|
29932 |
});
|
|
|
29933 |
return;
|
|
|
29934 |
}
|
|
|
29935 |
if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) {
|
|
|
29936 |
this.trigger('warn', {
|
|
|
29937 |
message: 'invalid key ID provided for Widevine'
|
|
|
29938 |
});
|
|
|
29939 |
return;
|
|
|
29940 |
} // if Widevine key attributes are valid, store them as `contentProtection`
|
|
|
29941 |
// on the manifest to emulate Widevine tag structure in a DASH mpd
|
|
|
29942 |
|
|
|
29943 |
this.manifest.contentProtection = this.manifest.contentProtection || {};
|
|
|
29944 |
this.manifest.contentProtection['com.widevine.alpha'] = {
|
|
|
29945 |
attributes: {
|
|
|
29946 |
schemeIdUri: entry.attributes.KEYFORMAT,
|
|
|
29947 |
// remove '0x' from the key id string
|
|
|
29948 |
keyId: entry.attributes.KEYID.substring(2)
|
|
|
29949 |
},
|
|
|
29950 |
// decode the base64-encoded PSSH box
|
|
|
29951 |
pssh: decodeB64ToUint8Array$1(entry.attributes.URI.split(',')[1])
|
|
|
29952 |
};
|
|
|
29953 |
return;
|
|
|
29954 |
}
|
|
|
29955 |
if (!entry.attributes.METHOD) {
|
|
|
29956 |
this.trigger('warn', {
|
|
|
29957 |
message: 'defaulting key method to AES-128'
|
|
|
29958 |
});
|
|
|
29959 |
} // setup an encryption key for upcoming segments
|
|
|
29960 |
|
|
|
29961 |
key = {
|
|
|
29962 |
method: entry.attributes.METHOD || 'AES-128',
|
|
|
29963 |
uri: entry.attributes.URI
|
|
|
29964 |
};
|
|
|
29965 |
if (typeof entry.attributes.IV !== 'undefined') {
|
|
|
29966 |
key.iv = entry.attributes.IV;
|
|
|
29967 |
}
|
|
|
29968 |
},
|
|
|
29969 |
'media-sequence'() {
|
|
|
29970 |
if (!isFinite(entry.number)) {
|
|
|
29971 |
this.trigger('warn', {
|
|
|
29972 |
message: 'ignoring invalid media sequence: ' + entry.number
|
|
|
29973 |
});
|
|
|
29974 |
return;
|
|
|
29975 |
}
|
|
|
29976 |
this.manifest.mediaSequence = entry.number;
|
|
|
29977 |
},
|
|
|
29978 |
'discontinuity-sequence'() {
|
|
|
29979 |
if (!isFinite(entry.number)) {
|
|
|
29980 |
this.trigger('warn', {
|
|
|
29981 |
message: 'ignoring invalid discontinuity sequence: ' + entry.number
|
|
|
29982 |
});
|
|
|
29983 |
return;
|
|
|
29984 |
}
|
|
|
29985 |
this.manifest.discontinuitySequence = entry.number;
|
|
|
29986 |
currentTimeline = entry.number;
|
|
|
29987 |
},
|
|
|
29988 |
'playlist-type'() {
|
|
|
29989 |
if (!/VOD|EVENT/.test(entry.playlistType)) {
|
|
|
29990 |
this.trigger('warn', {
|
|
|
29991 |
message: 'ignoring unknown playlist type: ' + entry.playlist
|
|
|
29992 |
});
|
|
|
29993 |
return;
|
|
|
29994 |
}
|
|
|
29995 |
this.manifest.playlistType = entry.playlistType;
|
|
|
29996 |
},
|
|
|
29997 |
map() {
|
|
|
29998 |
currentMap = {};
|
|
|
29999 |
if (entry.uri) {
|
|
|
30000 |
currentMap.uri = entry.uri;
|
|
|
30001 |
}
|
|
|
30002 |
if (entry.byterange) {
|
|
|
30003 |
currentMap.byterange = entry.byterange;
|
|
|
30004 |
}
|
|
|
30005 |
if (key) {
|
|
|
30006 |
currentMap.key = key;
|
|
|
30007 |
}
|
|
|
30008 |
},
|
|
|
30009 |
'stream-inf'() {
|
|
|
30010 |
this.manifest.playlists = uris;
|
|
|
30011 |
this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
|
|
|
30012 |
if (!entry.attributes) {
|
|
|
30013 |
this.trigger('warn', {
|
|
|
30014 |
message: 'ignoring empty stream-inf attributes'
|
|
|
30015 |
});
|
|
|
30016 |
return;
|
|
|
30017 |
}
|
|
|
30018 |
if (!currentUri.attributes) {
|
|
|
30019 |
currentUri.attributes = {};
|
|
|
30020 |
}
|
|
|
30021 |
_extends$1(currentUri.attributes, entry.attributes);
|
|
|
30022 |
},
|
|
|
30023 |
media() {
|
|
|
30024 |
this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
|
|
|
30025 |
if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
|
|
|
30026 |
this.trigger('warn', {
|
|
|
30027 |
message: 'ignoring incomplete or missing media group'
|
|
|
30028 |
});
|
|
|
30029 |
return;
|
|
|
30030 |
} // find the media group, creating defaults as necessary
|
|
|
30031 |
|
|
|
30032 |
const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
|
|
|
30033 |
mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
|
|
|
30034 |
mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata
|
|
|
30035 |
|
|
|
30036 |
rendition = {
|
|
|
30037 |
default: /yes/i.test(entry.attributes.DEFAULT)
|
|
|
30038 |
};
|
|
|
30039 |
if (rendition.default) {
|
|
|
30040 |
rendition.autoselect = true;
|
|
|
30041 |
} else {
|
|
|
30042 |
rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
|
|
|
30043 |
}
|
|
|
30044 |
if (entry.attributes.LANGUAGE) {
|
|
|
30045 |
rendition.language = entry.attributes.LANGUAGE;
|
|
|
30046 |
}
|
|
|
30047 |
if (entry.attributes.URI) {
|
|
|
30048 |
rendition.uri = entry.attributes.URI;
|
|
|
30049 |
}
|
|
|
30050 |
if (entry.attributes['INSTREAM-ID']) {
|
|
|
30051 |
rendition.instreamId = entry.attributes['INSTREAM-ID'];
|
|
|
30052 |
}
|
|
|
30053 |
if (entry.attributes.CHARACTERISTICS) {
|
|
|
30054 |
rendition.characteristics = entry.attributes.CHARACTERISTICS;
|
|
|
30055 |
}
|
|
|
30056 |
if (entry.attributes.FORCED) {
|
|
|
30057 |
rendition.forced = /yes/i.test(entry.attributes.FORCED);
|
|
|
30058 |
} // insert the new rendition
|
|
|
30059 |
|
|
|
30060 |
mediaGroup[entry.attributes.NAME] = rendition;
|
|
|
30061 |
},
|
|
|
30062 |
discontinuity() {
|
|
|
30063 |
currentTimeline += 1;
|
|
|
30064 |
currentUri.discontinuity = true;
|
|
|
30065 |
this.manifest.discontinuityStarts.push(uris.length);
|
|
|
30066 |
},
|
|
|
30067 |
'program-date-time'() {
|
|
|
30068 |
if (typeof this.manifest.dateTimeString === 'undefined') {
|
|
|
30069 |
// PROGRAM-DATE-TIME is a media-segment tag, but for backwards
|
|
|
30070 |
// compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag
|
|
|
30071 |
// to the manifest object
|
|
|
30072 |
// TODO: Consider removing this in future major version
|
|
|
30073 |
this.manifest.dateTimeString = entry.dateTimeString;
|
|
|
30074 |
this.manifest.dateTimeObject = entry.dateTimeObject;
|
|
|
30075 |
}
|
|
|
30076 |
currentUri.dateTimeString = entry.dateTimeString;
|
|
|
30077 |
currentUri.dateTimeObject = entry.dateTimeObject;
|
|
|
30078 |
const {
|
|
|
30079 |
lastProgramDateTime
|
|
|
30080 |
} = this;
|
|
|
30081 |
this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence.
|
|
|
30082 |
// Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference.
|
|
|
30083 |
|
|
|
30084 |
if (lastProgramDateTime === null) {
|
|
|
30085 |
// Extrapolate Program Date Time backward
|
|
|
30086 |
// Since it is first program date time occurrence we're assuming that
|
|
|
30087 |
// all this.manifest.segments have no program date time info
|
|
|
30088 |
this.manifest.segments.reduceRight((programDateTime, segment) => {
|
|
|
30089 |
segment.programDateTime = programDateTime - segment.duration * 1000;
|
|
|
30090 |
return segment.programDateTime;
|
|
|
30091 |
}, this.lastProgramDateTime);
|
|
|
30092 |
}
|
|
|
30093 |
},
|
|
|
30094 |
targetduration() {
|
|
|
30095 |
if (!isFinite(entry.duration) || entry.duration < 0) {
|
|
|
30096 |
this.trigger('warn', {
|
|
|
30097 |
message: 'ignoring invalid target duration: ' + entry.duration
|
|
|
30098 |
});
|
|
|
30099 |
return;
|
|
|
30100 |
}
|
|
|
30101 |
this.manifest.targetDuration = entry.duration;
|
|
|
30102 |
setHoldBack.call(this, this.manifest);
|
|
|
30103 |
},
|
|
|
30104 |
start() {
|
|
|
30105 |
if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) {
|
|
|
30106 |
this.trigger('warn', {
|
|
|
30107 |
message: 'ignoring start declaration without appropriate attribute list'
|
|
|
30108 |
});
|
|
|
30109 |
return;
|
|
|
30110 |
}
|
|
|
30111 |
this.manifest.start = {
|
|
|
30112 |
timeOffset: entry.attributes['TIME-OFFSET'],
|
|
|
30113 |
precise: entry.attributes.PRECISE
|
|
|
30114 |
};
|
|
|
30115 |
},
|
|
|
30116 |
'cue-out'() {
|
|
|
30117 |
currentUri.cueOut = entry.data;
|
|
|
30118 |
},
|
|
|
30119 |
'cue-out-cont'() {
|
|
|
30120 |
currentUri.cueOutCont = entry.data;
|
|
|
30121 |
},
|
|
|
30122 |
'cue-in'() {
|
|
|
30123 |
currentUri.cueIn = entry.data;
|
|
|
30124 |
},
|
|
|
30125 |
'skip'() {
|
|
|
30126 |
this.manifest.skip = camelCaseKeys(entry.attributes);
|
|
|
30127 |
this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']);
|
|
|
30128 |
},
|
|
|
30129 |
'part'() {
|
|
|
30130 |
hasParts = true; // parts are always specifed before a segment
|
|
|
30131 |
|
|
|
30132 |
const segmentIndex = this.manifest.segments.length;
|
|
|
30133 |
const part = camelCaseKeys(entry.attributes);
|
|
|
30134 |
currentUri.parts = currentUri.parts || [];
|
|
|
30135 |
currentUri.parts.push(part);
|
|
|
30136 |
if (part.byterange) {
|
|
|
30137 |
if (!part.byterange.hasOwnProperty('offset')) {
|
|
|
30138 |
part.byterange.offset = lastPartByterangeEnd;
|
|
|
30139 |
}
|
|
|
30140 |
lastPartByterangeEnd = part.byterange.offset + part.byterange.length;
|
|
|
30141 |
}
|
|
|
30142 |
const partIndex = currentUri.parts.length - 1;
|
|
|
30143 |
this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']);
|
|
|
30144 |
if (this.manifest.renditionReports) {
|
|
|
30145 |
this.manifest.renditionReports.forEach((r, i) => {
|
|
|
30146 |
if (!r.hasOwnProperty('lastPart')) {
|
|
|
30147 |
this.trigger('warn', {
|
|
|
30148 |
message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART`
|
|
|
30149 |
});
|
|
|
30150 |
}
|
|
|
30151 |
});
|
|
|
30152 |
}
|
|
|
30153 |
},
|
|
|
30154 |
'server-control'() {
|
|
|
30155 |
const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes);
|
|
|
30156 |
if (!attrs.hasOwnProperty('canBlockReload')) {
|
|
|
30157 |
attrs.canBlockReload = false;
|
|
|
30158 |
this.trigger('info', {
|
|
|
30159 |
message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false'
|
|
|
30160 |
});
|
|
|
30161 |
}
|
|
|
30162 |
setHoldBack.call(this, this.manifest);
|
|
|
30163 |
if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) {
|
|
|
30164 |
this.trigger('warn', {
|
|
|
30165 |
message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set'
|
|
|
30166 |
});
|
|
|
30167 |
}
|
|
|
30168 |
},
|
|
|
30169 |
'preload-hint'() {
|
|
|
30170 |
// parts are always specifed before a segment
|
|
|
30171 |
const segmentIndex = this.manifest.segments.length;
|
|
|
30172 |
const hint = camelCaseKeys(entry.attributes);
|
|
|
30173 |
const isPart = hint.type && hint.type === 'PART';
|
|
|
30174 |
currentUri.preloadHints = currentUri.preloadHints || [];
|
|
|
30175 |
currentUri.preloadHints.push(hint);
|
|
|
30176 |
if (hint.byterange) {
|
|
|
30177 |
if (!hint.byterange.hasOwnProperty('offset')) {
|
|
|
30178 |
// use last part byterange end or zero if not a part.
|
|
|
30179 |
hint.byterange.offset = isPart ? lastPartByterangeEnd : 0;
|
|
|
30180 |
if (isPart) {
|
|
|
30181 |
lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length;
|
|
|
30182 |
}
|
|
|
30183 |
}
|
|
|
30184 |
}
|
|
|
30185 |
const index = currentUri.preloadHints.length - 1;
|
|
|
30186 |
this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']);
|
|
|
30187 |
if (!hint.type) {
|
|
|
30188 |
return;
|
|
|
30189 |
} // search through all preload hints except for the current one for
|
|
|
30190 |
// a duplicate type.
|
|
|
30191 |
|
|
|
30192 |
for (let i = 0; i < currentUri.preloadHints.length - 1; i++) {
|
|
|
30193 |
const otherHint = currentUri.preloadHints[i];
|
|
|
30194 |
if (!otherHint.type) {
|
|
|
30195 |
continue;
|
|
|
30196 |
}
|
|
|
30197 |
if (otherHint.type === hint.type) {
|
|
|
30198 |
this.trigger('warn', {
|
|
|
30199 |
message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}`
|
|
|
30200 |
});
|
|
|
30201 |
}
|
|
|
30202 |
}
|
|
|
30203 |
},
|
|
|
30204 |
'rendition-report'() {
|
|
|
30205 |
const report = camelCaseKeys(entry.attributes);
|
|
|
30206 |
this.manifest.renditionReports = this.manifest.renditionReports || [];
|
|
|
30207 |
this.manifest.renditionReports.push(report);
|
|
|
30208 |
const index = this.manifest.renditionReports.length - 1;
|
|
|
30209 |
const required = ['LAST-MSN', 'URI'];
|
|
|
30210 |
if (hasParts) {
|
|
|
30211 |
required.push('LAST-PART');
|
|
|
30212 |
}
|
|
|
30213 |
this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required);
|
|
|
30214 |
},
|
|
|
30215 |
'part-inf'() {
|
|
|
30216 |
this.manifest.partInf = camelCaseKeys(entry.attributes);
|
|
|
30217 |
this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']);
|
|
|
30218 |
if (this.manifest.partInf.partTarget) {
|
|
|
30219 |
this.manifest.partTargetDuration = this.manifest.partInf.partTarget;
|
|
|
30220 |
}
|
|
|
30221 |
setHoldBack.call(this, this.manifest);
|
|
|
30222 |
},
|
|
|
30223 |
'daterange'() {
|
|
|
30224 |
this.manifest.dateRanges.push(camelCaseKeys(entry.attributes));
|
|
|
30225 |
const index = this.manifest.dateRanges.length - 1;
|
|
|
30226 |
this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']);
|
|
|
30227 |
const dateRange = this.manifest.dateRanges[index];
|
|
|
30228 |
if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) {
|
|
|
30229 |
this.trigger('warn', {
|
|
|
30230 |
message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
|
|
|
30231 |
});
|
|
|
30232 |
}
|
|
|
30233 |
if (dateRange.duration && dateRange.duration < 0) {
|
|
|
30234 |
this.trigger('warn', {
|
|
|
30235 |
message: 'EXT-X-DATERANGE DURATION must not be negative'
|
|
|
30236 |
});
|
|
|
30237 |
}
|
|
|
30238 |
if (dateRange.plannedDuration && dateRange.plannedDuration < 0) {
|
|
|
30239 |
this.trigger('warn', {
|
|
|
30240 |
message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
|
|
|
30241 |
});
|
|
|
30242 |
}
|
|
|
30243 |
const endOnNextYes = !!dateRange.endOnNext;
|
|
|
30244 |
if (endOnNextYes && !dateRange.class) {
|
|
|
30245 |
this.trigger('warn', {
|
|
|
30246 |
message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
|
|
|
30247 |
});
|
|
|
30248 |
}
|
|
|
30249 |
if (endOnNextYes && (dateRange.duration || dateRange.endDate)) {
|
|
|
30250 |
this.trigger('warn', {
|
|
|
30251 |
message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
|
|
|
30252 |
});
|
|
|
30253 |
}
|
|
|
30254 |
if (dateRange.duration && dateRange.endDate) {
|
|
|
30255 |
const startDate = dateRange.startDate;
|
|
|
30256 |
const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000;
|
|
|
30257 |
this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds);
|
|
|
30258 |
}
|
|
|
30259 |
if (!dateRangeTags[dateRange.id]) {
|
|
|
30260 |
dateRangeTags[dateRange.id] = dateRange;
|
|
|
30261 |
} else {
|
|
|
30262 |
for (const attribute in dateRangeTags[dateRange.id]) {
|
|
|
30263 |
if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) {
|
|
|
30264 |
this.trigger('warn', {
|
|
|
30265 |
message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
|
|
|
30266 |
});
|
|
|
30267 |
break;
|
|
|
30268 |
}
|
|
|
30269 |
} // if tags with the same ID do not have conflicting attributes, merge them
|
|
|
30270 |
|
|
|
30271 |
const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id);
|
|
|
30272 |
this.manifest.dateRanges[dateRangeWithSameId] = _extends$1(this.manifest.dateRanges[dateRangeWithSameId], dateRange);
|
|
|
30273 |
dateRangeTags[dateRange.id] = _extends$1(dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last
|
|
|
30274 |
|
|
|
30275 |
this.manifest.dateRanges.pop();
|
|
|
30276 |
}
|
|
|
30277 |
},
|
|
|
30278 |
'independent-segments'() {
|
|
|
30279 |
this.manifest.independentSegments = true;
|
|
|
30280 |
},
|
|
|
30281 |
'content-steering'() {
|
|
|
30282 |
this.manifest.contentSteering = camelCaseKeys(entry.attributes);
|
|
|
30283 |
this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']);
|
|
|
30284 |
}
|
|
|
30285 |
})[entry.tagType] || noop).call(self);
|
|
|
30286 |
},
|
|
|
30287 |
uri() {
|
|
|
30288 |
currentUri.uri = entry.uri;
|
|
|
30289 |
uris.push(currentUri); // if no explicit duration was declared, use the target duration
|
|
|
30290 |
|
|
|
30291 |
if (this.manifest.targetDuration && !('duration' in currentUri)) {
|
|
|
30292 |
this.trigger('warn', {
|
|
|
30293 |
message: 'defaulting segment duration to the target duration'
|
|
|
30294 |
});
|
|
|
30295 |
currentUri.duration = this.manifest.targetDuration;
|
|
|
30296 |
} // annotate with encryption information, if necessary
|
|
|
30297 |
|
|
|
30298 |
if (key) {
|
|
|
30299 |
currentUri.key = key;
|
|
|
30300 |
}
|
|
|
30301 |
currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary
|
|
|
30302 |
|
|
|
30303 |
if (currentMap) {
|
|
|
30304 |
currentUri.map = currentMap;
|
|
|
30305 |
} // reset the last byterange end as it needs to be 0 between parts
|
|
|
30306 |
|
|
|
30307 |
lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward
|
|
|
30308 |
|
|
|
30309 |
if (this.lastProgramDateTime !== null) {
|
|
|
30310 |
currentUri.programDateTime = this.lastProgramDateTime;
|
|
|
30311 |
this.lastProgramDateTime += currentUri.duration * 1000;
|
|
|
30312 |
} // prepare for the next URI
|
|
|
30313 |
|
|
|
30314 |
currentUri = {};
|
|
|
30315 |
},
|
|
|
30316 |
comment() {// comments are not important for playback
|
|
|
30317 |
},
|
|
|
30318 |
custom() {
|
|
|
30319 |
// if this is segment-level data attach the output to the segment
|
|
|
30320 |
if (entry.segment) {
|
|
|
30321 |
currentUri.custom = currentUri.custom || {};
|
|
|
30322 |
currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object
|
|
|
30323 |
} else {
|
|
|
30324 |
this.manifest.custom = this.manifest.custom || {};
|
|
|
30325 |
this.manifest.custom[entry.customType] = entry.data;
|
|
|
30326 |
}
|
|
|
30327 |
}
|
|
|
30328 |
})[entry.type].call(self);
|
|
|
30329 |
});
|
|
|
30330 |
}
|
|
|
30331 |
warnOnMissingAttributes_(identifier, attributes, required) {
|
|
|
30332 |
const missing = [];
|
|
|
30333 |
required.forEach(function (key) {
|
|
|
30334 |
if (!attributes.hasOwnProperty(key)) {
|
|
|
30335 |
missing.push(key);
|
|
|
30336 |
}
|
|
|
30337 |
});
|
|
|
30338 |
if (missing.length) {
|
|
|
30339 |
this.trigger('warn', {
|
|
|
30340 |
message: `${identifier} lacks required attribute(s): ${missing.join(', ')}`
|
|
|
30341 |
});
|
|
|
30342 |
}
|
|
|
30343 |
}
|
|
|
30344 |
/**
|
|
|
30345 |
* Parse the input string and update the manifest object.
|
|
|
30346 |
*
|
|
|
30347 |
* @param {string} chunk a potentially incomplete portion of the manifest
|
|
|
30348 |
*/
|
|
|
30349 |
|
|
|
30350 |
push(chunk) {
|
|
|
30351 |
this.lineStream.push(chunk);
|
|
|
30352 |
}
|
|
|
30353 |
/**
|
|
|
30354 |
* Flush any remaining input. This can be handy if the last line of an M3U8
|
|
|
30355 |
* manifest did not contain a trailing newline but the file has been
|
|
|
30356 |
* completely received.
|
|
|
30357 |
*/
|
|
|
30358 |
|
|
|
30359 |
end() {
|
|
|
30360 |
// flush any buffered input
|
|
|
30361 |
this.lineStream.push('\n');
|
|
|
30362 |
if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) {
|
|
|
30363 |
this.trigger('warn', {
|
|
|
30364 |
message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
|
|
|
30365 |
});
|
|
|
30366 |
}
|
|
|
30367 |
this.lastProgramDateTime = null;
|
|
|
30368 |
this.trigger('end');
|
|
|
30369 |
}
|
|
|
30370 |
/**
|
|
|
30371 |
* Add an additional parser for non-standard tags
|
|
|
30372 |
*
|
|
|
30373 |
* @param {Object} options a map of options for the added parser
|
|
|
30374 |
* @param {RegExp} options.expression a regular expression to match the custom header
|
|
|
30375 |
* @param {string} options.customType the custom type to register to the output
|
|
|
30376 |
* @param {Function} [options.dataParser] function to parse the line into an object
|
|
|
30377 |
* @param {boolean} [options.segment] should tag data be attached to the segment object
|
|
|
30378 |
*/
|
|
|
30379 |
|
|
|
30380 |
addParser(options) {
|
|
|
30381 |
this.parseStream.addParser(options);
|
|
|
30382 |
}
|
|
|
30383 |
/**
|
|
|
30384 |
* Add a custom header mapper
|
|
|
30385 |
*
|
|
|
30386 |
* @param {Object} options
|
|
|
30387 |
* @param {RegExp} options.expression a regular expression to match the custom header
|
|
|
30388 |
* @param {Function} options.map function to translate tag into a different tag
|
|
|
30389 |
*/
|
|
|
30390 |
|
|
|
30391 |
addTagMapper(options) {
|
|
|
30392 |
this.parseStream.addTagMapper(options);
|
|
|
30393 |
}
|
|
|
30394 |
}
|
|
|
30395 |
|
|
|
30396 |
var regexs = {
|
|
|
30397 |
// to determine mime types
|
|
|
30398 |
mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v|stpp.ttml.im1t)/,
|
|
|
30399 |
webm: /^(vp0?[89]|av0?1|opus|vorbis)/,
|
|
|
30400 |
ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/,
|
|
|
30401 |
// to determine if a codec is audio or video
|
|
|
30402 |
video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/,
|
|
|
30403 |
audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3|speex|aac)/,
|
|
|
30404 |
text: /^(stpp.ttml.im1t)/,
|
|
|
30405 |
// mux.js support regex
|
|
|
30406 |
muxerVideo: /^(avc0?1)/,
|
|
|
30407 |
muxerAudio: /^(mp4a)/,
|
|
|
30408 |
// match nothing as muxer does not support text right now.
|
|
|
30409 |
// there cannot never be a character before the start of a string
|
|
|
30410 |
// so this matches nothing.
|
|
|
30411 |
muxerText: /a^/
|
|
|
30412 |
};
|
|
|
30413 |
var mediaTypes = ['video', 'audio', 'text'];
|
|
|
30414 |
var upperMediaTypes = ['Video', 'Audio', 'Text'];
|
|
|
30415 |
/**
|
|
|
30416 |
* Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard
|
|
|
30417 |
* `avc1.<hhhhhh>`
|
|
|
30418 |
*
|
|
|
30419 |
* @param {string} codec
|
|
|
30420 |
* Codec string to translate
|
|
|
30421 |
* @return {string}
|
|
|
30422 |
* The translated codec string
|
|
|
30423 |
*/
|
|
|
30424 |
|
|
|
30425 |
var translateLegacyCodec = function translateLegacyCodec(codec) {
|
|
|
30426 |
if (!codec) {
|
|
|
30427 |
return codec;
|
|
|
30428 |
}
|
|
|
30429 |
return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) {
|
|
|
30430 |
var profileHex = ('00' + Number(profile).toString(16)).slice(-2);
|
|
|
30431 |
var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2);
|
|
|
30432 |
return 'avc1.' + profileHex + '00' + avcLevelHex;
|
|
|
30433 |
});
|
|
|
30434 |
};
|
|
|
30435 |
/**
|
|
|
30436 |
* @typedef {Object} ParsedCodecInfo
|
|
|
30437 |
* @property {number} codecCount
|
|
|
30438 |
* Number of codecs parsed
|
|
|
30439 |
* @property {string} [videoCodec]
|
|
|
30440 |
* Parsed video codec (if found)
|
|
|
30441 |
* @property {string} [videoObjectTypeIndicator]
|
|
|
30442 |
* Video object type indicator (if found)
|
|
|
30443 |
* @property {string|null} audioProfile
|
|
|
30444 |
* Audio profile
|
|
|
30445 |
*/
|
|
|
30446 |
|
|
|
30447 |
/**
|
|
|
30448 |
* Parses a codec string to retrieve the number of codecs specified, the video codec and
|
|
|
30449 |
* object type indicator, and the audio profile.
|
|
|
30450 |
*
|
|
|
30451 |
* @param {string} [codecString]
|
|
|
30452 |
* The codec string to parse
|
|
|
30453 |
* @return {ParsedCodecInfo}
|
|
|
30454 |
* Parsed codec info
|
|
|
30455 |
*/
|
|
|
30456 |
|
|
|
30457 |
var parseCodecs = function parseCodecs(codecString) {
|
|
|
30458 |
if (codecString === void 0) {
|
|
|
30459 |
codecString = '';
|
|
|
30460 |
}
|
|
|
30461 |
var codecs = codecString.split(',');
|
|
|
30462 |
var result = [];
|
|
|
30463 |
codecs.forEach(function (codec) {
|
|
|
30464 |
codec = codec.trim();
|
|
|
30465 |
var codecType;
|
|
|
30466 |
mediaTypes.forEach(function (name) {
|
|
|
30467 |
var match = regexs[name].exec(codec.toLowerCase());
|
|
|
30468 |
if (!match || match.length <= 1) {
|
|
|
30469 |
return;
|
|
|
30470 |
}
|
|
|
30471 |
codecType = name; // maintain codec case
|
|
|
30472 |
|
|
|
30473 |
var type = codec.substring(0, match[1].length);
|
|
|
30474 |
var details = codec.replace(type, '');
|
|
|
30475 |
result.push({
|
|
|
30476 |
type: type,
|
|
|
30477 |
details: details,
|
|
|
30478 |
mediaType: name
|
|
|
30479 |
});
|
|
|
30480 |
});
|
|
|
30481 |
if (!codecType) {
|
|
|
30482 |
result.push({
|
|
|
30483 |
type: codec,
|
|
|
30484 |
details: '',
|
|
|
30485 |
mediaType: 'unknown'
|
|
|
30486 |
});
|
|
|
30487 |
}
|
|
|
30488 |
});
|
|
|
30489 |
return result;
|
|
|
30490 |
};
|
|
|
30491 |
/**
|
|
|
30492 |
* Returns a ParsedCodecInfo object for the default alternate audio playlist if there is
|
|
|
30493 |
* a default alternate audio playlist for the provided audio group.
|
|
|
30494 |
*
|
|
|
30495 |
* @param {Object} master
|
|
|
30496 |
* The master playlist
|
|
|
30497 |
* @param {string} audioGroupId
|
|
|
30498 |
* ID of the audio group for which to find the default codec info
|
|
|
30499 |
* @return {ParsedCodecInfo}
|
|
|
30500 |
* Parsed codec info
|
|
|
30501 |
*/
|
|
|
30502 |
|
|
|
30503 |
var codecsFromDefault = function codecsFromDefault(master, audioGroupId) {
|
|
|
30504 |
if (!master.mediaGroups.AUDIO || !audioGroupId) {
|
|
|
30505 |
return null;
|
|
|
30506 |
}
|
|
|
30507 |
var audioGroup = master.mediaGroups.AUDIO[audioGroupId];
|
|
|
30508 |
if (!audioGroup) {
|
|
|
30509 |
return null;
|
|
|
30510 |
}
|
|
|
30511 |
for (var name in audioGroup) {
|
|
|
30512 |
var audioType = audioGroup[name];
|
|
|
30513 |
if (audioType.default && audioType.playlists) {
|
|
|
30514 |
// codec should be the same for all playlists within the audio type
|
|
|
30515 |
return parseCodecs(audioType.playlists[0].attributes.CODECS);
|
|
|
30516 |
}
|
|
|
30517 |
}
|
|
|
30518 |
return null;
|
|
|
30519 |
};
|
|
|
30520 |
var isAudioCodec = function isAudioCodec(codec) {
|
|
|
30521 |
if (codec === void 0) {
|
|
|
30522 |
codec = '';
|
|
|
30523 |
}
|
|
|
30524 |
return regexs.audio.test(codec.trim().toLowerCase());
|
|
|
30525 |
};
|
|
|
30526 |
var isTextCodec = function isTextCodec(codec) {
|
|
|
30527 |
if (codec === void 0) {
|
|
|
30528 |
codec = '';
|
|
|
30529 |
}
|
|
|
30530 |
return regexs.text.test(codec.trim().toLowerCase());
|
|
|
30531 |
};
|
|
|
30532 |
var getMimeForCodec = function getMimeForCodec(codecString) {
|
|
|
30533 |
if (!codecString || typeof codecString !== 'string') {
|
|
|
30534 |
return;
|
|
|
30535 |
}
|
|
|
30536 |
var codecs = codecString.toLowerCase().split(',').map(function (c) {
|
|
|
30537 |
return translateLegacyCodec(c.trim());
|
|
|
30538 |
}); // default to video type
|
|
|
30539 |
|
|
|
30540 |
var type = 'video'; // only change to audio type if the only codec we have is
|
|
|
30541 |
// audio
|
|
|
30542 |
|
|
|
30543 |
if (codecs.length === 1 && isAudioCodec(codecs[0])) {
|
|
|
30544 |
type = 'audio';
|
|
|
30545 |
} else if (codecs.length === 1 && isTextCodec(codecs[0])) {
|
|
|
30546 |
// text uses application/<container> for now
|
|
|
30547 |
type = 'application';
|
|
|
30548 |
} // default the container to mp4
|
|
|
30549 |
|
|
|
30550 |
var container = 'mp4'; // every codec must be able to go into the container
|
|
|
30551 |
// for that container to be the correct one
|
|
|
30552 |
|
|
|
30553 |
if (codecs.every(function (c) {
|
|
|
30554 |
return regexs.mp4.test(c);
|
|
|
30555 |
})) {
|
|
|
30556 |
container = 'mp4';
|
|
|
30557 |
} else if (codecs.every(function (c) {
|
|
|
30558 |
return regexs.webm.test(c);
|
|
|
30559 |
})) {
|
|
|
30560 |
container = 'webm';
|
|
|
30561 |
} else if (codecs.every(function (c) {
|
|
|
30562 |
return regexs.ogg.test(c);
|
|
|
30563 |
})) {
|
|
|
30564 |
container = 'ogg';
|
|
|
30565 |
}
|
|
|
30566 |
return type + "/" + container + ";codecs=\"" + codecString + "\"";
|
|
|
30567 |
};
|
|
|
30568 |
var browserSupportsCodec = function browserSupportsCodec(codecString) {
|
|
|
30569 |
if (codecString === void 0) {
|
|
|
30570 |
codecString = '';
|
|
|
30571 |
}
|
|
|
30572 |
return window.MediaSource && window.MediaSource.isTypeSupported && window.MediaSource.isTypeSupported(getMimeForCodec(codecString)) || false;
|
|
|
30573 |
};
|
|
|
30574 |
var muxerSupportsCodec = function muxerSupportsCodec(codecString) {
|
|
|
30575 |
if (codecString === void 0) {
|
|
|
30576 |
codecString = '';
|
|
|
30577 |
}
|
|
|
30578 |
return codecString.toLowerCase().split(',').every(function (codec) {
|
|
|
30579 |
codec = codec.trim(); // any match is supported.
|
|
|
30580 |
|
|
|
30581 |
for (var i = 0; i < upperMediaTypes.length; i++) {
|
|
|
30582 |
var type = upperMediaTypes[i];
|
|
|
30583 |
if (regexs["muxer" + type].test(codec)) {
|
|
|
30584 |
return true;
|
|
|
30585 |
}
|
|
|
30586 |
}
|
|
|
30587 |
return false;
|
|
|
30588 |
});
|
|
|
30589 |
};
|
|
|
30590 |
var DEFAULT_AUDIO_CODEC = 'mp4a.40.2';
|
|
|
30591 |
var DEFAULT_VIDEO_CODEC = 'avc1.4d400d';
|
|
|
30592 |
|
|
|
30593 |
var MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
|
|
|
30594 |
var DASH_REGEX = /^application\/dash\+xml/i;
|
|
|
30595 |
/**
|
|
|
30596 |
* Returns a string that describes the type of source based on a video source object's
|
|
|
30597 |
* media type.
|
|
|
30598 |
*
|
|
|
30599 |
* @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type}
|
|
|
30600 |
*
|
|
|
30601 |
* @param {string} type
|
|
|
30602 |
* Video source object media type
|
|
|
30603 |
* @return {('hls'|'dash'|'vhs-json'|null)}
|
|
|
30604 |
* VHS source type string
|
|
|
30605 |
*/
|
|
|
30606 |
|
|
|
30607 |
var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) {
|
|
|
30608 |
if (MPEGURL_REGEX.test(type)) {
|
|
|
30609 |
return 'hls';
|
|
|
30610 |
}
|
|
|
30611 |
if (DASH_REGEX.test(type)) {
|
|
|
30612 |
return 'dash';
|
|
|
30613 |
} // Denotes the special case of a manifest object passed to http-streaming instead of a
|
|
|
30614 |
// source URL.
|
|
|
30615 |
//
|
|
|
30616 |
// See https://en.wikipedia.org/wiki/Media_type for details on specifying media types.
|
|
|
30617 |
//
|
|
|
30618 |
// In this case, vnd stands for vendor, video.js for the organization, VHS for this
|
|
|
30619 |
// project, and the +json suffix identifies the structure of the media type.
|
|
|
30620 |
|
|
|
30621 |
if (type === 'application/vnd.videojs.vhs+json') {
|
|
|
30622 |
return 'vhs-json';
|
|
|
30623 |
}
|
|
|
30624 |
return null;
|
|
|
30625 |
};
|
|
|
30626 |
|
|
|
30627 |
// const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2));
|
|
|
30628 |
// we used to do this with log2 but BigInt does not support builtin math
|
|
|
30629 |
// Math.ceil(log2(x));
|
|
|
30630 |
|
|
|
30631 |
var countBits = function countBits(x) {
|
|
|
30632 |
return x.toString(2).length;
|
|
|
30633 |
}; // count the number of whole bytes it would take to represent a number
|
|
|
30634 |
|
|
|
30635 |
var countBytes = function countBytes(x) {
|
|
|
30636 |
return Math.ceil(countBits(x) / 8);
|
|
|
30637 |
};
|
|
|
30638 |
var isArrayBufferView = function isArrayBufferView(obj) {
|
|
|
30639 |
if (ArrayBuffer.isView === 'function') {
|
|
|
30640 |
return ArrayBuffer.isView(obj);
|
|
|
30641 |
}
|
|
|
30642 |
return obj && obj.buffer instanceof ArrayBuffer;
|
|
|
30643 |
};
|
|
|
30644 |
var isTypedArray = function isTypedArray(obj) {
|
|
|
30645 |
return isArrayBufferView(obj);
|
|
|
30646 |
};
|
|
|
30647 |
var toUint8 = function toUint8(bytes) {
|
|
|
30648 |
if (bytes instanceof Uint8Array) {
|
|
|
30649 |
return bytes;
|
|
|
30650 |
}
|
|
|
30651 |
if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) {
|
|
|
30652 |
// any non-number or NaN leads to empty uint8array
|
|
|
30653 |
// eslint-disable-next-line
|
|
|
30654 |
if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) {
|
|
|
30655 |
bytes = 0;
|
|
|
30656 |
} else {
|
|
|
30657 |
bytes = [bytes];
|
|
|
30658 |
}
|
|
|
30659 |
}
|
|
|
30660 |
return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0);
|
|
|
30661 |
};
|
|
|
30662 |
var BigInt = window.BigInt || Number;
|
|
|
30663 |
var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
|
|
|
30664 |
(function () {
|
|
|
30665 |
var a = new Uint16Array([0xFFCC]);
|
|
|
30666 |
var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
|
|
|
30667 |
if (b[0] === 0xFF) {
|
|
|
30668 |
return 'big';
|
|
|
30669 |
}
|
|
|
30670 |
if (b[0] === 0xCC) {
|
|
|
30671 |
return 'little';
|
|
|
30672 |
}
|
|
|
30673 |
return 'unknown';
|
|
|
30674 |
})();
|
|
|
30675 |
var bytesToNumber = function bytesToNumber(bytes, _temp) {
|
|
|
30676 |
var _ref = _temp === void 0 ? {} : _temp,
|
|
|
30677 |
_ref$signed = _ref.signed,
|
|
|
30678 |
signed = _ref$signed === void 0 ? false : _ref$signed,
|
|
|
30679 |
_ref$le = _ref.le,
|
|
|
30680 |
le = _ref$le === void 0 ? false : _ref$le;
|
|
|
30681 |
bytes = toUint8(bytes);
|
|
|
30682 |
var fn = le ? 'reduce' : 'reduceRight';
|
|
|
30683 |
var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn];
|
|
|
30684 |
var number = obj.call(bytes, function (total, byte, i) {
|
|
|
30685 |
var exponent = le ? i : Math.abs(i + 1 - bytes.length);
|
|
|
30686 |
return total + BigInt(byte) * BYTE_TABLE[exponent];
|
|
|
30687 |
}, BigInt(0));
|
|
|
30688 |
if (signed) {
|
|
|
30689 |
var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1);
|
|
|
30690 |
number = BigInt(number);
|
|
|
30691 |
if (number > max) {
|
|
|
30692 |
number -= max;
|
|
|
30693 |
number -= max;
|
|
|
30694 |
number -= BigInt(2);
|
|
|
30695 |
}
|
|
|
30696 |
}
|
|
|
30697 |
return Number(number);
|
|
|
30698 |
};
|
|
|
30699 |
var numberToBytes = function numberToBytes(number, _temp2) {
|
|
|
30700 |
var _ref2 = _temp2 === void 0 ? {} : _temp2,
|
|
|
30701 |
_ref2$le = _ref2.le,
|
|
|
30702 |
le = _ref2$le === void 0 ? false : _ref2$le;
|
|
|
30703 |
|
|
|
30704 |
// eslint-disable-next-line
|
|
|
30705 |
if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) {
|
|
|
30706 |
number = 0;
|
|
|
30707 |
}
|
|
|
30708 |
number = BigInt(number);
|
|
|
30709 |
var byteCount = countBytes(number);
|
|
|
30710 |
var bytes = new Uint8Array(new ArrayBuffer(byteCount));
|
|
|
30711 |
for (var i = 0; i < byteCount; i++) {
|
|
|
30712 |
var byteIndex = le ? i : Math.abs(i + 1 - bytes.length);
|
|
|
30713 |
bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF));
|
|
|
30714 |
if (number < 0) {
|
|
|
30715 |
bytes[byteIndex] = Math.abs(~bytes[byteIndex]);
|
|
|
30716 |
bytes[byteIndex] -= i === 0 ? 1 : 2;
|
|
|
30717 |
}
|
|
|
30718 |
}
|
|
|
30719 |
return bytes;
|
|
|
30720 |
};
|
|
|
30721 |
var stringToBytes = function stringToBytes(string, stringIsBytes) {
|
|
|
30722 |
if (typeof string !== 'string' && string && typeof string.toString === 'function') {
|
|
|
30723 |
string = string.toString();
|
|
|
30724 |
}
|
|
|
30725 |
if (typeof string !== 'string') {
|
|
|
30726 |
return new Uint8Array();
|
|
|
30727 |
} // If the string already is bytes, we don't have to do this
|
|
|
30728 |
// otherwise we do this so that we split multi length characters
|
|
|
30729 |
// into individual bytes
|
|
|
30730 |
|
|
|
30731 |
if (!stringIsBytes) {
|
|
|
30732 |
string = unescape(encodeURIComponent(string));
|
|
|
30733 |
}
|
|
|
30734 |
var view = new Uint8Array(string.length);
|
|
|
30735 |
for (var i = 0; i < string.length; i++) {
|
|
|
30736 |
view[i] = string.charCodeAt(i);
|
|
|
30737 |
}
|
|
|
30738 |
return view;
|
|
|
30739 |
};
|
|
|
30740 |
var concatTypedArrays = function concatTypedArrays() {
|
|
|
30741 |
for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
|
30742 |
buffers[_key] = arguments[_key];
|
|
|
30743 |
}
|
|
|
30744 |
buffers = buffers.filter(function (b) {
|
|
|
30745 |
return b && (b.byteLength || b.length) && typeof b !== 'string';
|
|
|
30746 |
});
|
|
|
30747 |
if (buffers.length <= 1) {
|
|
|
30748 |
// for 0 length we will return empty uint8
|
|
|
30749 |
// for 1 length we return the first uint8
|
|
|
30750 |
return toUint8(buffers[0]);
|
|
|
30751 |
}
|
|
|
30752 |
var totalLen = buffers.reduce(function (total, buf, i) {
|
|
|
30753 |
return total + (buf.byteLength || buf.length);
|
|
|
30754 |
}, 0);
|
|
|
30755 |
var tempBuffer = new Uint8Array(totalLen);
|
|
|
30756 |
var offset = 0;
|
|
|
30757 |
buffers.forEach(function (buf) {
|
|
|
30758 |
buf = toUint8(buf);
|
|
|
30759 |
tempBuffer.set(buf, offset);
|
|
|
30760 |
offset += buf.byteLength;
|
|
|
30761 |
});
|
|
|
30762 |
return tempBuffer;
|
|
|
30763 |
};
|
|
|
30764 |
/**
|
|
|
30765 |
* Check if the bytes "b" are contained within bytes "a".
|
|
|
30766 |
*
|
|
|
30767 |
* @param {Uint8Array|Array} a
|
|
|
30768 |
* Bytes to check in
|
|
|
30769 |
*
|
|
|
30770 |
* @param {Uint8Array|Array} b
|
|
|
30771 |
* Bytes to check for
|
|
|
30772 |
*
|
|
|
30773 |
* @param {Object} options
|
|
|
30774 |
* options
|
|
|
30775 |
*
|
|
|
30776 |
* @param {Array|Uint8Array} [offset=0]
|
|
|
30777 |
* offset to use when looking at bytes in a
|
|
|
30778 |
*
|
|
|
30779 |
* @param {Array|Uint8Array} [mask=[]]
|
|
|
30780 |
* mask to use on bytes before comparison.
|
|
|
30781 |
*
|
|
|
30782 |
* @return {boolean}
|
|
|
30783 |
* If all bytes in b are inside of a, taking into account
|
|
|
30784 |
* bit masks.
|
|
|
30785 |
*/
|
|
|
30786 |
|
|
|
30787 |
var bytesMatch = function bytesMatch(a, b, _temp3) {
|
|
|
30788 |
var _ref3 = _temp3 === void 0 ? {} : _temp3,
|
|
|
30789 |
_ref3$offset = _ref3.offset,
|
|
|
30790 |
offset = _ref3$offset === void 0 ? 0 : _ref3$offset,
|
|
|
30791 |
_ref3$mask = _ref3.mask,
|
|
|
30792 |
mask = _ref3$mask === void 0 ? [] : _ref3$mask;
|
|
|
30793 |
a = toUint8(a);
|
|
|
30794 |
b = toUint8(b); // ie 11 does not support uint8 every
|
|
|
30795 |
|
|
|
30796 |
var fn = b.every ? b.every : Array.prototype.every;
|
|
|
30797 |
return b.length && a.length - offset >= b.length &&
|
|
|
30798 |
// ie 11 doesn't support every on uin8
|
|
|
30799 |
fn.call(b, function (bByte, i) {
|
|
|
30800 |
var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i];
|
|
|
30801 |
return bByte === aByte;
|
|
|
30802 |
});
|
|
|
30803 |
};
|
|
|
30804 |
|
|
|
30805 |
/**
|
|
|
30806 |
* Loops through all supported media groups in master and calls the provided
|
|
|
30807 |
* callback for each group
|
|
|
30808 |
*
|
|
|
30809 |
* @param {Object} master
|
|
|
30810 |
* The parsed master manifest object
|
|
|
30811 |
* @param {string[]} groups
|
|
|
30812 |
* The media groups to call the callback for
|
|
|
30813 |
* @param {Function} callback
|
|
|
30814 |
* Callback to call for each media group
|
|
|
30815 |
*/
|
|
|
30816 |
var forEachMediaGroup$1 = function forEachMediaGroup(master, groups, callback) {
|
|
|
30817 |
groups.forEach(function (mediaType) {
|
|
|
30818 |
for (var groupKey in master.mediaGroups[mediaType]) {
|
|
|
30819 |
for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
|
|
|
30820 |
var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
|
|
|
30821 |
callback(mediaProperties, mediaType, groupKey, labelKey);
|
|
|
30822 |
}
|
|
|
30823 |
}
|
|
|
30824 |
});
|
|
|
30825 |
};
|
|
|
30826 |
|
|
|
30827 |
var atob = function atob(s) {
|
|
|
30828 |
return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
|
|
|
30829 |
};
|
|
|
30830 |
function decodeB64ToUint8Array(b64Text) {
|
|
|
30831 |
var decodedString = atob(b64Text);
|
|
|
30832 |
var array = new Uint8Array(decodedString.length);
|
|
|
30833 |
for (var i = 0; i < decodedString.length; i++) {
|
|
|
30834 |
array[i] = decodedString.charCodeAt(i);
|
|
|
30835 |
}
|
|
|
30836 |
return array;
|
|
|
30837 |
}
|
|
|
30838 |
|
|
|
30839 |
/**
|
|
|
30840 |
* Ponyfill for `Array.prototype.find` which is only available in ES6 runtimes.
|
|
|
30841 |
*
|
|
|
30842 |
* Works with anything that has a `length` property and index access properties, including NodeList.
|
|
|
30843 |
*
|
|
|
30844 |
* @template {unknown} T
|
|
|
30845 |
* @param {Array<T> | ({length:number, [number]: T})} list
|
|
|
30846 |
* @param {function (item: T, index: number, list:Array<T> | ({length:number, [number]: T})):boolean} predicate
|
|
|
30847 |
* @param {Partial<Pick<ArrayConstructor['prototype'], 'find'>>?} ac `Array.prototype` by default,
|
|
|
30848 |
* allows injecting a custom implementation in tests
|
|
|
30849 |
* @returns {T | undefined}
|
|
|
30850 |
*
|
|
|
30851 |
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
|
|
|
30852 |
* @see https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.find
|
|
|
30853 |
*/
|
|
|
30854 |
function find$1(list, predicate, ac) {
|
|
|
30855 |
if (ac === undefined) {
|
|
|
30856 |
ac = Array.prototype;
|
|
|
30857 |
}
|
|
|
30858 |
if (list && typeof ac.find === 'function') {
|
|
|
30859 |
return ac.find.call(list, predicate);
|
|
|
30860 |
}
|
|
|
30861 |
for (var i = 0; i < list.length; i++) {
|
|
|
30862 |
if (Object.prototype.hasOwnProperty.call(list, i)) {
|
|
|
30863 |
var item = list[i];
|
|
|
30864 |
if (predicate.call(undefined, item, i, list)) {
|
|
|
30865 |
return item;
|
|
|
30866 |
}
|
|
|
30867 |
}
|
|
|
30868 |
}
|
|
|
30869 |
}
|
|
|
30870 |
|
|
|
30871 |
/**
|
|
|
30872 |
* "Shallow freezes" an object to render it immutable.
|
|
|
30873 |
* Uses `Object.freeze` if available,
|
|
|
30874 |
* otherwise the immutability is only in the type.
|
|
|
30875 |
*
|
|
|
30876 |
* Is used to create "enum like" objects.
|
|
|
30877 |
*
|
|
|
30878 |
* @template T
|
|
|
30879 |
* @param {T} object the object to freeze
|
|
|
30880 |
* @param {Pick<ObjectConstructor, 'freeze'> = Object} oc `Object` by default,
|
|
|
30881 |
* allows to inject custom object constructor for tests
|
|
|
30882 |
* @returns {Readonly<T>}
|
|
|
30883 |
*
|
|
|
30884 |
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
|
|
|
30885 |
*/
|
|
|
30886 |
function freeze(object, oc) {
|
|
|
30887 |
if (oc === undefined) {
|
|
|
30888 |
oc = Object;
|
|
|
30889 |
}
|
|
|
30890 |
return oc && typeof oc.freeze === 'function' ? oc.freeze(object) : object;
|
|
|
30891 |
}
|
|
|
30892 |
|
|
|
30893 |
/**
|
|
|
30894 |
* Since we can not rely on `Object.assign` we provide a simplified version
|
|
|
30895 |
* that is sufficient for our needs.
|
|
|
30896 |
*
|
|
|
30897 |
* @param {Object} target
|
|
|
30898 |
* @param {Object | null | undefined} source
|
|
|
30899 |
*
|
|
|
30900 |
* @returns {Object} target
|
|
|
30901 |
* @throws TypeError if target is not an object
|
|
|
30902 |
*
|
|
|
30903 |
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
|
|
|
30904 |
* @see https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.assign
|
|
|
30905 |
*/
|
|
|
30906 |
function assign(target, source) {
|
|
|
30907 |
if (target === null || typeof target !== 'object') {
|
|
|
30908 |
throw new TypeError('target is not an object');
|
|
|
30909 |
}
|
|
|
30910 |
for (var key in source) {
|
|
|
30911 |
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
|
30912 |
target[key] = source[key];
|
|
|
30913 |
}
|
|
|
30914 |
}
|
|
|
30915 |
return target;
|
|
|
30916 |
}
|
|
|
30917 |
|
|
|
30918 |
/**
|
|
|
30919 |
* All mime types that are allowed as input to `DOMParser.parseFromString`
|
|
|
30920 |
*
|
|
|
30921 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#Argument02 MDN
|
|
|
30922 |
* @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#domparsersupportedtype WHATWG HTML Spec
|
|
|
30923 |
* @see DOMParser.prototype.parseFromString
|
|
|
30924 |
*/
|
|
|
30925 |
var MIME_TYPE = freeze({
|
|
|
30926 |
/**
|
|
|
30927 |
* `text/html`, the only mime type that triggers treating an XML document as HTML.
|
|
|
30928 |
*
|
|
|
30929 |
* @see DOMParser.SupportedType.isHTML
|
|
|
30930 |
* @see https://www.iana.org/assignments/media-types/text/html IANA MimeType registration
|
|
|
30931 |
* @see https://en.wikipedia.org/wiki/HTML Wikipedia
|
|
|
30932 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString MDN
|
|
|
30933 |
* @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring WHATWG HTML Spec
|
|
|
30934 |
*/
|
|
|
30935 |
HTML: 'text/html',
|
|
|
30936 |
/**
|
|
|
30937 |
* Helper method to check a mime type if it indicates an HTML document
|
|
|
30938 |
*
|
|
|
30939 |
* @param {string} [value]
|
|
|
30940 |
* @returns {boolean}
|
|
|
30941 |
*
|
|
|
30942 |
* @see https://www.iana.org/assignments/media-types/text/html IANA MimeType registration
|
|
|
30943 |
* @see https://en.wikipedia.org/wiki/HTML Wikipedia
|
|
|
30944 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString MDN
|
|
|
30945 |
* @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring */
|
|
|
30946 |
isHTML: function (value) {
|
|
|
30947 |
return value === MIME_TYPE.HTML;
|
|
|
30948 |
},
|
|
|
30949 |
/**
|
|
|
30950 |
* `application/xml`, the standard mime type for XML documents.
|
|
|
30951 |
*
|
|
|
30952 |
* @see https://www.iana.org/assignments/media-types/application/xml IANA MimeType registration
|
|
|
30953 |
* @see https://tools.ietf.org/html/rfc7303#section-9.1 RFC 7303
|
|
|
30954 |
* @see https://en.wikipedia.org/wiki/XML_and_MIME Wikipedia
|
|
|
30955 |
*/
|
|
|
30956 |
XML_APPLICATION: 'application/xml',
|
|
|
30957 |
/**
|
|
|
30958 |
* `text/html`, an alias for `application/xml`.
|
|
|
30959 |
*
|
|
|
30960 |
* @see https://tools.ietf.org/html/rfc7303#section-9.2 RFC 7303
|
|
|
30961 |
* @see https://www.iana.org/assignments/media-types/text/xml IANA MimeType registration
|
|
|
30962 |
* @see https://en.wikipedia.org/wiki/XML_and_MIME Wikipedia
|
|
|
30963 |
*/
|
|
|
30964 |
XML_TEXT: 'text/xml',
|
|
|
30965 |
/**
|
|
|
30966 |
* `application/xhtml+xml`, indicates an XML document that has the default HTML namespace,
|
|
|
30967 |
* but is parsed as an XML document.
|
|
|
30968 |
*
|
|
|
30969 |
* @see https://www.iana.org/assignments/media-types/application/xhtml+xml IANA MimeType registration
|
|
|
30970 |
* @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocument WHATWG DOM Spec
|
|
|
30971 |
* @see https://en.wikipedia.org/wiki/XHTML Wikipedia
|
|
|
30972 |
*/
|
|
|
30973 |
XML_XHTML_APPLICATION: 'application/xhtml+xml',
|
|
|
30974 |
/**
|
|
|
30975 |
* `image/svg+xml`,
|
|
|
30976 |
*
|
|
|
30977 |
* @see https://www.iana.org/assignments/media-types/image/svg+xml IANA MimeType registration
|
|
|
30978 |
* @see https://www.w3.org/TR/SVG11/ W3C SVG 1.1
|
|
|
30979 |
* @see https://en.wikipedia.org/wiki/Scalable_Vector_Graphics Wikipedia
|
|
|
30980 |
*/
|
|
|
30981 |
XML_SVG_IMAGE: 'image/svg+xml'
|
|
|
30982 |
});
|
|
|
30983 |
|
|
|
30984 |
/**
|
|
|
30985 |
* Namespaces that are used in this code base.
|
|
|
30986 |
*
|
|
|
30987 |
* @see http://www.w3.org/TR/REC-xml-names
|
|
|
30988 |
*/
|
|
|
30989 |
var NAMESPACE$3 = freeze({
|
|
|
30990 |
/**
|
|
|
30991 |
* The XHTML namespace.
|
|
|
30992 |
*
|
|
|
30993 |
* @see http://www.w3.org/1999/xhtml
|
|
|
30994 |
*/
|
|
|
30995 |
HTML: 'http://www.w3.org/1999/xhtml',
|
|
|
30996 |
/**
|
|
|
30997 |
* Checks if `uri` equals `NAMESPACE.HTML`.
|
|
|
30998 |
*
|
|
|
30999 |
* @param {string} [uri]
|
|
|
31000 |
*
|
|
|
31001 |
* @see NAMESPACE.HTML
|
|
|
31002 |
*/
|
|
|
31003 |
isHTML: function (uri) {
|
|
|
31004 |
return uri === NAMESPACE$3.HTML;
|
|
|
31005 |
},
|
|
|
31006 |
/**
|
|
|
31007 |
* The SVG namespace.
|
|
|
31008 |
*
|
|
|
31009 |
* @see http://www.w3.org/2000/svg
|
|
|
31010 |
*/
|
|
|
31011 |
SVG: 'http://www.w3.org/2000/svg',
|
|
|
31012 |
/**
|
|
|
31013 |
* The `xml:` namespace.
|
|
|
31014 |
*
|
|
|
31015 |
* @see http://www.w3.org/XML/1998/namespace
|
|
|
31016 |
*/
|
|
|
31017 |
XML: 'http://www.w3.org/XML/1998/namespace',
|
|
|
31018 |
/**
|
|
|
31019 |
* The `xmlns:` namespace
|
|
|
31020 |
*
|
|
|
31021 |
* @see https://www.w3.org/2000/xmlns/
|
|
|
31022 |
*/
|
|
|
31023 |
XMLNS: 'http://www.w3.org/2000/xmlns/'
|
|
|
31024 |
});
|
|
|
31025 |
var assign_1 = assign;
|
|
|
31026 |
var find_1 = find$1;
|
|
|
31027 |
var freeze_1 = freeze;
|
|
|
31028 |
var MIME_TYPE_1 = MIME_TYPE;
|
|
|
31029 |
var NAMESPACE_1 = NAMESPACE$3;
|
|
|
31030 |
var conventions = {
|
|
|
31031 |
assign: assign_1,
|
|
|
31032 |
find: find_1,
|
|
|
31033 |
freeze: freeze_1,
|
|
|
31034 |
MIME_TYPE: MIME_TYPE_1,
|
|
|
31035 |
NAMESPACE: NAMESPACE_1
|
|
|
31036 |
};
|
|
|
31037 |
|
|
|
31038 |
var find = conventions.find;
|
|
|
31039 |
var NAMESPACE$2 = conventions.NAMESPACE;
|
|
|
31040 |
|
|
|
31041 |
/**
|
|
|
31042 |
* A prerequisite for `[].filter`, to drop elements that are empty
|
|
|
31043 |
* @param {string} input
|
|
|
31044 |
* @returns {boolean}
|
|
|
31045 |
*/
|
|
|
31046 |
function notEmptyString(input) {
|
|
|
31047 |
return input !== '';
|
|
|
31048 |
}
|
|
|
31049 |
/**
|
|
|
31050 |
* @see https://infra.spec.whatwg.org/#split-on-ascii-whitespace
|
|
|
31051 |
* @see https://infra.spec.whatwg.org/#ascii-whitespace
|
|
|
31052 |
*
|
|
|
31053 |
* @param {string} input
|
|
|
31054 |
* @returns {string[]} (can be empty)
|
|
|
31055 |
*/
|
|
|
31056 |
function splitOnASCIIWhitespace(input) {
|
|
|
31057 |
// U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE
|
|
|
31058 |
return input ? input.split(/[\t\n\f\r ]+/).filter(notEmptyString) : [];
|
|
|
31059 |
}
|
|
|
31060 |
|
|
|
31061 |
/**
|
|
|
31062 |
* Adds element as a key to current if it is not already present.
|
|
|
31063 |
*
|
|
|
31064 |
* @param {Record<string, boolean | undefined>} current
|
|
|
31065 |
* @param {string} element
|
|
|
31066 |
* @returns {Record<string, boolean | undefined>}
|
|
|
31067 |
*/
|
|
|
31068 |
function orderedSetReducer(current, element) {
|
|
|
31069 |
if (!current.hasOwnProperty(element)) {
|
|
|
31070 |
current[element] = true;
|
|
|
31071 |
}
|
|
|
31072 |
return current;
|
|
|
31073 |
}
|
|
|
31074 |
|
|
|
31075 |
/**
|
|
|
31076 |
* @see https://infra.spec.whatwg.org/#ordered-set
|
|
|
31077 |
* @param {string} input
|
|
|
31078 |
* @returns {string[]}
|
|
|
31079 |
*/
|
|
|
31080 |
function toOrderedSet(input) {
|
|
|
31081 |
if (!input) return [];
|
|
|
31082 |
var list = splitOnASCIIWhitespace(input);
|
|
|
31083 |
return Object.keys(list.reduce(orderedSetReducer, {}));
|
|
|
31084 |
}
|
|
|
31085 |
|
|
|
31086 |
/**
|
|
|
31087 |
* Uses `list.indexOf` to implement something like `Array.prototype.includes`,
|
|
|
31088 |
* which we can not rely on being available.
|
|
|
31089 |
*
|
|
|
31090 |
* @param {any[]} list
|
|
|
31091 |
* @returns {function(any): boolean}
|
|
|
31092 |
*/
|
|
|
31093 |
function arrayIncludes(list) {
|
|
|
31094 |
return function (element) {
|
|
|
31095 |
return list && list.indexOf(element) !== -1;
|
|
|
31096 |
};
|
|
|
31097 |
}
|
|
|
31098 |
function copy(src, dest) {
|
|
|
31099 |
for (var p in src) {
|
|
|
31100 |
if (Object.prototype.hasOwnProperty.call(src, p)) {
|
|
|
31101 |
dest[p] = src[p];
|
|
|
31102 |
}
|
|
|
31103 |
}
|
|
|
31104 |
}
|
|
|
31105 |
|
|
|
31106 |
/**
|
|
|
31107 |
^\w+\.prototype\.([_\w]+)\s*=\s*((?:.*\{\s*?[\r\n][\s\S]*?^})|\S.*?(?=[;\r\n]));?
|
|
|
31108 |
^\w+\.prototype\.([_\w]+)\s*=\s*(\S.*?(?=[;\r\n]));?
|
|
|
31109 |
*/
|
|
|
31110 |
function _extends(Class, Super) {
|
|
|
31111 |
var pt = Class.prototype;
|
|
|
31112 |
if (!(pt instanceof Super)) {
|
|
|
31113 |
function t() {}
|
|
|
31114 |
t.prototype = Super.prototype;
|
|
|
31115 |
t = new t();
|
|
|
31116 |
copy(pt, t);
|
|
|
31117 |
Class.prototype = pt = t;
|
|
|
31118 |
}
|
|
|
31119 |
if (pt.constructor != Class) {
|
|
|
31120 |
if (typeof Class != 'function') {
|
|
|
31121 |
console.error("unknown Class:" + Class);
|
|
|
31122 |
}
|
|
|
31123 |
pt.constructor = Class;
|
|
|
31124 |
}
|
|
|
31125 |
}
|
|
|
31126 |
|
|
|
31127 |
// Node Types
|
|
|
31128 |
var NodeType = {};
|
|
|
31129 |
var ELEMENT_NODE = NodeType.ELEMENT_NODE = 1;
|
|
|
31130 |
var ATTRIBUTE_NODE = NodeType.ATTRIBUTE_NODE = 2;
|
|
|
31131 |
var TEXT_NODE = NodeType.TEXT_NODE = 3;
|
|
|
31132 |
var CDATA_SECTION_NODE = NodeType.CDATA_SECTION_NODE = 4;
|
|
|
31133 |
var ENTITY_REFERENCE_NODE = NodeType.ENTITY_REFERENCE_NODE = 5;
|
|
|
31134 |
var ENTITY_NODE = NodeType.ENTITY_NODE = 6;
|
|
|
31135 |
var PROCESSING_INSTRUCTION_NODE = NodeType.PROCESSING_INSTRUCTION_NODE = 7;
|
|
|
31136 |
var COMMENT_NODE = NodeType.COMMENT_NODE = 8;
|
|
|
31137 |
var DOCUMENT_NODE = NodeType.DOCUMENT_NODE = 9;
|
|
|
31138 |
var DOCUMENT_TYPE_NODE = NodeType.DOCUMENT_TYPE_NODE = 10;
|
|
|
31139 |
var DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT_NODE = 11;
|
|
|
31140 |
var NOTATION_NODE = NodeType.NOTATION_NODE = 12;
|
|
|
31141 |
|
|
|
31142 |
// ExceptionCode
|
|
|
31143 |
var ExceptionCode = {};
|
|
|
31144 |
var ExceptionMessage = {};
|
|
|
31145 |
ExceptionCode.INDEX_SIZE_ERR = (ExceptionMessage[1] = "Index size error", 1);
|
|
|
31146 |
ExceptionCode.DOMSTRING_SIZE_ERR = (ExceptionMessage[2] = "DOMString size error", 2);
|
|
|
31147 |
var HIERARCHY_REQUEST_ERR = ExceptionCode.HIERARCHY_REQUEST_ERR = (ExceptionMessage[3] = "Hierarchy request error", 3);
|
|
|
31148 |
ExceptionCode.WRONG_DOCUMENT_ERR = (ExceptionMessage[4] = "Wrong document", 4);
|
|
|
31149 |
ExceptionCode.INVALID_CHARACTER_ERR = (ExceptionMessage[5] = "Invalid character", 5);
|
|
|
31150 |
ExceptionCode.NO_DATA_ALLOWED_ERR = (ExceptionMessage[6] = "No data allowed", 6);
|
|
|
31151 |
ExceptionCode.NO_MODIFICATION_ALLOWED_ERR = (ExceptionMessage[7] = "No modification allowed", 7);
|
|
|
31152 |
var NOT_FOUND_ERR = ExceptionCode.NOT_FOUND_ERR = (ExceptionMessage[8] = "Not found", 8);
|
|
|
31153 |
ExceptionCode.NOT_SUPPORTED_ERR = (ExceptionMessage[9] = "Not supported", 9);
|
|
|
31154 |
var INUSE_ATTRIBUTE_ERR = ExceptionCode.INUSE_ATTRIBUTE_ERR = (ExceptionMessage[10] = "Attribute in use", 10);
|
|
|
31155 |
//level2
|
|
|
31156 |
ExceptionCode.INVALID_STATE_ERR = (ExceptionMessage[11] = "Invalid state", 11);
|
|
|
31157 |
ExceptionCode.SYNTAX_ERR = (ExceptionMessage[12] = "Syntax error", 12);
|
|
|
31158 |
ExceptionCode.INVALID_MODIFICATION_ERR = (ExceptionMessage[13] = "Invalid modification", 13);
|
|
|
31159 |
ExceptionCode.NAMESPACE_ERR = (ExceptionMessage[14] = "Invalid namespace", 14);
|
|
|
31160 |
ExceptionCode.INVALID_ACCESS_ERR = (ExceptionMessage[15] = "Invalid access", 15);
|
|
|
31161 |
|
|
|
31162 |
/**
|
|
|
31163 |
* DOM Level 2
|
|
|
31164 |
* Object DOMException
|
|
|
31165 |
* @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/ecma-script-binding.html
|
|
|
31166 |
* @see http://www.w3.org/TR/REC-DOM-Level-1/ecma-script-language-binding.html
|
|
|
31167 |
*/
|
|
|
31168 |
function DOMException(code, message) {
|
|
|
31169 |
if (message instanceof Error) {
|
|
|
31170 |
var error = message;
|
|
|
31171 |
} else {
|
|
|
31172 |
error = this;
|
|
|
31173 |
Error.call(this, ExceptionMessage[code]);
|
|
|
31174 |
this.message = ExceptionMessage[code];
|
|
|
31175 |
if (Error.captureStackTrace) Error.captureStackTrace(this, DOMException);
|
|
|
31176 |
}
|
|
|
31177 |
error.code = code;
|
|
|
31178 |
if (message) this.message = this.message + ": " + message;
|
|
|
31179 |
return error;
|
|
|
31180 |
}
|
|
|
31181 |
DOMException.prototype = Error.prototype;
|
|
|
31182 |
copy(ExceptionCode, DOMException);
|
|
|
31183 |
|
|
|
31184 |
/**
|
|
|
31185 |
* @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-536297177
|
|
|
31186 |
* The NodeList interface provides the abstraction of an ordered collection of nodes, without defining or constraining how this collection is implemented. NodeList objects in the DOM are live.
|
|
|
31187 |
* The items in the NodeList are accessible via an integral index, starting from 0.
|
|
|
31188 |
*/
|
|
|
31189 |
function NodeList() {}
|
|
|
31190 |
NodeList.prototype = {
|
|
|
31191 |
/**
|
|
|
31192 |
* The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive.
|
|
|
31193 |
* @standard level1
|
|
|
31194 |
*/
|
|
|
31195 |
length: 0,
|
|
|
31196 |
/**
|
|
|
31197 |
* Returns the indexth item in the collection. If index is greater than or equal to the number of nodes in the list, this returns null.
|
|
|
31198 |
* @standard level1
|
|
|
31199 |
* @param index unsigned long
|
|
|
31200 |
* Index into the collection.
|
|
|
31201 |
* @return Node
|
|
|
31202 |
* The node at the indexth position in the NodeList, or null if that is not a valid index.
|
|
|
31203 |
*/
|
|
|
31204 |
item: function (index) {
|
|
|
31205 |
return index >= 0 && index < this.length ? this[index] : null;
|
|
|
31206 |
},
|
|
|
31207 |
toString: function (isHTML, nodeFilter) {
|
|
|
31208 |
for (var buf = [], i = 0; i < this.length; i++) {
|
|
|
31209 |
serializeToString(this[i], buf, isHTML, nodeFilter);
|
|
|
31210 |
}
|
|
|
31211 |
return buf.join('');
|
|
|
31212 |
},
|
|
|
31213 |
/**
|
|
|
31214 |
* @private
|
|
|
31215 |
* @param {function (Node):boolean} predicate
|
|
|
31216 |
* @returns {Node[]}
|
|
|
31217 |
*/
|
|
|
31218 |
filter: function (predicate) {
|
|
|
31219 |
return Array.prototype.filter.call(this, predicate);
|
|
|
31220 |
},
|
|
|
31221 |
/**
|
|
|
31222 |
* @private
|
|
|
31223 |
* @param {Node} item
|
|
|
31224 |
* @returns {number}
|
|
|
31225 |
*/
|
|
|
31226 |
indexOf: function (item) {
|
|
|
31227 |
return Array.prototype.indexOf.call(this, item);
|
|
|
31228 |
}
|
|
|
31229 |
};
|
|
|
31230 |
function LiveNodeList(node, refresh) {
|
|
|
31231 |
this._node = node;
|
|
|
31232 |
this._refresh = refresh;
|
|
|
31233 |
_updateLiveList(this);
|
|
|
31234 |
}
|
|
|
31235 |
function _updateLiveList(list) {
|
|
|
31236 |
var inc = list._node._inc || list._node.ownerDocument._inc;
|
|
|
31237 |
if (list._inc !== inc) {
|
|
|
31238 |
var ls = list._refresh(list._node);
|
|
|
31239 |
__set__(list, 'length', ls.length);
|
|
|
31240 |
if (!list.$$length || ls.length < list.$$length) {
|
|
|
31241 |
for (var i = ls.length; (i in list); i++) {
|
|
|
31242 |
if (Object.prototype.hasOwnProperty.call(list, i)) {
|
|
|
31243 |
delete list[i];
|
|
|
31244 |
}
|
|
|
31245 |
}
|
|
|
31246 |
}
|
|
|
31247 |
copy(ls, list);
|
|
|
31248 |
list._inc = inc;
|
|
|
31249 |
}
|
|
|
31250 |
}
|
|
|
31251 |
LiveNodeList.prototype.item = function (i) {
|
|
|
31252 |
_updateLiveList(this);
|
|
|
31253 |
return this[i] || null;
|
|
|
31254 |
};
|
|
|
31255 |
_extends(LiveNodeList, NodeList);
|
|
|
31256 |
|
|
|
31257 |
/**
|
|
|
31258 |
* Objects implementing the NamedNodeMap interface are used
|
|
|
31259 |
* to represent collections of nodes that can be accessed by name.
|
|
|
31260 |
* Note that NamedNodeMap does not inherit from NodeList;
|
|
|
31261 |
* NamedNodeMaps are not maintained in any particular order.
|
|
|
31262 |
* Objects contained in an object implementing NamedNodeMap may also be accessed by an ordinal index,
|
|
|
31263 |
* but this is simply to allow convenient enumeration of the contents of a NamedNodeMap,
|
|
|
31264 |
* and does not imply that the DOM specifies an order to these Nodes.
|
|
|
31265 |
* NamedNodeMap objects in the DOM are live.
|
|
|
31266 |
* used for attributes or DocumentType entities
|
|
|
31267 |
*/
|
|
|
31268 |
function NamedNodeMap() {}
|
|
|
31269 |
function _findNodeIndex(list, node) {
|
|
|
31270 |
var i = list.length;
|
|
|
31271 |
while (i--) {
|
|
|
31272 |
if (list[i] === node) {
|
|
|
31273 |
return i;
|
|
|
31274 |
}
|
|
|
31275 |
}
|
|
|
31276 |
}
|
|
|
31277 |
function _addNamedNode(el, list, newAttr, oldAttr) {
|
|
|
31278 |
if (oldAttr) {
|
|
|
31279 |
list[_findNodeIndex(list, oldAttr)] = newAttr;
|
|
|
31280 |
} else {
|
|
|
31281 |
list[list.length++] = newAttr;
|
|
|
31282 |
}
|
|
|
31283 |
if (el) {
|
|
|
31284 |
newAttr.ownerElement = el;
|
|
|
31285 |
var doc = el.ownerDocument;
|
|
|
31286 |
if (doc) {
|
|
|
31287 |
oldAttr && _onRemoveAttribute(doc, el, oldAttr);
|
|
|
31288 |
_onAddAttribute(doc, el, newAttr);
|
|
|
31289 |
}
|
|
|
31290 |
}
|
|
|
31291 |
}
|
|
|
31292 |
function _removeNamedNode(el, list, attr) {
|
|
|
31293 |
//console.log('remove attr:'+attr)
|
|
|
31294 |
var i = _findNodeIndex(list, attr);
|
|
|
31295 |
if (i >= 0) {
|
|
|
31296 |
var lastIndex = list.length - 1;
|
|
|
31297 |
while (i < lastIndex) {
|
|
|
31298 |
list[i] = list[++i];
|
|
|
31299 |
}
|
|
|
31300 |
list.length = lastIndex;
|
|
|
31301 |
if (el) {
|
|
|
31302 |
var doc = el.ownerDocument;
|
|
|
31303 |
if (doc) {
|
|
|
31304 |
_onRemoveAttribute(doc, el, attr);
|
|
|
31305 |
attr.ownerElement = null;
|
|
|
31306 |
}
|
|
|
31307 |
}
|
|
|
31308 |
} else {
|
|
|
31309 |
throw new DOMException(NOT_FOUND_ERR, new Error(el.tagName + '@' + attr));
|
|
|
31310 |
}
|
|
|
31311 |
}
|
|
|
31312 |
NamedNodeMap.prototype = {
|
|
|
31313 |
length: 0,
|
|
|
31314 |
item: NodeList.prototype.item,
|
|
|
31315 |
getNamedItem: function (key) {
|
|
|
31316 |
// if(key.indexOf(':')>0 || key == 'xmlns'){
|
|
|
31317 |
// return null;
|
|
|
31318 |
// }
|
|
|
31319 |
//console.log()
|
|
|
31320 |
var i = this.length;
|
|
|
31321 |
while (i--) {
|
|
|
31322 |
var attr = this[i];
|
|
|
31323 |
//console.log(attr.nodeName,key)
|
|
|
31324 |
if (attr.nodeName == key) {
|
|
|
31325 |
return attr;
|
|
|
31326 |
}
|
|
|
31327 |
}
|
|
|
31328 |
},
|
|
|
31329 |
setNamedItem: function (attr) {
|
|
|
31330 |
var el = attr.ownerElement;
|
|
|
31331 |
if (el && el != this._ownerElement) {
|
|
|
31332 |
throw new DOMException(INUSE_ATTRIBUTE_ERR);
|
|
|
31333 |
}
|
|
|
31334 |
var oldAttr = this.getNamedItem(attr.nodeName);
|
|
|
31335 |
_addNamedNode(this._ownerElement, this, attr, oldAttr);
|
|
|
31336 |
return oldAttr;
|
|
|
31337 |
},
|
|
|
31338 |
/* returns Node */
|
|
|
31339 |
setNamedItemNS: function (attr) {
|
|
|
31340 |
// raises: WRONG_DOCUMENT_ERR,NO_MODIFICATION_ALLOWED_ERR,INUSE_ATTRIBUTE_ERR
|
|
|
31341 |
var el = attr.ownerElement,
|
|
|
31342 |
oldAttr;
|
|
|
31343 |
if (el && el != this._ownerElement) {
|
|
|
31344 |
throw new DOMException(INUSE_ATTRIBUTE_ERR);
|
|
|
31345 |
}
|
|
|
31346 |
oldAttr = this.getNamedItemNS(attr.namespaceURI, attr.localName);
|
|
|
31347 |
_addNamedNode(this._ownerElement, this, attr, oldAttr);
|
|
|
31348 |
return oldAttr;
|
|
|
31349 |
},
|
|
|
31350 |
/* returns Node */
|
|
|
31351 |
removeNamedItem: function (key) {
|
|
|
31352 |
var attr = this.getNamedItem(key);
|
|
|
31353 |
_removeNamedNode(this._ownerElement, this, attr);
|
|
|
31354 |
return attr;
|
|
|
31355 |
},
|
|
|
31356 |
// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR
|
|
|
31357 |
|
|
|
31358 |
//for level2
|
|
|
31359 |
removeNamedItemNS: function (namespaceURI, localName) {
|
|
|
31360 |
var attr = this.getNamedItemNS(namespaceURI, localName);
|
|
|
31361 |
_removeNamedNode(this._ownerElement, this, attr);
|
|
|
31362 |
return attr;
|
|
|
31363 |
},
|
|
|
31364 |
getNamedItemNS: function (namespaceURI, localName) {
|
|
|
31365 |
var i = this.length;
|
|
|
31366 |
while (i--) {
|
|
|
31367 |
var node = this[i];
|
|
|
31368 |
if (node.localName == localName && node.namespaceURI == namespaceURI) {
|
|
|
31369 |
return node;
|
|
|
31370 |
}
|
|
|
31371 |
}
|
|
|
31372 |
return null;
|
|
|
31373 |
}
|
|
|
31374 |
};
|
|
|
31375 |
|
|
|
31376 |
/**
|
|
|
31377 |
* The DOMImplementation interface represents an object providing methods
|
|
|
31378 |
* which are not dependent on any particular document.
|
|
|
31379 |
* Such an object is returned by the `Document.implementation` property.
|
|
|
31380 |
*
|
|
|
31381 |
* __The individual methods describe the differences compared to the specs.__
|
|
|
31382 |
*
|
|
|
31383 |
* @constructor
|
|
|
31384 |
*
|
|
|
31385 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation MDN
|
|
|
31386 |
* @see https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-102161490 DOM Level 1 Core (Initial)
|
|
|
31387 |
* @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-102161490 DOM Level 2 Core
|
|
|
31388 |
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-102161490 DOM Level 3 Core
|
|
|
31389 |
* @see https://dom.spec.whatwg.org/#domimplementation DOM Living Standard
|
|
|
31390 |
*/
|
|
|
31391 |
function DOMImplementation$1() {}
|
|
|
31392 |
DOMImplementation$1.prototype = {
|
|
|
31393 |
/**
|
|
|
31394 |
* The DOMImplementation.hasFeature() method returns a Boolean flag indicating if a given feature is supported.
|
|
|
31395 |
* The different implementations fairly diverged in what kind of features were reported.
|
|
|
31396 |
* The latest version of the spec settled to force this method to always return true, where the functionality was accurate and in use.
|
|
|
31397 |
*
|
|
|
31398 |
* @deprecated It is deprecated and modern browsers return true in all cases.
|
|
|
31399 |
*
|
|
|
31400 |
* @param {string} feature
|
|
|
31401 |
* @param {string} [version]
|
|
|
31402 |
* @returns {boolean} always true
|
|
|
31403 |
*
|
|
|
31404 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/hasFeature MDN
|
|
|
31405 |
* @see https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-5CED94D7 DOM Level 1 Core
|
|
|
31406 |
* @see https://dom.spec.whatwg.org/#dom-domimplementation-hasfeature DOM Living Standard
|
|
|
31407 |
*/
|
|
|
31408 |
hasFeature: function (feature, version) {
|
|
|
31409 |
return true;
|
|
|
31410 |
},
|
|
|
31411 |
/**
|
|
|
31412 |
* Creates an XML Document object of the specified type with its document element.
|
|
|
31413 |
*
|
|
|
31414 |
* __It behaves slightly different from the description in the living standard__:
|
|
|
31415 |
* - There is no interface/class `XMLDocument`, it returns a `Document` instance.
|
|
|
31416 |
* - `contentType`, `encoding`, `mode`, `origin`, `url` fields are currently not declared.
|
|
|
31417 |
* - this implementation is not validating names or qualified names
|
|
|
31418 |
* (when parsing XML strings, the SAX parser takes care of that)
|
|
|
31419 |
*
|
|
|
31420 |
* @param {string|null} namespaceURI
|
|
|
31421 |
* @param {string} qualifiedName
|
|
|
31422 |
* @param {DocumentType=null} doctype
|
|
|
31423 |
* @returns {Document}
|
|
|
31424 |
*
|
|
|
31425 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocument MDN
|
|
|
31426 |
* @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#Level-2-Core-DOM-createDocument DOM Level 2 Core (initial)
|
|
|
31427 |
* @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocument DOM Level 2 Core
|
|
|
31428 |
*
|
|
|
31429 |
* @see https://dom.spec.whatwg.org/#validate-and-extract DOM: Validate and extract
|
|
|
31430 |
* @see https://www.w3.org/TR/xml/#NT-NameStartChar XML Spec: Names
|
|
|
31431 |
* @see https://www.w3.org/TR/xml-names/#ns-qualnames XML Namespaces: Qualified names
|
|
|
31432 |
*/
|
|
|
31433 |
createDocument: function (namespaceURI, qualifiedName, doctype) {
|
|
|
31434 |
var doc = new Document();
|
|
|
31435 |
doc.implementation = this;
|
|
|
31436 |
doc.childNodes = new NodeList();
|
|
|
31437 |
doc.doctype = doctype || null;
|
|
|
31438 |
if (doctype) {
|
|
|
31439 |
doc.appendChild(doctype);
|
|
|
31440 |
}
|
|
|
31441 |
if (qualifiedName) {
|
|
|
31442 |
var root = doc.createElementNS(namespaceURI, qualifiedName);
|
|
|
31443 |
doc.appendChild(root);
|
|
|
31444 |
}
|
|
|
31445 |
return doc;
|
|
|
31446 |
},
|
|
|
31447 |
/**
|
|
|
31448 |
* Returns a doctype, with the given `qualifiedName`, `publicId`, and `systemId`.
|
|
|
31449 |
*
|
|
|
31450 |
* __This behavior is slightly different from the in the specs__:
|
|
|
31451 |
* - this implementation is not validating names or qualified names
|
|
|
31452 |
* (when parsing XML strings, the SAX parser takes care of that)
|
|
|
31453 |
*
|
|
|
31454 |
* @param {string} qualifiedName
|
|
|
31455 |
* @param {string} [publicId]
|
|
|
31456 |
* @param {string} [systemId]
|
|
|
31457 |
* @returns {DocumentType} which can either be used with `DOMImplementation.createDocument` upon document creation
|
|
|
31458 |
* or can be put into the document via methods like `Node.insertBefore()` or `Node.replaceChild()`
|
|
|
31459 |
*
|
|
|
31460 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocumentType MDN
|
|
|
31461 |
* @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#Level-2-Core-DOM-createDocType DOM Level 2 Core
|
|
|
31462 |
* @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocumenttype DOM Living Standard
|
|
|
31463 |
*
|
|
|
31464 |
* @see https://dom.spec.whatwg.org/#validate-and-extract DOM: Validate and extract
|
|
|
31465 |
* @see https://www.w3.org/TR/xml/#NT-NameStartChar XML Spec: Names
|
|
|
31466 |
* @see https://www.w3.org/TR/xml-names/#ns-qualnames XML Namespaces: Qualified names
|
|
|
31467 |
*/
|
|
|
31468 |
createDocumentType: function (qualifiedName, publicId, systemId) {
|
|
|
31469 |
var node = new DocumentType();
|
|
|
31470 |
node.name = qualifiedName;
|
|
|
31471 |
node.nodeName = qualifiedName;
|
|
|
31472 |
node.publicId = publicId || '';
|
|
|
31473 |
node.systemId = systemId || '';
|
|
|
31474 |
return node;
|
|
|
31475 |
}
|
|
|
31476 |
};
|
|
|
31477 |
|
|
|
31478 |
/**
|
|
|
31479 |
* @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247
|
|
|
31480 |
*/
|
|
|
31481 |
|
|
|
31482 |
function Node() {}
|
|
|
31483 |
Node.prototype = {
|
|
|
31484 |
firstChild: null,
|
|
|
31485 |
lastChild: null,
|
|
|
31486 |
previousSibling: null,
|
|
|
31487 |
nextSibling: null,
|
|
|
31488 |
attributes: null,
|
|
|
31489 |
parentNode: null,
|
|
|
31490 |
childNodes: null,
|
|
|
31491 |
ownerDocument: null,
|
|
|
31492 |
nodeValue: null,
|
|
|
31493 |
namespaceURI: null,
|
|
|
31494 |
prefix: null,
|
|
|
31495 |
localName: null,
|
|
|
31496 |
// Modified in DOM Level 2:
|
|
|
31497 |
insertBefore: function (newChild, refChild) {
|
|
|
31498 |
//raises
|
|
|
31499 |
return _insertBefore(this, newChild, refChild);
|
|
|
31500 |
},
|
|
|
31501 |
replaceChild: function (newChild, oldChild) {
|
|
|
31502 |
//raises
|
|
|
31503 |
_insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
|
|
|
31504 |
if (oldChild) {
|
|
|
31505 |
this.removeChild(oldChild);
|
|
|
31506 |
}
|
|
|
31507 |
},
|
|
|
31508 |
removeChild: function (oldChild) {
|
|
|
31509 |
return _removeChild(this, oldChild);
|
|
|
31510 |
},
|
|
|
31511 |
appendChild: function (newChild) {
|
|
|
31512 |
return this.insertBefore(newChild, null);
|
|
|
31513 |
},
|
|
|
31514 |
hasChildNodes: function () {
|
|
|
31515 |
return this.firstChild != null;
|
|
|
31516 |
},
|
|
|
31517 |
cloneNode: function (deep) {
|
|
|
31518 |
return cloneNode(this.ownerDocument || this, this, deep);
|
|
|
31519 |
},
|
|
|
31520 |
// Modified in DOM Level 2:
|
|
|
31521 |
normalize: function () {
|
|
|
31522 |
var child = this.firstChild;
|
|
|
31523 |
while (child) {
|
|
|
31524 |
var next = child.nextSibling;
|
|
|
31525 |
if (next && next.nodeType == TEXT_NODE && child.nodeType == TEXT_NODE) {
|
|
|
31526 |
this.removeChild(next);
|
|
|
31527 |
child.appendData(next.data);
|
|
|
31528 |
} else {
|
|
|
31529 |
child.normalize();
|
|
|
31530 |
child = next;
|
|
|
31531 |
}
|
|
|
31532 |
}
|
|
|
31533 |
},
|
|
|
31534 |
// Introduced in DOM Level 2:
|
|
|
31535 |
isSupported: function (feature, version) {
|
|
|
31536 |
return this.ownerDocument.implementation.hasFeature(feature, version);
|
|
|
31537 |
},
|
|
|
31538 |
// Introduced in DOM Level 2:
|
|
|
31539 |
hasAttributes: function () {
|
|
|
31540 |
return this.attributes.length > 0;
|
|
|
31541 |
},
|
|
|
31542 |
/**
|
|
|
31543 |
* Look up the prefix associated to the given namespace URI, starting from this node.
|
|
|
31544 |
* **The default namespace declarations are ignored by this method.**
|
|
|
31545 |
* See Namespace Prefix Lookup for details on the algorithm used by this method.
|
|
|
31546 |
*
|
|
|
31547 |
* _Note: The implementation seems to be incomplete when compared to the algorithm described in the specs._
|
|
|
31548 |
*
|
|
|
31549 |
* @param {string | null} namespaceURI
|
|
|
31550 |
* @returns {string | null}
|
|
|
31551 |
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespacePrefix
|
|
|
31552 |
* @see https://www.w3.org/TR/DOM-Level-3-Core/namespaces-algorithms.html#lookupNamespacePrefixAlgo
|
|
|
31553 |
* @see https://dom.spec.whatwg.org/#dom-node-lookupprefix
|
|
|
31554 |
* @see https://github.com/xmldom/xmldom/issues/322
|
|
|
31555 |
*/
|
|
|
31556 |
lookupPrefix: function (namespaceURI) {
|
|
|
31557 |
var el = this;
|
|
|
31558 |
while (el) {
|
|
|
31559 |
var map = el._nsMap;
|
|
|
31560 |
//console.dir(map)
|
|
|
31561 |
if (map) {
|
|
|
31562 |
for (var n in map) {
|
|
|
31563 |
if (Object.prototype.hasOwnProperty.call(map, n) && map[n] === namespaceURI) {
|
|
|
31564 |
return n;
|
|
|
31565 |
}
|
|
|
31566 |
}
|
|
|
31567 |
}
|
|
|
31568 |
el = el.nodeType == ATTRIBUTE_NODE ? el.ownerDocument : el.parentNode;
|
|
|
31569 |
}
|
|
|
31570 |
return null;
|
|
|
31571 |
},
|
|
|
31572 |
// Introduced in DOM Level 3:
|
|
|
31573 |
lookupNamespaceURI: function (prefix) {
|
|
|
31574 |
var el = this;
|
|
|
31575 |
while (el) {
|
|
|
31576 |
var map = el._nsMap;
|
|
|
31577 |
//console.dir(map)
|
|
|
31578 |
if (map) {
|
|
|
31579 |
if (Object.prototype.hasOwnProperty.call(map, prefix)) {
|
|
|
31580 |
return map[prefix];
|
|
|
31581 |
}
|
|
|
31582 |
}
|
|
|
31583 |
el = el.nodeType == ATTRIBUTE_NODE ? el.ownerDocument : el.parentNode;
|
|
|
31584 |
}
|
|
|
31585 |
return null;
|
|
|
31586 |
},
|
|
|
31587 |
// Introduced in DOM Level 3:
|
|
|
31588 |
isDefaultNamespace: function (namespaceURI) {
|
|
|
31589 |
var prefix = this.lookupPrefix(namespaceURI);
|
|
|
31590 |
return prefix == null;
|
|
|
31591 |
}
|
|
|
31592 |
};
|
|
|
31593 |
function _xmlEncoder(c) {
|
|
|
31594 |
return c == '<' && '<' || c == '>' && '>' || c == '&' && '&' || c == '"' && '"' || '&#' + c.charCodeAt() + ';';
|
|
|
31595 |
}
|
|
|
31596 |
copy(NodeType, Node);
|
|
|
31597 |
copy(NodeType, Node.prototype);
|
|
|
31598 |
|
|
|
31599 |
/**
|
|
|
31600 |
* @param callback return true for continue,false for break
|
|
|
31601 |
* @return boolean true: break visit;
|
|
|
31602 |
*/
|
|
|
31603 |
function _visitNode(node, callback) {
|
|
|
31604 |
if (callback(node)) {
|
|
|
31605 |
return true;
|
|
|
31606 |
}
|
|
|
31607 |
if (node = node.firstChild) {
|
|
|
31608 |
do {
|
|
|
31609 |
if (_visitNode(node, callback)) {
|
|
|
31610 |
return true;
|
|
|
31611 |
}
|
|
|
31612 |
} while (node = node.nextSibling);
|
|
|
31613 |
}
|
|
|
31614 |
}
|
|
|
31615 |
function Document() {
|
|
|
31616 |
this.ownerDocument = this;
|
|
|
31617 |
}
|
|
|
31618 |
function _onAddAttribute(doc, el, newAttr) {
|
|
|
31619 |
doc && doc._inc++;
|
|
|
31620 |
var ns = newAttr.namespaceURI;
|
|
|
31621 |
if (ns === NAMESPACE$2.XMLNS) {
|
|
|
31622 |
//update namespace
|
|
|
31623 |
el._nsMap[newAttr.prefix ? newAttr.localName : ''] = newAttr.value;
|
|
|
31624 |
}
|
|
|
31625 |
}
|
|
|
31626 |
function _onRemoveAttribute(doc, el, newAttr, remove) {
|
|
|
31627 |
doc && doc._inc++;
|
|
|
31628 |
var ns = newAttr.namespaceURI;
|
|
|
31629 |
if (ns === NAMESPACE$2.XMLNS) {
|
|
|
31630 |
//update namespace
|
|
|
31631 |
delete el._nsMap[newAttr.prefix ? newAttr.localName : ''];
|
|
|
31632 |
}
|
|
|
31633 |
}
|
|
|
31634 |
|
|
|
31635 |
/**
|
|
|
31636 |
* Updates `el.childNodes`, updating the indexed items and it's `length`.
|
|
|
31637 |
* Passing `newChild` means it will be appended.
|
|
|
31638 |
* Otherwise it's assumed that an item has been removed,
|
|
|
31639 |
* and `el.firstNode` and it's `.nextSibling` are used
|
|
|
31640 |
* to walk the current list of child nodes.
|
|
|
31641 |
*
|
|
|
31642 |
* @param {Document} doc
|
|
|
31643 |
* @param {Node} el
|
|
|
31644 |
* @param {Node} [newChild]
|
|
|
31645 |
* @private
|
|
|
31646 |
*/
|
|
|
31647 |
function _onUpdateChild(doc, el, newChild) {
|
|
|
31648 |
if (doc && doc._inc) {
|
|
|
31649 |
doc._inc++;
|
|
|
31650 |
//update childNodes
|
|
|
31651 |
var cs = el.childNodes;
|
|
|
31652 |
if (newChild) {
|
|
|
31653 |
cs[cs.length++] = newChild;
|
|
|
31654 |
} else {
|
|
|
31655 |
var child = el.firstChild;
|
|
|
31656 |
var i = 0;
|
|
|
31657 |
while (child) {
|
|
|
31658 |
cs[i++] = child;
|
|
|
31659 |
child = child.nextSibling;
|
|
|
31660 |
}
|
|
|
31661 |
cs.length = i;
|
|
|
31662 |
delete cs[cs.length];
|
|
|
31663 |
}
|
|
|
31664 |
}
|
|
|
31665 |
}
|
|
|
31666 |
|
|
|
31667 |
/**
|
|
|
31668 |
* Removes the connections between `parentNode` and `child`
|
|
|
31669 |
* and any existing `child.previousSibling` or `child.nextSibling`.
|
|
|
31670 |
*
|
|
|
31671 |
* @see https://github.com/xmldom/xmldom/issues/135
|
|
|
31672 |
* @see https://github.com/xmldom/xmldom/issues/145
|
|
|
31673 |
*
|
|
|
31674 |
* @param {Node} parentNode
|
|
|
31675 |
* @param {Node} child
|
|
|
31676 |
* @returns {Node} the child that was removed.
|
|
|
31677 |
* @private
|
|
|
31678 |
*/
|
|
|
31679 |
function _removeChild(parentNode, child) {
|
|
|
31680 |
var previous = child.previousSibling;
|
|
|
31681 |
var next = child.nextSibling;
|
|
|
31682 |
if (previous) {
|
|
|
31683 |
previous.nextSibling = next;
|
|
|
31684 |
} else {
|
|
|
31685 |
parentNode.firstChild = next;
|
|
|
31686 |
}
|
|
|
31687 |
if (next) {
|
|
|
31688 |
next.previousSibling = previous;
|
|
|
31689 |
} else {
|
|
|
31690 |
parentNode.lastChild = previous;
|
|
|
31691 |
}
|
|
|
31692 |
child.parentNode = null;
|
|
|
31693 |
child.previousSibling = null;
|
|
|
31694 |
child.nextSibling = null;
|
|
|
31695 |
_onUpdateChild(parentNode.ownerDocument, parentNode);
|
|
|
31696 |
return child;
|
|
|
31697 |
}
|
|
|
31698 |
|
|
|
31699 |
/**
|
|
|
31700 |
* Returns `true` if `node` can be a parent for insertion.
|
|
|
31701 |
* @param {Node} node
|
|
|
31702 |
* @returns {boolean}
|
|
|
31703 |
*/
|
|
|
31704 |
function hasValidParentNodeType(node) {
|
|
|
31705 |
return node && (node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE);
|
|
|
31706 |
}
|
|
|
31707 |
|
|
|
31708 |
/**
|
|
|
31709 |
* Returns `true` if `node` can be inserted according to it's `nodeType`.
|
|
|
31710 |
* @param {Node} node
|
|
|
31711 |
* @returns {boolean}
|
|
|
31712 |
*/
|
|
|
31713 |
function hasInsertableNodeType(node) {
|
|
|
31714 |
return node && (isElementNode(node) || isTextNode(node) || isDocTypeNode(node) || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.COMMENT_NODE || node.nodeType === Node.PROCESSING_INSTRUCTION_NODE);
|
|
|
31715 |
}
|
|
|
31716 |
|
|
|
31717 |
/**
|
|
|
31718 |
* Returns true if `node` is a DOCTYPE node
|
|
|
31719 |
* @param {Node} node
|
|
|
31720 |
* @returns {boolean}
|
|
|
31721 |
*/
|
|
|
31722 |
function isDocTypeNode(node) {
|
|
|
31723 |
return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
|
|
|
31724 |
}
|
|
|
31725 |
|
|
|
31726 |
/**
|
|
|
31727 |
* Returns true if the node is an element
|
|
|
31728 |
* @param {Node} node
|
|
|
31729 |
* @returns {boolean}
|
|
|
31730 |
*/
|
|
|
31731 |
function isElementNode(node) {
|
|
|
31732 |
return node && node.nodeType === Node.ELEMENT_NODE;
|
|
|
31733 |
}
|
|
|
31734 |
/**
|
|
|
31735 |
* Returns true if `node` is a text node
|
|
|
31736 |
* @param {Node} node
|
|
|
31737 |
* @returns {boolean}
|
|
|
31738 |
*/
|
|
|
31739 |
function isTextNode(node) {
|
|
|
31740 |
return node && node.nodeType === Node.TEXT_NODE;
|
|
|
31741 |
}
|
|
|
31742 |
|
|
|
31743 |
/**
|
|
|
31744 |
* Check if en element node can be inserted before `child`, or at the end if child is falsy,
|
|
|
31745 |
* according to the presence and position of a doctype node on the same level.
|
|
|
31746 |
*
|
|
|
31747 |
* @param {Document} doc The document node
|
|
|
31748 |
* @param {Node} child the node that would become the nextSibling if the element would be inserted
|
|
|
31749 |
* @returns {boolean} `true` if an element can be inserted before child
|
|
|
31750 |
* @private
|
|
|
31751 |
* https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
|
31752 |
*/
|
|
|
31753 |
function isElementInsertionPossible(doc, child) {
|
|
|
31754 |
var parentChildNodes = doc.childNodes || [];
|
|
|
31755 |
if (find(parentChildNodes, isElementNode) || isDocTypeNode(child)) {
|
|
|
31756 |
return false;
|
|
|
31757 |
}
|
|
|
31758 |
var docTypeNode = find(parentChildNodes, isDocTypeNode);
|
|
|
31759 |
return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
|
|
|
31760 |
}
|
|
|
31761 |
|
|
|
31762 |
/**
|
|
|
31763 |
* Check if en element node can be inserted before `child`, or at the end if child is falsy,
|
|
|
31764 |
* according to the presence and position of a doctype node on the same level.
|
|
|
31765 |
*
|
|
|
31766 |
* @param {Node} doc The document node
|
|
|
31767 |
* @param {Node} child the node that would become the nextSibling if the element would be inserted
|
|
|
31768 |
* @returns {boolean} `true` if an element can be inserted before child
|
|
|
31769 |
* @private
|
|
|
31770 |
* https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
|
31771 |
*/
|
|
|
31772 |
function isElementReplacementPossible(doc, child) {
|
|
|
31773 |
var parentChildNodes = doc.childNodes || [];
|
|
|
31774 |
function hasElementChildThatIsNotChild(node) {
|
|
|
31775 |
return isElementNode(node) && node !== child;
|
|
|
31776 |
}
|
|
|
31777 |
if (find(parentChildNodes, hasElementChildThatIsNotChild)) {
|
|
|
31778 |
return false;
|
|
|
31779 |
}
|
|
|
31780 |
var docTypeNode = find(parentChildNodes, isDocTypeNode);
|
|
|
31781 |
return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
|
|
|
31782 |
}
|
|
|
31783 |
|
|
|
31784 |
/**
|
|
|
31785 |
* @private
|
|
|
31786 |
* Steps 1-5 of the checks before inserting and before replacing a child are the same.
|
|
|
31787 |
*
|
|
|
31788 |
* @param {Node} parent the parent node to insert `node` into
|
|
|
31789 |
* @param {Node} node the node to insert
|
|
|
31790 |
* @param {Node=} child the node that should become the `nextSibling` of `node`
|
|
|
31791 |
* @returns {Node}
|
|
|
31792 |
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
|
|
|
31793 |
* @throws DOMException if `child` is provided but is not a child of `parent`.
|
|
|
31794 |
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
|
31795 |
* @see https://dom.spec.whatwg.org/#concept-node-replace
|
|
|
31796 |
*/
|
|
|
31797 |
function assertPreInsertionValidity1to5(parent, node, child) {
|
|
|
31798 |
// 1. If `parent` is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
|
|
|
31799 |
if (!hasValidParentNodeType(parent)) {
|
|
|
31800 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
|
|
|
31801 |
}
|
|
|
31802 |
// 2. If `node` is a host-including inclusive ancestor of `parent`, then throw a "HierarchyRequestError" DOMException.
|
|
|
31803 |
// not implemented!
|
|
|
31804 |
// 3. If `child` is non-null and its parent is not `parent`, then throw a "NotFoundError" DOMException.
|
|
|
31805 |
if (child && child.parentNode !== parent) {
|
|
|
31806 |
throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
|
|
|
31807 |
}
|
|
|
31808 |
if (
|
|
|
31809 |
// 4. If `node` is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
|
|
|
31810 |
!hasInsertableNodeType(node) ||
|
|
|
31811 |
// 5. If either `node` is a Text node and `parent` is a document,
|
|
|
31812 |
// the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
|
|
|
31813 |
// || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
|
|
|
31814 |
// or `node` is a doctype and `parent` is not a document, then throw a "HierarchyRequestError" DOMException.
|
|
|
31815 |
isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE) {
|
|
|
31816 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType);
|
|
|
31817 |
}
|
|
|
31818 |
}
|
|
|
31819 |
|
|
|
31820 |
/**
|
|
|
31821 |
* @private
|
|
|
31822 |
* Step 6 of the checks before inserting and before replacing a child are different.
|
|
|
31823 |
*
|
|
|
31824 |
* @param {Document} parent the parent node to insert `node` into
|
|
|
31825 |
* @param {Node} node the node to insert
|
|
|
31826 |
* @param {Node | undefined} child the node that should become the `nextSibling` of `node`
|
|
|
31827 |
* @returns {Node}
|
|
|
31828 |
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
|
|
|
31829 |
* @throws DOMException if `child` is provided but is not a child of `parent`.
|
|
|
31830 |
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
|
31831 |
* @see https://dom.spec.whatwg.org/#concept-node-replace
|
|
|
31832 |
*/
|
|
|
31833 |
function assertPreInsertionValidityInDocument(parent, node, child) {
|
|
|
31834 |
var parentChildNodes = parent.childNodes || [];
|
|
|
31835 |
var nodeChildNodes = node.childNodes || [];
|
|
|
31836 |
|
|
|
31837 |
// DocumentFragment
|
|
|
31838 |
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
|
31839 |
var nodeChildElements = nodeChildNodes.filter(isElementNode);
|
|
|
31840 |
// If node has more than one element child or has a Text node child.
|
|
|
31841 |
if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
|
|
|
31842 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
|
|
|
31843 |
}
|
|
|
31844 |
// Otherwise, if `node` has one element child and either `parent` has an element child,
|
|
|
31845 |
// `child` is a doctype, or `child` is non-null and a doctype is following `child`.
|
|
|
31846 |
if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
|
|
|
31847 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
|
|
|
31848 |
}
|
|
|
31849 |
}
|
|
|
31850 |
// Element
|
|
|
31851 |
if (isElementNode(node)) {
|
|
|
31852 |
// `parent` has an element child, `child` is a doctype,
|
|
|
31853 |
// or `child` is non-null and a doctype is following `child`.
|
|
|
31854 |
if (!isElementInsertionPossible(parent, child)) {
|
|
|
31855 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
|
|
|
31856 |
}
|
|
|
31857 |
}
|
|
|
31858 |
// DocumentType
|
|
|
31859 |
if (isDocTypeNode(node)) {
|
|
|
31860 |
// `parent` has a doctype child,
|
|
|
31861 |
if (find(parentChildNodes, isDocTypeNode)) {
|
|
|
31862 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
|
|
|
31863 |
}
|
|
|
31864 |
var parentElementChild = find(parentChildNodes, isElementNode);
|
|
|
31865 |
// `child` is non-null and an element is preceding `child`,
|
|
|
31866 |
if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
|
|
|
31867 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
|
|
|
31868 |
}
|
|
|
31869 |
// or `child` is null and `parent` has an element child.
|
|
|
31870 |
if (!child && parentElementChild) {
|
|
|
31871 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
|
|
|
31872 |
}
|
|
|
31873 |
}
|
|
|
31874 |
}
|
|
|
31875 |
|
|
|
31876 |
/**
|
|
|
31877 |
* @private
|
|
|
31878 |
* Step 6 of the checks before inserting and before replacing a child are different.
|
|
|
31879 |
*
|
|
|
31880 |
* @param {Document} parent the parent node to insert `node` into
|
|
|
31881 |
* @param {Node} node the node to insert
|
|
|
31882 |
* @param {Node | undefined} child the node that should become the `nextSibling` of `node`
|
|
|
31883 |
* @returns {Node}
|
|
|
31884 |
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
|
|
|
31885 |
* @throws DOMException if `child` is provided but is not a child of `parent`.
|
|
|
31886 |
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
|
31887 |
* @see https://dom.spec.whatwg.org/#concept-node-replace
|
|
|
31888 |
*/
|
|
|
31889 |
function assertPreReplacementValidityInDocument(parent, node, child) {
|
|
|
31890 |
var parentChildNodes = parent.childNodes || [];
|
|
|
31891 |
var nodeChildNodes = node.childNodes || [];
|
|
|
31892 |
|
|
|
31893 |
// DocumentFragment
|
|
|
31894 |
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
|
31895 |
var nodeChildElements = nodeChildNodes.filter(isElementNode);
|
|
|
31896 |
// If `node` has more than one element child or has a Text node child.
|
|
|
31897 |
if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
|
|
|
31898 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
|
|
|
31899 |
}
|
|
|
31900 |
// Otherwise, if `node` has one element child and either `parent` has an element child that is not `child` or a doctype is following `child`.
|
|
|
31901 |
if (nodeChildElements.length === 1 && !isElementReplacementPossible(parent, child)) {
|
|
|
31902 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
|
|
|
31903 |
}
|
|
|
31904 |
}
|
|
|
31905 |
// Element
|
|
|
31906 |
if (isElementNode(node)) {
|
|
|
31907 |
// `parent` has an element child that is not `child` or a doctype is following `child`.
|
|
|
31908 |
if (!isElementReplacementPossible(parent, child)) {
|
|
|
31909 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
|
|
|
31910 |
}
|
|
|
31911 |
}
|
|
|
31912 |
// DocumentType
|
|
|
31913 |
if (isDocTypeNode(node)) {
|
|
|
31914 |
function hasDoctypeChildThatIsNotChild(node) {
|
|
|
31915 |
return isDocTypeNode(node) && node !== child;
|
|
|
31916 |
}
|
|
|
31917 |
|
|
|
31918 |
// `parent` has a doctype child that is not `child`,
|
|
|
31919 |
if (find(parentChildNodes, hasDoctypeChildThatIsNotChild)) {
|
|
|
31920 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
|
|
|
31921 |
}
|
|
|
31922 |
var parentElementChild = find(parentChildNodes, isElementNode);
|
|
|
31923 |
// or an element is preceding `child`.
|
|
|
31924 |
if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
|
|
|
31925 |
throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
|
|
|
31926 |
}
|
|
|
31927 |
}
|
|
|
31928 |
}
|
|
|
31929 |
|
|
|
31930 |
/**
|
|
|
31931 |
* @private
|
|
|
31932 |
* @param {Node} parent the parent node to insert `node` into
|
|
|
31933 |
* @param {Node} node the node to insert
|
|
|
31934 |
* @param {Node=} child the node that should become the `nextSibling` of `node`
|
|
|
31935 |
* @returns {Node}
|
|
|
31936 |
* @throws DOMException for several node combinations that would create a DOM that is not well-formed.
|
|
|
31937 |
* @throws DOMException if `child` is provided but is not a child of `parent`.
|
|
|
31938 |
* @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
|
31939 |
*/
|
|
|
31940 |
function _insertBefore(parent, node, child, _inDocumentAssertion) {
|
|
|
31941 |
// To ensure pre-insertion validity of a node into a parent before a child, run these steps:
|
|
|
31942 |
assertPreInsertionValidity1to5(parent, node, child);
|
|
|
31943 |
|
|
|
31944 |
// If parent is a document, and any of the statements below, switched on the interface node implements,
|
|
|
31945 |
// are true, then throw a "HierarchyRequestError" DOMException.
|
|
|
31946 |
if (parent.nodeType === Node.DOCUMENT_NODE) {
|
|
|
31947 |
(_inDocumentAssertion || assertPreInsertionValidityInDocument)(parent, node, child);
|
|
|
31948 |
}
|
|
|
31949 |
var cp = node.parentNode;
|
|
|
31950 |
if (cp) {
|
|
|
31951 |
cp.removeChild(node); //remove and update
|
|
|
31952 |
}
|
|
|
31953 |
|
|
|
31954 |
if (node.nodeType === DOCUMENT_FRAGMENT_NODE) {
|
|
|
31955 |
var newFirst = node.firstChild;
|
|
|
31956 |
if (newFirst == null) {
|
|
|
31957 |
return node;
|
|
|
31958 |
}
|
|
|
31959 |
var newLast = node.lastChild;
|
|
|
31960 |
} else {
|
|
|
31961 |
newFirst = newLast = node;
|
|
|
31962 |
}
|
|
|
31963 |
var pre = child ? child.previousSibling : parent.lastChild;
|
|
|
31964 |
newFirst.previousSibling = pre;
|
|
|
31965 |
newLast.nextSibling = child;
|
|
|
31966 |
if (pre) {
|
|
|
31967 |
pre.nextSibling = newFirst;
|
|
|
31968 |
} else {
|
|
|
31969 |
parent.firstChild = newFirst;
|
|
|
31970 |
}
|
|
|
31971 |
if (child == null) {
|
|
|
31972 |
parent.lastChild = newLast;
|
|
|
31973 |
} else {
|
|
|
31974 |
child.previousSibling = newLast;
|
|
|
31975 |
}
|
|
|
31976 |
do {
|
|
|
31977 |
newFirst.parentNode = parent;
|
|
|
31978 |
} while (newFirst !== newLast && (newFirst = newFirst.nextSibling));
|
|
|
31979 |
_onUpdateChild(parent.ownerDocument || parent, parent);
|
|
|
31980 |
//console.log(parent.lastChild.nextSibling == null)
|
|
|
31981 |
if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
|
|
|
31982 |
node.firstChild = node.lastChild = null;
|
|
|
31983 |
}
|
|
|
31984 |
return node;
|
|
|
31985 |
}
|
|
|
31986 |
|
|
|
31987 |
/**
|
|
|
31988 |
* Appends `newChild` to `parentNode`.
|
|
|
31989 |
* If `newChild` is already connected to a `parentNode` it is first removed from it.
|
|
|
31990 |
*
|
|
|
31991 |
* @see https://github.com/xmldom/xmldom/issues/135
|
|
|
31992 |
* @see https://github.com/xmldom/xmldom/issues/145
|
|
|
31993 |
* @param {Node} parentNode
|
|
|
31994 |
* @param {Node} newChild
|
|
|
31995 |
* @returns {Node}
|
|
|
31996 |
* @private
|
|
|
31997 |
*/
|
|
|
31998 |
function _appendSingleChild(parentNode, newChild) {
|
|
|
31999 |
if (newChild.parentNode) {
|
|
|
32000 |
newChild.parentNode.removeChild(newChild);
|
|
|
32001 |
}
|
|
|
32002 |
newChild.parentNode = parentNode;
|
|
|
32003 |
newChild.previousSibling = parentNode.lastChild;
|
|
|
32004 |
newChild.nextSibling = null;
|
|
|
32005 |
if (newChild.previousSibling) {
|
|
|
32006 |
newChild.previousSibling.nextSibling = newChild;
|
|
|
32007 |
} else {
|
|
|
32008 |
parentNode.firstChild = newChild;
|
|
|
32009 |
}
|
|
|
32010 |
parentNode.lastChild = newChild;
|
|
|
32011 |
_onUpdateChild(parentNode.ownerDocument, parentNode, newChild);
|
|
|
32012 |
return newChild;
|
|
|
32013 |
}
|
|
|
32014 |
Document.prototype = {
|
|
|
32015 |
//implementation : null,
|
|
|
32016 |
nodeName: '#document',
|
|
|
32017 |
nodeType: DOCUMENT_NODE,
|
|
|
32018 |
/**
|
|
|
32019 |
* The DocumentType node of the document.
|
|
|
32020 |
*
|
|
|
32021 |
* @readonly
|
|
|
32022 |
* @type DocumentType
|
|
|
32023 |
*/
|
|
|
32024 |
doctype: null,
|
|
|
32025 |
documentElement: null,
|
|
|
32026 |
_inc: 1,
|
|
|
32027 |
insertBefore: function (newChild, refChild) {
|
|
|
32028 |
//raises
|
|
|
32029 |
if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
|
|
|
32030 |
var child = newChild.firstChild;
|
|
|
32031 |
while (child) {
|
|
|
32032 |
var next = child.nextSibling;
|
|
|
32033 |
this.insertBefore(child, refChild);
|
|
|
32034 |
child = next;
|
|
|
32035 |
}
|
|
|
32036 |
return newChild;
|
|
|
32037 |
}
|
|
|
32038 |
_insertBefore(this, newChild, refChild);
|
|
|
32039 |
newChild.ownerDocument = this;
|
|
|
32040 |
if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
|
|
|
32041 |
this.documentElement = newChild;
|
|
|
32042 |
}
|
|
|
32043 |
return newChild;
|
|
|
32044 |
},
|
|
|
32045 |
removeChild: function (oldChild) {
|
|
|
32046 |
if (this.documentElement == oldChild) {
|
|
|
32047 |
this.documentElement = null;
|
|
|
32048 |
}
|
|
|
32049 |
return _removeChild(this, oldChild);
|
|
|
32050 |
},
|
|
|
32051 |
replaceChild: function (newChild, oldChild) {
|
|
|
32052 |
//raises
|
|
|
32053 |
_insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
|
|
|
32054 |
newChild.ownerDocument = this;
|
|
|
32055 |
if (oldChild) {
|
|
|
32056 |
this.removeChild(oldChild);
|
|
|
32057 |
}
|
|
|
32058 |
if (isElementNode(newChild)) {
|
|
|
32059 |
this.documentElement = newChild;
|
|
|
32060 |
}
|
|
|
32061 |
},
|
|
|
32062 |
// Introduced in DOM Level 2:
|
|
|
32063 |
importNode: function (importedNode, deep) {
|
|
|
32064 |
return importNode(this, importedNode, deep);
|
|
|
32065 |
},
|
|
|
32066 |
// Introduced in DOM Level 2:
|
|
|
32067 |
getElementById: function (id) {
|
|
|
32068 |
var rtv = null;
|
|
|
32069 |
_visitNode(this.documentElement, function (node) {
|
|
|
32070 |
if (node.nodeType == ELEMENT_NODE) {
|
|
|
32071 |
if (node.getAttribute('id') == id) {
|
|
|
32072 |
rtv = node;
|
|
|
32073 |
return true;
|
|
|
32074 |
}
|
|
|
32075 |
}
|
|
|
32076 |
});
|
|
|
32077 |
return rtv;
|
|
|
32078 |
},
|
|
|
32079 |
/**
|
|
|
32080 |
* The `getElementsByClassName` method of `Document` interface returns an array-like object
|
|
|
32081 |
* of all child elements which have **all** of the given class name(s).
|
|
|
32082 |
*
|
|
|
32083 |
* Returns an empty list if `classeNames` is an empty string or only contains HTML white space characters.
|
|
|
32084 |
*
|
|
|
32085 |
*
|
|
|
32086 |
* Warning: This is a live LiveNodeList.
|
|
|
32087 |
* Changes in the DOM will reflect in the array as the changes occur.
|
|
|
32088 |
* If an element selected by this array no longer qualifies for the selector,
|
|
|
32089 |
* it will automatically be removed. Be aware of this for iteration purposes.
|
|
|
32090 |
*
|
|
|
32091 |
* @param {string} classNames is a string representing the class name(s) to match; multiple class names are separated by (ASCII-)whitespace
|
|
|
32092 |
*
|
|
|
32093 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName
|
|
|
32094 |
* @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname
|
|
|
32095 |
*/
|
|
|
32096 |
getElementsByClassName: function (classNames) {
|
|
|
32097 |
var classNamesSet = toOrderedSet(classNames);
|
|
|
32098 |
return new LiveNodeList(this, function (base) {
|
|
|
32099 |
var ls = [];
|
|
|
32100 |
if (classNamesSet.length > 0) {
|
|
|
32101 |
_visitNode(base.documentElement, function (node) {
|
|
|
32102 |
if (node !== base && node.nodeType === ELEMENT_NODE) {
|
|
|
32103 |
var nodeClassNames = node.getAttribute('class');
|
|
|
32104 |
// can be null if the attribute does not exist
|
|
|
32105 |
if (nodeClassNames) {
|
|
|
32106 |
// before splitting and iterating just compare them for the most common case
|
|
|
32107 |
var matches = classNames === nodeClassNames;
|
|
|
32108 |
if (!matches) {
|
|
|
32109 |
var nodeClassNamesSet = toOrderedSet(nodeClassNames);
|
|
|
32110 |
matches = classNamesSet.every(arrayIncludes(nodeClassNamesSet));
|
|
|
32111 |
}
|
|
|
32112 |
if (matches) {
|
|
|
32113 |
ls.push(node);
|
|
|
32114 |
}
|
|
|
32115 |
}
|
|
|
32116 |
}
|
|
|
32117 |
});
|
|
|
32118 |
}
|
|
|
32119 |
return ls;
|
|
|
32120 |
});
|
|
|
32121 |
},
|
|
|
32122 |
//document factory method:
|
|
|
32123 |
createElement: function (tagName) {
|
|
|
32124 |
var node = new Element();
|
|
|
32125 |
node.ownerDocument = this;
|
|
|
32126 |
node.nodeName = tagName;
|
|
|
32127 |
node.tagName = tagName;
|
|
|
32128 |
node.localName = tagName;
|
|
|
32129 |
node.childNodes = new NodeList();
|
|
|
32130 |
var attrs = node.attributes = new NamedNodeMap();
|
|
|
32131 |
attrs._ownerElement = node;
|
|
|
32132 |
return node;
|
|
|
32133 |
},
|
|
|
32134 |
createDocumentFragment: function () {
|
|
|
32135 |
var node = new DocumentFragment();
|
|
|
32136 |
node.ownerDocument = this;
|
|
|
32137 |
node.childNodes = new NodeList();
|
|
|
32138 |
return node;
|
|
|
32139 |
},
|
|
|
32140 |
createTextNode: function (data) {
|
|
|
32141 |
var node = new Text();
|
|
|
32142 |
node.ownerDocument = this;
|
|
|
32143 |
node.appendData(data);
|
|
|
32144 |
return node;
|
|
|
32145 |
},
|
|
|
32146 |
createComment: function (data) {
|
|
|
32147 |
var node = new Comment();
|
|
|
32148 |
node.ownerDocument = this;
|
|
|
32149 |
node.appendData(data);
|
|
|
32150 |
return node;
|
|
|
32151 |
},
|
|
|
32152 |
createCDATASection: function (data) {
|
|
|
32153 |
var node = new CDATASection();
|
|
|
32154 |
node.ownerDocument = this;
|
|
|
32155 |
node.appendData(data);
|
|
|
32156 |
return node;
|
|
|
32157 |
},
|
|
|
32158 |
createProcessingInstruction: function (target, data) {
|
|
|
32159 |
var node = new ProcessingInstruction();
|
|
|
32160 |
node.ownerDocument = this;
|
|
|
32161 |
node.tagName = node.nodeName = node.target = target;
|
|
|
32162 |
node.nodeValue = node.data = data;
|
|
|
32163 |
return node;
|
|
|
32164 |
},
|
|
|
32165 |
createAttribute: function (name) {
|
|
|
32166 |
var node = new Attr();
|
|
|
32167 |
node.ownerDocument = this;
|
|
|
32168 |
node.name = name;
|
|
|
32169 |
node.nodeName = name;
|
|
|
32170 |
node.localName = name;
|
|
|
32171 |
node.specified = true;
|
|
|
32172 |
return node;
|
|
|
32173 |
},
|
|
|
32174 |
createEntityReference: function (name) {
|
|
|
32175 |
var node = new EntityReference();
|
|
|
32176 |
node.ownerDocument = this;
|
|
|
32177 |
node.nodeName = name;
|
|
|
32178 |
return node;
|
|
|
32179 |
},
|
|
|
32180 |
// Introduced in DOM Level 2:
|
|
|
32181 |
createElementNS: function (namespaceURI, qualifiedName) {
|
|
|
32182 |
var node = new Element();
|
|
|
32183 |
var pl = qualifiedName.split(':');
|
|
|
32184 |
var attrs = node.attributes = new NamedNodeMap();
|
|
|
32185 |
node.childNodes = new NodeList();
|
|
|
32186 |
node.ownerDocument = this;
|
|
|
32187 |
node.nodeName = qualifiedName;
|
|
|
32188 |
node.tagName = qualifiedName;
|
|
|
32189 |
node.namespaceURI = namespaceURI;
|
|
|
32190 |
if (pl.length == 2) {
|
|
|
32191 |
node.prefix = pl[0];
|
|
|
32192 |
node.localName = pl[1];
|
|
|
32193 |
} else {
|
|
|
32194 |
//el.prefix = null;
|
|
|
32195 |
node.localName = qualifiedName;
|
|
|
32196 |
}
|
|
|
32197 |
attrs._ownerElement = node;
|
|
|
32198 |
return node;
|
|
|
32199 |
},
|
|
|
32200 |
// Introduced in DOM Level 2:
|
|
|
32201 |
createAttributeNS: function (namespaceURI, qualifiedName) {
|
|
|
32202 |
var node = new Attr();
|
|
|
32203 |
var pl = qualifiedName.split(':');
|
|
|
32204 |
node.ownerDocument = this;
|
|
|
32205 |
node.nodeName = qualifiedName;
|
|
|
32206 |
node.name = qualifiedName;
|
|
|
32207 |
node.namespaceURI = namespaceURI;
|
|
|
32208 |
node.specified = true;
|
|
|
32209 |
if (pl.length == 2) {
|
|
|
32210 |
node.prefix = pl[0];
|
|
|
32211 |
node.localName = pl[1];
|
|
|
32212 |
} else {
|
|
|
32213 |
//el.prefix = null;
|
|
|
32214 |
node.localName = qualifiedName;
|
|
|
32215 |
}
|
|
|
32216 |
return node;
|
|
|
32217 |
}
|
|
|
32218 |
};
|
|
|
32219 |
_extends(Document, Node);
|
|
|
32220 |
function Element() {
|
|
|
32221 |
this._nsMap = {};
|
|
|
32222 |
}
|
|
|
32223 |
Element.prototype = {
|
|
|
32224 |
nodeType: ELEMENT_NODE,
|
|
|
32225 |
hasAttribute: function (name) {
|
|
|
32226 |
return this.getAttributeNode(name) != null;
|
|
|
32227 |
},
|
|
|
32228 |
getAttribute: function (name) {
|
|
|
32229 |
var attr = this.getAttributeNode(name);
|
|
|
32230 |
return attr && attr.value || '';
|
|
|
32231 |
},
|
|
|
32232 |
getAttributeNode: function (name) {
|
|
|
32233 |
return this.attributes.getNamedItem(name);
|
|
|
32234 |
},
|
|
|
32235 |
setAttribute: function (name, value) {
|
|
|
32236 |
var attr = this.ownerDocument.createAttribute(name);
|
|
|
32237 |
attr.value = attr.nodeValue = "" + value;
|
|
|
32238 |
this.setAttributeNode(attr);
|
|
|
32239 |
},
|
|
|
32240 |
removeAttribute: function (name) {
|
|
|
32241 |
var attr = this.getAttributeNode(name);
|
|
|
32242 |
attr && this.removeAttributeNode(attr);
|
|
|
32243 |
},
|
|
|
32244 |
//four real opeartion method
|
|
|
32245 |
appendChild: function (newChild) {
|
|
|
32246 |
if (newChild.nodeType === DOCUMENT_FRAGMENT_NODE) {
|
|
|
32247 |
return this.insertBefore(newChild, null);
|
|
|
32248 |
} else {
|
|
|
32249 |
return _appendSingleChild(this, newChild);
|
|
|
32250 |
}
|
|
|
32251 |
},
|
|
|
32252 |
setAttributeNode: function (newAttr) {
|
|
|
32253 |
return this.attributes.setNamedItem(newAttr);
|
|
|
32254 |
},
|
|
|
32255 |
setAttributeNodeNS: function (newAttr) {
|
|
|
32256 |
return this.attributes.setNamedItemNS(newAttr);
|
|
|
32257 |
},
|
|
|
32258 |
removeAttributeNode: function (oldAttr) {
|
|
|
32259 |
//console.log(this == oldAttr.ownerElement)
|
|
|
32260 |
return this.attributes.removeNamedItem(oldAttr.nodeName);
|
|
|
32261 |
},
|
|
|
32262 |
//get real attribute name,and remove it by removeAttributeNode
|
|
|
32263 |
removeAttributeNS: function (namespaceURI, localName) {
|
|
|
32264 |
var old = this.getAttributeNodeNS(namespaceURI, localName);
|
|
|
32265 |
old && this.removeAttributeNode(old);
|
|
|
32266 |
},
|
|
|
32267 |
hasAttributeNS: function (namespaceURI, localName) {
|
|
|
32268 |
return this.getAttributeNodeNS(namespaceURI, localName) != null;
|
|
|
32269 |
},
|
|
|
32270 |
getAttributeNS: function (namespaceURI, localName) {
|
|
|
32271 |
var attr = this.getAttributeNodeNS(namespaceURI, localName);
|
|
|
32272 |
return attr && attr.value || '';
|
|
|
32273 |
},
|
|
|
32274 |
setAttributeNS: function (namespaceURI, qualifiedName, value) {
|
|
|
32275 |
var attr = this.ownerDocument.createAttributeNS(namespaceURI, qualifiedName);
|
|
|
32276 |
attr.value = attr.nodeValue = "" + value;
|
|
|
32277 |
this.setAttributeNode(attr);
|
|
|
32278 |
},
|
|
|
32279 |
getAttributeNodeNS: function (namespaceURI, localName) {
|
|
|
32280 |
return this.attributes.getNamedItemNS(namespaceURI, localName);
|
|
|
32281 |
},
|
|
|
32282 |
getElementsByTagName: function (tagName) {
|
|
|
32283 |
return new LiveNodeList(this, function (base) {
|
|
|
32284 |
var ls = [];
|
|
|
32285 |
_visitNode(base, function (node) {
|
|
|
32286 |
if (node !== base && node.nodeType == ELEMENT_NODE && (tagName === '*' || node.tagName == tagName)) {
|
|
|
32287 |
ls.push(node);
|
|
|
32288 |
}
|
|
|
32289 |
});
|
|
|
32290 |
return ls;
|
|
|
32291 |
});
|
|
|
32292 |
},
|
|
|
32293 |
getElementsByTagNameNS: function (namespaceURI, localName) {
|
|
|
32294 |
return new LiveNodeList(this, function (base) {
|
|
|
32295 |
var ls = [];
|
|
|
32296 |
_visitNode(base, function (node) {
|
|
|
32297 |
if (node !== base && node.nodeType === ELEMENT_NODE && (namespaceURI === '*' || node.namespaceURI === namespaceURI) && (localName === '*' || node.localName == localName)) {
|
|
|
32298 |
ls.push(node);
|
|
|
32299 |
}
|
|
|
32300 |
});
|
|
|
32301 |
return ls;
|
|
|
32302 |
});
|
|
|
32303 |
}
|
|
|
32304 |
};
|
|
|
32305 |
Document.prototype.getElementsByTagName = Element.prototype.getElementsByTagName;
|
|
|
32306 |
Document.prototype.getElementsByTagNameNS = Element.prototype.getElementsByTagNameNS;
|
|
|
32307 |
_extends(Element, Node);
|
|
|
32308 |
function Attr() {}
|
|
|
32309 |
Attr.prototype.nodeType = ATTRIBUTE_NODE;
|
|
|
32310 |
_extends(Attr, Node);
|
|
|
32311 |
function CharacterData() {}
|
|
|
32312 |
CharacterData.prototype = {
|
|
|
32313 |
data: '',
|
|
|
32314 |
substringData: function (offset, count) {
|
|
|
32315 |
return this.data.substring(offset, offset + count);
|
|
|
32316 |
},
|
|
|
32317 |
appendData: function (text) {
|
|
|
32318 |
text = this.data + text;
|
|
|
32319 |
this.nodeValue = this.data = text;
|
|
|
32320 |
this.length = text.length;
|
|
|
32321 |
},
|
|
|
32322 |
insertData: function (offset, text) {
|
|
|
32323 |
this.replaceData(offset, 0, text);
|
|
|
32324 |
},
|
|
|
32325 |
appendChild: function (newChild) {
|
|
|
32326 |
throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR]);
|
|
|
32327 |
},
|
|
|
32328 |
deleteData: function (offset, count) {
|
|
|
32329 |
this.replaceData(offset, count, "");
|
|
|
32330 |
},
|
|
|
32331 |
replaceData: function (offset, count, text) {
|
|
|
32332 |
var start = this.data.substring(0, offset);
|
|
|
32333 |
var end = this.data.substring(offset + count);
|
|
|
32334 |
text = start + text + end;
|
|
|
32335 |
this.nodeValue = this.data = text;
|
|
|
32336 |
this.length = text.length;
|
|
|
32337 |
}
|
|
|
32338 |
};
|
|
|
32339 |
_extends(CharacterData, Node);
|
|
|
32340 |
function Text() {}
|
|
|
32341 |
Text.prototype = {
|
|
|
32342 |
nodeName: "#text",
|
|
|
32343 |
nodeType: TEXT_NODE,
|
|
|
32344 |
splitText: function (offset) {
|
|
|
32345 |
var text = this.data;
|
|
|
32346 |
var newText = text.substring(offset);
|
|
|
32347 |
text = text.substring(0, offset);
|
|
|
32348 |
this.data = this.nodeValue = text;
|
|
|
32349 |
this.length = text.length;
|
|
|
32350 |
var newNode = this.ownerDocument.createTextNode(newText);
|
|
|
32351 |
if (this.parentNode) {
|
|
|
32352 |
this.parentNode.insertBefore(newNode, this.nextSibling);
|
|
|
32353 |
}
|
|
|
32354 |
return newNode;
|
|
|
32355 |
}
|
|
|
32356 |
};
|
|
|
32357 |
_extends(Text, CharacterData);
|
|
|
32358 |
function Comment() {}
|
|
|
32359 |
Comment.prototype = {
|
|
|
32360 |
nodeName: "#comment",
|
|
|
32361 |
nodeType: COMMENT_NODE
|
|
|
32362 |
};
|
|
|
32363 |
_extends(Comment, CharacterData);
|
|
|
32364 |
function CDATASection() {}
|
|
|
32365 |
CDATASection.prototype = {
|
|
|
32366 |
nodeName: "#cdata-section",
|
|
|
32367 |
nodeType: CDATA_SECTION_NODE
|
|
|
32368 |
};
|
|
|
32369 |
_extends(CDATASection, CharacterData);
|
|
|
32370 |
function DocumentType() {}
|
|
|
32371 |
DocumentType.prototype.nodeType = DOCUMENT_TYPE_NODE;
|
|
|
32372 |
_extends(DocumentType, Node);
|
|
|
32373 |
function Notation() {}
|
|
|
32374 |
Notation.prototype.nodeType = NOTATION_NODE;
|
|
|
32375 |
_extends(Notation, Node);
|
|
|
32376 |
function Entity() {}
|
|
|
32377 |
Entity.prototype.nodeType = ENTITY_NODE;
|
|
|
32378 |
_extends(Entity, Node);
|
|
|
32379 |
function EntityReference() {}
|
|
|
32380 |
EntityReference.prototype.nodeType = ENTITY_REFERENCE_NODE;
|
|
|
32381 |
_extends(EntityReference, Node);
|
|
|
32382 |
function DocumentFragment() {}
|
|
|
32383 |
DocumentFragment.prototype.nodeName = "#document-fragment";
|
|
|
32384 |
DocumentFragment.prototype.nodeType = DOCUMENT_FRAGMENT_NODE;
|
|
|
32385 |
_extends(DocumentFragment, Node);
|
|
|
32386 |
function ProcessingInstruction() {}
|
|
|
32387 |
ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE;
|
|
|
32388 |
_extends(ProcessingInstruction, Node);
|
|
|
32389 |
function XMLSerializer() {}
|
|
|
32390 |
XMLSerializer.prototype.serializeToString = function (node, isHtml, nodeFilter) {
|
|
|
32391 |
return nodeSerializeToString.call(node, isHtml, nodeFilter);
|
|
|
32392 |
};
|
|
|
32393 |
Node.prototype.toString = nodeSerializeToString;
|
|
|
32394 |
function nodeSerializeToString(isHtml, nodeFilter) {
|
|
|
32395 |
var buf = [];
|
|
|
32396 |
var refNode = this.nodeType == 9 && this.documentElement || this;
|
|
|
32397 |
var prefix = refNode.prefix;
|
|
|
32398 |
var uri = refNode.namespaceURI;
|
|
|
32399 |
if (uri && prefix == null) {
|
|
|
32400 |
//console.log(prefix)
|
|
|
32401 |
var prefix = refNode.lookupPrefix(uri);
|
|
|
32402 |
if (prefix == null) {
|
|
|
32403 |
//isHTML = true;
|
|
|
32404 |
var visibleNamespaces = [{
|
|
|
32405 |
namespace: uri,
|
|
|
32406 |
prefix: null
|
|
|
32407 |
}
|
|
|
32408 |
//{namespace:uri,prefix:''}
|
|
|
32409 |
];
|
|
|
32410 |
}
|
|
|
32411 |
}
|
|
|
32412 |
|
|
|
32413 |
serializeToString(this, buf, isHtml, nodeFilter, visibleNamespaces);
|
|
|
32414 |
//console.log('###',this.nodeType,uri,prefix,buf.join(''))
|
|
|
32415 |
return buf.join('');
|
|
|
32416 |
}
|
|
|
32417 |
function needNamespaceDefine(node, isHTML, visibleNamespaces) {
|
|
|
32418 |
var prefix = node.prefix || '';
|
|
|
32419 |
var uri = node.namespaceURI;
|
|
|
32420 |
// According to [Namespaces in XML 1.0](https://www.w3.org/TR/REC-xml-names/#ns-using) ,
|
|
|
32421 |
// and more specifically https://www.w3.org/TR/REC-xml-names/#nsc-NoPrefixUndecl :
|
|
|
32422 |
// > In a namespace declaration for a prefix [...], the attribute value MUST NOT be empty.
|
|
|
32423 |
// in a similar manner [Namespaces in XML 1.1](https://www.w3.org/TR/xml-names11/#ns-using)
|
|
|
32424 |
// and more specifically https://www.w3.org/TR/xml-names11/#nsc-NSDeclared :
|
|
|
32425 |
// > [...] Furthermore, the attribute value [...] must not be an empty string.
|
|
|
32426 |
// so serializing empty namespace value like xmlns:ds="" would produce an invalid XML document.
|
|
|
32427 |
if (!uri) {
|
|
|
32428 |
return false;
|
|
|
32429 |
}
|
|
|
32430 |
if (prefix === "xml" && uri === NAMESPACE$2.XML || uri === NAMESPACE$2.XMLNS) {
|
|
|
32431 |
return false;
|
|
|
32432 |
}
|
|
|
32433 |
var i = visibleNamespaces.length;
|
|
|
32434 |
while (i--) {
|
|
|
32435 |
var ns = visibleNamespaces[i];
|
|
|
32436 |
// get namespace prefix
|
|
|
32437 |
if (ns.prefix === prefix) {
|
|
|
32438 |
return ns.namespace !== uri;
|
|
|
32439 |
}
|
|
|
32440 |
}
|
|
|
32441 |
return true;
|
|
|
32442 |
}
|
|
|
32443 |
/**
|
|
|
32444 |
* Well-formed constraint: No < in Attribute Values
|
|
|
32445 |
* > The replacement text of any entity referred to directly or indirectly
|
|
|
32446 |
* > in an attribute value must not contain a <.
|
|
|
32447 |
* @see https://www.w3.org/TR/xml11/#CleanAttrVals
|
|
|
32448 |
* @see https://www.w3.org/TR/xml11/#NT-AttValue
|
|
|
32449 |
*
|
|
|
32450 |
* Literal whitespace other than space that appear in attribute values
|
|
|
32451 |
* are serialized as their entity references, so they will be preserved.
|
|
|
32452 |
* (In contrast to whitespace literals in the input which are normalized to spaces)
|
|
|
32453 |
* @see https://www.w3.org/TR/xml11/#AVNormalize
|
|
|
32454 |
* @see https://w3c.github.io/DOM-Parsing/#serializing-an-element-s-attributes
|
|
|
32455 |
*/
|
|
|
32456 |
function addSerializedAttribute(buf, qualifiedName, value) {
|
|
|
32457 |
buf.push(' ', qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"');
|
|
|
32458 |
}
|
|
|
32459 |
function serializeToString(node, buf, isHTML, nodeFilter, visibleNamespaces) {
|
|
|
32460 |
if (!visibleNamespaces) {
|
|
|
32461 |
visibleNamespaces = [];
|
|
|
32462 |
}
|
|
|
32463 |
if (nodeFilter) {
|
|
|
32464 |
node = nodeFilter(node);
|
|
|
32465 |
if (node) {
|
|
|
32466 |
if (typeof node == 'string') {
|
|
|
32467 |
buf.push(node);
|
|
|
32468 |
return;
|
|
|
32469 |
}
|
|
|
32470 |
} else {
|
|
|
32471 |
return;
|
|
|
32472 |
}
|
|
|
32473 |
//buf.sort.apply(attrs, attributeSorter);
|
|
|
32474 |
}
|
|
|
32475 |
|
|
|
32476 |
switch (node.nodeType) {
|
|
|
32477 |
case ELEMENT_NODE:
|
|
|
32478 |
var attrs = node.attributes;
|
|
|
32479 |
var len = attrs.length;
|
|
|
32480 |
var child = node.firstChild;
|
|
|
32481 |
var nodeName = node.tagName;
|
|
|
32482 |
isHTML = NAMESPACE$2.isHTML(node.namespaceURI) || isHTML;
|
|
|
32483 |
var prefixedNodeName = nodeName;
|
|
|
32484 |
if (!isHTML && !node.prefix && node.namespaceURI) {
|
|
|
32485 |
var defaultNS;
|
|
|
32486 |
// lookup current default ns from `xmlns` attribute
|
|
|
32487 |
for (var ai = 0; ai < attrs.length; ai++) {
|
|
|
32488 |
if (attrs.item(ai).name === 'xmlns') {
|
|
|
32489 |
defaultNS = attrs.item(ai).value;
|
|
|
32490 |
break;
|
|
|
32491 |
}
|
|
|
32492 |
}
|
|
|
32493 |
if (!defaultNS) {
|
|
|
32494 |
// lookup current default ns in visibleNamespaces
|
|
|
32495 |
for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
|
|
|
32496 |
var namespace = visibleNamespaces[nsi];
|
|
|
32497 |
if (namespace.prefix === '' && namespace.namespace === node.namespaceURI) {
|
|
|
32498 |
defaultNS = namespace.namespace;
|
|
|
32499 |
break;
|
|
|
32500 |
}
|
|
|
32501 |
}
|
|
|
32502 |
}
|
|
|
32503 |
if (defaultNS !== node.namespaceURI) {
|
|
|
32504 |
for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) {
|
|
|
32505 |
var namespace = visibleNamespaces[nsi];
|
|
|
32506 |
if (namespace.namespace === node.namespaceURI) {
|
|
|
32507 |
if (namespace.prefix) {
|
|
|
32508 |
prefixedNodeName = namespace.prefix + ':' + nodeName;
|
|
|
32509 |
}
|
|
|
32510 |
break;
|
|
|
32511 |
}
|
|
|
32512 |
}
|
|
|
32513 |
}
|
|
|
32514 |
}
|
|
|
32515 |
buf.push('<', prefixedNodeName);
|
|
|
32516 |
for (var i = 0; i < len; i++) {
|
|
|
32517 |
// add namespaces for attributes
|
|
|
32518 |
var attr = attrs.item(i);
|
|
|
32519 |
if (attr.prefix == 'xmlns') {
|
|
|
32520 |
visibleNamespaces.push({
|
|
|
32521 |
prefix: attr.localName,
|
|
|
32522 |
namespace: attr.value
|
|
|
32523 |
});
|
|
|
32524 |
} else if (attr.nodeName == 'xmlns') {
|
|
|
32525 |
visibleNamespaces.push({
|
|
|
32526 |
prefix: '',
|
|
|
32527 |
namespace: attr.value
|
|
|
32528 |
});
|
|
|
32529 |
}
|
|
|
32530 |
}
|
|
|
32531 |
for (var i = 0; i < len; i++) {
|
|
|
32532 |
var attr = attrs.item(i);
|
|
|
32533 |
if (needNamespaceDefine(attr, isHTML, visibleNamespaces)) {
|
|
|
32534 |
var prefix = attr.prefix || '';
|
|
|
32535 |
var uri = attr.namespaceURI;
|
|
|
32536 |
addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
|
|
|
32537 |
visibleNamespaces.push({
|
|
|
32538 |
prefix: prefix,
|
|
|
32539 |
namespace: uri
|
|
|
32540 |
});
|
|
|
32541 |
}
|
|
|
32542 |
serializeToString(attr, buf, isHTML, nodeFilter, visibleNamespaces);
|
|
|
32543 |
}
|
|
|
32544 |
|
|
|
32545 |
// add namespace for current node
|
|
|
32546 |
if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
|
|
|
32547 |
var prefix = node.prefix || '';
|
|
|
32548 |
var uri = node.namespaceURI;
|
|
|
32549 |
addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
|
|
|
32550 |
visibleNamespaces.push({
|
|
|
32551 |
prefix: prefix,
|
|
|
32552 |
namespace: uri
|
|
|
32553 |
});
|
|
|
32554 |
}
|
|
|
32555 |
if (child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)) {
|
|
|
32556 |
buf.push('>');
|
|
|
32557 |
//if is cdata child node
|
|
|
32558 |
if (isHTML && /^script$/i.test(nodeName)) {
|
|
|
32559 |
while (child) {
|
|
|
32560 |
if (child.data) {
|
|
|
32561 |
buf.push(child.data);
|
|
|
32562 |
} else {
|
|
|
32563 |
serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
|
|
|
32564 |
}
|
|
|
32565 |
child = child.nextSibling;
|
|
|
32566 |
}
|
|
|
32567 |
} else {
|
|
|
32568 |
while (child) {
|
|
|
32569 |
serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
|
|
|
32570 |
child = child.nextSibling;
|
|
|
32571 |
}
|
|
|
32572 |
}
|
|
|
32573 |
buf.push('</', prefixedNodeName, '>');
|
|
|
32574 |
} else {
|
|
|
32575 |
buf.push('/>');
|
|
|
32576 |
}
|
|
|
32577 |
// remove added visible namespaces
|
|
|
32578 |
//visibleNamespaces.length = startVisibleNamespaces;
|
|
|
32579 |
return;
|
|
|
32580 |
case DOCUMENT_NODE:
|
|
|
32581 |
case DOCUMENT_FRAGMENT_NODE:
|
|
|
32582 |
var child = node.firstChild;
|
|
|
32583 |
while (child) {
|
|
|
32584 |
serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
|
|
|
32585 |
child = child.nextSibling;
|
|
|
32586 |
}
|
|
|
32587 |
return;
|
|
|
32588 |
case ATTRIBUTE_NODE:
|
|
|
32589 |
return addSerializedAttribute(buf, node.name, node.value);
|
|
|
32590 |
case TEXT_NODE:
|
|
|
32591 |
/**
|
|
|
32592 |
* The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
|
|
|
32593 |
* except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
|
|
|
32594 |
* If they are needed elsewhere, they must be escaped using either numeric character references or the strings
|
|
|
32595 |
* `&` and `<` respectively.
|
|
|
32596 |
* The right angle bracket (>) may be represented using the string " > ", and must, for compatibility,
|
|
|
32597 |
* be escaped using either `>` or a character reference when it appears in the string `]]>` in content,
|
|
|
32598 |
* when that string is not marking the end of a CDATA section.
|
|
|
32599 |
*
|
|
|
32600 |
* In the content of elements, character data is any string of characters
|
|
|
32601 |
* which does not contain the start-delimiter of any markup
|
|
|
32602 |
* and does not include the CDATA-section-close delimiter, `]]>`.
|
|
|
32603 |
*
|
|
|
32604 |
* @see https://www.w3.org/TR/xml/#NT-CharData
|
|
|
32605 |
* @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node
|
|
|
32606 |
*/
|
|
|
32607 |
return buf.push(node.data.replace(/[<&>]/g, _xmlEncoder));
|
|
|
32608 |
case CDATA_SECTION_NODE:
|
|
|
32609 |
return buf.push('<![CDATA[', node.data, ']]>');
|
|
|
32610 |
case COMMENT_NODE:
|
|
|
32611 |
return buf.push("<!--", node.data, "-->");
|
|
|
32612 |
case DOCUMENT_TYPE_NODE:
|
|
|
32613 |
var pubid = node.publicId;
|
|
|
32614 |
var sysid = node.systemId;
|
|
|
32615 |
buf.push('<!DOCTYPE ', node.name);
|
|
|
32616 |
if (pubid) {
|
|
|
32617 |
buf.push(' PUBLIC ', pubid);
|
|
|
32618 |
if (sysid && sysid != '.') {
|
|
|
32619 |
buf.push(' ', sysid);
|
|
|
32620 |
}
|
|
|
32621 |
buf.push('>');
|
|
|
32622 |
} else if (sysid && sysid != '.') {
|
|
|
32623 |
buf.push(' SYSTEM ', sysid, '>');
|
|
|
32624 |
} else {
|
|
|
32625 |
var sub = node.internalSubset;
|
|
|
32626 |
if (sub) {
|
|
|
32627 |
buf.push(" [", sub, "]");
|
|
|
32628 |
}
|
|
|
32629 |
buf.push(">");
|
|
|
32630 |
}
|
|
|
32631 |
return;
|
|
|
32632 |
case PROCESSING_INSTRUCTION_NODE:
|
|
|
32633 |
return buf.push("<?", node.target, " ", node.data, "?>");
|
|
|
32634 |
case ENTITY_REFERENCE_NODE:
|
|
|
32635 |
return buf.push('&', node.nodeName, ';');
|
|
|
32636 |
//case ENTITY_NODE:
|
|
|
32637 |
//case NOTATION_NODE:
|
|
|
32638 |
default:
|
|
|
32639 |
buf.push('??', node.nodeName);
|
|
|
32640 |
}
|
|
|
32641 |
}
|
|
|
32642 |
function importNode(doc, node, deep) {
|
|
|
32643 |
var node2;
|
|
|
32644 |
switch (node.nodeType) {
|
|
|
32645 |
case ELEMENT_NODE:
|
|
|
32646 |
node2 = node.cloneNode(false);
|
|
|
32647 |
node2.ownerDocument = doc;
|
|
|
32648 |
//var attrs = node2.attributes;
|
|
|
32649 |
//var len = attrs.length;
|
|
|
32650 |
//for(var i=0;i<len;i++){
|
|
|
32651 |
//node2.setAttributeNodeNS(importNode(doc,attrs.item(i),deep));
|
|
|
32652 |
//}
|
|
|
32653 |
case DOCUMENT_FRAGMENT_NODE:
|
|
|
32654 |
break;
|
|
|
32655 |
case ATTRIBUTE_NODE:
|
|
|
32656 |
deep = true;
|
|
|
32657 |
break;
|
|
|
32658 |
//case ENTITY_REFERENCE_NODE:
|
|
|
32659 |
//case PROCESSING_INSTRUCTION_NODE:
|
|
|
32660 |
////case TEXT_NODE:
|
|
|
32661 |
//case CDATA_SECTION_NODE:
|
|
|
32662 |
//case COMMENT_NODE:
|
|
|
32663 |
// deep = false;
|
|
|
32664 |
// break;
|
|
|
32665 |
//case DOCUMENT_NODE:
|
|
|
32666 |
//case DOCUMENT_TYPE_NODE:
|
|
|
32667 |
//cannot be imported.
|
|
|
32668 |
//case ENTITY_NODE:
|
|
|
32669 |
//case NOTATION_NODE:
|
|
|
32670 |
//can not hit in level3
|
|
|
32671 |
//default:throw e;
|
|
|
32672 |
}
|
|
|
32673 |
|
|
|
32674 |
if (!node2) {
|
|
|
32675 |
node2 = node.cloneNode(false); //false
|
|
|
32676 |
}
|
|
|
32677 |
|
|
|
32678 |
node2.ownerDocument = doc;
|
|
|
32679 |
node2.parentNode = null;
|
|
|
32680 |
if (deep) {
|
|
|
32681 |
var child = node.firstChild;
|
|
|
32682 |
while (child) {
|
|
|
32683 |
node2.appendChild(importNode(doc, child, deep));
|
|
|
32684 |
child = child.nextSibling;
|
|
|
32685 |
}
|
|
|
32686 |
}
|
|
|
32687 |
return node2;
|
|
|
32688 |
}
|
|
|
32689 |
//
|
|
|
32690 |
//var _relationMap = {firstChild:1,lastChild:1,previousSibling:1,nextSibling:1,
|
|
|
32691 |
// attributes:1,childNodes:1,parentNode:1,documentElement:1,doctype,};
|
|
|
32692 |
function cloneNode(doc, node, deep) {
|
|
|
32693 |
var node2 = new node.constructor();
|
|
|
32694 |
for (var n in node) {
|
|
|
32695 |
if (Object.prototype.hasOwnProperty.call(node, n)) {
|
|
|
32696 |
var v = node[n];
|
|
|
32697 |
if (typeof v != "object") {
|
|
|
32698 |
if (v != node2[n]) {
|
|
|
32699 |
node2[n] = v;
|
|
|
32700 |
}
|
|
|
32701 |
}
|
|
|
32702 |
}
|
|
|
32703 |
}
|
|
|
32704 |
if (node.childNodes) {
|
|
|
32705 |
node2.childNodes = new NodeList();
|
|
|
32706 |
}
|
|
|
32707 |
node2.ownerDocument = doc;
|
|
|
32708 |
switch (node2.nodeType) {
|
|
|
32709 |
case ELEMENT_NODE:
|
|
|
32710 |
var attrs = node.attributes;
|
|
|
32711 |
var attrs2 = node2.attributes = new NamedNodeMap();
|
|
|
32712 |
var len = attrs.length;
|
|
|
32713 |
attrs2._ownerElement = node2;
|
|
|
32714 |
for (var i = 0; i < len; i++) {
|
|
|
32715 |
node2.setAttributeNode(cloneNode(doc, attrs.item(i), true));
|
|
|
32716 |
}
|
|
|
32717 |
break;
|
|
|
32718 |
case ATTRIBUTE_NODE:
|
|
|
32719 |
deep = true;
|
|
|
32720 |
}
|
|
|
32721 |
if (deep) {
|
|
|
32722 |
var child = node.firstChild;
|
|
|
32723 |
while (child) {
|
|
|
32724 |
node2.appendChild(cloneNode(doc, child, deep));
|
|
|
32725 |
child = child.nextSibling;
|
|
|
32726 |
}
|
|
|
32727 |
}
|
|
|
32728 |
return node2;
|
|
|
32729 |
}
|
|
|
32730 |
function __set__(object, key, value) {
|
|
|
32731 |
object[key] = value;
|
|
|
32732 |
}
|
|
|
32733 |
//do dynamic
|
|
|
32734 |
try {
|
|
|
32735 |
if (Object.defineProperty) {
|
|
|
32736 |
Object.defineProperty(LiveNodeList.prototype, 'length', {
|
|
|
32737 |
get: function () {
|
|
|
32738 |
_updateLiveList(this);
|
|
|
32739 |
return this.$$length;
|
|
|
32740 |
}
|
|
|
32741 |
});
|
|
|
32742 |
Object.defineProperty(Node.prototype, 'textContent', {
|
|
|
32743 |
get: function () {
|
|
|
32744 |
return getTextContent(this);
|
|
|
32745 |
},
|
|
|
32746 |
set: function (data) {
|
|
|
32747 |
switch (this.nodeType) {
|
|
|
32748 |
case ELEMENT_NODE:
|
|
|
32749 |
case DOCUMENT_FRAGMENT_NODE:
|
|
|
32750 |
while (this.firstChild) {
|
|
|
32751 |
this.removeChild(this.firstChild);
|
|
|
32752 |
}
|
|
|
32753 |
if (data || String(data)) {
|
|
|
32754 |
this.appendChild(this.ownerDocument.createTextNode(data));
|
|
|
32755 |
}
|
|
|
32756 |
break;
|
|
|
32757 |
default:
|
|
|
32758 |
this.data = data;
|
|
|
32759 |
this.value = data;
|
|
|
32760 |
this.nodeValue = data;
|
|
|
32761 |
}
|
|
|
32762 |
}
|
|
|
32763 |
});
|
|
|
32764 |
function getTextContent(node) {
|
|
|
32765 |
switch (node.nodeType) {
|
|
|
32766 |
case ELEMENT_NODE:
|
|
|
32767 |
case DOCUMENT_FRAGMENT_NODE:
|
|
|
32768 |
var buf = [];
|
|
|
32769 |
node = node.firstChild;
|
|
|
32770 |
while (node) {
|
|
|
32771 |
if (node.nodeType !== 7 && node.nodeType !== 8) {
|
|
|
32772 |
buf.push(getTextContent(node));
|
|
|
32773 |
}
|
|
|
32774 |
node = node.nextSibling;
|
|
|
32775 |
}
|
|
|
32776 |
return buf.join('');
|
|
|
32777 |
default:
|
|
|
32778 |
return node.nodeValue;
|
|
|
32779 |
}
|
|
|
32780 |
}
|
|
|
32781 |
__set__ = function (object, key, value) {
|
|
|
32782 |
//console.log(value)
|
|
|
32783 |
object['$$' + key] = value;
|
|
|
32784 |
};
|
|
|
32785 |
}
|
|
|
32786 |
} catch (e) {//ie8
|
|
|
32787 |
}
|
|
|
32788 |
|
|
|
32789 |
//if(typeof require == 'function'){
|
|
|
32790 |
var DocumentType_1 = DocumentType;
|
|
|
32791 |
var DOMException_1 = DOMException;
|
|
|
32792 |
var DOMImplementation_1 = DOMImplementation$1;
|
|
|
32793 |
var Element_1 = Element;
|
|
|
32794 |
var Node_1 = Node;
|
|
|
32795 |
var NodeList_1 = NodeList;
|
|
|
32796 |
var XMLSerializer_1 = XMLSerializer;
|
|
|
32797 |
//}
|
|
|
32798 |
|
|
|
32799 |
var dom = {
|
|
|
32800 |
DocumentType: DocumentType_1,
|
|
|
32801 |
DOMException: DOMException_1,
|
|
|
32802 |
DOMImplementation: DOMImplementation_1,
|
|
|
32803 |
Element: Element_1,
|
|
|
32804 |
Node: Node_1,
|
|
|
32805 |
NodeList: NodeList_1,
|
|
|
32806 |
XMLSerializer: XMLSerializer_1
|
|
|
32807 |
};
|
|
|
32808 |
|
|
|
32809 |
var entities = createCommonjsModule(function (module, exports) {
|
|
|
32810 |
|
|
|
32811 |
var freeze = conventions.freeze;
|
|
|
32812 |
|
|
|
32813 |
/**
|
|
|
32814 |
* The entities that are predefined in every XML document.
|
|
|
32815 |
*
|
|
|
32816 |
* @see https://www.w3.org/TR/2006/REC-xml11-20060816/#sec-predefined-ent W3C XML 1.1
|
|
|
32817 |
* @see https://www.w3.org/TR/2008/REC-xml-20081126/#sec-predefined-ent W3C XML 1.0
|
|
|
32818 |
* @see https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Predefined_entities_in_XML Wikipedia
|
|
|
32819 |
*/
|
|
|
32820 |
exports.XML_ENTITIES = freeze({
|
|
|
32821 |
amp: '&',
|
|
|
32822 |
apos: "'",
|
|
|
32823 |
gt: '>',
|
|
|
32824 |
lt: '<',
|
|
|
32825 |
quot: '"'
|
|
|
32826 |
});
|
|
|
32827 |
|
|
|
32828 |
/**
|
|
|
32829 |
* A map of all entities that are detected in an HTML document.
|
|
|
32830 |
* They contain all entries from `XML_ENTITIES`.
|
|
|
32831 |
*
|
|
|
32832 |
* @see XML_ENTITIES
|
|
|
32833 |
* @see DOMParser.parseFromString
|
|
|
32834 |
* @see DOMImplementation.prototype.createHTMLDocument
|
|
|
32835 |
* @see https://html.spec.whatwg.org/#named-character-references WHATWG HTML(5) Spec
|
|
|
32836 |
* @see https://html.spec.whatwg.org/entities.json JSON
|
|
|
32837 |
* @see https://www.w3.org/TR/xml-entity-names/ W3C XML Entity Names
|
|
|
32838 |
* @see https://www.w3.org/TR/html4/sgml/entities.html W3C HTML4/SGML
|
|
|
32839 |
* @see https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Character_entity_references_in_HTML Wikipedia (HTML)
|
|
|
32840 |
* @see https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Entities_representing_special_characters_in_XHTML Wikpedia (XHTML)
|
|
|
32841 |
*/
|
|
|
32842 |
exports.HTML_ENTITIES = freeze({
|
|
|
32843 |
Aacute: '\u00C1',
|
|
|
32844 |
aacute: '\u00E1',
|
|
|
32845 |
Abreve: '\u0102',
|
|
|
32846 |
abreve: '\u0103',
|
|
|
32847 |
ac: '\u223E',
|
|
|
32848 |
acd: '\u223F',
|
|
|
32849 |
acE: '\u223E\u0333',
|
|
|
32850 |
Acirc: '\u00C2',
|
|
|
32851 |
acirc: '\u00E2',
|
|
|
32852 |
acute: '\u00B4',
|
|
|
32853 |
Acy: '\u0410',
|
|
|
32854 |
acy: '\u0430',
|
|
|
32855 |
AElig: '\u00C6',
|
|
|
32856 |
aelig: '\u00E6',
|
|
|
32857 |
af: '\u2061',
|
|
|
32858 |
Afr: '\uD835\uDD04',
|
|
|
32859 |
afr: '\uD835\uDD1E',
|
|
|
32860 |
Agrave: '\u00C0',
|
|
|
32861 |
agrave: '\u00E0',
|
|
|
32862 |
alefsym: '\u2135',
|
|
|
32863 |
aleph: '\u2135',
|
|
|
32864 |
Alpha: '\u0391',
|
|
|
32865 |
alpha: '\u03B1',
|
|
|
32866 |
Amacr: '\u0100',
|
|
|
32867 |
amacr: '\u0101',
|
|
|
32868 |
amalg: '\u2A3F',
|
|
|
32869 |
AMP: '\u0026',
|
|
|
32870 |
amp: '\u0026',
|
|
|
32871 |
And: '\u2A53',
|
|
|
32872 |
and: '\u2227',
|
|
|
32873 |
andand: '\u2A55',
|
|
|
32874 |
andd: '\u2A5C',
|
|
|
32875 |
andslope: '\u2A58',
|
|
|
32876 |
andv: '\u2A5A',
|
|
|
32877 |
ang: '\u2220',
|
|
|
32878 |
ange: '\u29A4',
|
|
|
32879 |
angle: '\u2220',
|
|
|
32880 |
angmsd: '\u2221',
|
|
|
32881 |
angmsdaa: '\u29A8',
|
|
|
32882 |
angmsdab: '\u29A9',
|
|
|
32883 |
angmsdac: '\u29AA',
|
|
|
32884 |
angmsdad: '\u29AB',
|
|
|
32885 |
angmsdae: '\u29AC',
|
|
|
32886 |
angmsdaf: '\u29AD',
|
|
|
32887 |
angmsdag: '\u29AE',
|
|
|
32888 |
angmsdah: '\u29AF',
|
|
|
32889 |
angrt: '\u221F',
|
|
|
32890 |
angrtvb: '\u22BE',
|
|
|
32891 |
angrtvbd: '\u299D',
|
|
|
32892 |
angsph: '\u2222',
|
|
|
32893 |
angst: '\u00C5',
|
|
|
32894 |
angzarr: '\u237C',
|
|
|
32895 |
Aogon: '\u0104',
|
|
|
32896 |
aogon: '\u0105',
|
|
|
32897 |
Aopf: '\uD835\uDD38',
|
|
|
32898 |
aopf: '\uD835\uDD52',
|
|
|
32899 |
ap: '\u2248',
|
|
|
32900 |
apacir: '\u2A6F',
|
|
|
32901 |
apE: '\u2A70',
|
|
|
32902 |
ape: '\u224A',
|
|
|
32903 |
apid: '\u224B',
|
|
|
32904 |
apos: '\u0027',
|
|
|
32905 |
ApplyFunction: '\u2061',
|
|
|
32906 |
approx: '\u2248',
|
|
|
32907 |
approxeq: '\u224A',
|
|
|
32908 |
Aring: '\u00C5',
|
|
|
32909 |
aring: '\u00E5',
|
|
|
32910 |
Ascr: '\uD835\uDC9C',
|
|
|
32911 |
ascr: '\uD835\uDCB6',
|
|
|
32912 |
Assign: '\u2254',
|
|
|
32913 |
ast: '\u002A',
|
|
|
32914 |
asymp: '\u2248',
|
|
|
32915 |
asympeq: '\u224D',
|
|
|
32916 |
Atilde: '\u00C3',
|
|
|
32917 |
atilde: '\u00E3',
|
|
|
32918 |
Auml: '\u00C4',
|
|
|
32919 |
auml: '\u00E4',
|
|
|
32920 |
awconint: '\u2233',
|
|
|
32921 |
awint: '\u2A11',
|
|
|
32922 |
backcong: '\u224C',
|
|
|
32923 |
backepsilon: '\u03F6',
|
|
|
32924 |
backprime: '\u2035',
|
|
|
32925 |
backsim: '\u223D',
|
|
|
32926 |
backsimeq: '\u22CD',
|
|
|
32927 |
Backslash: '\u2216',
|
|
|
32928 |
Barv: '\u2AE7',
|
|
|
32929 |
barvee: '\u22BD',
|
|
|
32930 |
Barwed: '\u2306',
|
|
|
32931 |
barwed: '\u2305',
|
|
|
32932 |
barwedge: '\u2305',
|
|
|
32933 |
bbrk: '\u23B5',
|
|
|
32934 |
bbrktbrk: '\u23B6',
|
|
|
32935 |
bcong: '\u224C',
|
|
|
32936 |
Bcy: '\u0411',
|
|
|
32937 |
bcy: '\u0431',
|
|
|
32938 |
bdquo: '\u201E',
|
|
|
32939 |
becaus: '\u2235',
|
|
|
32940 |
Because: '\u2235',
|
|
|
32941 |
because: '\u2235',
|
|
|
32942 |
bemptyv: '\u29B0',
|
|
|
32943 |
bepsi: '\u03F6',
|
|
|
32944 |
bernou: '\u212C',
|
|
|
32945 |
Bernoullis: '\u212C',
|
|
|
32946 |
Beta: '\u0392',
|
|
|
32947 |
beta: '\u03B2',
|
|
|
32948 |
beth: '\u2136',
|
|
|
32949 |
between: '\u226C',
|
|
|
32950 |
Bfr: '\uD835\uDD05',
|
|
|
32951 |
bfr: '\uD835\uDD1F',
|
|
|
32952 |
bigcap: '\u22C2',
|
|
|
32953 |
bigcirc: '\u25EF',
|
|
|
32954 |
bigcup: '\u22C3',
|
|
|
32955 |
bigodot: '\u2A00',
|
|
|
32956 |
bigoplus: '\u2A01',
|
|
|
32957 |
bigotimes: '\u2A02',
|
|
|
32958 |
bigsqcup: '\u2A06',
|
|
|
32959 |
bigstar: '\u2605',
|
|
|
32960 |
bigtriangledown: '\u25BD',
|
|
|
32961 |
bigtriangleup: '\u25B3',
|
|
|
32962 |
biguplus: '\u2A04',
|
|
|
32963 |
bigvee: '\u22C1',
|
|
|
32964 |
bigwedge: '\u22C0',
|
|
|
32965 |
bkarow: '\u290D',
|
|
|
32966 |
blacklozenge: '\u29EB',
|
|
|
32967 |
blacksquare: '\u25AA',
|
|
|
32968 |
blacktriangle: '\u25B4',
|
|
|
32969 |
blacktriangledown: '\u25BE',
|
|
|
32970 |
blacktriangleleft: '\u25C2',
|
|
|
32971 |
blacktriangleright: '\u25B8',
|
|
|
32972 |
blank: '\u2423',
|
|
|
32973 |
blk12: '\u2592',
|
|
|
32974 |
blk14: '\u2591',
|
|
|
32975 |
blk34: '\u2593',
|
|
|
32976 |
block: '\u2588',
|
|
|
32977 |
bne: '\u003D\u20E5',
|
|
|
32978 |
bnequiv: '\u2261\u20E5',
|
|
|
32979 |
bNot: '\u2AED',
|
|
|
32980 |
bnot: '\u2310',
|
|
|
32981 |
Bopf: '\uD835\uDD39',
|
|
|
32982 |
bopf: '\uD835\uDD53',
|
|
|
32983 |
bot: '\u22A5',
|
|
|
32984 |
bottom: '\u22A5',
|
|
|
32985 |
bowtie: '\u22C8',
|
|
|
32986 |
boxbox: '\u29C9',
|
|
|
32987 |
boxDL: '\u2557',
|
|
|
32988 |
boxDl: '\u2556',
|
|
|
32989 |
boxdL: '\u2555',
|
|
|
32990 |
boxdl: '\u2510',
|
|
|
32991 |
boxDR: '\u2554',
|
|
|
32992 |
boxDr: '\u2553',
|
|
|
32993 |
boxdR: '\u2552',
|
|
|
32994 |
boxdr: '\u250C',
|
|
|
32995 |
boxH: '\u2550',
|
|
|
32996 |
boxh: '\u2500',
|
|
|
32997 |
boxHD: '\u2566',
|
|
|
32998 |
boxHd: '\u2564',
|
|
|
32999 |
boxhD: '\u2565',
|
|
|
33000 |
boxhd: '\u252C',
|
|
|
33001 |
boxHU: '\u2569',
|
|
|
33002 |
boxHu: '\u2567',
|
|
|
33003 |
boxhU: '\u2568',
|
|
|
33004 |
boxhu: '\u2534',
|
|
|
33005 |
boxminus: '\u229F',
|
|
|
33006 |
boxplus: '\u229E',
|
|
|
33007 |
boxtimes: '\u22A0',
|
|
|
33008 |
boxUL: '\u255D',
|
|
|
33009 |
boxUl: '\u255C',
|
|
|
33010 |
boxuL: '\u255B',
|
|
|
33011 |
boxul: '\u2518',
|
|
|
33012 |
boxUR: '\u255A',
|
|
|
33013 |
boxUr: '\u2559',
|
|
|
33014 |
boxuR: '\u2558',
|
|
|
33015 |
boxur: '\u2514',
|
|
|
33016 |
boxV: '\u2551',
|
|
|
33017 |
boxv: '\u2502',
|
|
|
33018 |
boxVH: '\u256C',
|
|
|
33019 |
boxVh: '\u256B',
|
|
|
33020 |
boxvH: '\u256A',
|
|
|
33021 |
boxvh: '\u253C',
|
|
|
33022 |
boxVL: '\u2563',
|
|
|
33023 |
boxVl: '\u2562',
|
|
|
33024 |
boxvL: '\u2561',
|
|
|
33025 |
boxvl: '\u2524',
|
|
|
33026 |
boxVR: '\u2560',
|
|
|
33027 |
boxVr: '\u255F',
|
|
|
33028 |
boxvR: '\u255E',
|
|
|
33029 |
boxvr: '\u251C',
|
|
|
33030 |
bprime: '\u2035',
|
|
|
33031 |
Breve: '\u02D8',
|
|
|
33032 |
breve: '\u02D8',
|
|
|
33033 |
brvbar: '\u00A6',
|
|
|
33034 |
Bscr: '\u212C',
|
|
|
33035 |
bscr: '\uD835\uDCB7',
|
|
|
33036 |
bsemi: '\u204F',
|
|
|
33037 |
bsim: '\u223D',
|
|
|
33038 |
bsime: '\u22CD',
|
|
|
33039 |
bsol: '\u005C',
|
|
|
33040 |
bsolb: '\u29C5',
|
|
|
33041 |
bsolhsub: '\u27C8',
|
|
|
33042 |
bull: '\u2022',
|
|
|
33043 |
bullet: '\u2022',
|
|
|
33044 |
bump: '\u224E',
|
|
|
33045 |
bumpE: '\u2AAE',
|
|
|
33046 |
bumpe: '\u224F',
|
|
|
33047 |
Bumpeq: '\u224E',
|
|
|
33048 |
bumpeq: '\u224F',
|
|
|
33049 |
Cacute: '\u0106',
|
|
|
33050 |
cacute: '\u0107',
|
|
|
33051 |
Cap: '\u22D2',
|
|
|
33052 |
cap: '\u2229',
|
|
|
33053 |
capand: '\u2A44',
|
|
|
33054 |
capbrcup: '\u2A49',
|
|
|
33055 |
capcap: '\u2A4B',
|
|
|
33056 |
capcup: '\u2A47',
|
|
|
33057 |
capdot: '\u2A40',
|
|
|
33058 |
CapitalDifferentialD: '\u2145',
|
|
|
33059 |
caps: '\u2229\uFE00',
|
|
|
33060 |
caret: '\u2041',
|
|
|
33061 |
caron: '\u02C7',
|
|
|
33062 |
Cayleys: '\u212D',
|
|
|
33063 |
ccaps: '\u2A4D',
|
|
|
33064 |
Ccaron: '\u010C',
|
|
|
33065 |
ccaron: '\u010D',
|
|
|
33066 |
Ccedil: '\u00C7',
|
|
|
33067 |
ccedil: '\u00E7',
|
|
|
33068 |
Ccirc: '\u0108',
|
|
|
33069 |
ccirc: '\u0109',
|
|
|
33070 |
Cconint: '\u2230',
|
|
|
33071 |
ccups: '\u2A4C',
|
|
|
33072 |
ccupssm: '\u2A50',
|
|
|
33073 |
Cdot: '\u010A',
|
|
|
33074 |
cdot: '\u010B',
|
|
|
33075 |
cedil: '\u00B8',
|
|
|
33076 |
Cedilla: '\u00B8',
|
|
|
33077 |
cemptyv: '\u29B2',
|
|
|
33078 |
cent: '\u00A2',
|
|
|
33079 |
CenterDot: '\u00B7',
|
|
|
33080 |
centerdot: '\u00B7',
|
|
|
33081 |
Cfr: '\u212D',
|
|
|
33082 |
cfr: '\uD835\uDD20',
|
|
|
33083 |
CHcy: '\u0427',
|
|
|
33084 |
chcy: '\u0447',
|
|
|
33085 |
check: '\u2713',
|
|
|
33086 |
checkmark: '\u2713',
|
|
|
33087 |
Chi: '\u03A7',
|
|
|
33088 |
chi: '\u03C7',
|
|
|
33089 |
cir: '\u25CB',
|
|
|
33090 |
circ: '\u02C6',
|
|
|
33091 |
circeq: '\u2257',
|
|
|
33092 |
circlearrowleft: '\u21BA',
|
|
|
33093 |
circlearrowright: '\u21BB',
|
|
|
33094 |
circledast: '\u229B',
|
|
|
33095 |
circledcirc: '\u229A',
|
|
|
33096 |
circleddash: '\u229D',
|
|
|
33097 |
CircleDot: '\u2299',
|
|
|
33098 |
circledR: '\u00AE',
|
|
|
33099 |
circledS: '\u24C8',
|
|
|
33100 |
CircleMinus: '\u2296',
|
|
|
33101 |
CirclePlus: '\u2295',
|
|
|
33102 |
CircleTimes: '\u2297',
|
|
|
33103 |
cirE: '\u29C3',
|
|
|
33104 |
cire: '\u2257',
|
|
|
33105 |
cirfnint: '\u2A10',
|
|
|
33106 |
cirmid: '\u2AEF',
|
|
|
33107 |
cirscir: '\u29C2',
|
|
|
33108 |
ClockwiseContourIntegral: '\u2232',
|
|
|
33109 |
CloseCurlyDoubleQuote: '\u201D',
|
|
|
33110 |
CloseCurlyQuote: '\u2019',
|
|
|
33111 |
clubs: '\u2663',
|
|
|
33112 |
clubsuit: '\u2663',
|
|
|
33113 |
Colon: '\u2237',
|
|
|
33114 |
colon: '\u003A',
|
|
|
33115 |
Colone: '\u2A74',
|
|
|
33116 |
colone: '\u2254',
|
|
|
33117 |
coloneq: '\u2254',
|
|
|
33118 |
comma: '\u002C',
|
|
|
33119 |
commat: '\u0040',
|
|
|
33120 |
comp: '\u2201',
|
|
|
33121 |
compfn: '\u2218',
|
|
|
33122 |
complement: '\u2201',
|
|
|
33123 |
complexes: '\u2102',
|
|
|
33124 |
cong: '\u2245',
|
|
|
33125 |
congdot: '\u2A6D',
|
|
|
33126 |
Congruent: '\u2261',
|
|
|
33127 |
Conint: '\u222F',
|
|
|
33128 |
conint: '\u222E',
|
|
|
33129 |
ContourIntegral: '\u222E',
|
|
|
33130 |
Copf: '\u2102',
|
|
|
33131 |
copf: '\uD835\uDD54',
|
|
|
33132 |
coprod: '\u2210',
|
|
|
33133 |
Coproduct: '\u2210',
|
|
|
33134 |
COPY: '\u00A9',
|
|
|
33135 |
copy: '\u00A9',
|
|
|
33136 |
copysr: '\u2117',
|
|
|
33137 |
CounterClockwiseContourIntegral: '\u2233',
|
|
|
33138 |
crarr: '\u21B5',
|
|
|
33139 |
Cross: '\u2A2F',
|
|
|
33140 |
cross: '\u2717',
|
|
|
33141 |
Cscr: '\uD835\uDC9E',
|
|
|
33142 |
cscr: '\uD835\uDCB8',
|
|
|
33143 |
csub: '\u2ACF',
|
|
|
33144 |
csube: '\u2AD1',
|
|
|
33145 |
csup: '\u2AD0',
|
|
|
33146 |
csupe: '\u2AD2',
|
|
|
33147 |
ctdot: '\u22EF',
|
|
|
33148 |
cudarrl: '\u2938',
|
|
|
33149 |
cudarrr: '\u2935',
|
|
|
33150 |
cuepr: '\u22DE',
|
|
|
33151 |
cuesc: '\u22DF',
|
|
|
33152 |
cularr: '\u21B6',
|
|
|
33153 |
cularrp: '\u293D',
|
|
|
33154 |
Cup: '\u22D3',
|
|
|
33155 |
cup: '\u222A',
|
|
|
33156 |
cupbrcap: '\u2A48',
|
|
|
33157 |
CupCap: '\u224D',
|
|
|
33158 |
cupcap: '\u2A46',
|
|
|
33159 |
cupcup: '\u2A4A',
|
|
|
33160 |
cupdot: '\u228D',
|
|
|
33161 |
cupor: '\u2A45',
|
|
|
33162 |
cups: '\u222A\uFE00',
|
|
|
33163 |
curarr: '\u21B7',
|
|
|
33164 |
curarrm: '\u293C',
|
|
|
33165 |
curlyeqprec: '\u22DE',
|
|
|
33166 |
curlyeqsucc: '\u22DF',
|
|
|
33167 |
curlyvee: '\u22CE',
|
|
|
33168 |
curlywedge: '\u22CF',
|
|
|
33169 |
curren: '\u00A4',
|
|
|
33170 |
curvearrowleft: '\u21B6',
|
|
|
33171 |
curvearrowright: '\u21B7',
|
|
|
33172 |
cuvee: '\u22CE',
|
|
|
33173 |
cuwed: '\u22CF',
|
|
|
33174 |
cwconint: '\u2232',
|
|
|
33175 |
cwint: '\u2231',
|
|
|
33176 |
cylcty: '\u232D',
|
|
|
33177 |
Dagger: '\u2021',
|
|
|
33178 |
dagger: '\u2020',
|
|
|
33179 |
daleth: '\u2138',
|
|
|
33180 |
Darr: '\u21A1',
|
|
|
33181 |
dArr: '\u21D3',
|
|
|
33182 |
darr: '\u2193',
|
|
|
33183 |
dash: '\u2010',
|
|
|
33184 |
Dashv: '\u2AE4',
|
|
|
33185 |
dashv: '\u22A3',
|
|
|
33186 |
dbkarow: '\u290F',
|
|
|
33187 |
dblac: '\u02DD',
|
|
|
33188 |
Dcaron: '\u010E',
|
|
|
33189 |
dcaron: '\u010F',
|
|
|
33190 |
Dcy: '\u0414',
|
|
|
33191 |
dcy: '\u0434',
|
|
|
33192 |
DD: '\u2145',
|
|
|
33193 |
dd: '\u2146',
|
|
|
33194 |
ddagger: '\u2021',
|
|
|
33195 |
ddarr: '\u21CA',
|
|
|
33196 |
DDotrahd: '\u2911',
|
|
|
33197 |
ddotseq: '\u2A77',
|
|
|
33198 |
deg: '\u00B0',
|
|
|
33199 |
Del: '\u2207',
|
|
|
33200 |
Delta: '\u0394',
|
|
|
33201 |
delta: '\u03B4',
|
|
|
33202 |
demptyv: '\u29B1',
|
|
|
33203 |
dfisht: '\u297F',
|
|
|
33204 |
Dfr: '\uD835\uDD07',
|
|
|
33205 |
dfr: '\uD835\uDD21',
|
|
|
33206 |
dHar: '\u2965',
|
|
|
33207 |
dharl: '\u21C3',
|
|
|
33208 |
dharr: '\u21C2',
|
|
|
33209 |
DiacriticalAcute: '\u00B4',
|
|
|
33210 |
DiacriticalDot: '\u02D9',
|
|
|
33211 |
DiacriticalDoubleAcute: '\u02DD',
|
|
|
33212 |
DiacriticalGrave: '\u0060',
|
|
|
33213 |
DiacriticalTilde: '\u02DC',
|
|
|
33214 |
diam: '\u22C4',
|
|
|
33215 |
Diamond: '\u22C4',
|
|
|
33216 |
diamond: '\u22C4',
|
|
|
33217 |
diamondsuit: '\u2666',
|
|
|
33218 |
diams: '\u2666',
|
|
|
33219 |
die: '\u00A8',
|
|
|
33220 |
DifferentialD: '\u2146',
|
|
|
33221 |
digamma: '\u03DD',
|
|
|
33222 |
disin: '\u22F2',
|
|
|
33223 |
div: '\u00F7',
|
|
|
33224 |
divide: '\u00F7',
|
|
|
33225 |
divideontimes: '\u22C7',
|
|
|
33226 |
divonx: '\u22C7',
|
|
|
33227 |
DJcy: '\u0402',
|
|
|
33228 |
djcy: '\u0452',
|
|
|
33229 |
dlcorn: '\u231E',
|
|
|
33230 |
dlcrop: '\u230D',
|
|
|
33231 |
dollar: '\u0024',
|
|
|
33232 |
Dopf: '\uD835\uDD3B',
|
|
|
33233 |
dopf: '\uD835\uDD55',
|
|
|
33234 |
Dot: '\u00A8',
|
|
|
33235 |
dot: '\u02D9',
|
|
|
33236 |
DotDot: '\u20DC',
|
|
|
33237 |
doteq: '\u2250',
|
|
|
33238 |
doteqdot: '\u2251',
|
|
|
33239 |
DotEqual: '\u2250',
|
|
|
33240 |
dotminus: '\u2238',
|
|
|
33241 |
dotplus: '\u2214',
|
|
|
33242 |
dotsquare: '\u22A1',
|
|
|
33243 |
doublebarwedge: '\u2306',
|
|
|
33244 |
DoubleContourIntegral: '\u222F',
|
|
|
33245 |
DoubleDot: '\u00A8',
|
|
|
33246 |
DoubleDownArrow: '\u21D3',
|
|
|
33247 |
DoubleLeftArrow: '\u21D0',
|
|
|
33248 |
DoubleLeftRightArrow: '\u21D4',
|
|
|
33249 |
DoubleLeftTee: '\u2AE4',
|
|
|
33250 |
DoubleLongLeftArrow: '\u27F8',
|
|
|
33251 |
DoubleLongLeftRightArrow: '\u27FA',
|
|
|
33252 |
DoubleLongRightArrow: '\u27F9',
|
|
|
33253 |
DoubleRightArrow: '\u21D2',
|
|
|
33254 |
DoubleRightTee: '\u22A8',
|
|
|
33255 |
DoubleUpArrow: '\u21D1',
|
|
|
33256 |
DoubleUpDownArrow: '\u21D5',
|
|
|
33257 |
DoubleVerticalBar: '\u2225',
|
|
|
33258 |
DownArrow: '\u2193',
|
|
|
33259 |
Downarrow: '\u21D3',
|
|
|
33260 |
downarrow: '\u2193',
|
|
|
33261 |
DownArrowBar: '\u2913',
|
|
|
33262 |
DownArrowUpArrow: '\u21F5',
|
|
|
33263 |
DownBreve: '\u0311',
|
|
|
33264 |
downdownarrows: '\u21CA',
|
|
|
33265 |
downharpoonleft: '\u21C3',
|
|
|
33266 |
downharpoonright: '\u21C2',
|
|
|
33267 |
DownLeftRightVector: '\u2950',
|
|
|
33268 |
DownLeftTeeVector: '\u295E',
|
|
|
33269 |
DownLeftVector: '\u21BD',
|
|
|
33270 |
DownLeftVectorBar: '\u2956',
|
|
|
33271 |
DownRightTeeVector: '\u295F',
|
|
|
33272 |
DownRightVector: '\u21C1',
|
|
|
33273 |
DownRightVectorBar: '\u2957',
|
|
|
33274 |
DownTee: '\u22A4',
|
|
|
33275 |
DownTeeArrow: '\u21A7',
|
|
|
33276 |
drbkarow: '\u2910',
|
|
|
33277 |
drcorn: '\u231F',
|
|
|
33278 |
drcrop: '\u230C',
|
|
|
33279 |
Dscr: '\uD835\uDC9F',
|
|
|
33280 |
dscr: '\uD835\uDCB9',
|
|
|
33281 |
DScy: '\u0405',
|
|
|
33282 |
dscy: '\u0455',
|
|
|
33283 |
dsol: '\u29F6',
|
|
|
33284 |
Dstrok: '\u0110',
|
|
|
33285 |
dstrok: '\u0111',
|
|
|
33286 |
dtdot: '\u22F1',
|
|
|
33287 |
dtri: '\u25BF',
|
|
|
33288 |
dtrif: '\u25BE',
|
|
|
33289 |
duarr: '\u21F5',
|
|
|
33290 |
duhar: '\u296F',
|
|
|
33291 |
dwangle: '\u29A6',
|
|
|
33292 |
DZcy: '\u040F',
|
|
|
33293 |
dzcy: '\u045F',
|
|
|
33294 |
dzigrarr: '\u27FF',
|
|
|
33295 |
Eacute: '\u00C9',
|
|
|
33296 |
eacute: '\u00E9',
|
|
|
33297 |
easter: '\u2A6E',
|
|
|
33298 |
Ecaron: '\u011A',
|
|
|
33299 |
ecaron: '\u011B',
|
|
|
33300 |
ecir: '\u2256',
|
|
|
33301 |
Ecirc: '\u00CA',
|
|
|
33302 |
ecirc: '\u00EA',
|
|
|
33303 |
ecolon: '\u2255',
|
|
|
33304 |
Ecy: '\u042D',
|
|
|
33305 |
ecy: '\u044D',
|
|
|
33306 |
eDDot: '\u2A77',
|
|
|
33307 |
Edot: '\u0116',
|
|
|
33308 |
eDot: '\u2251',
|
|
|
33309 |
edot: '\u0117',
|
|
|
33310 |
ee: '\u2147',
|
|
|
33311 |
efDot: '\u2252',
|
|
|
33312 |
Efr: '\uD835\uDD08',
|
|
|
33313 |
efr: '\uD835\uDD22',
|
|
|
33314 |
eg: '\u2A9A',
|
|
|
33315 |
Egrave: '\u00C8',
|
|
|
33316 |
egrave: '\u00E8',
|
|
|
33317 |
egs: '\u2A96',
|
|
|
33318 |
egsdot: '\u2A98',
|
|
|
33319 |
el: '\u2A99',
|
|
|
33320 |
Element: '\u2208',
|
|
|
33321 |
elinters: '\u23E7',
|
|
|
33322 |
ell: '\u2113',
|
|
|
33323 |
els: '\u2A95',
|
|
|
33324 |
elsdot: '\u2A97',
|
|
|
33325 |
Emacr: '\u0112',
|
|
|
33326 |
emacr: '\u0113',
|
|
|
33327 |
empty: '\u2205',
|
|
|
33328 |
emptyset: '\u2205',
|
|
|
33329 |
EmptySmallSquare: '\u25FB',
|
|
|
33330 |
emptyv: '\u2205',
|
|
|
33331 |
EmptyVerySmallSquare: '\u25AB',
|
|
|
33332 |
emsp: '\u2003',
|
|
|
33333 |
emsp13: '\u2004',
|
|
|
33334 |
emsp14: '\u2005',
|
|
|
33335 |
ENG: '\u014A',
|
|
|
33336 |
eng: '\u014B',
|
|
|
33337 |
ensp: '\u2002',
|
|
|
33338 |
Eogon: '\u0118',
|
|
|
33339 |
eogon: '\u0119',
|
|
|
33340 |
Eopf: '\uD835\uDD3C',
|
|
|
33341 |
eopf: '\uD835\uDD56',
|
|
|
33342 |
epar: '\u22D5',
|
|
|
33343 |
eparsl: '\u29E3',
|
|
|
33344 |
eplus: '\u2A71',
|
|
|
33345 |
epsi: '\u03B5',
|
|
|
33346 |
Epsilon: '\u0395',
|
|
|
33347 |
epsilon: '\u03B5',
|
|
|
33348 |
epsiv: '\u03F5',
|
|
|
33349 |
eqcirc: '\u2256',
|
|
|
33350 |
eqcolon: '\u2255',
|
|
|
33351 |
eqsim: '\u2242',
|
|
|
33352 |
eqslantgtr: '\u2A96',
|
|
|
33353 |
eqslantless: '\u2A95',
|
|
|
33354 |
Equal: '\u2A75',
|
|
|
33355 |
equals: '\u003D',
|
|
|
33356 |
EqualTilde: '\u2242',
|
|
|
33357 |
equest: '\u225F',
|
|
|
33358 |
Equilibrium: '\u21CC',
|
|
|
33359 |
equiv: '\u2261',
|
|
|
33360 |
equivDD: '\u2A78',
|
|
|
33361 |
eqvparsl: '\u29E5',
|
|
|
33362 |
erarr: '\u2971',
|
|
|
33363 |
erDot: '\u2253',
|
|
|
33364 |
Escr: '\u2130',
|
|
|
33365 |
escr: '\u212F',
|
|
|
33366 |
esdot: '\u2250',
|
|
|
33367 |
Esim: '\u2A73',
|
|
|
33368 |
esim: '\u2242',
|
|
|
33369 |
Eta: '\u0397',
|
|
|
33370 |
eta: '\u03B7',
|
|
|
33371 |
ETH: '\u00D0',
|
|
|
33372 |
eth: '\u00F0',
|
|
|
33373 |
Euml: '\u00CB',
|
|
|
33374 |
euml: '\u00EB',
|
|
|
33375 |
euro: '\u20AC',
|
|
|
33376 |
excl: '\u0021',
|
|
|
33377 |
exist: '\u2203',
|
|
|
33378 |
Exists: '\u2203',
|
|
|
33379 |
expectation: '\u2130',
|
|
|
33380 |
ExponentialE: '\u2147',
|
|
|
33381 |
exponentiale: '\u2147',
|
|
|
33382 |
fallingdotseq: '\u2252',
|
|
|
33383 |
Fcy: '\u0424',
|
|
|
33384 |
fcy: '\u0444',
|
|
|
33385 |
female: '\u2640',
|
|
|
33386 |
ffilig: '\uFB03',
|
|
|
33387 |
fflig: '\uFB00',
|
|
|
33388 |
ffllig: '\uFB04',
|
|
|
33389 |
Ffr: '\uD835\uDD09',
|
|
|
33390 |
ffr: '\uD835\uDD23',
|
|
|
33391 |
filig: '\uFB01',
|
|
|
33392 |
FilledSmallSquare: '\u25FC',
|
|
|
33393 |
FilledVerySmallSquare: '\u25AA',
|
|
|
33394 |
fjlig: '\u0066\u006A',
|
|
|
33395 |
flat: '\u266D',
|
|
|
33396 |
fllig: '\uFB02',
|
|
|
33397 |
fltns: '\u25B1',
|
|
|
33398 |
fnof: '\u0192',
|
|
|
33399 |
Fopf: '\uD835\uDD3D',
|
|
|
33400 |
fopf: '\uD835\uDD57',
|
|
|
33401 |
ForAll: '\u2200',
|
|
|
33402 |
forall: '\u2200',
|
|
|
33403 |
fork: '\u22D4',
|
|
|
33404 |
forkv: '\u2AD9',
|
|
|
33405 |
Fouriertrf: '\u2131',
|
|
|
33406 |
fpartint: '\u2A0D',
|
|
|
33407 |
frac12: '\u00BD',
|
|
|
33408 |
frac13: '\u2153',
|
|
|
33409 |
frac14: '\u00BC',
|
|
|
33410 |
frac15: '\u2155',
|
|
|
33411 |
frac16: '\u2159',
|
|
|
33412 |
frac18: '\u215B',
|
|
|
33413 |
frac23: '\u2154',
|
|
|
33414 |
frac25: '\u2156',
|
|
|
33415 |
frac34: '\u00BE',
|
|
|
33416 |
frac35: '\u2157',
|
|
|
33417 |
frac38: '\u215C',
|
|
|
33418 |
frac45: '\u2158',
|
|
|
33419 |
frac56: '\u215A',
|
|
|
33420 |
frac58: '\u215D',
|
|
|
33421 |
frac78: '\u215E',
|
|
|
33422 |
frasl: '\u2044',
|
|
|
33423 |
frown: '\u2322',
|
|
|
33424 |
Fscr: '\u2131',
|
|
|
33425 |
fscr: '\uD835\uDCBB',
|
|
|
33426 |
gacute: '\u01F5',
|
|
|
33427 |
Gamma: '\u0393',
|
|
|
33428 |
gamma: '\u03B3',
|
|
|
33429 |
Gammad: '\u03DC',
|
|
|
33430 |
gammad: '\u03DD',
|
|
|
33431 |
gap: '\u2A86',
|
|
|
33432 |
Gbreve: '\u011E',
|
|
|
33433 |
gbreve: '\u011F',
|
|
|
33434 |
Gcedil: '\u0122',
|
|
|
33435 |
Gcirc: '\u011C',
|
|
|
33436 |
gcirc: '\u011D',
|
|
|
33437 |
Gcy: '\u0413',
|
|
|
33438 |
gcy: '\u0433',
|
|
|
33439 |
Gdot: '\u0120',
|
|
|
33440 |
gdot: '\u0121',
|
|
|
33441 |
gE: '\u2267',
|
|
|
33442 |
ge: '\u2265',
|
|
|
33443 |
gEl: '\u2A8C',
|
|
|
33444 |
gel: '\u22DB',
|
|
|
33445 |
geq: '\u2265',
|
|
|
33446 |
geqq: '\u2267',
|
|
|
33447 |
geqslant: '\u2A7E',
|
|
|
33448 |
ges: '\u2A7E',
|
|
|
33449 |
gescc: '\u2AA9',
|
|
|
33450 |
gesdot: '\u2A80',
|
|
|
33451 |
gesdoto: '\u2A82',
|
|
|
33452 |
gesdotol: '\u2A84',
|
|
|
33453 |
gesl: '\u22DB\uFE00',
|
|
|
33454 |
gesles: '\u2A94',
|
|
|
33455 |
Gfr: '\uD835\uDD0A',
|
|
|
33456 |
gfr: '\uD835\uDD24',
|
|
|
33457 |
Gg: '\u22D9',
|
|
|
33458 |
gg: '\u226B',
|
|
|
33459 |
ggg: '\u22D9',
|
|
|
33460 |
gimel: '\u2137',
|
|
|
33461 |
GJcy: '\u0403',
|
|
|
33462 |
gjcy: '\u0453',
|
|
|
33463 |
gl: '\u2277',
|
|
|
33464 |
gla: '\u2AA5',
|
|
|
33465 |
glE: '\u2A92',
|
|
|
33466 |
glj: '\u2AA4',
|
|
|
33467 |
gnap: '\u2A8A',
|
|
|
33468 |
gnapprox: '\u2A8A',
|
|
|
33469 |
gnE: '\u2269',
|
|
|
33470 |
gne: '\u2A88',
|
|
|
33471 |
gneq: '\u2A88',
|
|
|
33472 |
gneqq: '\u2269',
|
|
|
33473 |
gnsim: '\u22E7',
|
|
|
33474 |
Gopf: '\uD835\uDD3E',
|
|
|
33475 |
gopf: '\uD835\uDD58',
|
|
|
33476 |
grave: '\u0060',
|
|
|
33477 |
GreaterEqual: '\u2265',
|
|
|
33478 |
GreaterEqualLess: '\u22DB',
|
|
|
33479 |
GreaterFullEqual: '\u2267',
|
|
|
33480 |
GreaterGreater: '\u2AA2',
|
|
|
33481 |
GreaterLess: '\u2277',
|
|
|
33482 |
GreaterSlantEqual: '\u2A7E',
|
|
|
33483 |
GreaterTilde: '\u2273',
|
|
|
33484 |
Gscr: '\uD835\uDCA2',
|
|
|
33485 |
gscr: '\u210A',
|
|
|
33486 |
gsim: '\u2273',
|
|
|
33487 |
gsime: '\u2A8E',
|
|
|
33488 |
gsiml: '\u2A90',
|
|
|
33489 |
Gt: '\u226B',
|
|
|
33490 |
GT: '\u003E',
|
|
|
33491 |
gt: '\u003E',
|
|
|
33492 |
gtcc: '\u2AA7',
|
|
|
33493 |
gtcir: '\u2A7A',
|
|
|
33494 |
gtdot: '\u22D7',
|
|
|
33495 |
gtlPar: '\u2995',
|
|
|
33496 |
gtquest: '\u2A7C',
|
|
|
33497 |
gtrapprox: '\u2A86',
|
|
|
33498 |
gtrarr: '\u2978',
|
|
|
33499 |
gtrdot: '\u22D7',
|
|
|
33500 |
gtreqless: '\u22DB',
|
|
|
33501 |
gtreqqless: '\u2A8C',
|
|
|
33502 |
gtrless: '\u2277',
|
|
|
33503 |
gtrsim: '\u2273',
|
|
|
33504 |
gvertneqq: '\u2269\uFE00',
|
|
|
33505 |
gvnE: '\u2269\uFE00',
|
|
|
33506 |
Hacek: '\u02C7',
|
|
|
33507 |
hairsp: '\u200A',
|
|
|
33508 |
half: '\u00BD',
|
|
|
33509 |
hamilt: '\u210B',
|
|
|
33510 |
HARDcy: '\u042A',
|
|
|
33511 |
hardcy: '\u044A',
|
|
|
33512 |
hArr: '\u21D4',
|
|
|
33513 |
harr: '\u2194',
|
|
|
33514 |
harrcir: '\u2948',
|
|
|
33515 |
harrw: '\u21AD',
|
|
|
33516 |
Hat: '\u005E',
|
|
|
33517 |
hbar: '\u210F',
|
|
|
33518 |
Hcirc: '\u0124',
|
|
|
33519 |
hcirc: '\u0125',
|
|
|
33520 |
hearts: '\u2665',
|
|
|
33521 |
heartsuit: '\u2665',
|
|
|
33522 |
hellip: '\u2026',
|
|
|
33523 |
hercon: '\u22B9',
|
|
|
33524 |
Hfr: '\u210C',
|
|
|
33525 |
hfr: '\uD835\uDD25',
|
|
|
33526 |
HilbertSpace: '\u210B',
|
|
|
33527 |
hksearow: '\u2925',
|
|
|
33528 |
hkswarow: '\u2926',
|
|
|
33529 |
hoarr: '\u21FF',
|
|
|
33530 |
homtht: '\u223B',
|
|
|
33531 |
hookleftarrow: '\u21A9',
|
|
|
33532 |
hookrightarrow: '\u21AA',
|
|
|
33533 |
Hopf: '\u210D',
|
|
|
33534 |
hopf: '\uD835\uDD59',
|
|
|
33535 |
horbar: '\u2015',
|
|
|
33536 |
HorizontalLine: '\u2500',
|
|
|
33537 |
Hscr: '\u210B',
|
|
|
33538 |
hscr: '\uD835\uDCBD',
|
|
|
33539 |
hslash: '\u210F',
|
|
|
33540 |
Hstrok: '\u0126',
|
|
|
33541 |
hstrok: '\u0127',
|
|
|
33542 |
HumpDownHump: '\u224E',
|
|
|
33543 |
HumpEqual: '\u224F',
|
|
|
33544 |
hybull: '\u2043',
|
|
|
33545 |
hyphen: '\u2010',
|
|
|
33546 |
Iacute: '\u00CD',
|
|
|
33547 |
iacute: '\u00ED',
|
|
|
33548 |
ic: '\u2063',
|
|
|
33549 |
Icirc: '\u00CE',
|
|
|
33550 |
icirc: '\u00EE',
|
|
|
33551 |
Icy: '\u0418',
|
|
|
33552 |
icy: '\u0438',
|
|
|
33553 |
Idot: '\u0130',
|
|
|
33554 |
IEcy: '\u0415',
|
|
|
33555 |
iecy: '\u0435',
|
|
|
33556 |
iexcl: '\u00A1',
|
|
|
33557 |
iff: '\u21D4',
|
|
|
33558 |
Ifr: '\u2111',
|
|
|
33559 |
ifr: '\uD835\uDD26',
|
|
|
33560 |
Igrave: '\u00CC',
|
|
|
33561 |
igrave: '\u00EC',
|
|
|
33562 |
ii: '\u2148',
|
|
|
33563 |
iiiint: '\u2A0C',
|
|
|
33564 |
iiint: '\u222D',
|
|
|
33565 |
iinfin: '\u29DC',
|
|
|
33566 |
iiota: '\u2129',
|
|
|
33567 |
IJlig: '\u0132',
|
|
|
33568 |
ijlig: '\u0133',
|
|
|
33569 |
Im: '\u2111',
|
|
|
33570 |
Imacr: '\u012A',
|
|
|
33571 |
imacr: '\u012B',
|
|
|
33572 |
image: '\u2111',
|
|
|
33573 |
ImaginaryI: '\u2148',
|
|
|
33574 |
imagline: '\u2110',
|
|
|
33575 |
imagpart: '\u2111',
|
|
|
33576 |
imath: '\u0131',
|
|
|
33577 |
imof: '\u22B7',
|
|
|
33578 |
imped: '\u01B5',
|
|
|
33579 |
Implies: '\u21D2',
|
|
|
33580 |
in: '\u2208',
|
|
|
33581 |
incare: '\u2105',
|
|
|
33582 |
infin: '\u221E',
|
|
|
33583 |
infintie: '\u29DD',
|
|
|
33584 |
inodot: '\u0131',
|
|
|
33585 |
Int: '\u222C',
|
|
|
33586 |
int: '\u222B',
|
|
|
33587 |
intcal: '\u22BA',
|
|
|
33588 |
integers: '\u2124',
|
|
|
33589 |
Integral: '\u222B',
|
|
|
33590 |
intercal: '\u22BA',
|
|
|
33591 |
Intersection: '\u22C2',
|
|
|
33592 |
intlarhk: '\u2A17',
|
|
|
33593 |
intprod: '\u2A3C',
|
|
|
33594 |
InvisibleComma: '\u2063',
|
|
|
33595 |
InvisibleTimes: '\u2062',
|
|
|
33596 |
IOcy: '\u0401',
|
|
|
33597 |
iocy: '\u0451',
|
|
|
33598 |
Iogon: '\u012E',
|
|
|
33599 |
iogon: '\u012F',
|
|
|
33600 |
Iopf: '\uD835\uDD40',
|
|
|
33601 |
iopf: '\uD835\uDD5A',
|
|
|
33602 |
Iota: '\u0399',
|
|
|
33603 |
iota: '\u03B9',
|
|
|
33604 |
iprod: '\u2A3C',
|
|
|
33605 |
iquest: '\u00BF',
|
|
|
33606 |
Iscr: '\u2110',
|
|
|
33607 |
iscr: '\uD835\uDCBE',
|
|
|
33608 |
isin: '\u2208',
|
|
|
33609 |
isindot: '\u22F5',
|
|
|
33610 |
isinE: '\u22F9',
|
|
|
33611 |
isins: '\u22F4',
|
|
|
33612 |
isinsv: '\u22F3',
|
|
|
33613 |
isinv: '\u2208',
|
|
|
33614 |
it: '\u2062',
|
|
|
33615 |
Itilde: '\u0128',
|
|
|
33616 |
itilde: '\u0129',
|
|
|
33617 |
Iukcy: '\u0406',
|
|
|
33618 |
iukcy: '\u0456',
|
|
|
33619 |
Iuml: '\u00CF',
|
|
|
33620 |
iuml: '\u00EF',
|
|
|
33621 |
Jcirc: '\u0134',
|
|
|
33622 |
jcirc: '\u0135',
|
|
|
33623 |
Jcy: '\u0419',
|
|
|
33624 |
jcy: '\u0439',
|
|
|
33625 |
Jfr: '\uD835\uDD0D',
|
|
|
33626 |
jfr: '\uD835\uDD27',
|
|
|
33627 |
jmath: '\u0237',
|
|
|
33628 |
Jopf: '\uD835\uDD41',
|
|
|
33629 |
jopf: '\uD835\uDD5B',
|
|
|
33630 |
Jscr: '\uD835\uDCA5',
|
|
|
33631 |
jscr: '\uD835\uDCBF',
|
|
|
33632 |
Jsercy: '\u0408',
|
|
|
33633 |
jsercy: '\u0458',
|
|
|
33634 |
Jukcy: '\u0404',
|
|
|
33635 |
jukcy: '\u0454',
|
|
|
33636 |
Kappa: '\u039A',
|
|
|
33637 |
kappa: '\u03BA',
|
|
|
33638 |
kappav: '\u03F0',
|
|
|
33639 |
Kcedil: '\u0136',
|
|
|
33640 |
kcedil: '\u0137',
|
|
|
33641 |
Kcy: '\u041A',
|
|
|
33642 |
kcy: '\u043A',
|
|
|
33643 |
Kfr: '\uD835\uDD0E',
|
|
|
33644 |
kfr: '\uD835\uDD28',
|
|
|
33645 |
kgreen: '\u0138',
|
|
|
33646 |
KHcy: '\u0425',
|
|
|
33647 |
khcy: '\u0445',
|
|
|
33648 |
KJcy: '\u040C',
|
|
|
33649 |
kjcy: '\u045C',
|
|
|
33650 |
Kopf: '\uD835\uDD42',
|
|
|
33651 |
kopf: '\uD835\uDD5C',
|
|
|
33652 |
Kscr: '\uD835\uDCA6',
|
|
|
33653 |
kscr: '\uD835\uDCC0',
|
|
|
33654 |
lAarr: '\u21DA',
|
|
|
33655 |
Lacute: '\u0139',
|
|
|
33656 |
lacute: '\u013A',
|
|
|
33657 |
laemptyv: '\u29B4',
|
|
|
33658 |
lagran: '\u2112',
|
|
|
33659 |
Lambda: '\u039B',
|
|
|
33660 |
lambda: '\u03BB',
|
|
|
33661 |
Lang: '\u27EA',
|
|
|
33662 |
lang: '\u27E8',
|
|
|
33663 |
langd: '\u2991',
|
|
|
33664 |
langle: '\u27E8',
|
|
|
33665 |
lap: '\u2A85',
|
|
|
33666 |
Laplacetrf: '\u2112',
|
|
|
33667 |
laquo: '\u00AB',
|
|
|
33668 |
Larr: '\u219E',
|
|
|
33669 |
lArr: '\u21D0',
|
|
|
33670 |
larr: '\u2190',
|
|
|
33671 |
larrb: '\u21E4',
|
|
|
33672 |
larrbfs: '\u291F',
|
|
|
33673 |
larrfs: '\u291D',
|
|
|
33674 |
larrhk: '\u21A9',
|
|
|
33675 |
larrlp: '\u21AB',
|
|
|
33676 |
larrpl: '\u2939',
|
|
|
33677 |
larrsim: '\u2973',
|
|
|
33678 |
larrtl: '\u21A2',
|
|
|
33679 |
lat: '\u2AAB',
|
|
|
33680 |
lAtail: '\u291B',
|
|
|
33681 |
latail: '\u2919',
|
|
|
33682 |
late: '\u2AAD',
|
|
|
33683 |
lates: '\u2AAD\uFE00',
|
|
|
33684 |
lBarr: '\u290E',
|
|
|
33685 |
lbarr: '\u290C',
|
|
|
33686 |
lbbrk: '\u2772',
|
|
|
33687 |
lbrace: '\u007B',
|
|
|
33688 |
lbrack: '\u005B',
|
|
|
33689 |
lbrke: '\u298B',
|
|
|
33690 |
lbrksld: '\u298F',
|
|
|
33691 |
lbrkslu: '\u298D',
|
|
|
33692 |
Lcaron: '\u013D',
|
|
|
33693 |
lcaron: '\u013E',
|
|
|
33694 |
Lcedil: '\u013B',
|
|
|
33695 |
lcedil: '\u013C',
|
|
|
33696 |
lceil: '\u2308',
|
|
|
33697 |
lcub: '\u007B',
|
|
|
33698 |
Lcy: '\u041B',
|
|
|
33699 |
lcy: '\u043B',
|
|
|
33700 |
ldca: '\u2936',
|
|
|
33701 |
ldquo: '\u201C',
|
|
|
33702 |
ldquor: '\u201E',
|
|
|
33703 |
ldrdhar: '\u2967',
|
|
|
33704 |
ldrushar: '\u294B',
|
|
|
33705 |
ldsh: '\u21B2',
|
|
|
33706 |
lE: '\u2266',
|
|
|
33707 |
le: '\u2264',
|
|
|
33708 |
LeftAngleBracket: '\u27E8',
|
|
|
33709 |
LeftArrow: '\u2190',
|
|
|
33710 |
Leftarrow: '\u21D0',
|
|
|
33711 |
leftarrow: '\u2190',
|
|
|
33712 |
LeftArrowBar: '\u21E4',
|
|
|
33713 |
LeftArrowRightArrow: '\u21C6',
|
|
|
33714 |
leftarrowtail: '\u21A2',
|
|
|
33715 |
LeftCeiling: '\u2308',
|
|
|
33716 |
LeftDoubleBracket: '\u27E6',
|
|
|
33717 |
LeftDownTeeVector: '\u2961',
|
|
|
33718 |
LeftDownVector: '\u21C3',
|
|
|
33719 |
LeftDownVectorBar: '\u2959',
|
|
|
33720 |
LeftFloor: '\u230A',
|
|
|
33721 |
leftharpoondown: '\u21BD',
|
|
|
33722 |
leftharpoonup: '\u21BC',
|
|
|
33723 |
leftleftarrows: '\u21C7',
|
|
|
33724 |
LeftRightArrow: '\u2194',
|
|
|
33725 |
Leftrightarrow: '\u21D4',
|
|
|
33726 |
leftrightarrow: '\u2194',
|
|
|
33727 |
leftrightarrows: '\u21C6',
|
|
|
33728 |
leftrightharpoons: '\u21CB',
|
|
|
33729 |
leftrightsquigarrow: '\u21AD',
|
|
|
33730 |
LeftRightVector: '\u294E',
|
|
|
33731 |
LeftTee: '\u22A3',
|
|
|
33732 |
LeftTeeArrow: '\u21A4',
|
|
|
33733 |
LeftTeeVector: '\u295A',
|
|
|
33734 |
leftthreetimes: '\u22CB',
|
|
|
33735 |
LeftTriangle: '\u22B2',
|
|
|
33736 |
LeftTriangleBar: '\u29CF',
|
|
|
33737 |
LeftTriangleEqual: '\u22B4',
|
|
|
33738 |
LeftUpDownVector: '\u2951',
|
|
|
33739 |
LeftUpTeeVector: '\u2960',
|
|
|
33740 |
LeftUpVector: '\u21BF',
|
|
|
33741 |
LeftUpVectorBar: '\u2958',
|
|
|
33742 |
LeftVector: '\u21BC',
|
|
|
33743 |
LeftVectorBar: '\u2952',
|
|
|
33744 |
lEg: '\u2A8B',
|
|
|
33745 |
leg: '\u22DA',
|
|
|
33746 |
leq: '\u2264',
|
|
|
33747 |
leqq: '\u2266',
|
|
|
33748 |
leqslant: '\u2A7D',
|
|
|
33749 |
les: '\u2A7D',
|
|
|
33750 |
lescc: '\u2AA8',
|
|
|
33751 |
lesdot: '\u2A7F',
|
|
|
33752 |
lesdoto: '\u2A81',
|
|
|
33753 |
lesdotor: '\u2A83',
|
|
|
33754 |
lesg: '\u22DA\uFE00',
|
|
|
33755 |
lesges: '\u2A93',
|
|
|
33756 |
lessapprox: '\u2A85',
|
|
|
33757 |
lessdot: '\u22D6',
|
|
|
33758 |
lesseqgtr: '\u22DA',
|
|
|
33759 |
lesseqqgtr: '\u2A8B',
|
|
|
33760 |
LessEqualGreater: '\u22DA',
|
|
|
33761 |
LessFullEqual: '\u2266',
|
|
|
33762 |
LessGreater: '\u2276',
|
|
|
33763 |
lessgtr: '\u2276',
|
|
|
33764 |
LessLess: '\u2AA1',
|
|
|
33765 |
lesssim: '\u2272',
|
|
|
33766 |
LessSlantEqual: '\u2A7D',
|
|
|
33767 |
LessTilde: '\u2272',
|
|
|
33768 |
lfisht: '\u297C',
|
|
|
33769 |
lfloor: '\u230A',
|
|
|
33770 |
Lfr: '\uD835\uDD0F',
|
|
|
33771 |
lfr: '\uD835\uDD29',
|
|
|
33772 |
lg: '\u2276',
|
|
|
33773 |
lgE: '\u2A91',
|
|
|
33774 |
lHar: '\u2962',
|
|
|
33775 |
lhard: '\u21BD',
|
|
|
33776 |
lharu: '\u21BC',
|
|
|
33777 |
lharul: '\u296A',
|
|
|
33778 |
lhblk: '\u2584',
|
|
|
33779 |
LJcy: '\u0409',
|
|
|
33780 |
ljcy: '\u0459',
|
|
|
33781 |
Ll: '\u22D8',
|
|
|
33782 |
ll: '\u226A',
|
|
|
33783 |
llarr: '\u21C7',
|
|
|
33784 |
llcorner: '\u231E',
|
|
|
33785 |
Lleftarrow: '\u21DA',
|
|
|
33786 |
llhard: '\u296B',
|
|
|
33787 |
lltri: '\u25FA',
|
|
|
33788 |
Lmidot: '\u013F',
|
|
|
33789 |
lmidot: '\u0140',
|
|
|
33790 |
lmoust: '\u23B0',
|
|
|
33791 |
lmoustache: '\u23B0',
|
|
|
33792 |
lnap: '\u2A89',
|
|
|
33793 |
lnapprox: '\u2A89',
|
|
|
33794 |
lnE: '\u2268',
|
|
|
33795 |
lne: '\u2A87',
|
|
|
33796 |
lneq: '\u2A87',
|
|
|
33797 |
lneqq: '\u2268',
|
|
|
33798 |
lnsim: '\u22E6',
|
|
|
33799 |
loang: '\u27EC',
|
|
|
33800 |
loarr: '\u21FD',
|
|
|
33801 |
lobrk: '\u27E6',
|
|
|
33802 |
LongLeftArrow: '\u27F5',
|
|
|
33803 |
Longleftarrow: '\u27F8',
|
|
|
33804 |
longleftarrow: '\u27F5',
|
|
|
33805 |
LongLeftRightArrow: '\u27F7',
|
|
|
33806 |
Longleftrightarrow: '\u27FA',
|
|
|
33807 |
longleftrightarrow: '\u27F7',
|
|
|
33808 |
longmapsto: '\u27FC',
|
|
|
33809 |
LongRightArrow: '\u27F6',
|
|
|
33810 |
Longrightarrow: '\u27F9',
|
|
|
33811 |
longrightarrow: '\u27F6',
|
|
|
33812 |
looparrowleft: '\u21AB',
|
|
|
33813 |
looparrowright: '\u21AC',
|
|
|
33814 |
lopar: '\u2985',
|
|
|
33815 |
Lopf: '\uD835\uDD43',
|
|
|
33816 |
lopf: '\uD835\uDD5D',
|
|
|
33817 |
loplus: '\u2A2D',
|
|
|
33818 |
lotimes: '\u2A34',
|
|
|
33819 |
lowast: '\u2217',
|
|
|
33820 |
lowbar: '\u005F',
|
|
|
33821 |
LowerLeftArrow: '\u2199',
|
|
|
33822 |
LowerRightArrow: '\u2198',
|
|
|
33823 |
loz: '\u25CA',
|
|
|
33824 |
lozenge: '\u25CA',
|
|
|
33825 |
lozf: '\u29EB',
|
|
|
33826 |
lpar: '\u0028',
|
|
|
33827 |
lparlt: '\u2993',
|
|
|
33828 |
lrarr: '\u21C6',
|
|
|
33829 |
lrcorner: '\u231F',
|
|
|
33830 |
lrhar: '\u21CB',
|
|
|
33831 |
lrhard: '\u296D',
|
|
|
33832 |
lrm: '\u200E',
|
|
|
33833 |
lrtri: '\u22BF',
|
|
|
33834 |
lsaquo: '\u2039',
|
|
|
33835 |
Lscr: '\u2112',
|
|
|
33836 |
lscr: '\uD835\uDCC1',
|
|
|
33837 |
Lsh: '\u21B0',
|
|
|
33838 |
lsh: '\u21B0',
|
|
|
33839 |
lsim: '\u2272',
|
|
|
33840 |
lsime: '\u2A8D',
|
|
|
33841 |
lsimg: '\u2A8F',
|
|
|
33842 |
lsqb: '\u005B',
|
|
|
33843 |
lsquo: '\u2018',
|
|
|
33844 |
lsquor: '\u201A',
|
|
|
33845 |
Lstrok: '\u0141',
|
|
|
33846 |
lstrok: '\u0142',
|
|
|
33847 |
Lt: '\u226A',
|
|
|
33848 |
LT: '\u003C',
|
|
|
33849 |
lt: '\u003C',
|
|
|
33850 |
ltcc: '\u2AA6',
|
|
|
33851 |
ltcir: '\u2A79',
|
|
|
33852 |
ltdot: '\u22D6',
|
|
|
33853 |
lthree: '\u22CB',
|
|
|
33854 |
ltimes: '\u22C9',
|
|
|
33855 |
ltlarr: '\u2976',
|
|
|
33856 |
ltquest: '\u2A7B',
|
|
|
33857 |
ltri: '\u25C3',
|
|
|
33858 |
ltrie: '\u22B4',
|
|
|
33859 |
ltrif: '\u25C2',
|
|
|
33860 |
ltrPar: '\u2996',
|
|
|
33861 |
lurdshar: '\u294A',
|
|
|
33862 |
luruhar: '\u2966',
|
|
|
33863 |
lvertneqq: '\u2268\uFE00',
|
|
|
33864 |
lvnE: '\u2268\uFE00',
|
|
|
33865 |
macr: '\u00AF',
|
|
|
33866 |
male: '\u2642',
|
|
|
33867 |
malt: '\u2720',
|
|
|
33868 |
maltese: '\u2720',
|
|
|
33869 |
Map: '\u2905',
|
|
|
33870 |
map: '\u21A6',
|
|
|
33871 |
mapsto: '\u21A6',
|
|
|
33872 |
mapstodown: '\u21A7',
|
|
|
33873 |
mapstoleft: '\u21A4',
|
|
|
33874 |
mapstoup: '\u21A5',
|
|
|
33875 |
marker: '\u25AE',
|
|
|
33876 |
mcomma: '\u2A29',
|
|
|
33877 |
Mcy: '\u041C',
|
|
|
33878 |
mcy: '\u043C',
|
|
|
33879 |
mdash: '\u2014',
|
|
|
33880 |
mDDot: '\u223A',
|
|
|
33881 |
measuredangle: '\u2221',
|
|
|
33882 |
MediumSpace: '\u205F',
|
|
|
33883 |
Mellintrf: '\u2133',
|
|
|
33884 |
Mfr: '\uD835\uDD10',
|
|
|
33885 |
mfr: '\uD835\uDD2A',
|
|
|
33886 |
mho: '\u2127',
|
|
|
33887 |
micro: '\u00B5',
|
|
|
33888 |
mid: '\u2223',
|
|
|
33889 |
midast: '\u002A',
|
|
|
33890 |
midcir: '\u2AF0',
|
|
|
33891 |
middot: '\u00B7',
|
|
|
33892 |
minus: '\u2212',
|
|
|
33893 |
minusb: '\u229F',
|
|
|
33894 |
minusd: '\u2238',
|
|
|
33895 |
minusdu: '\u2A2A',
|
|
|
33896 |
MinusPlus: '\u2213',
|
|
|
33897 |
mlcp: '\u2ADB',
|
|
|
33898 |
mldr: '\u2026',
|
|
|
33899 |
mnplus: '\u2213',
|
|
|
33900 |
models: '\u22A7',
|
|
|
33901 |
Mopf: '\uD835\uDD44',
|
|
|
33902 |
mopf: '\uD835\uDD5E',
|
|
|
33903 |
mp: '\u2213',
|
|
|
33904 |
Mscr: '\u2133',
|
|
|
33905 |
mscr: '\uD835\uDCC2',
|
|
|
33906 |
mstpos: '\u223E',
|
|
|
33907 |
Mu: '\u039C',
|
|
|
33908 |
mu: '\u03BC',
|
|
|
33909 |
multimap: '\u22B8',
|
|
|
33910 |
mumap: '\u22B8',
|
|
|
33911 |
nabla: '\u2207',
|
|
|
33912 |
Nacute: '\u0143',
|
|
|
33913 |
nacute: '\u0144',
|
|
|
33914 |
nang: '\u2220\u20D2',
|
|
|
33915 |
nap: '\u2249',
|
|
|
33916 |
napE: '\u2A70\u0338',
|
|
|
33917 |
napid: '\u224B\u0338',
|
|
|
33918 |
napos: '\u0149',
|
|
|
33919 |
napprox: '\u2249',
|
|
|
33920 |
natur: '\u266E',
|
|
|
33921 |
natural: '\u266E',
|
|
|
33922 |
naturals: '\u2115',
|
|
|
33923 |
nbsp: '\u00A0',
|
|
|
33924 |
nbump: '\u224E\u0338',
|
|
|
33925 |
nbumpe: '\u224F\u0338',
|
|
|
33926 |
ncap: '\u2A43',
|
|
|
33927 |
Ncaron: '\u0147',
|
|
|
33928 |
ncaron: '\u0148',
|
|
|
33929 |
Ncedil: '\u0145',
|
|
|
33930 |
ncedil: '\u0146',
|
|
|
33931 |
ncong: '\u2247',
|
|
|
33932 |
ncongdot: '\u2A6D\u0338',
|
|
|
33933 |
ncup: '\u2A42',
|
|
|
33934 |
Ncy: '\u041D',
|
|
|
33935 |
ncy: '\u043D',
|
|
|
33936 |
ndash: '\u2013',
|
|
|
33937 |
ne: '\u2260',
|
|
|
33938 |
nearhk: '\u2924',
|
|
|
33939 |
neArr: '\u21D7',
|
|
|
33940 |
nearr: '\u2197',
|
|
|
33941 |
nearrow: '\u2197',
|
|
|
33942 |
nedot: '\u2250\u0338',
|
|
|
33943 |
NegativeMediumSpace: '\u200B',
|
|
|
33944 |
NegativeThickSpace: '\u200B',
|
|
|
33945 |
NegativeThinSpace: '\u200B',
|
|
|
33946 |
NegativeVeryThinSpace: '\u200B',
|
|
|
33947 |
nequiv: '\u2262',
|
|
|
33948 |
nesear: '\u2928',
|
|
|
33949 |
nesim: '\u2242\u0338',
|
|
|
33950 |
NestedGreaterGreater: '\u226B',
|
|
|
33951 |
NestedLessLess: '\u226A',
|
|
|
33952 |
NewLine: '\u000A',
|
|
|
33953 |
nexist: '\u2204',
|
|
|
33954 |
nexists: '\u2204',
|
|
|
33955 |
Nfr: '\uD835\uDD11',
|
|
|
33956 |
nfr: '\uD835\uDD2B',
|
|
|
33957 |
ngE: '\u2267\u0338',
|
|
|
33958 |
nge: '\u2271',
|
|
|
33959 |
ngeq: '\u2271',
|
|
|
33960 |
ngeqq: '\u2267\u0338',
|
|
|
33961 |
ngeqslant: '\u2A7E\u0338',
|
|
|
33962 |
nges: '\u2A7E\u0338',
|
|
|
33963 |
nGg: '\u22D9\u0338',
|
|
|
33964 |
ngsim: '\u2275',
|
|
|
33965 |
nGt: '\u226B\u20D2',
|
|
|
33966 |
ngt: '\u226F',
|
|
|
33967 |
ngtr: '\u226F',
|
|
|
33968 |
nGtv: '\u226B\u0338',
|
|
|
33969 |
nhArr: '\u21CE',
|
|
|
33970 |
nharr: '\u21AE',
|
|
|
33971 |
nhpar: '\u2AF2',
|
|
|
33972 |
ni: '\u220B',
|
|
|
33973 |
nis: '\u22FC',
|
|
|
33974 |
nisd: '\u22FA',
|
|
|
33975 |
niv: '\u220B',
|
|
|
33976 |
NJcy: '\u040A',
|
|
|
33977 |
njcy: '\u045A',
|
|
|
33978 |
nlArr: '\u21CD',
|
|
|
33979 |
nlarr: '\u219A',
|
|
|
33980 |
nldr: '\u2025',
|
|
|
33981 |
nlE: '\u2266\u0338',
|
|
|
33982 |
nle: '\u2270',
|
|
|
33983 |
nLeftarrow: '\u21CD',
|
|
|
33984 |
nleftarrow: '\u219A',
|
|
|
33985 |
nLeftrightarrow: '\u21CE',
|
|
|
33986 |
nleftrightarrow: '\u21AE',
|
|
|
33987 |
nleq: '\u2270',
|
|
|
33988 |
nleqq: '\u2266\u0338',
|
|
|
33989 |
nleqslant: '\u2A7D\u0338',
|
|
|
33990 |
nles: '\u2A7D\u0338',
|
|
|
33991 |
nless: '\u226E',
|
|
|
33992 |
nLl: '\u22D8\u0338',
|
|
|
33993 |
nlsim: '\u2274',
|
|
|
33994 |
nLt: '\u226A\u20D2',
|
|
|
33995 |
nlt: '\u226E',
|
|
|
33996 |
nltri: '\u22EA',
|
|
|
33997 |
nltrie: '\u22EC',
|
|
|
33998 |
nLtv: '\u226A\u0338',
|
|
|
33999 |
nmid: '\u2224',
|
|
|
34000 |
NoBreak: '\u2060',
|
|
|
34001 |
NonBreakingSpace: '\u00A0',
|
|
|
34002 |
Nopf: '\u2115',
|
|
|
34003 |
nopf: '\uD835\uDD5F',
|
|
|
34004 |
Not: '\u2AEC',
|
|
|
34005 |
not: '\u00AC',
|
|
|
34006 |
NotCongruent: '\u2262',
|
|
|
34007 |
NotCupCap: '\u226D',
|
|
|
34008 |
NotDoubleVerticalBar: '\u2226',
|
|
|
34009 |
NotElement: '\u2209',
|
|
|
34010 |
NotEqual: '\u2260',
|
|
|
34011 |
NotEqualTilde: '\u2242\u0338',
|
|
|
34012 |
NotExists: '\u2204',
|
|
|
34013 |
NotGreater: '\u226F',
|
|
|
34014 |
NotGreaterEqual: '\u2271',
|
|
|
34015 |
NotGreaterFullEqual: '\u2267\u0338',
|
|
|
34016 |
NotGreaterGreater: '\u226B\u0338',
|
|
|
34017 |
NotGreaterLess: '\u2279',
|
|
|
34018 |
NotGreaterSlantEqual: '\u2A7E\u0338',
|
|
|
34019 |
NotGreaterTilde: '\u2275',
|
|
|
34020 |
NotHumpDownHump: '\u224E\u0338',
|
|
|
34021 |
NotHumpEqual: '\u224F\u0338',
|
|
|
34022 |
notin: '\u2209',
|
|
|
34023 |
notindot: '\u22F5\u0338',
|
|
|
34024 |
notinE: '\u22F9\u0338',
|
|
|
34025 |
notinva: '\u2209',
|
|
|
34026 |
notinvb: '\u22F7',
|
|
|
34027 |
notinvc: '\u22F6',
|
|
|
34028 |
NotLeftTriangle: '\u22EA',
|
|
|
34029 |
NotLeftTriangleBar: '\u29CF\u0338',
|
|
|
34030 |
NotLeftTriangleEqual: '\u22EC',
|
|
|
34031 |
NotLess: '\u226E',
|
|
|
34032 |
NotLessEqual: '\u2270',
|
|
|
34033 |
NotLessGreater: '\u2278',
|
|
|
34034 |
NotLessLess: '\u226A\u0338',
|
|
|
34035 |
NotLessSlantEqual: '\u2A7D\u0338',
|
|
|
34036 |
NotLessTilde: '\u2274',
|
|
|
34037 |
NotNestedGreaterGreater: '\u2AA2\u0338',
|
|
|
34038 |
NotNestedLessLess: '\u2AA1\u0338',
|
|
|
34039 |
notni: '\u220C',
|
|
|
34040 |
notniva: '\u220C',
|
|
|
34041 |
notnivb: '\u22FE',
|
|
|
34042 |
notnivc: '\u22FD',
|
|
|
34043 |
NotPrecedes: '\u2280',
|
|
|
34044 |
NotPrecedesEqual: '\u2AAF\u0338',
|
|
|
34045 |
NotPrecedesSlantEqual: '\u22E0',
|
|
|
34046 |
NotReverseElement: '\u220C',
|
|
|
34047 |
NotRightTriangle: '\u22EB',
|
|
|
34048 |
NotRightTriangleBar: '\u29D0\u0338',
|
|
|
34049 |
NotRightTriangleEqual: '\u22ED',
|
|
|
34050 |
NotSquareSubset: '\u228F\u0338',
|
|
|
34051 |
NotSquareSubsetEqual: '\u22E2',
|
|
|
34052 |
NotSquareSuperset: '\u2290\u0338',
|
|
|
34053 |
NotSquareSupersetEqual: '\u22E3',
|
|
|
34054 |
NotSubset: '\u2282\u20D2',
|
|
|
34055 |
NotSubsetEqual: '\u2288',
|
|
|
34056 |
NotSucceeds: '\u2281',
|
|
|
34057 |
NotSucceedsEqual: '\u2AB0\u0338',
|
|
|
34058 |
NotSucceedsSlantEqual: '\u22E1',
|
|
|
34059 |
NotSucceedsTilde: '\u227F\u0338',
|
|
|
34060 |
NotSuperset: '\u2283\u20D2',
|
|
|
34061 |
NotSupersetEqual: '\u2289',
|
|
|
34062 |
NotTilde: '\u2241',
|
|
|
34063 |
NotTildeEqual: '\u2244',
|
|
|
34064 |
NotTildeFullEqual: '\u2247',
|
|
|
34065 |
NotTildeTilde: '\u2249',
|
|
|
34066 |
NotVerticalBar: '\u2224',
|
|
|
34067 |
npar: '\u2226',
|
|
|
34068 |
nparallel: '\u2226',
|
|
|
34069 |
nparsl: '\u2AFD\u20E5',
|
|
|
34070 |
npart: '\u2202\u0338',
|
|
|
34071 |
npolint: '\u2A14',
|
|
|
34072 |
npr: '\u2280',
|
|
|
34073 |
nprcue: '\u22E0',
|
|
|
34074 |
npre: '\u2AAF\u0338',
|
|
|
34075 |
nprec: '\u2280',
|
|
|
34076 |
npreceq: '\u2AAF\u0338',
|
|
|
34077 |
nrArr: '\u21CF',
|
|
|
34078 |
nrarr: '\u219B',
|
|
|
34079 |
nrarrc: '\u2933\u0338',
|
|
|
34080 |
nrarrw: '\u219D\u0338',
|
|
|
34081 |
nRightarrow: '\u21CF',
|
|
|
34082 |
nrightarrow: '\u219B',
|
|
|
34083 |
nrtri: '\u22EB',
|
|
|
34084 |
nrtrie: '\u22ED',
|
|
|
34085 |
nsc: '\u2281',
|
|
|
34086 |
nsccue: '\u22E1',
|
|
|
34087 |
nsce: '\u2AB0\u0338',
|
|
|
34088 |
Nscr: '\uD835\uDCA9',
|
|
|
34089 |
nscr: '\uD835\uDCC3',
|
|
|
34090 |
nshortmid: '\u2224',
|
|
|
34091 |
nshortparallel: '\u2226',
|
|
|
34092 |
nsim: '\u2241',
|
|
|
34093 |
nsime: '\u2244',
|
|
|
34094 |
nsimeq: '\u2244',
|
|
|
34095 |
nsmid: '\u2224',
|
|
|
34096 |
nspar: '\u2226',
|
|
|
34097 |
nsqsube: '\u22E2',
|
|
|
34098 |
nsqsupe: '\u22E3',
|
|
|
34099 |
nsub: '\u2284',
|
|
|
34100 |
nsubE: '\u2AC5\u0338',
|
|
|
34101 |
nsube: '\u2288',
|
|
|
34102 |
nsubset: '\u2282\u20D2',
|
|
|
34103 |
nsubseteq: '\u2288',
|
|
|
34104 |
nsubseteqq: '\u2AC5\u0338',
|
|
|
34105 |
nsucc: '\u2281',
|
|
|
34106 |
nsucceq: '\u2AB0\u0338',
|
|
|
34107 |
nsup: '\u2285',
|
|
|
34108 |
nsupE: '\u2AC6\u0338',
|
|
|
34109 |
nsupe: '\u2289',
|
|
|
34110 |
nsupset: '\u2283\u20D2',
|
|
|
34111 |
nsupseteq: '\u2289',
|
|
|
34112 |
nsupseteqq: '\u2AC6\u0338',
|
|
|
34113 |
ntgl: '\u2279',
|
|
|
34114 |
Ntilde: '\u00D1',
|
|
|
34115 |
ntilde: '\u00F1',
|
|
|
34116 |
ntlg: '\u2278',
|
|
|
34117 |
ntriangleleft: '\u22EA',
|
|
|
34118 |
ntrianglelefteq: '\u22EC',
|
|
|
34119 |
ntriangleright: '\u22EB',
|
|
|
34120 |
ntrianglerighteq: '\u22ED',
|
|
|
34121 |
Nu: '\u039D',
|
|
|
34122 |
nu: '\u03BD',
|
|
|
34123 |
num: '\u0023',
|
|
|
34124 |
numero: '\u2116',
|
|
|
34125 |
numsp: '\u2007',
|
|
|
34126 |
nvap: '\u224D\u20D2',
|
|
|
34127 |
nVDash: '\u22AF',
|
|
|
34128 |
nVdash: '\u22AE',
|
|
|
34129 |
nvDash: '\u22AD',
|
|
|
34130 |
nvdash: '\u22AC',
|
|
|
34131 |
nvge: '\u2265\u20D2',
|
|
|
34132 |
nvgt: '\u003E\u20D2',
|
|
|
34133 |
nvHarr: '\u2904',
|
|
|
34134 |
nvinfin: '\u29DE',
|
|
|
34135 |
nvlArr: '\u2902',
|
|
|
34136 |
nvle: '\u2264\u20D2',
|
|
|
34137 |
nvlt: '\u003C\u20D2',
|
|
|
34138 |
nvltrie: '\u22B4\u20D2',
|
|
|
34139 |
nvrArr: '\u2903',
|
|
|
34140 |
nvrtrie: '\u22B5\u20D2',
|
|
|
34141 |
nvsim: '\u223C\u20D2',
|
|
|
34142 |
nwarhk: '\u2923',
|
|
|
34143 |
nwArr: '\u21D6',
|
|
|
34144 |
nwarr: '\u2196',
|
|
|
34145 |
nwarrow: '\u2196',
|
|
|
34146 |
nwnear: '\u2927',
|
|
|
34147 |
Oacute: '\u00D3',
|
|
|
34148 |
oacute: '\u00F3',
|
|
|
34149 |
oast: '\u229B',
|
|
|
34150 |
ocir: '\u229A',
|
|
|
34151 |
Ocirc: '\u00D4',
|
|
|
34152 |
ocirc: '\u00F4',
|
|
|
34153 |
Ocy: '\u041E',
|
|
|
34154 |
ocy: '\u043E',
|
|
|
34155 |
odash: '\u229D',
|
|
|
34156 |
Odblac: '\u0150',
|
|
|
34157 |
odblac: '\u0151',
|
|
|
34158 |
odiv: '\u2A38',
|
|
|
34159 |
odot: '\u2299',
|
|
|
34160 |
odsold: '\u29BC',
|
|
|
34161 |
OElig: '\u0152',
|
|
|
34162 |
oelig: '\u0153',
|
|
|
34163 |
ofcir: '\u29BF',
|
|
|
34164 |
Ofr: '\uD835\uDD12',
|
|
|
34165 |
ofr: '\uD835\uDD2C',
|
|
|
34166 |
ogon: '\u02DB',
|
|
|
34167 |
Ograve: '\u00D2',
|
|
|
34168 |
ograve: '\u00F2',
|
|
|
34169 |
ogt: '\u29C1',
|
|
|
34170 |
ohbar: '\u29B5',
|
|
|
34171 |
ohm: '\u03A9',
|
|
|
34172 |
oint: '\u222E',
|
|
|
34173 |
olarr: '\u21BA',
|
|
|
34174 |
olcir: '\u29BE',
|
|
|
34175 |
olcross: '\u29BB',
|
|
|
34176 |
oline: '\u203E',
|
|
|
34177 |
olt: '\u29C0',
|
|
|
34178 |
Omacr: '\u014C',
|
|
|
34179 |
omacr: '\u014D',
|
|
|
34180 |
Omega: '\u03A9',
|
|
|
34181 |
omega: '\u03C9',
|
|
|
34182 |
Omicron: '\u039F',
|
|
|
34183 |
omicron: '\u03BF',
|
|
|
34184 |
omid: '\u29B6',
|
|
|
34185 |
ominus: '\u2296',
|
|
|
34186 |
Oopf: '\uD835\uDD46',
|
|
|
34187 |
oopf: '\uD835\uDD60',
|
|
|
34188 |
opar: '\u29B7',
|
|
|
34189 |
OpenCurlyDoubleQuote: '\u201C',
|
|
|
34190 |
OpenCurlyQuote: '\u2018',
|
|
|
34191 |
operp: '\u29B9',
|
|
|
34192 |
oplus: '\u2295',
|
|
|
34193 |
Or: '\u2A54',
|
|
|
34194 |
or: '\u2228',
|
|
|
34195 |
orarr: '\u21BB',
|
|
|
34196 |
ord: '\u2A5D',
|
|
|
34197 |
order: '\u2134',
|
|
|
34198 |
orderof: '\u2134',
|
|
|
34199 |
ordf: '\u00AA',
|
|
|
34200 |
ordm: '\u00BA',
|
|
|
34201 |
origof: '\u22B6',
|
|
|
34202 |
oror: '\u2A56',
|
|
|
34203 |
orslope: '\u2A57',
|
|
|
34204 |
orv: '\u2A5B',
|
|
|
34205 |
oS: '\u24C8',
|
|
|
34206 |
Oscr: '\uD835\uDCAA',
|
|
|
34207 |
oscr: '\u2134',
|
|
|
34208 |
Oslash: '\u00D8',
|
|
|
34209 |
oslash: '\u00F8',
|
|
|
34210 |
osol: '\u2298',
|
|
|
34211 |
Otilde: '\u00D5',
|
|
|
34212 |
otilde: '\u00F5',
|
|
|
34213 |
Otimes: '\u2A37',
|
|
|
34214 |
otimes: '\u2297',
|
|
|
34215 |
otimesas: '\u2A36',
|
|
|
34216 |
Ouml: '\u00D6',
|
|
|
34217 |
ouml: '\u00F6',
|
|
|
34218 |
ovbar: '\u233D',
|
|
|
34219 |
OverBar: '\u203E',
|
|
|
34220 |
OverBrace: '\u23DE',
|
|
|
34221 |
OverBracket: '\u23B4',
|
|
|
34222 |
OverParenthesis: '\u23DC',
|
|
|
34223 |
par: '\u2225',
|
|
|
34224 |
para: '\u00B6',
|
|
|
34225 |
parallel: '\u2225',
|
|
|
34226 |
parsim: '\u2AF3',
|
|
|
34227 |
parsl: '\u2AFD',
|
|
|
34228 |
part: '\u2202',
|
|
|
34229 |
PartialD: '\u2202',
|
|
|
34230 |
Pcy: '\u041F',
|
|
|
34231 |
pcy: '\u043F',
|
|
|
34232 |
percnt: '\u0025',
|
|
|
34233 |
period: '\u002E',
|
|
|
34234 |
permil: '\u2030',
|
|
|
34235 |
perp: '\u22A5',
|
|
|
34236 |
pertenk: '\u2031',
|
|
|
34237 |
Pfr: '\uD835\uDD13',
|
|
|
34238 |
pfr: '\uD835\uDD2D',
|
|
|
34239 |
Phi: '\u03A6',
|
|
|
34240 |
phi: '\u03C6',
|
|
|
34241 |
phiv: '\u03D5',
|
|
|
34242 |
phmmat: '\u2133',
|
|
|
34243 |
phone: '\u260E',
|
|
|
34244 |
Pi: '\u03A0',
|
|
|
34245 |
pi: '\u03C0',
|
|
|
34246 |
pitchfork: '\u22D4',
|
|
|
34247 |
piv: '\u03D6',
|
|
|
34248 |
planck: '\u210F',
|
|
|
34249 |
planckh: '\u210E',
|
|
|
34250 |
plankv: '\u210F',
|
|
|
34251 |
plus: '\u002B',
|
|
|
34252 |
plusacir: '\u2A23',
|
|
|
34253 |
plusb: '\u229E',
|
|
|
34254 |
pluscir: '\u2A22',
|
|
|
34255 |
plusdo: '\u2214',
|
|
|
34256 |
plusdu: '\u2A25',
|
|
|
34257 |
pluse: '\u2A72',
|
|
|
34258 |
PlusMinus: '\u00B1',
|
|
|
34259 |
plusmn: '\u00B1',
|
|
|
34260 |
plussim: '\u2A26',
|
|
|
34261 |
plustwo: '\u2A27',
|
|
|
34262 |
pm: '\u00B1',
|
|
|
34263 |
Poincareplane: '\u210C',
|
|
|
34264 |
pointint: '\u2A15',
|
|
|
34265 |
Popf: '\u2119',
|
|
|
34266 |
popf: '\uD835\uDD61',
|
|
|
34267 |
pound: '\u00A3',
|
|
|
34268 |
Pr: '\u2ABB',
|
|
|
34269 |
pr: '\u227A',
|
|
|
34270 |
prap: '\u2AB7',
|
|
|
34271 |
prcue: '\u227C',
|
|
|
34272 |
prE: '\u2AB3',
|
|
|
34273 |
pre: '\u2AAF',
|
|
|
34274 |
prec: '\u227A',
|
|
|
34275 |
precapprox: '\u2AB7',
|
|
|
34276 |
preccurlyeq: '\u227C',
|
|
|
34277 |
Precedes: '\u227A',
|
|
|
34278 |
PrecedesEqual: '\u2AAF',
|
|
|
34279 |
PrecedesSlantEqual: '\u227C',
|
|
|
34280 |
PrecedesTilde: '\u227E',
|
|
|
34281 |
preceq: '\u2AAF',
|
|
|
34282 |
precnapprox: '\u2AB9',
|
|
|
34283 |
precneqq: '\u2AB5',
|
|
|
34284 |
precnsim: '\u22E8',
|
|
|
34285 |
precsim: '\u227E',
|
|
|
34286 |
Prime: '\u2033',
|
|
|
34287 |
prime: '\u2032',
|
|
|
34288 |
primes: '\u2119',
|
|
|
34289 |
prnap: '\u2AB9',
|
|
|
34290 |
prnE: '\u2AB5',
|
|
|
34291 |
prnsim: '\u22E8',
|
|
|
34292 |
prod: '\u220F',
|
|
|
34293 |
Product: '\u220F',
|
|
|
34294 |
profalar: '\u232E',
|
|
|
34295 |
profline: '\u2312',
|
|
|
34296 |
profsurf: '\u2313',
|
|
|
34297 |
prop: '\u221D',
|
|
|
34298 |
Proportion: '\u2237',
|
|
|
34299 |
Proportional: '\u221D',
|
|
|
34300 |
propto: '\u221D',
|
|
|
34301 |
prsim: '\u227E',
|
|
|
34302 |
prurel: '\u22B0',
|
|
|
34303 |
Pscr: '\uD835\uDCAB',
|
|
|
34304 |
pscr: '\uD835\uDCC5',
|
|
|
34305 |
Psi: '\u03A8',
|
|
|
34306 |
psi: '\u03C8',
|
|
|
34307 |
puncsp: '\u2008',
|
|
|
34308 |
Qfr: '\uD835\uDD14',
|
|
|
34309 |
qfr: '\uD835\uDD2E',
|
|
|
34310 |
qint: '\u2A0C',
|
|
|
34311 |
Qopf: '\u211A',
|
|
|
34312 |
qopf: '\uD835\uDD62',
|
|
|
34313 |
qprime: '\u2057',
|
|
|
34314 |
Qscr: '\uD835\uDCAC',
|
|
|
34315 |
qscr: '\uD835\uDCC6',
|
|
|
34316 |
quaternions: '\u210D',
|
|
|
34317 |
quatint: '\u2A16',
|
|
|
34318 |
quest: '\u003F',
|
|
|
34319 |
questeq: '\u225F',
|
|
|
34320 |
QUOT: '\u0022',
|
|
|
34321 |
quot: '\u0022',
|
|
|
34322 |
rAarr: '\u21DB',
|
|
|
34323 |
race: '\u223D\u0331',
|
|
|
34324 |
Racute: '\u0154',
|
|
|
34325 |
racute: '\u0155',
|
|
|
34326 |
radic: '\u221A',
|
|
|
34327 |
raemptyv: '\u29B3',
|
|
|
34328 |
Rang: '\u27EB',
|
|
|
34329 |
rang: '\u27E9',
|
|
|
34330 |
rangd: '\u2992',
|
|
|
34331 |
range: '\u29A5',
|
|
|
34332 |
rangle: '\u27E9',
|
|
|
34333 |
raquo: '\u00BB',
|
|
|
34334 |
Rarr: '\u21A0',
|
|
|
34335 |
rArr: '\u21D2',
|
|
|
34336 |
rarr: '\u2192',
|
|
|
34337 |
rarrap: '\u2975',
|
|
|
34338 |
rarrb: '\u21E5',
|
|
|
34339 |
rarrbfs: '\u2920',
|
|
|
34340 |
rarrc: '\u2933',
|
|
|
34341 |
rarrfs: '\u291E',
|
|
|
34342 |
rarrhk: '\u21AA',
|
|
|
34343 |
rarrlp: '\u21AC',
|
|
|
34344 |
rarrpl: '\u2945',
|
|
|
34345 |
rarrsim: '\u2974',
|
|
|
34346 |
Rarrtl: '\u2916',
|
|
|
34347 |
rarrtl: '\u21A3',
|
|
|
34348 |
rarrw: '\u219D',
|
|
|
34349 |
rAtail: '\u291C',
|
|
|
34350 |
ratail: '\u291A',
|
|
|
34351 |
ratio: '\u2236',
|
|
|
34352 |
rationals: '\u211A',
|
|
|
34353 |
RBarr: '\u2910',
|
|
|
34354 |
rBarr: '\u290F',
|
|
|
34355 |
rbarr: '\u290D',
|
|
|
34356 |
rbbrk: '\u2773',
|
|
|
34357 |
rbrace: '\u007D',
|
|
|
34358 |
rbrack: '\u005D',
|
|
|
34359 |
rbrke: '\u298C',
|
|
|
34360 |
rbrksld: '\u298E',
|
|
|
34361 |
rbrkslu: '\u2990',
|
|
|
34362 |
Rcaron: '\u0158',
|
|
|
34363 |
rcaron: '\u0159',
|
|
|
34364 |
Rcedil: '\u0156',
|
|
|
34365 |
rcedil: '\u0157',
|
|
|
34366 |
rceil: '\u2309',
|
|
|
34367 |
rcub: '\u007D',
|
|
|
34368 |
Rcy: '\u0420',
|
|
|
34369 |
rcy: '\u0440',
|
|
|
34370 |
rdca: '\u2937',
|
|
|
34371 |
rdldhar: '\u2969',
|
|
|
34372 |
rdquo: '\u201D',
|
|
|
34373 |
rdquor: '\u201D',
|
|
|
34374 |
rdsh: '\u21B3',
|
|
|
34375 |
Re: '\u211C',
|
|
|
34376 |
real: '\u211C',
|
|
|
34377 |
realine: '\u211B',
|
|
|
34378 |
realpart: '\u211C',
|
|
|
34379 |
reals: '\u211D',
|
|
|
34380 |
rect: '\u25AD',
|
|
|
34381 |
REG: '\u00AE',
|
|
|
34382 |
reg: '\u00AE',
|
|
|
34383 |
ReverseElement: '\u220B',
|
|
|
34384 |
ReverseEquilibrium: '\u21CB',
|
|
|
34385 |
ReverseUpEquilibrium: '\u296F',
|
|
|
34386 |
rfisht: '\u297D',
|
|
|
34387 |
rfloor: '\u230B',
|
|
|
34388 |
Rfr: '\u211C',
|
|
|
34389 |
rfr: '\uD835\uDD2F',
|
|
|
34390 |
rHar: '\u2964',
|
|
|
34391 |
rhard: '\u21C1',
|
|
|
34392 |
rharu: '\u21C0',
|
|
|
34393 |
rharul: '\u296C',
|
|
|
34394 |
Rho: '\u03A1',
|
|
|
34395 |
rho: '\u03C1',
|
|
|
34396 |
rhov: '\u03F1',
|
|
|
34397 |
RightAngleBracket: '\u27E9',
|
|
|
34398 |
RightArrow: '\u2192',
|
|
|
34399 |
Rightarrow: '\u21D2',
|
|
|
34400 |
rightarrow: '\u2192',
|
|
|
34401 |
RightArrowBar: '\u21E5',
|
|
|
34402 |
RightArrowLeftArrow: '\u21C4',
|
|
|
34403 |
rightarrowtail: '\u21A3',
|
|
|
34404 |
RightCeiling: '\u2309',
|
|
|
34405 |
RightDoubleBracket: '\u27E7',
|
|
|
34406 |
RightDownTeeVector: '\u295D',
|
|
|
34407 |
RightDownVector: '\u21C2',
|
|
|
34408 |
RightDownVectorBar: '\u2955',
|
|
|
34409 |
RightFloor: '\u230B',
|
|
|
34410 |
rightharpoondown: '\u21C1',
|
|
|
34411 |
rightharpoonup: '\u21C0',
|
|
|
34412 |
rightleftarrows: '\u21C4',
|
|
|
34413 |
rightleftharpoons: '\u21CC',
|
|
|
34414 |
rightrightarrows: '\u21C9',
|
|
|
34415 |
rightsquigarrow: '\u219D',
|
|
|
34416 |
RightTee: '\u22A2',
|
|
|
34417 |
RightTeeArrow: '\u21A6',
|
|
|
34418 |
RightTeeVector: '\u295B',
|
|
|
34419 |
rightthreetimes: '\u22CC',
|
|
|
34420 |
RightTriangle: '\u22B3',
|
|
|
34421 |
RightTriangleBar: '\u29D0',
|
|
|
34422 |
RightTriangleEqual: '\u22B5',
|
|
|
34423 |
RightUpDownVector: '\u294F',
|
|
|
34424 |
RightUpTeeVector: '\u295C',
|
|
|
34425 |
RightUpVector: '\u21BE',
|
|
|
34426 |
RightUpVectorBar: '\u2954',
|
|
|
34427 |
RightVector: '\u21C0',
|
|
|
34428 |
RightVectorBar: '\u2953',
|
|
|
34429 |
ring: '\u02DA',
|
|
|
34430 |
risingdotseq: '\u2253',
|
|
|
34431 |
rlarr: '\u21C4',
|
|
|
34432 |
rlhar: '\u21CC',
|
|
|
34433 |
rlm: '\u200F',
|
|
|
34434 |
rmoust: '\u23B1',
|
|
|
34435 |
rmoustache: '\u23B1',
|
|
|
34436 |
rnmid: '\u2AEE',
|
|
|
34437 |
roang: '\u27ED',
|
|
|
34438 |
roarr: '\u21FE',
|
|
|
34439 |
robrk: '\u27E7',
|
|
|
34440 |
ropar: '\u2986',
|
|
|
34441 |
Ropf: '\u211D',
|
|
|
34442 |
ropf: '\uD835\uDD63',
|
|
|
34443 |
roplus: '\u2A2E',
|
|
|
34444 |
rotimes: '\u2A35',
|
|
|
34445 |
RoundImplies: '\u2970',
|
|
|
34446 |
rpar: '\u0029',
|
|
|
34447 |
rpargt: '\u2994',
|
|
|
34448 |
rppolint: '\u2A12',
|
|
|
34449 |
rrarr: '\u21C9',
|
|
|
34450 |
Rrightarrow: '\u21DB',
|
|
|
34451 |
rsaquo: '\u203A',
|
|
|
34452 |
Rscr: '\u211B',
|
|
|
34453 |
rscr: '\uD835\uDCC7',
|
|
|
34454 |
Rsh: '\u21B1',
|
|
|
34455 |
rsh: '\u21B1',
|
|
|
34456 |
rsqb: '\u005D',
|
|
|
34457 |
rsquo: '\u2019',
|
|
|
34458 |
rsquor: '\u2019',
|
|
|
34459 |
rthree: '\u22CC',
|
|
|
34460 |
rtimes: '\u22CA',
|
|
|
34461 |
rtri: '\u25B9',
|
|
|
34462 |
rtrie: '\u22B5',
|
|
|
34463 |
rtrif: '\u25B8',
|
|
|
34464 |
rtriltri: '\u29CE',
|
|
|
34465 |
RuleDelayed: '\u29F4',
|
|
|
34466 |
ruluhar: '\u2968',
|
|
|
34467 |
rx: '\u211E',
|
|
|
34468 |
Sacute: '\u015A',
|
|
|
34469 |
sacute: '\u015B',
|
|
|
34470 |
sbquo: '\u201A',
|
|
|
34471 |
Sc: '\u2ABC',
|
|
|
34472 |
sc: '\u227B',
|
|
|
34473 |
scap: '\u2AB8',
|
|
|
34474 |
Scaron: '\u0160',
|
|
|
34475 |
scaron: '\u0161',
|
|
|
34476 |
sccue: '\u227D',
|
|
|
34477 |
scE: '\u2AB4',
|
|
|
34478 |
sce: '\u2AB0',
|
|
|
34479 |
Scedil: '\u015E',
|
|
|
34480 |
scedil: '\u015F',
|
|
|
34481 |
Scirc: '\u015C',
|
|
|
34482 |
scirc: '\u015D',
|
|
|
34483 |
scnap: '\u2ABA',
|
|
|
34484 |
scnE: '\u2AB6',
|
|
|
34485 |
scnsim: '\u22E9',
|
|
|
34486 |
scpolint: '\u2A13',
|
|
|
34487 |
scsim: '\u227F',
|
|
|
34488 |
Scy: '\u0421',
|
|
|
34489 |
scy: '\u0441',
|
|
|
34490 |
sdot: '\u22C5',
|
|
|
34491 |
sdotb: '\u22A1',
|
|
|
34492 |
sdote: '\u2A66',
|
|
|
34493 |
searhk: '\u2925',
|
|
|
34494 |
seArr: '\u21D8',
|
|
|
34495 |
searr: '\u2198',
|
|
|
34496 |
searrow: '\u2198',
|
|
|
34497 |
sect: '\u00A7',
|
|
|
34498 |
semi: '\u003B',
|
|
|
34499 |
seswar: '\u2929',
|
|
|
34500 |
setminus: '\u2216',
|
|
|
34501 |
setmn: '\u2216',
|
|
|
34502 |
sext: '\u2736',
|
|
|
34503 |
Sfr: '\uD835\uDD16',
|
|
|
34504 |
sfr: '\uD835\uDD30',
|
|
|
34505 |
sfrown: '\u2322',
|
|
|
34506 |
sharp: '\u266F',
|
|
|
34507 |
SHCHcy: '\u0429',
|
|
|
34508 |
shchcy: '\u0449',
|
|
|
34509 |
SHcy: '\u0428',
|
|
|
34510 |
shcy: '\u0448',
|
|
|
34511 |
ShortDownArrow: '\u2193',
|
|
|
34512 |
ShortLeftArrow: '\u2190',
|
|
|
34513 |
shortmid: '\u2223',
|
|
|
34514 |
shortparallel: '\u2225',
|
|
|
34515 |
ShortRightArrow: '\u2192',
|
|
|
34516 |
ShortUpArrow: '\u2191',
|
|
|
34517 |
shy: '\u00AD',
|
|
|
34518 |
Sigma: '\u03A3',
|
|
|
34519 |
sigma: '\u03C3',
|
|
|
34520 |
sigmaf: '\u03C2',
|
|
|
34521 |
sigmav: '\u03C2',
|
|
|
34522 |
sim: '\u223C',
|
|
|
34523 |
simdot: '\u2A6A',
|
|
|
34524 |
sime: '\u2243',
|
|
|
34525 |
simeq: '\u2243',
|
|
|
34526 |
simg: '\u2A9E',
|
|
|
34527 |
simgE: '\u2AA0',
|
|
|
34528 |
siml: '\u2A9D',
|
|
|
34529 |
simlE: '\u2A9F',
|
|
|
34530 |
simne: '\u2246',
|
|
|
34531 |
simplus: '\u2A24',
|
|
|
34532 |
simrarr: '\u2972',
|
|
|
34533 |
slarr: '\u2190',
|
|
|
34534 |
SmallCircle: '\u2218',
|
|
|
34535 |
smallsetminus: '\u2216',
|
|
|
34536 |
smashp: '\u2A33',
|
|
|
34537 |
smeparsl: '\u29E4',
|
|
|
34538 |
smid: '\u2223',
|
|
|
34539 |
smile: '\u2323',
|
|
|
34540 |
smt: '\u2AAA',
|
|
|
34541 |
smte: '\u2AAC',
|
|
|
34542 |
smtes: '\u2AAC\uFE00',
|
|
|
34543 |
SOFTcy: '\u042C',
|
|
|
34544 |
softcy: '\u044C',
|
|
|
34545 |
sol: '\u002F',
|
|
|
34546 |
solb: '\u29C4',
|
|
|
34547 |
solbar: '\u233F',
|
|
|
34548 |
Sopf: '\uD835\uDD4A',
|
|
|
34549 |
sopf: '\uD835\uDD64',
|
|
|
34550 |
spades: '\u2660',
|
|
|
34551 |
spadesuit: '\u2660',
|
|
|
34552 |
spar: '\u2225',
|
|
|
34553 |
sqcap: '\u2293',
|
|
|
34554 |
sqcaps: '\u2293\uFE00',
|
|
|
34555 |
sqcup: '\u2294',
|
|
|
34556 |
sqcups: '\u2294\uFE00',
|
|
|
34557 |
Sqrt: '\u221A',
|
|
|
34558 |
sqsub: '\u228F',
|
|
|
34559 |
sqsube: '\u2291',
|
|
|
34560 |
sqsubset: '\u228F',
|
|
|
34561 |
sqsubseteq: '\u2291',
|
|
|
34562 |
sqsup: '\u2290',
|
|
|
34563 |
sqsupe: '\u2292',
|
|
|
34564 |
sqsupset: '\u2290',
|
|
|
34565 |
sqsupseteq: '\u2292',
|
|
|
34566 |
squ: '\u25A1',
|
|
|
34567 |
Square: '\u25A1',
|
|
|
34568 |
square: '\u25A1',
|
|
|
34569 |
SquareIntersection: '\u2293',
|
|
|
34570 |
SquareSubset: '\u228F',
|
|
|
34571 |
SquareSubsetEqual: '\u2291',
|
|
|
34572 |
SquareSuperset: '\u2290',
|
|
|
34573 |
SquareSupersetEqual: '\u2292',
|
|
|
34574 |
SquareUnion: '\u2294',
|
|
|
34575 |
squarf: '\u25AA',
|
|
|
34576 |
squf: '\u25AA',
|
|
|
34577 |
srarr: '\u2192',
|
|
|
34578 |
Sscr: '\uD835\uDCAE',
|
|
|
34579 |
sscr: '\uD835\uDCC8',
|
|
|
34580 |
ssetmn: '\u2216',
|
|
|
34581 |
ssmile: '\u2323',
|
|
|
34582 |
sstarf: '\u22C6',
|
|
|
34583 |
Star: '\u22C6',
|
|
|
34584 |
star: '\u2606',
|
|
|
34585 |
starf: '\u2605',
|
|
|
34586 |
straightepsilon: '\u03F5',
|
|
|
34587 |
straightphi: '\u03D5',
|
|
|
34588 |
strns: '\u00AF',
|
|
|
34589 |
Sub: '\u22D0',
|
|
|
34590 |
sub: '\u2282',
|
|
|
34591 |
subdot: '\u2ABD',
|
|
|
34592 |
subE: '\u2AC5',
|
|
|
34593 |
sube: '\u2286',
|
|
|
34594 |
subedot: '\u2AC3',
|
|
|
34595 |
submult: '\u2AC1',
|
|
|
34596 |
subnE: '\u2ACB',
|
|
|
34597 |
subne: '\u228A',
|
|
|
34598 |
subplus: '\u2ABF',
|
|
|
34599 |
subrarr: '\u2979',
|
|
|
34600 |
Subset: '\u22D0',
|
|
|
34601 |
subset: '\u2282',
|
|
|
34602 |
subseteq: '\u2286',
|
|
|
34603 |
subseteqq: '\u2AC5',
|
|
|
34604 |
SubsetEqual: '\u2286',
|
|
|
34605 |
subsetneq: '\u228A',
|
|
|
34606 |
subsetneqq: '\u2ACB',
|
|
|
34607 |
subsim: '\u2AC7',
|
|
|
34608 |
subsub: '\u2AD5',
|
|
|
34609 |
subsup: '\u2AD3',
|
|
|
34610 |
succ: '\u227B',
|
|
|
34611 |
succapprox: '\u2AB8',
|
|
|
34612 |
succcurlyeq: '\u227D',
|
|
|
34613 |
Succeeds: '\u227B',
|
|
|
34614 |
SucceedsEqual: '\u2AB0',
|
|
|
34615 |
SucceedsSlantEqual: '\u227D',
|
|
|
34616 |
SucceedsTilde: '\u227F',
|
|
|
34617 |
succeq: '\u2AB0',
|
|
|
34618 |
succnapprox: '\u2ABA',
|
|
|
34619 |
succneqq: '\u2AB6',
|
|
|
34620 |
succnsim: '\u22E9',
|
|
|
34621 |
succsim: '\u227F',
|
|
|
34622 |
SuchThat: '\u220B',
|
|
|
34623 |
Sum: '\u2211',
|
|
|
34624 |
sum: '\u2211',
|
|
|
34625 |
sung: '\u266A',
|
|
|
34626 |
Sup: '\u22D1',
|
|
|
34627 |
sup: '\u2283',
|
|
|
34628 |
sup1: '\u00B9',
|
|
|
34629 |
sup2: '\u00B2',
|
|
|
34630 |
sup3: '\u00B3',
|
|
|
34631 |
supdot: '\u2ABE',
|
|
|
34632 |
supdsub: '\u2AD8',
|
|
|
34633 |
supE: '\u2AC6',
|
|
|
34634 |
supe: '\u2287',
|
|
|
34635 |
supedot: '\u2AC4',
|
|
|
34636 |
Superset: '\u2283',
|
|
|
34637 |
SupersetEqual: '\u2287',
|
|
|
34638 |
suphsol: '\u27C9',
|
|
|
34639 |
suphsub: '\u2AD7',
|
|
|
34640 |
suplarr: '\u297B',
|
|
|
34641 |
supmult: '\u2AC2',
|
|
|
34642 |
supnE: '\u2ACC',
|
|
|
34643 |
supne: '\u228B',
|
|
|
34644 |
supplus: '\u2AC0',
|
|
|
34645 |
Supset: '\u22D1',
|
|
|
34646 |
supset: '\u2283',
|
|
|
34647 |
supseteq: '\u2287',
|
|
|
34648 |
supseteqq: '\u2AC6',
|
|
|
34649 |
supsetneq: '\u228B',
|
|
|
34650 |
supsetneqq: '\u2ACC',
|
|
|
34651 |
supsim: '\u2AC8',
|
|
|
34652 |
supsub: '\u2AD4',
|
|
|
34653 |
supsup: '\u2AD6',
|
|
|
34654 |
swarhk: '\u2926',
|
|
|
34655 |
swArr: '\u21D9',
|
|
|
34656 |
swarr: '\u2199',
|
|
|
34657 |
swarrow: '\u2199',
|
|
|
34658 |
swnwar: '\u292A',
|
|
|
34659 |
szlig: '\u00DF',
|
|
|
34660 |
Tab: '\u0009',
|
|
|
34661 |
target: '\u2316',
|
|
|
34662 |
Tau: '\u03A4',
|
|
|
34663 |
tau: '\u03C4',
|
|
|
34664 |
tbrk: '\u23B4',
|
|
|
34665 |
Tcaron: '\u0164',
|
|
|
34666 |
tcaron: '\u0165',
|
|
|
34667 |
Tcedil: '\u0162',
|
|
|
34668 |
tcedil: '\u0163',
|
|
|
34669 |
Tcy: '\u0422',
|
|
|
34670 |
tcy: '\u0442',
|
|
|
34671 |
tdot: '\u20DB',
|
|
|
34672 |
telrec: '\u2315',
|
|
|
34673 |
Tfr: '\uD835\uDD17',
|
|
|
34674 |
tfr: '\uD835\uDD31',
|
|
|
34675 |
there4: '\u2234',
|
|
|
34676 |
Therefore: '\u2234',
|
|
|
34677 |
therefore: '\u2234',
|
|
|
34678 |
Theta: '\u0398',
|
|
|
34679 |
theta: '\u03B8',
|
|
|
34680 |
thetasym: '\u03D1',
|
|
|
34681 |
thetav: '\u03D1',
|
|
|
34682 |
thickapprox: '\u2248',
|
|
|
34683 |
thicksim: '\u223C',
|
|
|
34684 |
ThickSpace: '\u205F\u200A',
|
|
|
34685 |
thinsp: '\u2009',
|
|
|
34686 |
ThinSpace: '\u2009',
|
|
|
34687 |
thkap: '\u2248',
|
|
|
34688 |
thksim: '\u223C',
|
|
|
34689 |
THORN: '\u00DE',
|
|
|
34690 |
thorn: '\u00FE',
|
|
|
34691 |
Tilde: '\u223C',
|
|
|
34692 |
tilde: '\u02DC',
|
|
|
34693 |
TildeEqual: '\u2243',
|
|
|
34694 |
TildeFullEqual: '\u2245',
|
|
|
34695 |
TildeTilde: '\u2248',
|
|
|
34696 |
times: '\u00D7',
|
|
|
34697 |
timesb: '\u22A0',
|
|
|
34698 |
timesbar: '\u2A31',
|
|
|
34699 |
timesd: '\u2A30',
|
|
|
34700 |
tint: '\u222D',
|
|
|
34701 |
toea: '\u2928',
|
|
|
34702 |
top: '\u22A4',
|
|
|
34703 |
topbot: '\u2336',
|
|
|
34704 |
topcir: '\u2AF1',
|
|
|
34705 |
Topf: '\uD835\uDD4B',
|
|
|
34706 |
topf: '\uD835\uDD65',
|
|
|
34707 |
topfork: '\u2ADA',
|
|
|
34708 |
tosa: '\u2929',
|
|
|
34709 |
tprime: '\u2034',
|
|
|
34710 |
TRADE: '\u2122',
|
|
|
34711 |
trade: '\u2122',
|
|
|
34712 |
triangle: '\u25B5',
|
|
|
34713 |
triangledown: '\u25BF',
|
|
|
34714 |
triangleleft: '\u25C3',
|
|
|
34715 |
trianglelefteq: '\u22B4',
|
|
|
34716 |
triangleq: '\u225C',
|
|
|
34717 |
triangleright: '\u25B9',
|
|
|
34718 |
trianglerighteq: '\u22B5',
|
|
|
34719 |
tridot: '\u25EC',
|
|
|
34720 |
trie: '\u225C',
|
|
|
34721 |
triminus: '\u2A3A',
|
|
|
34722 |
TripleDot: '\u20DB',
|
|
|
34723 |
triplus: '\u2A39',
|
|
|
34724 |
trisb: '\u29CD',
|
|
|
34725 |
tritime: '\u2A3B',
|
|
|
34726 |
trpezium: '\u23E2',
|
|
|
34727 |
Tscr: '\uD835\uDCAF',
|
|
|
34728 |
tscr: '\uD835\uDCC9',
|
|
|
34729 |
TScy: '\u0426',
|
|
|
34730 |
tscy: '\u0446',
|
|
|
34731 |
TSHcy: '\u040B',
|
|
|
34732 |
tshcy: '\u045B',
|
|
|
34733 |
Tstrok: '\u0166',
|
|
|
34734 |
tstrok: '\u0167',
|
|
|
34735 |
twixt: '\u226C',
|
|
|
34736 |
twoheadleftarrow: '\u219E',
|
|
|
34737 |
twoheadrightarrow: '\u21A0',
|
|
|
34738 |
Uacute: '\u00DA',
|
|
|
34739 |
uacute: '\u00FA',
|
|
|
34740 |
Uarr: '\u219F',
|
|
|
34741 |
uArr: '\u21D1',
|
|
|
34742 |
uarr: '\u2191',
|
|
|
34743 |
Uarrocir: '\u2949',
|
|
|
34744 |
Ubrcy: '\u040E',
|
|
|
34745 |
ubrcy: '\u045E',
|
|
|
34746 |
Ubreve: '\u016C',
|
|
|
34747 |
ubreve: '\u016D',
|
|
|
34748 |
Ucirc: '\u00DB',
|
|
|
34749 |
ucirc: '\u00FB',
|
|
|
34750 |
Ucy: '\u0423',
|
|
|
34751 |
ucy: '\u0443',
|
|
|
34752 |
udarr: '\u21C5',
|
|
|
34753 |
Udblac: '\u0170',
|
|
|
34754 |
udblac: '\u0171',
|
|
|
34755 |
udhar: '\u296E',
|
|
|
34756 |
ufisht: '\u297E',
|
|
|
34757 |
Ufr: '\uD835\uDD18',
|
|
|
34758 |
ufr: '\uD835\uDD32',
|
|
|
34759 |
Ugrave: '\u00D9',
|
|
|
34760 |
ugrave: '\u00F9',
|
|
|
34761 |
uHar: '\u2963',
|
|
|
34762 |
uharl: '\u21BF',
|
|
|
34763 |
uharr: '\u21BE',
|
|
|
34764 |
uhblk: '\u2580',
|
|
|
34765 |
ulcorn: '\u231C',
|
|
|
34766 |
ulcorner: '\u231C',
|
|
|
34767 |
ulcrop: '\u230F',
|
|
|
34768 |
ultri: '\u25F8',
|
|
|
34769 |
Umacr: '\u016A',
|
|
|
34770 |
umacr: '\u016B',
|
|
|
34771 |
uml: '\u00A8',
|
|
|
34772 |
UnderBar: '\u005F',
|
|
|
34773 |
UnderBrace: '\u23DF',
|
|
|
34774 |
UnderBracket: '\u23B5',
|
|
|
34775 |
UnderParenthesis: '\u23DD',
|
|
|
34776 |
Union: '\u22C3',
|
|
|
34777 |
UnionPlus: '\u228E',
|
|
|
34778 |
Uogon: '\u0172',
|
|
|
34779 |
uogon: '\u0173',
|
|
|
34780 |
Uopf: '\uD835\uDD4C',
|
|
|
34781 |
uopf: '\uD835\uDD66',
|
|
|
34782 |
UpArrow: '\u2191',
|
|
|
34783 |
Uparrow: '\u21D1',
|
|
|
34784 |
uparrow: '\u2191',
|
|
|
34785 |
UpArrowBar: '\u2912',
|
|
|
34786 |
UpArrowDownArrow: '\u21C5',
|
|
|
34787 |
UpDownArrow: '\u2195',
|
|
|
34788 |
Updownarrow: '\u21D5',
|
|
|
34789 |
updownarrow: '\u2195',
|
|
|
34790 |
UpEquilibrium: '\u296E',
|
|
|
34791 |
upharpoonleft: '\u21BF',
|
|
|
34792 |
upharpoonright: '\u21BE',
|
|
|
34793 |
uplus: '\u228E',
|
|
|
34794 |
UpperLeftArrow: '\u2196',
|
|
|
34795 |
UpperRightArrow: '\u2197',
|
|
|
34796 |
Upsi: '\u03D2',
|
|
|
34797 |
upsi: '\u03C5',
|
|
|
34798 |
upsih: '\u03D2',
|
|
|
34799 |
Upsilon: '\u03A5',
|
|
|
34800 |
upsilon: '\u03C5',
|
|
|
34801 |
UpTee: '\u22A5',
|
|
|
34802 |
UpTeeArrow: '\u21A5',
|
|
|
34803 |
upuparrows: '\u21C8',
|
|
|
34804 |
urcorn: '\u231D',
|
|
|
34805 |
urcorner: '\u231D',
|
|
|
34806 |
urcrop: '\u230E',
|
|
|
34807 |
Uring: '\u016E',
|
|
|
34808 |
uring: '\u016F',
|
|
|
34809 |
urtri: '\u25F9',
|
|
|
34810 |
Uscr: '\uD835\uDCB0',
|
|
|
34811 |
uscr: '\uD835\uDCCA',
|
|
|
34812 |
utdot: '\u22F0',
|
|
|
34813 |
Utilde: '\u0168',
|
|
|
34814 |
utilde: '\u0169',
|
|
|
34815 |
utri: '\u25B5',
|
|
|
34816 |
utrif: '\u25B4',
|
|
|
34817 |
uuarr: '\u21C8',
|
|
|
34818 |
Uuml: '\u00DC',
|
|
|
34819 |
uuml: '\u00FC',
|
|
|
34820 |
uwangle: '\u29A7',
|
|
|
34821 |
vangrt: '\u299C',
|
|
|
34822 |
varepsilon: '\u03F5',
|
|
|
34823 |
varkappa: '\u03F0',
|
|
|
34824 |
varnothing: '\u2205',
|
|
|
34825 |
varphi: '\u03D5',
|
|
|
34826 |
varpi: '\u03D6',
|
|
|
34827 |
varpropto: '\u221D',
|
|
|
34828 |
vArr: '\u21D5',
|
|
|
34829 |
varr: '\u2195',
|
|
|
34830 |
varrho: '\u03F1',
|
|
|
34831 |
varsigma: '\u03C2',
|
|
|
34832 |
varsubsetneq: '\u228A\uFE00',
|
|
|
34833 |
varsubsetneqq: '\u2ACB\uFE00',
|
|
|
34834 |
varsupsetneq: '\u228B\uFE00',
|
|
|
34835 |
varsupsetneqq: '\u2ACC\uFE00',
|
|
|
34836 |
vartheta: '\u03D1',
|
|
|
34837 |
vartriangleleft: '\u22B2',
|
|
|
34838 |
vartriangleright: '\u22B3',
|
|
|
34839 |
Vbar: '\u2AEB',
|
|
|
34840 |
vBar: '\u2AE8',
|
|
|
34841 |
vBarv: '\u2AE9',
|
|
|
34842 |
Vcy: '\u0412',
|
|
|
34843 |
vcy: '\u0432',
|
|
|
34844 |
VDash: '\u22AB',
|
|
|
34845 |
Vdash: '\u22A9',
|
|
|
34846 |
vDash: '\u22A8',
|
|
|
34847 |
vdash: '\u22A2',
|
|
|
34848 |
Vdashl: '\u2AE6',
|
|
|
34849 |
Vee: '\u22C1',
|
|
|
34850 |
vee: '\u2228',
|
|
|
34851 |
veebar: '\u22BB',
|
|
|
34852 |
veeeq: '\u225A',
|
|
|
34853 |
vellip: '\u22EE',
|
|
|
34854 |
Verbar: '\u2016',
|
|
|
34855 |
verbar: '\u007C',
|
|
|
34856 |
Vert: '\u2016',
|
|
|
34857 |
vert: '\u007C',
|
|
|
34858 |
VerticalBar: '\u2223',
|
|
|
34859 |
VerticalLine: '\u007C',
|
|
|
34860 |
VerticalSeparator: '\u2758',
|
|
|
34861 |
VerticalTilde: '\u2240',
|
|
|
34862 |
VeryThinSpace: '\u200A',
|
|
|
34863 |
Vfr: '\uD835\uDD19',
|
|
|
34864 |
vfr: '\uD835\uDD33',
|
|
|
34865 |
vltri: '\u22B2',
|
|
|
34866 |
vnsub: '\u2282\u20D2',
|
|
|
34867 |
vnsup: '\u2283\u20D2',
|
|
|
34868 |
Vopf: '\uD835\uDD4D',
|
|
|
34869 |
vopf: '\uD835\uDD67',
|
|
|
34870 |
vprop: '\u221D',
|
|
|
34871 |
vrtri: '\u22B3',
|
|
|
34872 |
Vscr: '\uD835\uDCB1',
|
|
|
34873 |
vscr: '\uD835\uDCCB',
|
|
|
34874 |
vsubnE: '\u2ACB\uFE00',
|
|
|
34875 |
vsubne: '\u228A\uFE00',
|
|
|
34876 |
vsupnE: '\u2ACC\uFE00',
|
|
|
34877 |
vsupne: '\u228B\uFE00',
|
|
|
34878 |
Vvdash: '\u22AA',
|
|
|
34879 |
vzigzag: '\u299A',
|
|
|
34880 |
Wcirc: '\u0174',
|
|
|
34881 |
wcirc: '\u0175',
|
|
|
34882 |
wedbar: '\u2A5F',
|
|
|
34883 |
Wedge: '\u22C0',
|
|
|
34884 |
wedge: '\u2227',
|
|
|
34885 |
wedgeq: '\u2259',
|
|
|
34886 |
weierp: '\u2118',
|
|
|
34887 |
Wfr: '\uD835\uDD1A',
|
|
|
34888 |
wfr: '\uD835\uDD34',
|
|
|
34889 |
Wopf: '\uD835\uDD4E',
|
|
|
34890 |
wopf: '\uD835\uDD68',
|
|
|
34891 |
wp: '\u2118',
|
|
|
34892 |
wr: '\u2240',
|
|
|
34893 |
wreath: '\u2240',
|
|
|
34894 |
Wscr: '\uD835\uDCB2',
|
|
|
34895 |
wscr: '\uD835\uDCCC',
|
|
|
34896 |
xcap: '\u22C2',
|
|
|
34897 |
xcirc: '\u25EF',
|
|
|
34898 |
xcup: '\u22C3',
|
|
|
34899 |
xdtri: '\u25BD',
|
|
|
34900 |
Xfr: '\uD835\uDD1B',
|
|
|
34901 |
xfr: '\uD835\uDD35',
|
|
|
34902 |
xhArr: '\u27FA',
|
|
|
34903 |
xharr: '\u27F7',
|
|
|
34904 |
Xi: '\u039E',
|
|
|
34905 |
xi: '\u03BE',
|
|
|
34906 |
xlArr: '\u27F8',
|
|
|
34907 |
xlarr: '\u27F5',
|
|
|
34908 |
xmap: '\u27FC',
|
|
|
34909 |
xnis: '\u22FB',
|
|
|
34910 |
xodot: '\u2A00',
|
|
|
34911 |
Xopf: '\uD835\uDD4F',
|
|
|
34912 |
xopf: '\uD835\uDD69',
|
|
|
34913 |
xoplus: '\u2A01',
|
|
|
34914 |
xotime: '\u2A02',
|
|
|
34915 |
xrArr: '\u27F9',
|
|
|
34916 |
xrarr: '\u27F6',
|
|
|
34917 |
Xscr: '\uD835\uDCB3',
|
|
|
34918 |
xscr: '\uD835\uDCCD',
|
|
|
34919 |
xsqcup: '\u2A06',
|
|
|
34920 |
xuplus: '\u2A04',
|
|
|
34921 |
xutri: '\u25B3',
|
|
|
34922 |
xvee: '\u22C1',
|
|
|
34923 |
xwedge: '\u22C0',
|
|
|
34924 |
Yacute: '\u00DD',
|
|
|
34925 |
yacute: '\u00FD',
|
|
|
34926 |
YAcy: '\u042F',
|
|
|
34927 |
yacy: '\u044F',
|
|
|
34928 |
Ycirc: '\u0176',
|
|
|
34929 |
ycirc: '\u0177',
|
|
|
34930 |
Ycy: '\u042B',
|
|
|
34931 |
ycy: '\u044B',
|
|
|
34932 |
yen: '\u00A5',
|
|
|
34933 |
Yfr: '\uD835\uDD1C',
|
|
|
34934 |
yfr: '\uD835\uDD36',
|
|
|
34935 |
YIcy: '\u0407',
|
|
|
34936 |
yicy: '\u0457',
|
|
|
34937 |
Yopf: '\uD835\uDD50',
|
|
|
34938 |
yopf: '\uD835\uDD6A',
|
|
|
34939 |
Yscr: '\uD835\uDCB4',
|
|
|
34940 |
yscr: '\uD835\uDCCE',
|
|
|
34941 |
YUcy: '\u042E',
|
|
|
34942 |
yucy: '\u044E',
|
|
|
34943 |
Yuml: '\u0178',
|
|
|
34944 |
yuml: '\u00FF',
|
|
|
34945 |
Zacute: '\u0179',
|
|
|
34946 |
zacute: '\u017A',
|
|
|
34947 |
Zcaron: '\u017D',
|
|
|
34948 |
zcaron: '\u017E',
|
|
|
34949 |
Zcy: '\u0417',
|
|
|
34950 |
zcy: '\u0437',
|
|
|
34951 |
Zdot: '\u017B',
|
|
|
34952 |
zdot: '\u017C',
|
|
|
34953 |
zeetrf: '\u2128',
|
|
|
34954 |
ZeroWidthSpace: '\u200B',
|
|
|
34955 |
Zeta: '\u0396',
|
|
|
34956 |
zeta: '\u03B6',
|
|
|
34957 |
Zfr: '\u2128',
|
|
|
34958 |
zfr: '\uD835\uDD37',
|
|
|
34959 |
ZHcy: '\u0416',
|
|
|
34960 |
zhcy: '\u0436',
|
|
|
34961 |
zigrarr: '\u21DD',
|
|
|
34962 |
Zopf: '\u2124',
|
|
|
34963 |
zopf: '\uD835\uDD6B',
|
|
|
34964 |
Zscr: '\uD835\uDCB5',
|
|
|
34965 |
zscr: '\uD835\uDCCF',
|
|
|
34966 |
zwj: '\u200D',
|
|
|
34967 |
zwnj: '\u200C'
|
|
|
34968 |
});
|
|
|
34969 |
|
|
|
34970 |
/**
|
|
|
34971 |
* @deprecated use `HTML_ENTITIES` instead
|
|
|
34972 |
* @see HTML_ENTITIES
|
|
|
34973 |
*/
|
|
|
34974 |
exports.entityMap = exports.HTML_ENTITIES;
|
|
|
34975 |
});
|
|
|
34976 |
entities.XML_ENTITIES;
|
|
|
34977 |
entities.HTML_ENTITIES;
|
|
|
34978 |
entities.entityMap;
|
|
|
34979 |
|
|
|
34980 |
var NAMESPACE$1 = conventions.NAMESPACE;
|
|
|
34981 |
|
|
|
34982 |
//[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
|
|
|
34983 |
//[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
|
|
|
34984 |
//[5] Name ::= NameStartChar (NameChar)*
|
|
|
34985 |
var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/; //\u10000-\uEFFFF
|
|
|
34986 |
var nameChar = new RegExp("[\\-\\.0-9" + nameStartChar.source.slice(1, -1) + "\\u00B7\\u0300-\\u036F\\u203F-\\u2040]");
|
|
|
34987 |
var tagNamePattern = new RegExp('^' + nameStartChar.source + nameChar.source + '*(?:\:' + nameStartChar.source + nameChar.source + '*)?$');
|
|
|
34988 |
//var tagNamePattern = /^[a-zA-Z_][\w\-\.]*(?:\:[a-zA-Z_][\w\-\.]*)?$/
|
|
|
34989 |
//var handlers = 'resolveEntity,getExternalSubset,characters,endDocument,endElement,endPrefixMapping,ignorableWhitespace,processingInstruction,setDocumentLocator,skippedEntity,startDocument,startElement,startPrefixMapping,notationDecl,unparsedEntityDecl,error,fatalError,warning,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,comment,endCDATA,endDTD,endEntity,startCDATA,startDTD,startEntity'.split(',')
|
|
|
34990 |
|
|
|
34991 |
//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE
|
|
|
34992 |
//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE
|
|
|
34993 |
var S_TAG = 0; //tag name offerring
|
|
|
34994 |
var S_ATTR = 1; //attr name offerring
|
|
|
34995 |
var S_ATTR_SPACE = 2; //attr name end and space offer
|
|
|
34996 |
var S_EQ = 3; //=space?
|
|
|
34997 |
var S_ATTR_NOQUOT_VALUE = 4; //attr value(no quot value only)
|
|
|
34998 |
var S_ATTR_END = 5; //attr value end and no space(quot end)
|
|
|
34999 |
var S_TAG_SPACE = 6; //(attr value end || tag end ) && (space offer)
|
|
|
35000 |
var S_TAG_CLOSE = 7; //closed el<el />
|
|
|
35001 |
|
|
|
35002 |
/**
|
|
|
35003 |
* Creates an error that will not be caught by XMLReader aka the SAX parser.
|
|
|
35004 |
*
|
|
|
35005 |
* @param {string} message
|
|
|
35006 |
* @param {any?} locator Optional, can provide details about the location in the source
|
|
|
35007 |
* @constructor
|
|
|
35008 |
*/
|
|
|
35009 |
function ParseError$1(message, locator) {
|
|
|
35010 |
this.message = message;
|
|
|
35011 |
this.locator = locator;
|
|
|
35012 |
if (Error.captureStackTrace) Error.captureStackTrace(this, ParseError$1);
|
|
|
35013 |
}
|
|
|
35014 |
ParseError$1.prototype = new Error();
|
|
|
35015 |
ParseError$1.prototype.name = ParseError$1.name;
|
|
|
35016 |
function XMLReader$1() {}
|
|
|
35017 |
XMLReader$1.prototype = {
|
|
|
35018 |
parse: function (source, defaultNSMap, entityMap) {
|
|
|
35019 |
var domBuilder = this.domBuilder;
|
|
|
35020 |
domBuilder.startDocument();
|
|
|
35021 |
_copy(defaultNSMap, defaultNSMap = {});
|
|
|
35022 |
parse$1(source, defaultNSMap, entityMap, domBuilder, this.errorHandler);
|
|
|
35023 |
domBuilder.endDocument();
|
|
|
35024 |
}
|
|
|
35025 |
};
|
|
|
35026 |
function parse$1(source, defaultNSMapCopy, entityMap, domBuilder, errorHandler) {
|
|
|
35027 |
function fixedFromCharCode(code) {
|
|
|
35028 |
// String.prototype.fromCharCode does not supports
|
|
|
35029 |
// > 2 bytes unicode chars directly
|
|
|
35030 |
if (code > 0xffff) {
|
|
|
35031 |
code -= 0x10000;
|
|
|
35032 |
var surrogate1 = 0xd800 + (code >> 10),
|
|
|
35033 |
surrogate2 = 0xdc00 + (code & 0x3ff);
|
|
|
35034 |
return String.fromCharCode(surrogate1, surrogate2);
|
|
|
35035 |
} else {
|
|
|
35036 |
return String.fromCharCode(code);
|
|
|
35037 |
}
|
|
|
35038 |
}
|
|
|
35039 |
function entityReplacer(a) {
|
|
|
35040 |
var k = a.slice(1, -1);
|
|
|
35041 |
if (Object.hasOwnProperty.call(entityMap, k)) {
|
|
|
35042 |
return entityMap[k];
|
|
|
35043 |
} else if (k.charAt(0) === '#') {
|
|
|
35044 |
return fixedFromCharCode(parseInt(k.substr(1).replace('x', '0x')));
|
|
|
35045 |
} else {
|
|
|
35046 |
errorHandler.error('entity not found:' + a);
|
|
|
35047 |
return a;
|
|
|
35048 |
}
|
|
|
35049 |
}
|
|
|
35050 |
function appendText(end) {
|
|
|
35051 |
//has some bugs
|
|
|
35052 |
if (end > start) {
|
|
|
35053 |
var xt = source.substring(start, end).replace(/&#?\w+;/g, entityReplacer);
|
|
|
35054 |
locator && position(start);
|
|
|
35055 |
domBuilder.characters(xt, 0, end - start);
|
|
|
35056 |
start = end;
|
|
|
35057 |
}
|
|
|
35058 |
}
|
|
|
35059 |
function position(p, m) {
|
|
|
35060 |
while (p >= lineEnd && (m = linePattern.exec(source))) {
|
|
|
35061 |
lineStart = m.index;
|
|
|
35062 |
lineEnd = lineStart + m[0].length;
|
|
|
35063 |
locator.lineNumber++;
|
|
|
35064 |
//console.log('line++:',locator,startPos,endPos)
|
|
|
35065 |
}
|
|
|
35066 |
|
|
|
35067 |
locator.columnNumber = p - lineStart + 1;
|
|
|
35068 |
}
|
|
|
35069 |
var lineStart = 0;
|
|
|
35070 |
var lineEnd = 0;
|
|
|
35071 |
var linePattern = /.*(?:\r\n?|\n)|.*$/g;
|
|
|
35072 |
var locator = domBuilder.locator;
|
|
|
35073 |
var parseStack = [{
|
|
|
35074 |
currentNSMap: defaultNSMapCopy
|
|
|
35075 |
}];
|
|
|
35076 |
var closeMap = {};
|
|
|
35077 |
var start = 0;
|
|
|
35078 |
while (true) {
|
|
|
35079 |
try {
|
|
|
35080 |
var tagStart = source.indexOf('<', start);
|
|
|
35081 |
if (tagStart < 0) {
|
|
|
35082 |
if (!source.substr(start).match(/^\s*$/)) {
|
|
|
35083 |
var doc = domBuilder.doc;
|
|
|
35084 |
var text = doc.createTextNode(source.substr(start));
|
|
|
35085 |
doc.appendChild(text);
|
|
|
35086 |
domBuilder.currentElement = text;
|
|
|
35087 |
}
|
|
|
35088 |
return;
|
|
|
35089 |
}
|
|
|
35090 |
if (tagStart > start) {
|
|
|
35091 |
appendText(tagStart);
|
|
|
35092 |
}
|
|
|
35093 |
switch (source.charAt(tagStart + 1)) {
|
|
|
35094 |
case '/':
|
|
|
35095 |
var end = source.indexOf('>', tagStart + 3);
|
|
|
35096 |
var tagName = source.substring(tagStart + 2, end).replace(/[ \t\n\r]+$/g, '');
|
|
|
35097 |
var config = parseStack.pop();
|
|
|
35098 |
if (end < 0) {
|
|
|
35099 |
tagName = source.substring(tagStart + 2).replace(/[\s<].*/, '');
|
|
|
35100 |
errorHandler.error("end tag name: " + tagName + ' is not complete:' + config.tagName);
|
|
|
35101 |
end = tagStart + 1 + tagName.length;
|
|
|
35102 |
} else if (tagName.match(/\s</)) {
|
|
|
35103 |
tagName = tagName.replace(/[\s<].*/, '');
|
|
|
35104 |
errorHandler.error("end tag name: " + tagName + ' maybe not complete');
|
|
|
35105 |
end = tagStart + 1 + tagName.length;
|
|
|
35106 |
}
|
|
|
35107 |
var localNSMap = config.localNSMap;
|
|
|
35108 |
var endMatch = config.tagName == tagName;
|
|
|
35109 |
var endIgnoreCaseMach = endMatch || config.tagName && config.tagName.toLowerCase() == tagName.toLowerCase();
|
|
|
35110 |
if (endIgnoreCaseMach) {
|
|
|
35111 |
domBuilder.endElement(config.uri, config.localName, tagName);
|
|
|
35112 |
if (localNSMap) {
|
|
|
35113 |
for (var prefix in localNSMap) {
|
|
|
35114 |
if (Object.prototype.hasOwnProperty.call(localNSMap, prefix)) {
|
|
|
35115 |
domBuilder.endPrefixMapping(prefix);
|
|
|
35116 |
}
|
|
|
35117 |
}
|
|
|
35118 |
}
|
|
|
35119 |
if (!endMatch) {
|
|
|
35120 |
errorHandler.fatalError("end tag name: " + tagName + ' is not match the current start tagName:' + config.tagName); // No known test case
|
|
|
35121 |
}
|
|
|
35122 |
} else {
|
|
|
35123 |
parseStack.push(config);
|
|
|
35124 |
}
|
|
|
35125 |
end++;
|
|
|
35126 |
break;
|
|
|
35127 |
// end elment
|
|
|
35128 |
case '?':
|
|
|
35129 |
// <?...?>
|
|
|
35130 |
locator && position(tagStart);
|
|
|
35131 |
end = parseInstruction(source, tagStart, domBuilder);
|
|
|
35132 |
break;
|
|
|
35133 |
case '!':
|
|
|
35134 |
// <!doctype,<![CDATA,<!--
|
|
|
35135 |
locator && position(tagStart);
|
|
|
35136 |
end = parseDCC(source, tagStart, domBuilder, errorHandler);
|
|
|
35137 |
break;
|
|
|
35138 |
default:
|
|
|
35139 |
locator && position(tagStart);
|
|
|
35140 |
var el = new ElementAttributes();
|
|
|
35141 |
var currentNSMap = parseStack[parseStack.length - 1].currentNSMap;
|
|
|
35142 |
//elStartEnd
|
|
|
35143 |
var end = parseElementStartPart(source, tagStart, el, currentNSMap, entityReplacer, errorHandler);
|
|
|
35144 |
var len = el.length;
|
|
|
35145 |
if (!el.closed && fixSelfClosed(source, end, el.tagName, closeMap)) {
|
|
|
35146 |
el.closed = true;
|
|
|
35147 |
if (!entityMap.nbsp) {
|
|
|
35148 |
errorHandler.warning('unclosed xml attribute');
|
|
|
35149 |
}
|
|
|
35150 |
}
|
|
|
35151 |
if (locator && len) {
|
|
|
35152 |
var locator2 = copyLocator(locator, {});
|
|
|
35153 |
//try{//attribute position fixed
|
|
|
35154 |
for (var i = 0; i < len; i++) {
|
|
|
35155 |
var a = el[i];
|
|
|
35156 |
position(a.offset);
|
|
|
35157 |
a.locator = copyLocator(locator, {});
|
|
|
35158 |
}
|
|
|
35159 |
domBuilder.locator = locator2;
|
|
|
35160 |
if (appendElement$1(el, domBuilder, currentNSMap)) {
|
|
|
35161 |
parseStack.push(el);
|
|
|
35162 |
}
|
|
|
35163 |
domBuilder.locator = locator;
|
|
|
35164 |
} else {
|
|
|
35165 |
if (appendElement$1(el, domBuilder, currentNSMap)) {
|
|
|
35166 |
parseStack.push(el);
|
|
|
35167 |
}
|
|
|
35168 |
}
|
|
|
35169 |
if (NAMESPACE$1.isHTML(el.uri) && !el.closed) {
|
|
|
35170 |
end = parseHtmlSpecialContent(source, end, el.tagName, entityReplacer, domBuilder);
|
|
|
35171 |
} else {
|
|
|
35172 |
end++;
|
|
|
35173 |
}
|
|
|
35174 |
}
|
|
|
35175 |
} catch (e) {
|
|
|
35176 |
if (e instanceof ParseError$1) {
|
|
|
35177 |
throw e;
|
|
|
35178 |
}
|
|
|
35179 |
errorHandler.error('element parse error: ' + e);
|
|
|
35180 |
end = -1;
|
|
|
35181 |
}
|
|
|
35182 |
if (end > start) {
|
|
|
35183 |
start = end;
|
|
|
35184 |
} else {
|
|
|
35185 |
//TODO: 这里有可能sax回退,有位置错误风险
|
|
|
35186 |
appendText(Math.max(tagStart, start) + 1);
|
|
|
35187 |
}
|
|
|
35188 |
}
|
|
|
35189 |
}
|
|
|
35190 |
function copyLocator(f, t) {
|
|
|
35191 |
t.lineNumber = f.lineNumber;
|
|
|
35192 |
t.columnNumber = f.columnNumber;
|
|
|
35193 |
return t;
|
|
|
35194 |
}
|
|
|
35195 |
|
|
|
35196 |
/**
|
|
|
35197 |
* @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack);
|
|
|
35198 |
* @return end of the elementStartPart(end of elementEndPart for selfClosed el)
|
|
|
35199 |
*/
|
|
|
35200 |
function parseElementStartPart(source, start, el, currentNSMap, entityReplacer, errorHandler) {
|
|
|
35201 |
/**
|
|
|
35202 |
* @param {string} qname
|
|
|
35203 |
* @param {string} value
|
|
|
35204 |
* @param {number} startIndex
|
|
|
35205 |
*/
|
|
|
35206 |
function addAttribute(qname, value, startIndex) {
|
|
|
35207 |
if (el.attributeNames.hasOwnProperty(qname)) {
|
|
|
35208 |
errorHandler.fatalError('Attribute ' + qname + ' redefined');
|
|
|
35209 |
}
|
|
|
35210 |
el.addValue(qname,
|
|
|
35211 |
// @see https://www.w3.org/TR/xml/#AVNormalize
|
|
|
35212 |
// since the xmldom sax parser does not "interpret" DTD the following is not implemented:
|
|
|
35213 |
// - recursive replacement of (DTD) entity references
|
|
|
35214 |
// - trimming and collapsing multiple spaces into a single one for attributes that are not of type CDATA
|
|
|
35215 |
value.replace(/[\t\n\r]/g, ' ').replace(/&#?\w+;/g, entityReplacer), startIndex);
|
|
|
35216 |
}
|
|
|
35217 |
var attrName;
|
|
|
35218 |
var value;
|
|
|
35219 |
var p = ++start;
|
|
|
35220 |
var s = S_TAG; //status
|
|
|
35221 |
while (true) {
|
|
|
35222 |
var c = source.charAt(p);
|
|
|
35223 |
switch (c) {
|
|
|
35224 |
case '=':
|
|
|
35225 |
if (s === S_ATTR) {
|
|
|
35226 |
//attrName
|
|
|
35227 |
attrName = source.slice(start, p);
|
|
|
35228 |
s = S_EQ;
|
|
|
35229 |
} else if (s === S_ATTR_SPACE) {
|
|
|
35230 |
s = S_EQ;
|
|
|
35231 |
} else {
|
|
|
35232 |
//fatalError: equal must after attrName or space after attrName
|
|
|
35233 |
throw new Error('attribute equal must after attrName'); // No known test case
|
|
|
35234 |
}
|
|
|
35235 |
|
|
|
35236 |
break;
|
|
|
35237 |
case '\'':
|
|
|
35238 |
case '"':
|
|
|
35239 |
if (s === S_EQ || s === S_ATTR //|| s == S_ATTR_SPACE
|
|
|
35240 |
) {
|
|
|
35241 |
//equal
|
|
|
35242 |
if (s === S_ATTR) {
|
|
|
35243 |
errorHandler.warning('attribute value must after "="');
|
|
|
35244 |
attrName = source.slice(start, p);
|
|
|
35245 |
}
|
|
|
35246 |
start = p + 1;
|
|
|
35247 |
p = source.indexOf(c, start);
|
|
|
35248 |
if (p > 0) {
|
|
|
35249 |
value = source.slice(start, p);
|
|
|
35250 |
addAttribute(attrName, value, start - 1);
|
|
|
35251 |
s = S_ATTR_END;
|
|
|
35252 |
} else {
|
|
|
35253 |
//fatalError: no end quot match
|
|
|
35254 |
throw new Error('attribute value no end \'' + c + '\' match');
|
|
|
35255 |
}
|
|
|
35256 |
} else if (s == S_ATTR_NOQUOT_VALUE) {
|
|
|
35257 |
value = source.slice(start, p);
|
|
|
35258 |
addAttribute(attrName, value, start);
|
|
|
35259 |
errorHandler.warning('attribute "' + attrName + '" missed start quot(' + c + ')!!');
|
|
|
35260 |
start = p + 1;
|
|
|
35261 |
s = S_ATTR_END;
|
|
|
35262 |
} else {
|
|
|
35263 |
//fatalError: no equal before
|
|
|
35264 |
throw new Error('attribute value must after "="'); // No known test case
|
|
|
35265 |
}
|
|
|
35266 |
|
|
|
35267 |
break;
|
|
|
35268 |
case '/':
|
|
|
35269 |
switch (s) {
|
|
|
35270 |
case S_TAG:
|
|
|
35271 |
el.setTagName(source.slice(start, p));
|
|
|
35272 |
case S_ATTR_END:
|
|
|
35273 |
case S_TAG_SPACE:
|
|
|
35274 |
case S_TAG_CLOSE:
|
|
|
35275 |
s = S_TAG_CLOSE;
|
|
|
35276 |
el.closed = true;
|
|
|
35277 |
case S_ATTR_NOQUOT_VALUE:
|
|
|
35278 |
case S_ATTR:
|
|
|
35279 |
break;
|
|
|
35280 |
case S_ATTR_SPACE:
|
|
|
35281 |
el.closed = true;
|
|
|
35282 |
break;
|
|
|
35283 |
//case S_EQ:
|
|
|
35284 |
default:
|
|
|
35285 |
throw new Error("attribute invalid close char('/')");
|
|
|
35286 |
// No known test case
|
|
|
35287 |
}
|
|
|
35288 |
|
|
|
35289 |
break;
|
|
|
35290 |
case '':
|
|
|
35291 |
//end document
|
|
|
35292 |
errorHandler.error('unexpected end of input');
|
|
|
35293 |
if (s == S_TAG) {
|
|
|
35294 |
el.setTagName(source.slice(start, p));
|
|
|
35295 |
}
|
|
|
35296 |
return p;
|
|
|
35297 |
case '>':
|
|
|
35298 |
switch (s) {
|
|
|
35299 |
case S_TAG:
|
|
|
35300 |
el.setTagName(source.slice(start, p));
|
|
|
35301 |
case S_ATTR_END:
|
|
|
35302 |
case S_TAG_SPACE:
|
|
|
35303 |
case S_TAG_CLOSE:
|
|
|
35304 |
break;
|
|
|
35305 |
//normal
|
|
|
35306 |
case S_ATTR_NOQUOT_VALUE: //Compatible state
|
|
|
35307 |
case S_ATTR:
|
|
|
35308 |
value = source.slice(start, p);
|
|
|
35309 |
if (value.slice(-1) === '/') {
|
|
|
35310 |
el.closed = true;
|
|
|
35311 |
value = value.slice(0, -1);
|
|
|
35312 |
}
|
|
|
35313 |
case S_ATTR_SPACE:
|
|
|
35314 |
if (s === S_ATTR_SPACE) {
|
|
|
35315 |
value = attrName;
|
|
|
35316 |
}
|
|
|
35317 |
if (s == S_ATTR_NOQUOT_VALUE) {
|
|
|
35318 |
errorHandler.warning('attribute "' + value + '" missed quot(")!');
|
|
|
35319 |
addAttribute(attrName, value, start);
|
|
|
35320 |
} else {
|
|
|
35321 |
if (!NAMESPACE$1.isHTML(currentNSMap['']) || !value.match(/^(?:disabled|checked|selected)$/i)) {
|
|
|
35322 |
errorHandler.warning('attribute "' + value + '" missed value!! "' + value + '" instead!!');
|
|
|
35323 |
}
|
|
|
35324 |
addAttribute(value, value, start);
|
|
|
35325 |
}
|
|
|
35326 |
break;
|
|
|
35327 |
case S_EQ:
|
|
|
35328 |
throw new Error('attribute value missed!!');
|
|
|
35329 |
}
|
|
|
35330 |
// console.log(tagName,tagNamePattern,tagNamePattern.test(tagName))
|
|
|
35331 |
return p;
|
|
|
35332 |
/*xml space '\x20' | #x9 | #xD | #xA; */
|
|
|
35333 |
case '\u0080':
|
|
|
35334 |
c = ' ';
|
|
|
35335 |
default:
|
|
|
35336 |
if (c <= ' ') {
|
|
|
35337 |
//space
|
|
|
35338 |
switch (s) {
|
|
|
35339 |
case S_TAG:
|
|
|
35340 |
el.setTagName(source.slice(start, p)); //tagName
|
|
|
35341 |
s = S_TAG_SPACE;
|
|
|
35342 |
break;
|
|
|
35343 |
case S_ATTR:
|
|
|
35344 |
attrName = source.slice(start, p);
|
|
|
35345 |
s = S_ATTR_SPACE;
|
|
|
35346 |
break;
|
|
|
35347 |
case S_ATTR_NOQUOT_VALUE:
|
|
|
35348 |
var value = source.slice(start, p);
|
|
|
35349 |
errorHandler.warning('attribute "' + value + '" missed quot(")!!');
|
|
|
35350 |
addAttribute(attrName, value, start);
|
|
|
35351 |
case S_ATTR_END:
|
|
|
35352 |
s = S_TAG_SPACE;
|
|
|
35353 |
break;
|
|
|
35354 |
//case S_TAG_SPACE:
|
|
|
35355 |
//case S_EQ:
|
|
|
35356 |
//case S_ATTR_SPACE:
|
|
|
35357 |
// void();break;
|
|
|
35358 |
//case S_TAG_CLOSE:
|
|
|
35359 |
//ignore warning
|
|
|
35360 |
}
|
|
|
35361 |
} else {
|
|
|
35362 |
//not space
|
|
|
35363 |
//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE
|
|
|
35364 |
//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE
|
|
|
35365 |
switch (s) {
|
|
|
35366 |
//case S_TAG:void();break;
|
|
|
35367 |
//case S_ATTR:void();break;
|
|
|
35368 |
//case S_ATTR_NOQUOT_VALUE:void();break;
|
|
|
35369 |
case S_ATTR_SPACE:
|
|
|
35370 |
el.tagName;
|
|
|
35371 |
if (!NAMESPACE$1.isHTML(currentNSMap['']) || !attrName.match(/^(?:disabled|checked|selected)$/i)) {
|
|
|
35372 |
errorHandler.warning('attribute "' + attrName + '" missed value!! "' + attrName + '" instead2!!');
|
|
|
35373 |
}
|
|
|
35374 |
addAttribute(attrName, attrName, start);
|
|
|
35375 |
start = p;
|
|
|
35376 |
s = S_ATTR;
|
|
|
35377 |
break;
|
|
|
35378 |
case S_ATTR_END:
|
|
|
35379 |
errorHandler.warning('attribute space is required"' + attrName + '"!!');
|
|
|
35380 |
case S_TAG_SPACE:
|
|
|
35381 |
s = S_ATTR;
|
|
|
35382 |
start = p;
|
|
|
35383 |
break;
|
|
|
35384 |
case S_EQ:
|
|
|
35385 |
s = S_ATTR_NOQUOT_VALUE;
|
|
|
35386 |
start = p;
|
|
|
35387 |
break;
|
|
|
35388 |
case S_TAG_CLOSE:
|
|
|
35389 |
throw new Error("elements closed character '/' and '>' must be connected to");
|
|
|
35390 |
}
|
|
|
35391 |
}
|
|
|
35392 |
} //end outer switch
|
|
|
35393 |
//console.log('p++',p)
|
|
|
35394 |
p++;
|
|
|
35395 |
}
|
|
|
35396 |
}
|
|
|
35397 |
/**
|
|
|
35398 |
* @return true if has new namespace define
|
|
|
35399 |
*/
|
|
|
35400 |
function appendElement$1(el, domBuilder, currentNSMap) {
|
|
|
35401 |
var tagName = el.tagName;
|
|
|
35402 |
var localNSMap = null;
|
|
|
35403 |
//var currentNSMap = parseStack[parseStack.length-1].currentNSMap;
|
|
|
35404 |
var i = el.length;
|
|
|
35405 |
while (i--) {
|
|
|
35406 |
var a = el[i];
|
|
|
35407 |
var qName = a.qName;
|
|
|
35408 |
var value = a.value;
|
|
|
35409 |
var nsp = qName.indexOf(':');
|
|
|
35410 |
if (nsp > 0) {
|
|
|
35411 |
var prefix = a.prefix = qName.slice(0, nsp);
|
|
|
35412 |
var localName = qName.slice(nsp + 1);
|
|
|
35413 |
var nsPrefix = prefix === 'xmlns' && localName;
|
|
|
35414 |
} else {
|
|
|
35415 |
localName = qName;
|
|
|
35416 |
prefix = null;
|
|
|
35417 |
nsPrefix = qName === 'xmlns' && '';
|
|
|
35418 |
}
|
|
|
35419 |
//can not set prefix,because prefix !== ''
|
|
|
35420 |
a.localName = localName;
|
|
|
35421 |
//prefix == null for no ns prefix attribute
|
|
|
35422 |
if (nsPrefix !== false) {
|
|
|
35423 |
//hack!!
|
|
|
35424 |
if (localNSMap == null) {
|
|
|
35425 |
localNSMap = {};
|
|
|
35426 |
//console.log(currentNSMap,0)
|
|
|
35427 |
_copy(currentNSMap, currentNSMap = {});
|
|
|
35428 |
//console.log(currentNSMap,1)
|
|
|
35429 |
}
|
|
|
35430 |
|
|
|
35431 |
currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value;
|
|
|
35432 |
a.uri = NAMESPACE$1.XMLNS;
|
|
|
35433 |
domBuilder.startPrefixMapping(nsPrefix, value);
|
|
|
35434 |
}
|
|
|
35435 |
}
|
|
|
35436 |
var i = el.length;
|
|
|
35437 |
while (i--) {
|
|
|
35438 |
a = el[i];
|
|
|
35439 |
var prefix = a.prefix;
|
|
|
35440 |
if (prefix) {
|
|
|
35441 |
//no prefix attribute has no namespace
|
|
|
35442 |
if (prefix === 'xml') {
|
|
|
35443 |
a.uri = NAMESPACE$1.XML;
|
|
|
35444 |
}
|
|
|
35445 |
if (prefix !== 'xmlns') {
|
|
|
35446 |
a.uri = currentNSMap[prefix || ''];
|
|
|
35447 |
|
|
|
35448 |
//{console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)}
|
|
|
35449 |
}
|
|
|
35450 |
}
|
|
|
35451 |
}
|
|
|
35452 |
|
|
|
35453 |
var nsp = tagName.indexOf(':');
|
|
|
35454 |
if (nsp > 0) {
|
|
|
35455 |
prefix = el.prefix = tagName.slice(0, nsp);
|
|
|
35456 |
localName = el.localName = tagName.slice(nsp + 1);
|
|
|
35457 |
} else {
|
|
|
35458 |
prefix = null; //important!!
|
|
|
35459 |
localName = el.localName = tagName;
|
|
|
35460 |
}
|
|
|
35461 |
//no prefix element has default namespace
|
|
|
35462 |
var ns = el.uri = currentNSMap[prefix || ''];
|
|
|
35463 |
domBuilder.startElement(ns, localName, tagName, el);
|
|
|
35464 |
//endPrefixMapping and startPrefixMapping have not any help for dom builder
|
|
|
35465 |
//localNSMap = null
|
|
|
35466 |
if (el.closed) {
|
|
|
35467 |
domBuilder.endElement(ns, localName, tagName);
|
|
|
35468 |
if (localNSMap) {
|
|
|
35469 |
for (prefix in localNSMap) {
|
|
|
35470 |
if (Object.prototype.hasOwnProperty.call(localNSMap, prefix)) {
|
|
|
35471 |
domBuilder.endPrefixMapping(prefix);
|
|
|
35472 |
}
|
|
|
35473 |
}
|
|
|
35474 |
}
|
|
|
35475 |
} else {
|
|
|
35476 |
el.currentNSMap = currentNSMap;
|
|
|
35477 |
el.localNSMap = localNSMap;
|
|
|
35478 |
//parseStack.push(el);
|
|
|
35479 |
return true;
|
|
|
35480 |
}
|
|
|
35481 |
}
|
|
|
35482 |
function parseHtmlSpecialContent(source, elStartEnd, tagName, entityReplacer, domBuilder) {
|
|
|
35483 |
if (/^(?:script|textarea)$/i.test(tagName)) {
|
|
|
35484 |
var elEndStart = source.indexOf('</' + tagName + '>', elStartEnd);
|
|
|
35485 |
var text = source.substring(elStartEnd + 1, elEndStart);
|
|
|
35486 |
if (/[&<]/.test(text)) {
|
|
|
35487 |
if (/^script$/i.test(tagName)) {
|
|
|
35488 |
//if(!/\]\]>/.test(text)){
|
|
|
35489 |
//lexHandler.startCDATA();
|
|
|
35490 |
domBuilder.characters(text, 0, text.length);
|
|
|
35491 |
//lexHandler.endCDATA();
|
|
|
35492 |
return elEndStart;
|
|
|
35493 |
//}
|
|
|
35494 |
} //}else{//text area
|
|
|
35495 |
text = text.replace(/&#?\w+;/g, entityReplacer);
|
|
|
35496 |
domBuilder.characters(text, 0, text.length);
|
|
|
35497 |
return elEndStart;
|
|
|
35498 |
//}
|
|
|
35499 |
}
|
|
|
35500 |
}
|
|
|
35501 |
|
|
|
35502 |
return elStartEnd + 1;
|
|
|
35503 |
}
|
|
|
35504 |
function fixSelfClosed(source, elStartEnd, tagName, closeMap) {
|
|
|
35505 |
//if(tagName in closeMap){
|
|
|
35506 |
var pos = closeMap[tagName];
|
|
|
35507 |
if (pos == null) {
|
|
|
35508 |
//console.log(tagName)
|
|
|
35509 |
pos = source.lastIndexOf('</' + tagName + '>');
|
|
|
35510 |
if (pos < elStartEnd) {
|
|
|
35511 |
//忘记闭合
|
|
|
35512 |
pos = source.lastIndexOf('</' + tagName);
|
|
|
35513 |
}
|
|
|
35514 |
closeMap[tagName] = pos;
|
|
|
35515 |
}
|
|
|
35516 |
return pos < elStartEnd;
|
|
|
35517 |
//}
|
|
|
35518 |
}
|
|
|
35519 |
|
|
|
35520 |
function _copy(source, target) {
|
|
|
35521 |
for (var n in source) {
|
|
|
35522 |
if (Object.prototype.hasOwnProperty.call(source, n)) {
|
|
|
35523 |
target[n] = source[n];
|
|
|
35524 |
}
|
|
|
35525 |
}
|
|
|
35526 |
}
|
|
|
35527 |
function parseDCC(source, start, domBuilder, errorHandler) {
|
|
|
35528 |
//sure start with '<!'
|
|
|
35529 |
var next = source.charAt(start + 2);
|
|
|
35530 |
switch (next) {
|
|
|
35531 |
case '-':
|
|
|
35532 |
if (source.charAt(start + 3) === '-') {
|
|
|
35533 |
var end = source.indexOf('-->', start + 4);
|
|
|
35534 |
//append comment source.substring(4,end)//<!--
|
|
|
35535 |
if (end > start) {
|
|
|
35536 |
domBuilder.comment(source, start + 4, end - start - 4);
|
|
|
35537 |
return end + 3;
|
|
|
35538 |
} else {
|
|
|
35539 |
errorHandler.error("Unclosed comment");
|
|
|
35540 |
return -1;
|
|
|
35541 |
}
|
|
|
35542 |
} else {
|
|
|
35543 |
//error
|
|
|
35544 |
return -1;
|
|
|
35545 |
}
|
|
|
35546 |
default:
|
|
|
35547 |
if (source.substr(start + 3, 6) == 'CDATA[') {
|
|
|
35548 |
var end = source.indexOf(']]>', start + 9);
|
|
|
35549 |
domBuilder.startCDATA();
|
|
|
35550 |
domBuilder.characters(source, start + 9, end - start - 9);
|
|
|
35551 |
domBuilder.endCDATA();
|
|
|
35552 |
return end + 3;
|
|
|
35553 |
}
|
|
|
35554 |
//<!DOCTYPE
|
|
|
35555 |
//startDTD(java.lang.String name, java.lang.String publicId, java.lang.String systemId)
|
|
|
35556 |
var matchs = split(source, start);
|
|
|
35557 |
var len = matchs.length;
|
|
|
35558 |
if (len > 1 && /!doctype/i.test(matchs[0][0])) {
|
|
|
35559 |
var name = matchs[1][0];
|
|
|
35560 |
var pubid = false;
|
|
|
35561 |
var sysid = false;
|
|
|
35562 |
if (len > 3) {
|
|
|
35563 |
if (/^public$/i.test(matchs[2][0])) {
|
|
|
35564 |
pubid = matchs[3][0];
|
|
|
35565 |
sysid = len > 4 && matchs[4][0];
|
|
|
35566 |
} else if (/^system$/i.test(matchs[2][0])) {
|
|
|
35567 |
sysid = matchs[3][0];
|
|
|
35568 |
}
|
|
|
35569 |
}
|
|
|
35570 |
var lastMatch = matchs[len - 1];
|
|
|
35571 |
domBuilder.startDTD(name, pubid, sysid);
|
|
|
35572 |
domBuilder.endDTD();
|
|
|
35573 |
return lastMatch.index + lastMatch[0].length;
|
|
|
35574 |
}
|
|
|
35575 |
}
|
|
|
35576 |
return -1;
|
|
|
35577 |
}
|
|
|
35578 |
function parseInstruction(source, start, domBuilder) {
|
|
|
35579 |
var end = source.indexOf('?>', start);
|
|
|
35580 |
if (end) {
|
|
|
35581 |
var match = source.substring(start, end).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/);
|
|
|
35582 |
if (match) {
|
|
|
35583 |
match[0].length;
|
|
|
35584 |
domBuilder.processingInstruction(match[1], match[2]);
|
|
|
35585 |
return end + 2;
|
|
|
35586 |
} else {
|
|
|
35587 |
//error
|
|
|
35588 |
return -1;
|
|
|
35589 |
}
|
|
|
35590 |
}
|
|
|
35591 |
return -1;
|
|
|
35592 |
}
|
|
|
35593 |
function ElementAttributes() {
|
|
|
35594 |
this.attributeNames = {};
|
|
|
35595 |
}
|
|
|
35596 |
ElementAttributes.prototype = {
|
|
|
35597 |
setTagName: function (tagName) {
|
|
|
35598 |
if (!tagNamePattern.test(tagName)) {
|
|
|
35599 |
throw new Error('invalid tagName:' + tagName);
|
|
|
35600 |
}
|
|
|
35601 |
this.tagName = tagName;
|
|
|
35602 |
},
|
|
|
35603 |
addValue: function (qName, value, offset) {
|
|
|
35604 |
if (!tagNamePattern.test(qName)) {
|
|
|
35605 |
throw new Error('invalid attribute:' + qName);
|
|
|
35606 |
}
|
|
|
35607 |
this.attributeNames[qName] = this.length;
|
|
|
35608 |
this[this.length++] = {
|
|
|
35609 |
qName: qName,
|
|
|
35610 |
value: value,
|
|
|
35611 |
offset: offset
|
|
|
35612 |
};
|
|
|
35613 |
},
|
|
|
35614 |
length: 0,
|
|
|
35615 |
getLocalName: function (i) {
|
|
|
35616 |
return this[i].localName;
|
|
|
35617 |
},
|
|
|
35618 |
getLocator: function (i) {
|
|
|
35619 |
return this[i].locator;
|
|
|
35620 |
},
|
|
|
35621 |
getQName: function (i) {
|
|
|
35622 |
return this[i].qName;
|
|
|
35623 |
},
|
|
|
35624 |
getURI: function (i) {
|
|
|
35625 |
return this[i].uri;
|
|
|
35626 |
},
|
|
|
35627 |
getValue: function (i) {
|
|
|
35628 |
return this[i].value;
|
|
|
35629 |
}
|
|
|
35630 |
// ,getIndex:function(uri, localName)){
|
|
|
35631 |
// if(localName){
|
|
|
35632 |
//
|
|
|
35633 |
// }else{
|
|
|
35634 |
// var qName = uri
|
|
|
35635 |
// }
|
|
|
35636 |
// },
|
|
|
35637 |
// getValue:function(){return this.getValue(this.getIndex.apply(this,arguments))},
|
|
|
35638 |
// getType:function(uri,localName){}
|
|
|
35639 |
// getType:function(i){},
|
|
|
35640 |
};
|
|
|
35641 |
|
|
|
35642 |
function split(source, start) {
|
|
|
35643 |
var match;
|
|
|
35644 |
var buf = [];
|
|
|
35645 |
var reg = /'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g;
|
|
|
35646 |
reg.lastIndex = start;
|
|
|
35647 |
reg.exec(source); //skip <
|
|
|
35648 |
while (match = reg.exec(source)) {
|
|
|
35649 |
buf.push(match);
|
|
|
35650 |
if (match[1]) return buf;
|
|
|
35651 |
}
|
|
|
35652 |
}
|
|
|
35653 |
var XMLReader_1 = XMLReader$1;
|
|
|
35654 |
var ParseError_1 = ParseError$1;
|
|
|
35655 |
var sax = {
|
|
|
35656 |
XMLReader: XMLReader_1,
|
|
|
35657 |
ParseError: ParseError_1
|
|
|
35658 |
};
|
|
|
35659 |
|
|
|
35660 |
var DOMImplementation = dom.DOMImplementation;
|
|
|
35661 |
var NAMESPACE = conventions.NAMESPACE;
|
|
|
35662 |
var ParseError = sax.ParseError;
|
|
|
35663 |
var XMLReader = sax.XMLReader;
|
|
|
35664 |
|
|
|
35665 |
/**
|
|
|
35666 |
* Normalizes line ending according to https://www.w3.org/TR/xml11/#sec-line-ends:
|
|
|
35667 |
*
|
|
|
35668 |
* > XML parsed entities are often stored in computer files which,
|
|
|
35669 |
* > for editing convenience, are organized into lines.
|
|
|
35670 |
* > These lines are typically separated by some combination
|
|
|
35671 |
* > of the characters CARRIAGE RETURN (#xD) and LINE FEED (#xA).
|
|
|
35672 |
* >
|
|
|
35673 |
* > To simplify the tasks of applications, the XML processor must behave
|
|
|
35674 |
* > as if it normalized all line breaks in external parsed entities (including the document entity)
|
|
|
35675 |
* > on input, before parsing, by translating all of the following to a single #xA character:
|
|
|
35676 |
* >
|
|
|
35677 |
* > 1. the two-character sequence #xD #xA
|
|
|
35678 |
* > 2. the two-character sequence #xD #x85
|
|
|
35679 |
* > 3. the single character #x85
|
|
|
35680 |
* > 4. the single character #x2028
|
|
|
35681 |
* > 5. any #xD character that is not immediately followed by #xA or #x85.
|
|
|
35682 |
*
|
|
|
35683 |
* @param {string} input
|
|
|
35684 |
* @returns {string}
|
|
|
35685 |
*/
|
|
|
35686 |
function normalizeLineEndings(input) {
|
|
|
35687 |
return input.replace(/\r[\n\u0085]/g, '\n').replace(/[\r\u0085\u2028]/g, '\n');
|
|
|
35688 |
}
|
|
|
35689 |
|
|
|
35690 |
/**
|
|
|
35691 |
* @typedef Locator
|
|
|
35692 |
* @property {number} [columnNumber]
|
|
|
35693 |
* @property {number} [lineNumber]
|
|
|
35694 |
*/
|
|
|
35695 |
|
|
|
35696 |
/**
|
|
|
35697 |
* @typedef DOMParserOptions
|
|
|
35698 |
* @property {DOMHandler} [domBuilder]
|
|
|
35699 |
* @property {Function} [errorHandler]
|
|
|
35700 |
* @property {(string) => string} [normalizeLineEndings] used to replace line endings before parsing
|
|
|
35701 |
* defaults to `normalizeLineEndings`
|
|
|
35702 |
* @property {Locator} [locator]
|
|
|
35703 |
* @property {Record<string, string>} [xmlns]
|
|
|
35704 |
*
|
|
|
35705 |
* @see normalizeLineEndings
|
|
|
35706 |
*/
|
|
|
35707 |
|
|
|
35708 |
/**
|
|
|
35709 |
* The DOMParser interface provides the ability to parse XML or HTML source code
|
|
|
35710 |
* from a string into a DOM `Document`.
|
|
|
35711 |
*
|
|
|
35712 |
* _xmldom is different from the spec in that it allows an `options` parameter,
|
|
|
35713 |
* to override the default behavior._
|
|
|
35714 |
*
|
|
|
35715 |
* @param {DOMParserOptions} [options]
|
|
|
35716 |
* @constructor
|
|
|
35717 |
*
|
|
|
35718 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
|
|
|
35719 |
* @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-parsing-and-serialization
|
|
|
35720 |
*/
|
|
|
35721 |
function DOMParser$1(options) {
|
|
|
35722 |
this.options = options || {
|
|
|
35723 |
locator: {}
|
|
|
35724 |
};
|
|
|
35725 |
}
|
|
|
35726 |
DOMParser$1.prototype.parseFromString = function (source, mimeType) {
|
|
|
35727 |
var options = this.options;
|
|
|
35728 |
var sax = new XMLReader();
|
|
|
35729 |
var domBuilder = options.domBuilder || new DOMHandler(); //contentHandler and LexicalHandler
|
|
|
35730 |
var errorHandler = options.errorHandler;
|
|
|
35731 |
var locator = options.locator;
|
|
|
35732 |
var defaultNSMap = options.xmlns || {};
|
|
|
35733 |
var isHTML = /\/x?html?$/.test(mimeType); //mimeType.toLowerCase().indexOf('html') > -1;
|
|
|
35734 |
var entityMap = isHTML ? entities.HTML_ENTITIES : entities.XML_ENTITIES;
|
|
|
35735 |
if (locator) {
|
|
|
35736 |
domBuilder.setDocumentLocator(locator);
|
|
|
35737 |
}
|
|
|
35738 |
sax.errorHandler = buildErrorHandler(errorHandler, domBuilder, locator);
|
|
|
35739 |
sax.domBuilder = options.domBuilder || domBuilder;
|
|
|
35740 |
if (isHTML) {
|
|
|
35741 |
defaultNSMap[''] = NAMESPACE.HTML;
|
|
|
35742 |
}
|
|
|
35743 |
defaultNSMap.xml = defaultNSMap.xml || NAMESPACE.XML;
|
|
|
35744 |
var normalize = options.normalizeLineEndings || normalizeLineEndings;
|
|
|
35745 |
if (source && typeof source === 'string') {
|
|
|
35746 |
sax.parse(normalize(source), defaultNSMap, entityMap);
|
|
|
35747 |
} else {
|
|
|
35748 |
sax.errorHandler.error('invalid doc source');
|
|
|
35749 |
}
|
|
|
35750 |
return domBuilder.doc;
|
|
|
35751 |
};
|
|
|
35752 |
function buildErrorHandler(errorImpl, domBuilder, locator) {
|
|
|
35753 |
if (!errorImpl) {
|
|
|
35754 |
if (domBuilder instanceof DOMHandler) {
|
|
|
35755 |
return domBuilder;
|
|
|
35756 |
}
|
|
|
35757 |
errorImpl = domBuilder;
|
|
|
35758 |
}
|
|
|
35759 |
var errorHandler = {};
|
|
|
35760 |
var isCallback = errorImpl instanceof Function;
|
|
|
35761 |
locator = locator || {};
|
|
|
35762 |
function build(key) {
|
|
|
35763 |
var fn = errorImpl[key];
|
|
|
35764 |
if (!fn && isCallback) {
|
|
|
35765 |
fn = errorImpl.length == 2 ? function (msg) {
|
|
|
35766 |
errorImpl(key, msg);
|
|
|
35767 |
} : errorImpl;
|
|
|
35768 |
}
|
|
|
35769 |
errorHandler[key] = fn && function (msg) {
|
|
|
35770 |
fn('[xmldom ' + key + ']\t' + msg + _locator(locator));
|
|
|
35771 |
} || function () {};
|
|
|
35772 |
}
|
|
|
35773 |
build('warning');
|
|
|
35774 |
build('error');
|
|
|
35775 |
build('fatalError');
|
|
|
35776 |
return errorHandler;
|
|
|
35777 |
}
|
|
|
35778 |
|
|
|
35779 |
//console.log('#\n\n\n\n\n\n\n####')
|
|
|
35780 |
/**
|
|
|
35781 |
* +ContentHandler+ErrorHandler
|
|
|
35782 |
* +LexicalHandler+EntityResolver2
|
|
|
35783 |
* -DeclHandler-DTDHandler
|
|
|
35784 |
*
|
|
|
35785 |
* DefaultHandler:EntityResolver, DTDHandler, ContentHandler, ErrorHandler
|
|
|
35786 |
* DefaultHandler2:DefaultHandler,LexicalHandler, DeclHandler, EntityResolver2
|
|
|
35787 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/helpers/DefaultHandler.html
|
|
|
35788 |
*/
|
|
|
35789 |
function DOMHandler() {
|
|
|
35790 |
this.cdata = false;
|
|
|
35791 |
}
|
|
|
35792 |
function position(locator, node) {
|
|
|
35793 |
node.lineNumber = locator.lineNumber;
|
|
|
35794 |
node.columnNumber = locator.columnNumber;
|
|
|
35795 |
}
|
|
|
35796 |
/**
|
|
|
35797 |
* @see org.xml.sax.ContentHandler#startDocument
|
|
|
35798 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/ContentHandler.html
|
|
|
35799 |
*/
|
|
|
35800 |
DOMHandler.prototype = {
|
|
|
35801 |
startDocument: function () {
|
|
|
35802 |
this.doc = new DOMImplementation().createDocument(null, null, null);
|
|
|
35803 |
if (this.locator) {
|
|
|
35804 |
this.doc.documentURI = this.locator.systemId;
|
|
|
35805 |
}
|
|
|
35806 |
},
|
|
|
35807 |
startElement: function (namespaceURI, localName, qName, attrs) {
|
|
|
35808 |
var doc = this.doc;
|
|
|
35809 |
var el = doc.createElementNS(namespaceURI, qName || localName);
|
|
|
35810 |
var len = attrs.length;
|
|
|
35811 |
appendElement(this, el);
|
|
|
35812 |
this.currentElement = el;
|
|
|
35813 |
this.locator && position(this.locator, el);
|
|
|
35814 |
for (var i = 0; i < len; i++) {
|
|
|
35815 |
var namespaceURI = attrs.getURI(i);
|
|
|
35816 |
var value = attrs.getValue(i);
|
|
|
35817 |
var qName = attrs.getQName(i);
|
|
|
35818 |
var attr = doc.createAttributeNS(namespaceURI, qName);
|
|
|
35819 |
this.locator && position(attrs.getLocator(i), attr);
|
|
|
35820 |
attr.value = attr.nodeValue = value;
|
|
|
35821 |
el.setAttributeNode(attr);
|
|
|
35822 |
}
|
|
|
35823 |
},
|
|
|
35824 |
endElement: function (namespaceURI, localName, qName) {
|
|
|
35825 |
var current = this.currentElement;
|
|
|
35826 |
current.tagName;
|
|
|
35827 |
this.currentElement = current.parentNode;
|
|
|
35828 |
},
|
|
|
35829 |
startPrefixMapping: function (prefix, uri) {},
|
|
|
35830 |
endPrefixMapping: function (prefix) {},
|
|
|
35831 |
processingInstruction: function (target, data) {
|
|
|
35832 |
var ins = this.doc.createProcessingInstruction(target, data);
|
|
|
35833 |
this.locator && position(this.locator, ins);
|
|
|
35834 |
appendElement(this, ins);
|
|
|
35835 |
},
|
|
|
35836 |
ignorableWhitespace: function (ch, start, length) {},
|
|
|
35837 |
characters: function (chars, start, length) {
|
|
|
35838 |
chars = _toString.apply(this, arguments);
|
|
|
35839 |
//console.log(chars)
|
|
|
35840 |
if (chars) {
|
|
|
35841 |
if (this.cdata) {
|
|
|
35842 |
var charNode = this.doc.createCDATASection(chars);
|
|
|
35843 |
} else {
|
|
|
35844 |
var charNode = this.doc.createTextNode(chars);
|
|
|
35845 |
}
|
|
|
35846 |
if (this.currentElement) {
|
|
|
35847 |
this.currentElement.appendChild(charNode);
|
|
|
35848 |
} else if (/^\s*$/.test(chars)) {
|
|
|
35849 |
this.doc.appendChild(charNode);
|
|
|
35850 |
//process xml
|
|
|
35851 |
}
|
|
|
35852 |
|
|
|
35853 |
this.locator && position(this.locator, charNode);
|
|
|
35854 |
}
|
|
|
35855 |
},
|
|
|
35856 |
skippedEntity: function (name) {},
|
|
|
35857 |
endDocument: function () {
|
|
|
35858 |
this.doc.normalize();
|
|
|
35859 |
},
|
|
|
35860 |
setDocumentLocator: function (locator) {
|
|
|
35861 |
if (this.locator = locator) {
|
|
|
35862 |
// && !('lineNumber' in locator)){
|
|
|
35863 |
locator.lineNumber = 0;
|
|
|
35864 |
}
|
|
|
35865 |
},
|
|
|
35866 |
//LexicalHandler
|
|
|
35867 |
comment: function (chars, start, length) {
|
|
|
35868 |
chars = _toString.apply(this, arguments);
|
|
|
35869 |
var comm = this.doc.createComment(chars);
|
|
|
35870 |
this.locator && position(this.locator, comm);
|
|
|
35871 |
appendElement(this, comm);
|
|
|
35872 |
},
|
|
|
35873 |
startCDATA: function () {
|
|
|
35874 |
//used in characters() methods
|
|
|
35875 |
this.cdata = true;
|
|
|
35876 |
},
|
|
|
35877 |
endCDATA: function () {
|
|
|
35878 |
this.cdata = false;
|
|
|
35879 |
},
|
|
|
35880 |
startDTD: function (name, publicId, systemId) {
|
|
|
35881 |
var impl = this.doc.implementation;
|
|
|
35882 |
if (impl && impl.createDocumentType) {
|
|
|
35883 |
var dt = impl.createDocumentType(name, publicId, systemId);
|
|
|
35884 |
this.locator && position(this.locator, dt);
|
|
|
35885 |
appendElement(this, dt);
|
|
|
35886 |
this.doc.doctype = dt;
|
|
|
35887 |
}
|
|
|
35888 |
},
|
|
|
35889 |
/**
|
|
|
35890 |
* @see org.xml.sax.ErrorHandler
|
|
|
35891 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/ErrorHandler.html
|
|
|
35892 |
*/
|
|
|
35893 |
warning: function (error) {
|
|
|
35894 |
console.warn('[xmldom warning]\t' + error, _locator(this.locator));
|
|
|
35895 |
},
|
|
|
35896 |
error: function (error) {
|
|
|
35897 |
console.error('[xmldom error]\t' + error, _locator(this.locator));
|
|
|
35898 |
},
|
|
|
35899 |
fatalError: function (error) {
|
|
|
35900 |
throw new ParseError(error, this.locator);
|
|
|
35901 |
}
|
|
|
35902 |
};
|
|
|
35903 |
function _locator(l) {
|
|
|
35904 |
if (l) {
|
|
|
35905 |
return '\n@' + (l.systemId || '') + '#[line:' + l.lineNumber + ',col:' + l.columnNumber + ']';
|
|
|
35906 |
}
|
|
|
35907 |
}
|
|
|
35908 |
function _toString(chars, start, length) {
|
|
|
35909 |
if (typeof chars == 'string') {
|
|
|
35910 |
return chars.substr(start, length);
|
|
|
35911 |
} else {
|
|
|
35912 |
//java sax connect width xmldom on rhino(what about: "? && !(chars instanceof String)")
|
|
|
35913 |
if (chars.length >= start + length || start) {
|
|
|
35914 |
return new java.lang.String(chars, start, length) + '';
|
|
|
35915 |
}
|
|
|
35916 |
return chars;
|
|
|
35917 |
}
|
|
|
35918 |
}
|
|
|
35919 |
|
|
|
35920 |
/*
|
|
|
35921 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/ext/LexicalHandler.html
|
|
|
35922 |
* used method of org.xml.sax.ext.LexicalHandler:
|
|
|
35923 |
* #comment(chars, start, length)
|
|
|
35924 |
* #startCDATA()
|
|
|
35925 |
* #endCDATA()
|
|
|
35926 |
* #startDTD(name, publicId, systemId)
|
|
|
35927 |
*
|
|
|
35928 |
*
|
|
|
35929 |
* IGNORED method of org.xml.sax.ext.LexicalHandler:
|
|
|
35930 |
* #endDTD()
|
|
|
35931 |
* #startEntity(name)
|
|
|
35932 |
* #endEntity(name)
|
|
|
35933 |
*
|
|
|
35934 |
*
|
|
|
35935 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/ext/DeclHandler.html
|
|
|
35936 |
* IGNORED method of org.xml.sax.ext.DeclHandler
|
|
|
35937 |
* #attributeDecl(eName, aName, type, mode, value)
|
|
|
35938 |
* #elementDecl(name, model)
|
|
|
35939 |
* #externalEntityDecl(name, publicId, systemId)
|
|
|
35940 |
* #internalEntityDecl(name, value)
|
|
|
35941 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/ext/EntityResolver2.html
|
|
|
35942 |
* IGNORED method of org.xml.sax.EntityResolver2
|
|
|
35943 |
* #resolveEntity(String name,String publicId,String baseURI,String systemId)
|
|
|
35944 |
* #resolveEntity(publicId, systemId)
|
|
|
35945 |
* #getExternalSubset(name, baseURI)
|
|
|
35946 |
* @link http://www.saxproject.org/apidoc/org/xml/sax/DTDHandler.html
|
|
|
35947 |
* IGNORED method of org.xml.sax.DTDHandler
|
|
|
35948 |
* #notationDecl(name, publicId, systemId) {};
|
|
|
35949 |
* #unparsedEntityDecl(name, publicId, systemId, notationName) {};
|
|
|
35950 |
*/
|
|
|
35951 |
"endDTD,startEntity,endEntity,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,resolveEntity,getExternalSubset,notationDecl,unparsedEntityDecl".replace(/\w+/g, function (key) {
|
|
|
35952 |
DOMHandler.prototype[key] = function () {
|
|
|
35953 |
return null;
|
|
|
35954 |
};
|
|
|
35955 |
});
|
|
|
35956 |
|
|
|
35957 |
/* Private static helpers treated below as private instance methods, so don't need to add these to the public API; we might use a Relator to also get rid of non-standard public properties */
|
|
|
35958 |
function appendElement(hander, node) {
|
|
|
35959 |
if (!hander.currentElement) {
|
|
|
35960 |
hander.doc.appendChild(node);
|
|
|
35961 |
} else {
|
|
|
35962 |
hander.currentElement.appendChild(node);
|
|
|
35963 |
}
|
|
|
35964 |
} //appendChild and setAttributeNS are preformance key
|
|
|
35965 |
|
|
|
35966 |
var __DOMHandler = DOMHandler;
|
|
|
35967 |
var normalizeLineEndings_1 = normalizeLineEndings;
|
|
|
35968 |
var DOMParser_1 = DOMParser$1;
|
|
|
35969 |
var domParser = {
|
|
|
35970 |
__DOMHandler: __DOMHandler,
|
|
|
35971 |
normalizeLineEndings: normalizeLineEndings_1,
|
|
|
35972 |
DOMParser: DOMParser_1
|
|
|
35973 |
};
|
|
|
35974 |
|
|
|
35975 |
var DOMParser = domParser.DOMParser;
|
|
|
35976 |
|
|
|
35977 |
/*! @name mpd-parser @version 1.3.0 @license Apache-2.0 */
|
|
|
35978 |
const isObject = obj => {
|
|
|
35979 |
return !!obj && typeof obj === 'object';
|
|
|
35980 |
};
|
|
|
35981 |
const merge$1 = (...objects) => {
|
|
|
35982 |
return objects.reduce((result, source) => {
|
|
|
35983 |
if (typeof source !== 'object') {
|
|
|
35984 |
return result;
|
|
|
35985 |
}
|
|
|
35986 |
Object.keys(source).forEach(key => {
|
|
|
35987 |
if (Array.isArray(result[key]) && Array.isArray(source[key])) {
|
|
|
35988 |
result[key] = result[key].concat(source[key]);
|
|
|
35989 |
} else if (isObject(result[key]) && isObject(source[key])) {
|
|
|
35990 |
result[key] = merge$1(result[key], source[key]);
|
|
|
35991 |
} else {
|
|
|
35992 |
result[key] = source[key];
|
|
|
35993 |
}
|
|
|
35994 |
});
|
|
|
35995 |
return result;
|
|
|
35996 |
}, {});
|
|
|
35997 |
};
|
|
|
35998 |
const values = o => Object.keys(o).map(k => o[k]);
|
|
|
35999 |
const range = (start, end) => {
|
|
|
36000 |
const result = [];
|
|
|
36001 |
for (let i = start; i < end; i++) {
|
|
|
36002 |
result.push(i);
|
|
|
36003 |
}
|
|
|
36004 |
return result;
|
|
|
36005 |
};
|
|
|
36006 |
const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
|
|
|
36007 |
const from = list => {
|
|
|
36008 |
if (!list.length) {
|
|
|
36009 |
return [];
|
|
|
36010 |
}
|
|
|
36011 |
const result = [];
|
|
|
36012 |
for (let i = 0; i < list.length; i++) {
|
|
|
36013 |
result.push(list[i]);
|
|
|
36014 |
}
|
|
|
36015 |
return result;
|
|
|
36016 |
};
|
|
|
36017 |
const findIndexes = (l, key) => l.reduce((a, e, i) => {
|
|
|
36018 |
if (e[key]) {
|
|
|
36019 |
a.push(i);
|
|
|
36020 |
}
|
|
|
36021 |
return a;
|
|
|
36022 |
}, []);
|
|
|
36023 |
/**
|
|
|
36024 |
* Returns a union of the included lists provided each element can be identified by a key.
|
|
|
36025 |
*
|
|
|
36026 |
* @param {Array} list - list of lists to get the union of
|
|
|
36027 |
* @param {Function} keyFunction - the function to use as a key for each element
|
|
|
36028 |
*
|
|
|
36029 |
* @return {Array} the union of the arrays
|
|
|
36030 |
*/
|
|
|
36031 |
|
|
|
36032 |
const union = (lists, keyFunction) => {
|
|
|
36033 |
return values(lists.reduce((acc, list) => {
|
|
|
36034 |
list.forEach(el => {
|
|
|
36035 |
acc[keyFunction(el)] = el;
|
|
|
36036 |
});
|
|
|
36037 |
return acc;
|
|
|
36038 |
}, {}));
|
|
|
36039 |
};
|
|
|
36040 |
var errors = {
|
|
|
36041 |
INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
|
|
|
36042 |
INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
|
|
|
36043 |
DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
|
|
|
36044 |
DASH_INVALID_XML: 'DASH_INVALID_XML',
|
|
|
36045 |
NO_BASE_URL: 'NO_BASE_URL',
|
|
|
36046 |
MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
|
|
|
36047 |
SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
|
|
|
36048 |
UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
|
|
|
36049 |
};
|
|
|
36050 |
|
|
|
36051 |
/**
|
|
|
36052 |
* @typedef {Object} SingleUri
|
|
|
36053 |
* @property {string} uri - relative location of segment
|
|
|
36054 |
* @property {string} resolvedUri - resolved location of segment
|
|
|
36055 |
* @property {Object} byterange - Object containing information on how to make byte range
|
|
|
36056 |
* requests following byte-range-spec per RFC2616.
|
|
|
36057 |
* @property {String} byterange.length - length of range request
|
|
|
36058 |
* @property {String} byterange.offset - byte offset of range request
|
|
|
36059 |
*
|
|
|
36060 |
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
|
|
|
36061 |
*/
|
|
|
36062 |
|
|
|
36063 |
/**
|
|
|
36064 |
* Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
|
|
|
36065 |
* that conforms to how m3u8-parser is structured
|
|
|
36066 |
*
|
|
|
36067 |
* @see https://github.com/videojs/m3u8-parser
|
|
|
36068 |
*
|
|
|
36069 |
* @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
|
|
|
36070 |
* @param {string} source - source url for segment
|
|
|
36071 |
* @param {string} range - optional range used for range calls,
|
|
|
36072 |
* follows RFC 2616, Clause 14.35.1
|
|
|
36073 |
* @return {SingleUri} full segment information transformed into a format similar
|
|
|
36074 |
* to m3u8-parser
|
|
|
36075 |
*/
|
|
|
36076 |
|
|
|
36077 |
const urlTypeToSegment = ({
|
|
|
36078 |
baseUrl = '',
|
|
|
36079 |
source = '',
|
|
|
36080 |
range = '',
|
|
|
36081 |
indexRange = ''
|
|
|
36082 |
}) => {
|
|
|
36083 |
const segment = {
|
|
|
36084 |
uri: source,
|
|
|
36085 |
resolvedUri: resolveUrl$1(baseUrl || '', source)
|
|
|
36086 |
};
|
|
|
36087 |
if (range || indexRange) {
|
|
|
36088 |
const rangeStr = range ? range : indexRange;
|
|
|
36089 |
const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible
|
|
|
36090 |
|
|
|
36091 |
let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10);
|
|
|
36092 |
let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER
|
|
|
36093 |
|
|
|
36094 |
if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') {
|
|
|
36095 |
startRange = Number(startRange);
|
|
|
36096 |
}
|
|
|
36097 |
if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') {
|
|
|
36098 |
endRange = Number(endRange);
|
|
|
36099 |
}
|
|
|
36100 |
let length;
|
|
|
36101 |
if (typeof endRange === 'bigint' || typeof startRange === 'bigint') {
|
|
|
36102 |
length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1);
|
|
|
36103 |
} else {
|
|
|
36104 |
length = endRange - startRange + 1;
|
|
|
36105 |
}
|
|
|
36106 |
if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) {
|
|
|
36107 |
length = Number(length);
|
|
|
36108 |
} // byterange should be inclusive according to
|
|
|
36109 |
// RFC 2616, Clause 14.35.1
|
|
|
36110 |
|
|
|
36111 |
segment.byterange = {
|
|
|
36112 |
length,
|
|
|
36113 |
offset: startRange
|
|
|
36114 |
};
|
|
|
36115 |
}
|
|
|
36116 |
return segment;
|
|
|
36117 |
};
|
|
|
36118 |
const byteRangeToString = byterange => {
|
|
|
36119 |
// `endRange` is one less than `offset + length` because the HTTP range
|
|
|
36120 |
// header uses inclusive ranges
|
|
|
36121 |
let endRange;
|
|
|
36122 |
if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
|
|
|
36123 |
endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
|
|
|
36124 |
} else {
|
|
|
36125 |
endRange = byterange.offset + byterange.length - 1;
|
|
|
36126 |
}
|
|
|
36127 |
return `${byterange.offset}-${endRange}`;
|
|
|
36128 |
};
|
|
|
36129 |
|
|
|
36130 |
/**
|
|
|
36131 |
* parse the end number attribue that can be a string
|
|
|
36132 |
* number, or undefined.
|
|
|
36133 |
*
|
|
|
36134 |
* @param {string|number|undefined} endNumber
|
|
|
36135 |
* The end number attribute.
|
|
|
36136 |
*
|
|
|
36137 |
* @return {number|null}
|
|
|
36138 |
* The result of parsing the end number.
|
|
|
36139 |
*/
|
|
|
36140 |
|
|
|
36141 |
const parseEndNumber = endNumber => {
|
|
|
36142 |
if (endNumber && typeof endNumber !== 'number') {
|
|
|
36143 |
endNumber = parseInt(endNumber, 10);
|
|
|
36144 |
}
|
|
|
36145 |
if (isNaN(endNumber)) {
|
|
|
36146 |
return null;
|
|
|
36147 |
}
|
|
|
36148 |
return endNumber;
|
|
|
36149 |
};
|
|
|
36150 |
/**
|
|
|
36151 |
* Functions for calculating the range of available segments in static and dynamic
|
|
|
36152 |
* manifests.
|
|
|
36153 |
*/
|
|
|
36154 |
|
|
|
36155 |
const segmentRange = {
|
|
|
36156 |
/**
|
|
|
36157 |
* Returns the entire range of available segments for a static MPD
|
|
|
36158 |
*
|
|
|
36159 |
* @param {Object} attributes
|
|
|
36160 |
* Inheritied MPD attributes
|
|
|
36161 |
* @return {{ start: number, end: number }}
|
|
|
36162 |
* The start and end numbers for available segments
|
|
|
36163 |
*/
|
|
|
36164 |
static(attributes) {
|
|
|
36165 |
const {
|
|
|
36166 |
duration,
|
|
|
36167 |
timescale = 1,
|
|
|
36168 |
sourceDuration,
|
|
|
36169 |
periodDuration
|
|
|
36170 |
} = attributes;
|
|
|
36171 |
const endNumber = parseEndNumber(attributes.endNumber);
|
|
|
36172 |
const segmentDuration = duration / timescale;
|
|
|
36173 |
if (typeof endNumber === 'number') {
|
|
|
36174 |
return {
|
|
|
36175 |
start: 0,
|
|
|
36176 |
end: endNumber
|
|
|
36177 |
};
|
|
|
36178 |
}
|
|
|
36179 |
if (typeof periodDuration === 'number') {
|
|
|
36180 |
return {
|
|
|
36181 |
start: 0,
|
|
|
36182 |
end: periodDuration / segmentDuration
|
|
|
36183 |
};
|
|
|
36184 |
}
|
|
|
36185 |
return {
|
|
|
36186 |
start: 0,
|
|
|
36187 |
end: sourceDuration / segmentDuration
|
|
|
36188 |
};
|
|
|
36189 |
},
|
|
|
36190 |
/**
|
|
|
36191 |
* Returns the current live window range of available segments for a dynamic MPD
|
|
|
36192 |
*
|
|
|
36193 |
* @param {Object} attributes
|
|
|
36194 |
* Inheritied MPD attributes
|
|
|
36195 |
* @return {{ start: number, end: number }}
|
|
|
36196 |
* The start and end numbers for available segments
|
|
|
36197 |
*/
|
|
|
36198 |
dynamic(attributes) {
|
|
|
36199 |
const {
|
|
|
36200 |
NOW,
|
|
|
36201 |
clientOffset,
|
|
|
36202 |
availabilityStartTime,
|
|
|
36203 |
timescale = 1,
|
|
|
36204 |
duration,
|
|
|
36205 |
periodStart = 0,
|
|
|
36206 |
minimumUpdatePeriod = 0,
|
|
|
36207 |
timeShiftBufferDepth = Infinity
|
|
|
36208 |
} = attributes;
|
|
|
36209 |
const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated
|
|
|
36210 |
// after retrieving UTC server time.
|
|
|
36211 |
|
|
|
36212 |
const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock.
|
|
|
36213 |
// Convert the period start time to EPOCH.
|
|
|
36214 |
|
|
|
36215 |
const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update.
|
|
|
36216 |
|
|
|
36217 |
const periodEndWC = now + minimumUpdatePeriod;
|
|
|
36218 |
const periodDuration = periodEndWC - periodStartWC;
|
|
|
36219 |
const segmentCount = Math.ceil(periodDuration * timescale / duration);
|
|
|
36220 |
const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
|
|
|
36221 |
const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
|
|
|
36222 |
return {
|
|
|
36223 |
start: Math.max(0, availableStart),
|
|
|
36224 |
end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
|
|
|
36225 |
};
|
|
|
36226 |
}
|
|
|
36227 |
};
|
|
|
36228 |
/**
|
|
|
36229 |
* Maps a range of numbers to objects with information needed to build the corresponding
|
|
|
36230 |
* segment list
|
|
|
36231 |
*
|
|
|
36232 |
* @name toSegmentsCallback
|
|
|
36233 |
* @function
|
|
|
36234 |
* @param {number} number
|
|
|
36235 |
* Number of the segment
|
|
|
36236 |
* @param {number} index
|
|
|
36237 |
* Index of the number in the range list
|
|
|
36238 |
* @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
|
|
|
36239 |
* Object with segment timing and duration info
|
|
|
36240 |
*/
|
|
|
36241 |
|
|
|
36242 |
/**
|
|
|
36243 |
* Returns a callback for Array.prototype.map for mapping a range of numbers to
|
|
|
36244 |
* information needed to build the segment list.
|
|
|
36245 |
*
|
|
|
36246 |
* @param {Object} attributes
|
|
|
36247 |
* Inherited MPD attributes
|
|
|
36248 |
* @return {toSegmentsCallback}
|
|
|
36249 |
* Callback map function
|
|
|
36250 |
*/
|
|
|
36251 |
|
|
|
36252 |
const toSegments = attributes => number => {
|
|
|
36253 |
const {
|
|
|
36254 |
duration,
|
|
|
36255 |
timescale = 1,
|
|
|
36256 |
periodStart,
|
|
|
36257 |
startNumber = 1
|
|
|
36258 |
} = attributes;
|
|
|
36259 |
return {
|
|
|
36260 |
number: startNumber + number,
|
|
|
36261 |
duration: duration / timescale,
|
|
|
36262 |
timeline: periodStart,
|
|
|
36263 |
time: number * duration
|
|
|
36264 |
};
|
|
|
36265 |
};
|
|
|
36266 |
/**
|
|
|
36267 |
* Returns a list of objects containing segment timing and duration info used for
|
|
|
36268 |
* building the list of segments. This uses the @duration attribute specified
|
|
|
36269 |
* in the MPD manifest to derive the range of segments.
|
|
|
36270 |
*
|
|
|
36271 |
* @param {Object} attributes
|
|
|
36272 |
* Inherited MPD attributes
|
|
|
36273 |
* @return {{number: number, duration: number, time: number, timeline: number}[]}
|
|
|
36274 |
* List of Objects with segment timing and duration info
|
|
|
36275 |
*/
|
|
|
36276 |
|
|
|
36277 |
const parseByDuration = attributes => {
|
|
|
36278 |
const {
|
|
|
36279 |
type,
|
|
|
36280 |
duration,
|
|
|
36281 |
timescale = 1,
|
|
|
36282 |
periodDuration,
|
|
|
36283 |
sourceDuration
|
|
|
36284 |
} = attributes;
|
|
|
36285 |
const {
|
|
|
36286 |
start,
|
|
|
36287 |
end
|
|
|
36288 |
} = segmentRange[type](attributes);
|
|
|
36289 |
const segments = range(start, end).map(toSegments(attributes));
|
|
|
36290 |
if (type === 'static') {
|
|
|
36291 |
const index = segments.length - 1; // section is either a period or the full source
|
|
|
36292 |
|
|
|
36293 |
const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration
|
|
|
36294 |
|
|
|
36295 |
segments[index].duration = sectionDuration - duration / timescale * index;
|
|
|
36296 |
}
|
|
|
36297 |
return segments;
|
|
|
36298 |
};
|
|
|
36299 |
|
|
|
36300 |
/**
|
|
|
36301 |
* Translates SegmentBase into a set of segments.
|
|
|
36302 |
* (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
|
|
|
36303 |
* node should be translated into segment.
|
|
|
36304 |
*
|
|
|
36305 |
* @param {Object} attributes
|
|
|
36306 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
36307 |
* names as keys
|
|
|
36308 |
* @return {Object.<Array>} list of segments
|
|
|
36309 |
*/
|
|
|
36310 |
|
|
|
36311 |
const segmentsFromBase = attributes => {
|
|
|
36312 |
const {
|
|
|
36313 |
baseUrl,
|
|
|
36314 |
initialization = {},
|
|
|
36315 |
sourceDuration,
|
|
|
36316 |
indexRange = '',
|
|
|
36317 |
periodStart,
|
|
|
36318 |
presentationTime,
|
|
|
36319 |
number = 0,
|
|
|
36320 |
duration
|
|
|
36321 |
} = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
|
|
|
36322 |
|
|
|
36323 |
if (!baseUrl) {
|
|
|
36324 |
throw new Error(errors.NO_BASE_URL);
|
|
|
36325 |
}
|
|
|
36326 |
const initSegment = urlTypeToSegment({
|
|
|
36327 |
baseUrl,
|
|
|
36328 |
source: initialization.sourceURL,
|
|
|
36329 |
range: initialization.range
|
|
|
36330 |
});
|
|
|
36331 |
const segment = urlTypeToSegment({
|
|
|
36332 |
baseUrl,
|
|
|
36333 |
source: baseUrl,
|
|
|
36334 |
indexRange
|
|
|
36335 |
});
|
|
|
36336 |
segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source
|
|
|
36337 |
// (since SegmentBase is only for one total segment)
|
|
|
36338 |
|
|
|
36339 |
if (duration) {
|
|
|
36340 |
const segmentTimeInfo = parseByDuration(attributes);
|
|
|
36341 |
if (segmentTimeInfo.length) {
|
|
|
36342 |
segment.duration = segmentTimeInfo[0].duration;
|
|
|
36343 |
segment.timeline = segmentTimeInfo[0].timeline;
|
|
|
36344 |
}
|
|
|
36345 |
} else if (sourceDuration) {
|
|
|
36346 |
segment.duration = sourceDuration;
|
|
|
36347 |
segment.timeline = periodStart;
|
|
|
36348 |
} // If presentation time is provided, these segments are being generated by SIDX
|
|
|
36349 |
// references, and should use the time provided. For the general case of SegmentBase,
|
|
|
36350 |
// there should only be one segment in the period, so its presentation time is the same
|
|
|
36351 |
// as its period start.
|
|
|
36352 |
|
|
|
36353 |
segment.presentationTime = presentationTime || periodStart;
|
|
|
36354 |
segment.number = number;
|
|
|
36355 |
return [segment];
|
|
|
36356 |
};
|
|
|
36357 |
/**
|
|
|
36358 |
* Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
|
|
|
36359 |
* according to the sidx information given.
|
|
|
36360 |
*
|
|
|
36361 |
* playlist.sidx has metadadata about the sidx where-as the sidx param
|
|
|
36362 |
* is the parsed sidx box itself.
|
|
|
36363 |
*
|
|
|
36364 |
* @param {Object} playlist the playlist to update the sidx information for
|
|
|
36365 |
* @param {Object} sidx the parsed sidx box
|
|
|
36366 |
* @return {Object} the playlist object with the updated sidx information
|
|
|
36367 |
*/
|
|
|
36368 |
|
|
|
36369 |
const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => {
|
|
|
36370 |
// Retain init segment information
|
|
|
36371 |
const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing
|
|
|
36372 |
|
|
|
36373 |
const sourceDuration = playlist.sidx.duration; // Retain source timeline
|
|
|
36374 |
|
|
|
36375 |
const timeline = playlist.timeline || 0;
|
|
|
36376 |
const sidxByteRange = playlist.sidx.byterange;
|
|
|
36377 |
const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx
|
|
|
36378 |
|
|
|
36379 |
const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes
|
|
|
36380 |
|
|
|
36381 |
const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
|
|
|
36382 |
const segments = [];
|
|
|
36383 |
const type = playlist.endList ? 'static' : 'dynamic';
|
|
|
36384 |
const periodStart = playlist.sidx.timeline;
|
|
|
36385 |
let presentationTime = periodStart;
|
|
|
36386 |
let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box
|
|
|
36387 |
|
|
|
36388 |
let startIndex; // eslint-disable-next-line
|
|
|
36389 |
|
|
|
36390 |
if (typeof sidx.firstOffset === 'bigint') {
|
|
|
36391 |
startIndex = window.BigInt(sidxEnd) + sidx.firstOffset;
|
|
|
36392 |
} else {
|
|
|
36393 |
startIndex = sidxEnd + sidx.firstOffset;
|
|
|
36394 |
}
|
|
|
36395 |
for (let i = 0; i < mediaReferences.length; i++) {
|
|
|
36396 |
const reference = sidx.references[i]; // size of the referenced (sub)segment
|
|
|
36397 |
|
|
|
36398 |
const size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale
|
|
|
36399 |
// this will be converted to seconds when generating segments
|
|
|
36400 |
|
|
|
36401 |
const duration = reference.subsegmentDuration; // should be an inclusive range
|
|
|
36402 |
|
|
|
36403 |
let endIndex; // eslint-disable-next-line
|
|
|
36404 |
|
|
|
36405 |
if (typeof startIndex === 'bigint') {
|
|
|
36406 |
endIndex = startIndex + window.BigInt(size) - window.BigInt(1);
|
|
|
36407 |
} else {
|
|
|
36408 |
endIndex = startIndex + size - 1;
|
|
|
36409 |
}
|
|
|
36410 |
const indexRange = `${startIndex}-${endIndex}`;
|
|
|
36411 |
const attributes = {
|
|
|
36412 |
baseUrl,
|
|
|
36413 |
timescale,
|
|
|
36414 |
timeline,
|
|
|
36415 |
periodStart,
|
|
|
36416 |
presentationTime,
|
|
|
36417 |
number,
|
|
|
36418 |
duration,
|
|
|
36419 |
sourceDuration,
|
|
|
36420 |
indexRange,
|
|
|
36421 |
type
|
|
|
36422 |
};
|
|
|
36423 |
const segment = segmentsFromBase(attributes)[0];
|
|
|
36424 |
if (initSegment) {
|
|
|
36425 |
segment.map = initSegment;
|
|
|
36426 |
}
|
|
|
36427 |
segments.push(segment);
|
|
|
36428 |
if (typeof startIndex === 'bigint') {
|
|
|
36429 |
startIndex += window.BigInt(size);
|
|
|
36430 |
} else {
|
|
|
36431 |
startIndex += size;
|
|
|
36432 |
}
|
|
|
36433 |
presentationTime += duration / timescale;
|
|
|
36434 |
number++;
|
|
|
36435 |
}
|
|
|
36436 |
playlist.segments = segments;
|
|
|
36437 |
return playlist;
|
|
|
36438 |
};
|
|
|
36439 |
const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen)
|
|
|
36440 |
|
|
|
36441 |
const TIME_FUDGE = 1 / 60;
|
|
|
36442 |
/**
|
|
|
36443 |
* Given a list of timelineStarts, combines, dedupes, and sorts them.
|
|
|
36444 |
*
|
|
|
36445 |
* @param {TimelineStart[]} timelineStarts - list of timeline starts
|
|
|
36446 |
*
|
|
|
36447 |
* @return {TimelineStart[]} the combined and deduped timeline starts
|
|
|
36448 |
*/
|
|
|
36449 |
|
|
|
36450 |
const getUniqueTimelineStarts = timelineStarts => {
|
|
|
36451 |
return union(timelineStarts, ({
|
|
|
36452 |
timeline
|
|
|
36453 |
}) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1);
|
|
|
36454 |
};
|
|
|
36455 |
/**
|
|
|
36456 |
* Finds the playlist with the matching NAME attribute.
|
|
|
36457 |
*
|
|
|
36458 |
* @param {Array} playlists - playlists to search through
|
|
|
36459 |
* @param {string} name - the NAME attribute to search for
|
|
|
36460 |
*
|
|
|
36461 |
* @return {Object|null} the matching playlist object, or null
|
|
|
36462 |
*/
|
|
|
36463 |
|
|
|
36464 |
const findPlaylistWithName = (playlists, name) => {
|
|
|
36465 |
for (let i = 0; i < playlists.length; i++) {
|
|
|
36466 |
if (playlists[i].attributes.NAME === name) {
|
|
|
36467 |
return playlists[i];
|
|
|
36468 |
}
|
|
|
36469 |
}
|
|
|
36470 |
return null;
|
|
|
36471 |
};
|
|
|
36472 |
/**
|
|
|
36473 |
* Gets a flattened array of media group playlists.
|
|
|
36474 |
*
|
|
|
36475 |
* @param {Object} manifest - the main manifest object
|
|
|
36476 |
*
|
|
|
36477 |
* @return {Array} the media group playlists
|
|
|
36478 |
*/
|
|
|
36479 |
|
|
|
36480 |
const getMediaGroupPlaylists = manifest => {
|
|
|
36481 |
let mediaGroupPlaylists = [];
|
|
|
36482 |
forEachMediaGroup$1(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
|
|
|
36483 |
mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
|
|
|
36484 |
});
|
|
|
36485 |
return mediaGroupPlaylists;
|
|
|
36486 |
};
|
|
|
36487 |
/**
|
|
|
36488 |
* Updates the playlist's media sequence numbers.
|
|
|
36489 |
*
|
|
|
36490 |
* @param {Object} config - options object
|
|
|
36491 |
* @param {Object} config.playlist - the playlist to update
|
|
|
36492 |
* @param {number} config.mediaSequence - the mediaSequence number to start with
|
|
|
36493 |
*/
|
|
|
36494 |
|
|
|
36495 |
const updateMediaSequenceForPlaylist = ({
|
|
|
36496 |
playlist,
|
|
|
36497 |
mediaSequence
|
|
|
36498 |
}) => {
|
|
|
36499 |
playlist.mediaSequence = mediaSequence;
|
|
|
36500 |
playlist.segments.forEach((segment, index) => {
|
|
|
36501 |
segment.number = playlist.mediaSequence + index;
|
|
|
36502 |
});
|
|
|
36503 |
};
|
|
|
36504 |
/**
|
|
|
36505 |
* Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists
|
|
|
36506 |
* and a complete list of timeline starts.
|
|
|
36507 |
*
|
|
|
36508 |
* If no matching playlist is found, only the discontinuity sequence number of the playlist
|
|
|
36509 |
* will be updated.
|
|
|
36510 |
*
|
|
|
36511 |
* Since early available timelines are not supported, at least one segment must be present.
|
|
|
36512 |
*
|
|
|
36513 |
* @param {Object} config - options object
|
|
|
36514 |
* @param {Object[]} oldPlaylists - the old playlists to use as a reference
|
|
|
36515 |
* @param {Object[]} newPlaylists - the new playlists to update
|
|
|
36516 |
* @param {Object} timelineStarts - all timelineStarts seen in the stream to this point
|
|
|
36517 |
*/
|
|
|
36518 |
|
|
|
36519 |
const updateSequenceNumbers = ({
|
|
|
36520 |
oldPlaylists,
|
|
|
36521 |
newPlaylists,
|
|
|
36522 |
timelineStarts
|
|
|
36523 |
}) => {
|
|
|
36524 |
newPlaylists.forEach(playlist => {
|
|
|
36525 |
playlist.discontinuitySequence = timelineStarts.findIndex(function ({
|
|
|
36526 |
timeline
|
|
|
36527 |
}) {
|
|
|
36528 |
return timeline === playlist.timeline;
|
|
|
36529 |
}); // Playlists NAMEs come from DASH Representation IDs, which are mandatory
|
|
|
36530 |
// (see ISO_23009-1-2012 5.3.5.2).
|
|
|
36531 |
//
|
|
|
36532 |
// If the same Representation existed in a prior Period, it will retain the same NAME.
|
|
|
36533 |
|
|
|
36534 |
const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME);
|
|
|
36535 |
if (!oldPlaylist) {
|
|
|
36536 |
// Since this is a new playlist, the media sequence values can start from 0 without
|
|
|
36537 |
// consequence.
|
|
|
36538 |
return;
|
|
|
36539 |
} // TODO better support for live SIDX
|
|
|
36540 |
//
|
|
|
36541 |
// As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD).
|
|
|
36542 |
// This is evident by a playlist only having a single SIDX reference. In a multiperiod
|
|
|
36543 |
// playlist there would need to be multiple SIDX references. In addition, live SIDX is
|
|
|
36544 |
// not supported when the SIDX properties change on refreshes.
|
|
|
36545 |
//
|
|
|
36546 |
// In the future, if support needs to be added, the merging logic here can be called
|
|
|
36547 |
// after SIDX references are resolved. For now, exit early to prevent exceptions being
|
|
|
36548 |
// thrown due to undefined references.
|
|
|
36549 |
|
|
|
36550 |
if (playlist.sidx) {
|
|
|
36551 |
return;
|
|
|
36552 |
} // Since we don't yet support early available timelines, we don't need to support
|
|
|
36553 |
// playlists with no segments.
|
|
|
36554 |
|
|
|
36555 |
const firstNewSegment = playlist.segments[0];
|
|
|
36556 |
const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) {
|
|
|
36557 |
return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE;
|
|
|
36558 |
}); // No matching segment from the old playlist means the entire playlist was refreshed.
|
|
|
36559 |
// In this case the media sequence should account for this update, and the new segments
|
|
|
36560 |
// should be marked as discontinuous from the prior content, since the last prior
|
|
|
36561 |
// timeline was removed.
|
|
|
36562 |
|
|
|
36563 |
if (oldMatchingSegmentIndex === -1) {
|
|
|
36564 |
updateMediaSequenceForPlaylist({
|
|
|
36565 |
playlist,
|
|
|
36566 |
mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length
|
|
|
36567 |
});
|
|
|
36568 |
playlist.segments[0].discontinuity = true;
|
|
|
36569 |
playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content.
|
|
|
36570 |
//
|
|
|
36571 |
// If the new playlist's timeline is the same as the last seen segment's timeline,
|
|
|
36572 |
// then a discontinuity can be added to identify that there's potentially missing
|
|
|
36573 |
// content. If there's no missing content, the discontinuity should still be rather
|
|
|
36574 |
// harmless. It's possible that if segment durations are accurate enough, that the
|
|
|
36575 |
// existence of a gap can be determined using the presentation times and durations,
|
|
|
36576 |
// but if the segment timing info is off, it may introduce more problems than simply
|
|
|
36577 |
// adding the discontinuity.
|
|
|
36578 |
//
|
|
|
36579 |
// If the new playlist's timeline is different from the last seen segment's timeline,
|
|
|
36580 |
// then a discontinuity can be added to identify that this is the first seen segment
|
|
|
36581 |
// of a new timeline. However, the logic at the start of this function that
|
|
|
36582 |
// determined the disconinuity sequence by timeline index is now off by one (the
|
|
|
36583 |
// discontinuity of the newest timeline hasn't yet fallen off the manifest...since
|
|
|
36584 |
// we added it), so the disconinuity sequence must be decremented.
|
|
|
36585 |
//
|
|
|
36586 |
// A period may also have a duration of zero, so the case of no segments is handled
|
|
|
36587 |
// here even though we don't yet support early available periods.
|
|
|
36588 |
|
|
|
36589 |
if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) {
|
|
|
36590 |
playlist.discontinuitySequence--;
|
|
|
36591 |
}
|
|
|
36592 |
return;
|
|
|
36593 |
} // If the first segment matched with a prior segment on a discontinuity (it's matching
|
|
|
36594 |
// on the first segment of a period), then the discontinuitySequence shouldn't be the
|
|
|
36595 |
// timeline's matching one, but instead should be the one prior, and the first segment
|
|
|
36596 |
// of the new manifest should be marked with a discontinuity.
|
|
|
36597 |
//
|
|
|
36598 |
// The reason for this special case is that discontinuity sequence shows how many
|
|
|
36599 |
// discontinuities have fallen off of the playlist, and discontinuities are marked on
|
|
|
36600 |
// the first segment of a new "timeline." Because of this, while DASH will retain that
|
|
|
36601 |
// Period while the "timeline" exists, HLS keeps track of it via the discontinuity
|
|
|
36602 |
// sequence, and that first segment is an indicator, but can be removed before that
|
|
|
36603 |
// timeline is gone.
|
|
|
36604 |
|
|
|
36605 |
const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex];
|
|
|
36606 |
if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) {
|
|
|
36607 |
firstNewSegment.discontinuity = true;
|
|
|
36608 |
playlist.discontinuityStarts.unshift(0);
|
|
|
36609 |
playlist.discontinuitySequence--;
|
|
|
36610 |
}
|
|
|
36611 |
updateMediaSequenceForPlaylist({
|
|
|
36612 |
playlist,
|
|
|
36613 |
mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number
|
|
|
36614 |
});
|
|
|
36615 |
});
|
|
|
36616 |
};
|
|
|
36617 |
/**
|
|
|
36618 |
* Given an old parsed manifest object and a new parsed manifest object, updates the
|
|
|
36619 |
* sequence and timing values within the new manifest to ensure that it lines up with the
|
|
|
36620 |
* old.
|
|
|
36621 |
*
|
|
|
36622 |
* @param {Array} oldManifest - the old main manifest object
|
|
|
36623 |
* @param {Array} newManifest - the new main manifest object
|
|
|
36624 |
*
|
|
|
36625 |
* @return {Object} the updated new manifest object
|
|
|
36626 |
*/
|
|
|
36627 |
|
|
|
36628 |
const positionManifestOnTimeline = ({
|
|
|
36629 |
oldManifest,
|
|
|
36630 |
newManifest
|
|
|
36631 |
}) => {
|
|
|
36632 |
// Starting from v4.1.2 of the IOP, section 4.4.3.3 states:
|
|
|
36633 |
//
|
|
|
36634 |
// "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates."
|
|
|
36635 |
//
|
|
|
36636 |
// This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160
|
|
|
36637 |
//
|
|
|
36638 |
// Because of this change, and the difficulty of supporting periods with changing start
|
|
|
36639 |
// times, periods with changing start times are not supported. This makes the logic much
|
|
|
36640 |
// simpler, since periods with the same start time can be considerred the same period
|
|
|
36641 |
// across refreshes.
|
|
|
36642 |
//
|
|
|
36643 |
// To give an example as to the difficulty of handling periods where the start time may
|
|
|
36644 |
// change, if a single period manifest is refreshed with another manifest with a single
|
|
|
36645 |
// period, and both the start and end times are increased, then the only way to determine
|
|
|
36646 |
// if it's a new period or an old one that has changed is to look through the segments of
|
|
|
36647 |
// each playlist and determine the presentation time bounds to find a match. In addition,
|
|
|
36648 |
// if the period start changed to exceed the old period end, then there would be no
|
|
|
36649 |
// match, and it would not be possible to determine whether the refreshed period is a new
|
|
|
36650 |
// one or the old one.
|
|
|
36651 |
const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest));
|
|
|
36652 |
const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that
|
|
|
36653 |
// there's a "memory leak" in that it will never stop growing, in reality, only a couple
|
|
|
36654 |
// of properties are saved for each seen Period. Even long running live streams won't
|
|
|
36655 |
// generate too many Periods, unless the stream is watched for decades. In the future,
|
|
|
36656 |
// this can be optimized by mapping to discontinuity sequence numbers for each timeline,
|
|
|
36657 |
// but it may not become an issue, and the additional info can be useful for debugging.
|
|
|
36658 |
|
|
|
36659 |
newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]);
|
|
|
36660 |
updateSequenceNumbers({
|
|
|
36661 |
oldPlaylists,
|
|
|
36662 |
newPlaylists,
|
|
|
36663 |
timelineStarts: newManifest.timelineStarts
|
|
|
36664 |
});
|
|
|
36665 |
return newManifest;
|
|
|
36666 |
};
|
|
|
36667 |
const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
|
|
|
36668 |
const mergeDiscontiguousPlaylists = playlists => {
|
|
|
36669 |
// Break out playlists into groups based on their baseUrl
|
|
|
36670 |
const playlistsByBaseUrl = playlists.reduce(function (acc, cur) {
|
|
|
36671 |
if (!acc[cur.attributes.baseUrl]) {
|
|
|
36672 |
acc[cur.attributes.baseUrl] = [];
|
|
|
36673 |
}
|
|
|
36674 |
acc[cur.attributes.baseUrl].push(cur);
|
|
|
36675 |
return acc;
|
|
|
36676 |
}, {});
|
|
|
36677 |
let allPlaylists = [];
|
|
|
36678 |
Object.values(playlistsByBaseUrl).forEach(playlistGroup => {
|
|
|
36679 |
const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
|
|
|
36680 |
// assuming playlist IDs are the same across periods
|
|
|
36681 |
// TODO: handle multiperiod where representation sets are not the same
|
|
|
36682 |
// across periods
|
|
|
36683 |
const name = playlist.attributes.id + (playlist.attributes.lang || '');
|
|
|
36684 |
if (!acc[name]) {
|
|
|
36685 |
// First Period
|
|
|
36686 |
acc[name] = playlist;
|
|
|
36687 |
acc[name].attributes.timelineStarts = [];
|
|
|
36688 |
} else {
|
|
|
36689 |
// Subsequent Periods
|
|
|
36690 |
if (playlist.segments) {
|
|
|
36691 |
// first segment of subsequent periods signal a discontinuity
|
|
|
36692 |
if (playlist.segments[0]) {
|
|
|
36693 |
playlist.segments[0].discontinuity = true;
|
|
|
36694 |
}
|
|
|
36695 |
acc[name].segments.push(...playlist.segments);
|
|
|
36696 |
} // bubble up contentProtection, this assumes all DRM content
|
|
|
36697 |
// has the same contentProtection
|
|
|
36698 |
|
|
|
36699 |
if (playlist.attributes.contentProtection) {
|
|
|
36700 |
acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
|
|
|
36701 |
}
|
|
|
36702 |
}
|
|
|
36703 |
acc[name].attributes.timelineStarts.push({
|
|
|
36704 |
// Although they represent the same number, it's important to have both to make it
|
|
|
36705 |
// compatible with HLS potentially having a similar attribute.
|
|
|
36706 |
start: playlist.attributes.periodStart,
|
|
|
36707 |
timeline: playlist.attributes.periodStart
|
|
|
36708 |
});
|
|
|
36709 |
return acc;
|
|
|
36710 |
}, {}));
|
|
|
36711 |
allPlaylists = allPlaylists.concat(mergedPlaylists);
|
|
|
36712 |
});
|
|
|
36713 |
return allPlaylists.map(playlist => {
|
|
|
36714 |
playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
|
|
|
36715 |
return playlist;
|
|
|
36716 |
});
|
|
|
36717 |
};
|
|
|
36718 |
const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
|
|
|
36719 |
const sidxKey = generateSidxKey(playlist.sidx);
|
|
|
36720 |
const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
|
|
|
36721 |
if (sidxMatch) {
|
|
|
36722 |
addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri);
|
|
|
36723 |
}
|
|
|
36724 |
return playlist;
|
|
|
36725 |
};
|
|
|
36726 |
const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
|
|
|
36727 |
if (!Object.keys(sidxMapping).length) {
|
|
|
36728 |
return playlists;
|
|
|
36729 |
}
|
|
|
36730 |
for (const i in playlists) {
|
|
|
36731 |
playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
|
|
|
36732 |
}
|
|
|
36733 |
return playlists;
|
|
|
36734 |
};
|
|
|
36735 |
const formatAudioPlaylist = ({
|
|
|
36736 |
attributes,
|
|
|
36737 |
segments,
|
|
|
36738 |
sidx,
|
|
|
36739 |
mediaSequence,
|
|
|
36740 |
discontinuitySequence,
|
|
|
36741 |
discontinuityStarts
|
|
|
36742 |
}, isAudioOnly) => {
|
|
|
36743 |
const playlist = {
|
|
|
36744 |
attributes: {
|
|
|
36745 |
NAME: attributes.id,
|
|
|
36746 |
BANDWIDTH: attributes.bandwidth,
|
|
|
36747 |
CODECS: attributes.codecs,
|
|
|
36748 |
['PROGRAM-ID']: 1
|
|
|
36749 |
},
|
|
|
36750 |
uri: '',
|
|
|
36751 |
endList: attributes.type === 'static',
|
|
|
36752 |
timeline: attributes.periodStart,
|
|
|
36753 |
resolvedUri: attributes.baseUrl || '',
|
|
|
36754 |
targetDuration: attributes.duration,
|
|
|
36755 |
discontinuitySequence,
|
|
|
36756 |
discontinuityStarts,
|
|
|
36757 |
timelineStarts: attributes.timelineStarts,
|
|
|
36758 |
mediaSequence,
|
|
|
36759 |
segments
|
|
|
36760 |
};
|
|
|
36761 |
if (attributes.contentProtection) {
|
|
|
36762 |
playlist.contentProtection = attributes.contentProtection;
|
|
|
36763 |
}
|
|
|
36764 |
if (attributes.serviceLocation) {
|
|
|
36765 |
playlist.attributes.serviceLocation = attributes.serviceLocation;
|
|
|
36766 |
}
|
|
|
36767 |
if (sidx) {
|
|
|
36768 |
playlist.sidx = sidx;
|
|
|
36769 |
}
|
|
|
36770 |
if (isAudioOnly) {
|
|
|
36771 |
playlist.attributes.AUDIO = 'audio';
|
|
|
36772 |
playlist.attributes.SUBTITLES = 'subs';
|
|
|
36773 |
}
|
|
|
36774 |
return playlist;
|
|
|
36775 |
};
|
|
|
36776 |
const formatVttPlaylist = ({
|
|
|
36777 |
attributes,
|
|
|
36778 |
segments,
|
|
|
36779 |
mediaSequence,
|
|
|
36780 |
discontinuityStarts,
|
|
|
36781 |
discontinuitySequence
|
|
|
36782 |
}) => {
|
|
|
36783 |
if (typeof segments === 'undefined') {
|
|
|
36784 |
// vtt tracks may use single file in BaseURL
|
|
|
36785 |
segments = [{
|
|
|
36786 |
uri: attributes.baseUrl,
|
|
|
36787 |
timeline: attributes.periodStart,
|
|
|
36788 |
resolvedUri: attributes.baseUrl || '',
|
|
|
36789 |
duration: attributes.sourceDuration,
|
|
|
36790 |
number: 0
|
|
|
36791 |
}]; // targetDuration should be the same duration as the only segment
|
|
|
36792 |
|
|
|
36793 |
attributes.duration = attributes.sourceDuration;
|
|
|
36794 |
}
|
|
|
36795 |
const m3u8Attributes = {
|
|
|
36796 |
NAME: attributes.id,
|
|
|
36797 |
BANDWIDTH: attributes.bandwidth,
|
|
|
36798 |
['PROGRAM-ID']: 1
|
|
|
36799 |
};
|
|
|
36800 |
if (attributes.codecs) {
|
|
|
36801 |
m3u8Attributes.CODECS = attributes.codecs;
|
|
|
36802 |
}
|
|
|
36803 |
const vttPlaylist = {
|
|
|
36804 |
attributes: m3u8Attributes,
|
|
|
36805 |
uri: '',
|
|
|
36806 |
endList: attributes.type === 'static',
|
|
|
36807 |
timeline: attributes.periodStart,
|
|
|
36808 |
resolvedUri: attributes.baseUrl || '',
|
|
|
36809 |
targetDuration: attributes.duration,
|
|
|
36810 |
timelineStarts: attributes.timelineStarts,
|
|
|
36811 |
discontinuityStarts,
|
|
|
36812 |
discontinuitySequence,
|
|
|
36813 |
mediaSequence,
|
|
|
36814 |
segments
|
|
|
36815 |
};
|
|
|
36816 |
if (attributes.serviceLocation) {
|
|
|
36817 |
vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
|
|
|
36818 |
}
|
|
|
36819 |
return vttPlaylist;
|
|
|
36820 |
};
|
|
|
36821 |
const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
|
|
|
36822 |
let mainPlaylist;
|
|
|
36823 |
const formattedPlaylists = playlists.reduce((a, playlist) => {
|
|
|
36824 |
const role = playlist.attributes.role && playlist.attributes.role.value || '';
|
|
|
36825 |
const language = playlist.attributes.lang || '';
|
|
|
36826 |
let label = playlist.attributes.label || 'main';
|
|
|
36827 |
if (language && !playlist.attributes.label) {
|
|
|
36828 |
const roleLabel = role ? ` (${role})` : '';
|
|
|
36829 |
label = `${playlist.attributes.lang}${roleLabel}`;
|
|
|
36830 |
}
|
|
|
36831 |
if (!a[label]) {
|
|
|
36832 |
a[label] = {
|
|
|
36833 |
language,
|
|
|
36834 |
autoselect: true,
|
|
|
36835 |
default: role === 'main',
|
|
|
36836 |
playlists: [],
|
|
|
36837 |
uri: ''
|
|
|
36838 |
};
|
|
|
36839 |
}
|
|
|
36840 |
const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);
|
|
|
36841 |
a[label].playlists.push(formatted);
|
|
|
36842 |
if (typeof mainPlaylist === 'undefined' && role === 'main') {
|
|
|
36843 |
mainPlaylist = playlist;
|
|
|
36844 |
mainPlaylist.default = true;
|
|
|
36845 |
}
|
|
|
36846 |
return a;
|
|
|
36847 |
}, {}); // if no playlists have role "main", mark the first as main
|
|
|
36848 |
|
|
|
36849 |
if (!mainPlaylist) {
|
|
|
36850 |
const firstLabel = Object.keys(formattedPlaylists)[0];
|
|
|
36851 |
formattedPlaylists[firstLabel].default = true;
|
|
|
36852 |
}
|
|
|
36853 |
return formattedPlaylists;
|
|
|
36854 |
};
|
|
|
36855 |
const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
|
|
|
36856 |
return playlists.reduce((a, playlist) => {
|
|
|
36857 |
const label = playlist.attributes.label || playlist.attributes.lang || 'text';
|
|
|
36858 |
if (!a[label]) {
|
|
|
36859 |
a[label] = {
|
|
|
36860 |
language: label,
|
|
|
36861 |
default: false,
|
|
|
36862 |
autoselect: false,
|
|
|
36863 |
playlists: [],
|
|
|
36864 |
uri: ''
|
|
|
36865 |
};
|
|
|
36866 |
}
|
|
|
36867 |
a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));
|
|
|
36868 |
return a;
|
|
|
36869 |
}, {});
|
|
|
36870 |
};
|
|
|
36871 |
const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => {
|
|
|
36872 |
if (!svc) {
|
|
|
36873 |
return svcObj;
|
|
|
36874 |
}
|
|
|
36875 |
svc.forEach(service => {
|
|
|
36876 |
const {
|
|
|
36877 |
channel,
|
|
|
36878 |
language
|
|
|
36879 |
} = service;
|
|
|
36880 |
svcObj[language] = {
|
|
|
36881 |
autoselect: false,
|
|
|
36882 |
default: false,
|
|
|
36883 |
instreamId: channel,
|
|
|
36884 |
language
|
|
|
36885 |
};
|
|
|
36886 |
if (service.hasOwnProperty('aspectRatio')) {
|
|
|
36887 |
svcObj[language].aspectRatio = service.aspectRatio;
|
|
|
36888 |
}
|
|
|
36889 |
if (service.hasOwnProperty('easyReader')) {
|
|
|
36890 |
svcObj[language].easyReader = service.easyReader;
|
|
|
36891 |
}
|
|
|
36892 |
if (service.hasOwnProperty('3D')) {
|
|
|
36893 |
svcObj[language]['3D'] = service['3D'];
|
|
|
36894 |
}
|
|
|
36895 |
});
|
|
|
36896 |
return svcObj;
|
|
|
36897 |
}, {});
|
|
|
36898 |
const formatVideoPlaylist = ({
|
|
|
36899 |
attributes,
|
|
|
36900 |
segments,
|
|
|
36901 |
sidx,
|
|
|
36902 |
discontinuityStarts
|
|
|
36903 |
}) => {
|
|
|
36904 |
const playlist = {
|
|
|
36905 |
attributes: {
|
|
|
36906 |
NAME: attributes.id,
|
|
|
36907 |
AUDIO: 'audio',
|
|
|
36908 |
SUBTITLES: 'subs',
|
|
|
36909 |
RESOLUTION: {
|
|
|
36910 |
width: attributes.width,
|
|
|
36911 |
height: attributes.height
|
|
|
36912 |
},
|
|
|
36913 |
CODECS: attributes.codecs,
|
|
|
36914 |
BANDWIDTH: attributes.bandwidth,
|
|
|
36915 |
['PROGRAM-ID']: 1
|
|
|
36916 |
},
|
|
|
36917 |
uri: '',
|
|
|
36918 |
endList: attributes.type === 'static',
|
|
|
36919 |
timeline: attributes.periodStart,
|
|
|
36920 |
resolvedUri: attributes.baseUrl || '',
|
|
|
36921 |
targetDuration: attributes.duration,
|
|
|
36922 |
discontinuityStarts,
|
|
|
36923 |
timelineStarts: attributes.timelineStarts,
|
|
|
36924 |
segments
|
|
|
36925 |
};
|
|
|
36926 |
if (attributes.frameRate) {
|
|
|
36927 |
playlist.attributes['FRAME-RATE'] = attributes.frameRate;
|
|
|
36928 |
}
|
|
|
36929 |
if (attributes.contentProtection) {
|
|
|
36930 |
playlist.contentProtection = attributes.contentProtection;
|
|
|
36931 |
}
|
|
|
36932 |
if (attributes.serviceLocation) {
|
|
|
36933 |
playlist.attributes.serviceLocation = attributes.serviceLocation;
|
|
|
36934 |
}
|
|
|
36935 |
if (sidx) {
|
|
|
36936 |
playlist.sidx = sidx;
|
|
|
36937 |
}
|
|
|
36938 |
return playlist;
|
|
|
36939 |
};
|
|
|
36940 |
const videoOnly = ({
|
|
|
36941 |
attributes
|
|
|
36942 |
}) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
|
|
|
36943 |
const audioOnly = ({
|
|
|
36944 |
attributes
|
|
|
36945 |
}) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
|
|
|
36946 |
const vttOnly = ({
|
|
|
36947 |
attributes
|
|
|
36948 |
}) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
|
|
|
36949 |
/**
|
|
|
36950 |
* Contains start and timeline properties denoting a timeline start. For DASH, these will
|
|
|
36951 |
* be the same number.
|
|
|
36952 |
*
|
|
|
36953 |
* @typedef {Object} TimelineStart
|
|
|
36954 |
* @property {number} start - the start time of the timeline
|
|
|
36955 |
* @property {number} timeline - the timeline number
|
|
|
36956 |
*/
|
|
|
36957 |
|
|
|
36958 |
/**
|
|
|
36959 |
* Adds appropriate media and discontinuity sequence values to the segments and playlists.
|
|
|
36960 |
*
|
|
|
36961 |
* Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a
|
|
|
36962 |
* DASH specific attribute used in constructing segment URI's from templates. However, from
|
|
|
36963 |
* an HLS perspective, the `number` attribute on a segment would be its `mediaSequence`
|
|
|
36964 |
* value, which should start at the original media sequence value (or 0) and increment by 1
|
|
|
36965 |
* for each segment thereafter. Since DASH's `startNumber` values are independent per
|
|
|
36966 |
* period, it doesn't make sense to use it for `number`. Instead, assume everything starts
|
|
|
36967 |
* from a 0 mediaSequence value and increment from there.
|
|
|
36968 |
*
|
|
|
36969 |
* Note that VHS currently doesn't use the `number` property, but it can be helpful for
|
|
|
36970 |
* debugging and making sense of the manifest.
|
|
|
36971 |
*
|
|
|
36972 |
* For live playlists, to account for values increasing in manifests when periods are
|
|
|
36973 |
* removed on refreshes, merging logic should be used to update the numbers to their
|
|
|
36974 |
* appropriate values (to ensure they're sequential and increasing).
|
|
|
36975 |
*
|
|
|
36976 |
* @param {Object[]} playlists - the playlists to update
|
|
|
36977 |
* @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest
|
|
|
36978 |
*/
|
|
|
36979 |
|
|
|
36980 |
const addMediaSequenceValues = (playlists, timelineStarts) => {
|
|
|
36981 |
// increment all segments sequentially
|
|
|
36982 |
playlists.forEach(playlist => {
|
|
|
36983 |
playlist.mediaSequence = 0;
|
|
|
36984 |
playlist.discontinuitySequence = timelineStarts.findIndex(function ({
|
|
|
36985 |
timeline
|
|
|
36986 |
}) {
|
|
|
36987 |
return timeline === playlist.timeline;
|
|
|
36988 |
});
|
|
|
36989 |
if (!playlist.segments) {
|
|
|
36990 |
return;
|
|
|
36991 |
}
|
|
|
36992 |
playlist.segments.forEach((segment, index) => {
|
|
|
36993 |
segment.number = index;
|
|
|
36994 |
});
|
|
|
36995 |
});
|
|
|
36996 |
};
|
|
|
36997 |
/**
|
|
|
36998 |
* Given a media group object, flattens all playlists within the media group into a single
|
|
|
36999 |
* array.
|
|
|
37000 |
*
|
|
|
37001 |
* @param {Object} mediaGroupObject - the media group object
|
|
|
37002 |
*
|
|
|
37003 |
* @return {Object[]}
|
|
|
37004 |
* The media group playlists
|
|
|
37005 |
*/
|
|
|
37006 |
|
|
|
37007 |
const flattenMediaGroupPlaylists = mediaGroupObject => {
|
|
|
37008 |
if (!mediaGroupObject) {
|
|
|
37009 |
return [];
|
|
|
37010 |
}
|
|
|
37011 |
return Object.keys(mediaGroupObject).reduce((acc, label) => {
|
|
|
37012 |
const labelContents = mediaGroupObject[label];
|
|
|
37013 |
return acc.concat(labelContents.playlists);
|
|
|
37014 |
}, []);
|
|
|
37015 |
};
|
|
|
37016 |
const toM3u8 = ({
|
|
|
37017 |
dashPlaylists,
|
|
|
37018 |
locations,
|
|
|
37019 |
contentSteering,
|
|
|
37020 |
sidxMapping = {},
|
|
|
37021 |
previousManifest,
|
|
|
37022 |
eventStream
|
|
|
37023 |
}) => {
|
|
|
37024 |
if (!dashPlaylists.length) {
|
|
|
37025 |
return {};
|
|
|
37026 |
} // grab all main manifest attributes
|
|
|
37027 |
|
|
|
37028 |
const {
|
|
|
37029 |
sourceDuration: duration,
|
|
|
37030 |
type,
|
|
|
37031 |
suggestedPresentationDelay,
|
|
|
37032 |
minimumUpdatePeriod
|
|
|
37033 |
} = dashPlaylists[0].attributes;
|
|
|
37034 |
const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
|
|
|
37035 |
const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
|
|
|
37036 |
const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly));
|
|
|
37037 |
const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean);
|
|
|
37038 |
const manifest = {
|
|
|
37039 |
allowCache: true,
|
|
|
37040 |
discontinuityStarts: [],
|
|
|
37041 |
segments: [],
|
|
|
37042 |
endList: true,
|
|
|
37043 |
mediaGroups: {
|
|
|
37044 |
AUDIO: {},
|
|
|
37045 |
VIDEO: {},
|
|
|
37046 |
['CLOSED-CAPTIONS']: {},
|
|
|
37047 |
SUBTITLES: {}
|
|
|
37048 |
},
|
|
|
37049 |
uri: '',
|
|
|
37050 |
duration,
|
|
|
37051 |
playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
|
|
|
37052 |
};
|
|
|
37053 |
if (minimumUpdatePeriod >= 0) {
|
|
|
37054 |
manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000;
|
|
|
37055 |
}
|
|
|
37056 |
if (locations) {
|
|
|
37057 |
manifest.locations = locations;
|
|
|
37058 |
}
|
|
|
37059 |
if (contentSteering) {
|
|
|
37060 |
manifest.contentSteering = contentSteering;
|
|
|
37061 |
}
|
|
|
37062 |
if (type === 'dynamic') {
|
|
|
37063 |
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
|
|
|
37064 |
}
|
|
|
37065 |
if (eventStream && eventStream.length > 0) {
|
|
|
37066 |
manifest.eventStream = eventStream;
|
|
|
37067 |
}
|
|
|
37068 |
const isAudioOnly = manifest.playlists.length === 0;
|
|
|
37069 |
const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null;
|
|
|
37070 |
const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null;
|
|
|
37071 |
const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup));
|
|
|
37072 |
const playlistTimelineStarts = formattedPlaylists.map(({
|
|
|
37073 |
timelineStarts
|
|
|
37074 |
}) => timelineStarts);
|
|
|
37075 |
manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts);
|
|
|
37076 |
addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts);
|
|
|
37077 |
if (organizedAudioGroup) {
|
|
|
37078 |
manifest.mediaGroups.AUDIO.audio = organizedAudioGroup;
|
|
|
37079 |
}
|
|
|
37080 |
if (organizedVttGroup) {
|
|
|
37081 |
manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup;
|
|
|
37082 |
}
|
|
|
37083 |
if (captions.length) {
|
|
|
37084 |
manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions);
|
|
|
37085 |
}
|
|
|
37086 |
if (previousManifest) {
|
|
|
37087 |
return positionManifestOnTimeline({
|
|
|
37088 |
oldManifest: previousManifest,
|
|
|
37089 |
newManifest: manifest
|
|
|
37090 |
});
|
|
|
37091 |
}
|
|
|
37092 |
return manifest;
|
|
|
37093 |
};
|
|
|
37094 |
|
|
|
37095 |
/**
|
|
|
37096 |
* Calculates the R (repetition) value for a live stream (for the final segment
|
|
|
37097 |
* in a manifest where the r value is negative 1)
|
|
|
37098 |
*
|
|
|
37099 |
* @param {Object} attributes
|
|
|
37100 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
37101 |
* names as keys
|
|
|
37102 |
* @param {number} time
|
|
|
37103 |
* current time (typically the total time up until the final segment)
|
|
|
37104 |
* @param {number} duration
|
|
|
37105 |
* duration property for the given <S />
|
|
|
37106 |
*
|
|
|
37107 |
* @return {number}
|
|
|
37108 |
* R value to reach the end of the given period
|
|
|
37109 |
*/
|
|
|
37110 |
const getLiveRValue = (attributes, time, duration) => {
|
|
|
37111 |
const {
|
|
|
37112 |
NOW,
|
|
|
37113 |
clientOffset,
|
|
|
37114 |
availabilityStartTime,
|
|
|
37115 |
timescale = 1,
|
|
|
37116 |
periodStart = 0,
|
|
|
37117 |
minimumUpdatePeriod = 0
|
|
|
37118 |
} = attributes;
|
|
|
37119 |
const now = (NOW + clientOffset) / 1000;
|
|
|
37120 |
const periodStartWC = availabilityStartTime + periodStart;
|
|
|
37121 |
const periodEndWC = now + minimumUpdatePeriod;
|
|
|
37122 |
const periodDuration = periodEndWC - periodStartWC;
|
|
|
37123 |
return Math.ceil((periodDuration * timescale - time) / duration);
|
|
|
37124 |
};
|
|
|
37125 |
/**
|
|
|
37126 |
* Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
|
|
|
37127 |
* timing and duration
|
|
|
37128 |
*
|
|
|
37129 |
* @param {Object} attributes
|
|
|
37130 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
37131 |
* names as keys
|
|
|
37132 |
* @param {Object[]} segmentTimeline
|
|
|
37133 |
* List of objects representing the attributes of each S element contained within
|
|
|
37134 |
*
|
|
|
37135 |
* @return {{number: number, duration: number, time: number, timeline: number}[]}
|
|
|
37136 |
* List of Objects with segment timing and duration info
|
|
|
37137 |
*/
|
|
|
37138 |
|
|
|
37139 |
const parseByTimeline = (attributes, segmentTimeline) => {
|
|
|
37140 |
const {
|
|
|
37141 |
type,
|
|
|
37142 |
minimumUpdatePeriod = 0,
|
|
|
37143 |
media = '',
|
|
|
37144 |
sourceDuration,
|
|
|
37145 |
timescale = 1,
|
|
|
37146 |
startNumber = 1,
|
|
|
37147 |
periodStart: timeline
|
|
|
37148 |
} = attributes;
|
|
|
37149 |
const segments = [];
|
|
|
37150 |
let time = -1;
|
|
|
37151 |
for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
|
|
|
37152 |
const S = segmentTimeline[sIndex];
|
|
|
37153 |
const duration = S.d;
|
|
|
37154 |
const repeat = S.r || 0;
|
|
|
37155 |
const segmentTime = S.t || 0;
|
|
|
37156 |
if (time < 0) {
|
|
|
37157 |
// first segment
|
|
|
37158 |
time = segmentTime;
|
|
|
37159 |
}
|
|
|
37160 |
if (segmentTime && segmentTime > time) {
|
|
|
37161 |
// discontinuity
|
|
|
37162 |
// TODO: How to handle this type of discontinuity
|
|
|
37163 |
// timeline++ here would treat it like HLS discontuity and content would
|
|
|
37164 |
// get appended without gap
|
|
|
37165 |
// E.G.
|
|
|
37166 |
// <S t="0" d="1" />
|
|
|
37167 |
// <S d="1" />
|
|
|
37168 |
// <S d="1" />
|
|
|
37169 |
// <S t="5" d="1" />
|
|
|
37170 |
// would have $Time$ values of [0, 1, 2, 5]
|
|
|
37171 |
// should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
|
|
|
37172 |
// or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
|
|
|
37173 |
// does the value of sourceDuration consider this when calculating arbitrary
|
|
|
37174 |
// negative @r repeat value?
|
|
|
37175 |
// E.G. Same elements as above with this added at the end
|
|
|
37176 |
// <S d="1" r="-1" />
|
|
|
37177 |
// with a sourceDuration of 10
|
|
|
37178 |
// Would the 2 gaps be included in the time duration calculations resulting in
|
|
|
37179 |
// 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
|
|
|
37180 |
// with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
|
|
|
37181 |
time = segmentTime;
|
|
|
37182 |
}
|
|
|
37183 |
let count;
|
|
|
37184 |
if (repeat < 0) {
|
|
|
37185 |
const nextS = sIndex + 1;
|
|
|
37186 |
if (nextS === segmentTimeline.length) {
|
|
|
37187 |
// last segment
|
|
|
37188 |
if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) {
|
|
|
37189 |
count = getLiveRValue(attributes, time, duration);
|
|
|
37190 |
} else {
|
|
|
37191 |
// TODO: This may be incorrect depending on conclusion of TODO above
|
|
|
37192 |
count = (sourceDuration * timescale - time) / duration;
|
|
|
37193 |
}
|
|
|
37194 |
} else {
|
|
|
37195 |
count = (segmentTimeline[nextS].t - time) / duration;
|
|
|
37196 |
}
|
|
|
37197 |
} else {
|
|
|
37198 |
count = repeat + 1;
|
|
|
37199 |
}
|
|
|
37200 |
const end = startNumber + segments.length + count;
|
|
|
37201 |
let number = startNumber + segments.length;
|
|
|
37202 |
while (number < end) {
|
|
|
37203 |
segments.push({
|
|
|
37204 |
number,
|
|
|
37205 |
duration: duration / timescale,
|
|
|
37206 |
time,
|
|
|
37207 |
timeline
|
|
|
37208 |
});
|
|
|
37209 |
time += duration;
|
|
|
37210 |
number++;
|
|
|
37211 |
}
|
|
|
37212 |
}
|
|
|
37213 |
return segments;
|
|
|
37214 |
};
|
|
|
37215 |
const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
|
|
|
37216 |
/**
|
|
|
37217 |
* Replaces template identifiers with corresponding values. To be used as the callback
|
|
|
37218 |
* for String.prototype.replace
|
|
|
37219 |
*
|
|
|
37220 |
* @name replaceCallback
|
|
|
37221 |
* @function
|
|
|
37222 |
* @param {string} match
|
|
|
37223 |
* Entire match of identifier
|
|
|
37224 |
* @param {string} identifier
|
|
|
37225 |
* Name of matched identifier
|
|
|
37226 |
* @param {string} format
|
|
|
37227 |
* Format tag string. Its presence indicates that padding is expected
|
|
|
37228 |
* @param {string} width
|
|
|
37229 |
* Desired length of the replaced value. Values less than this width shall be left
|
|
|
37230 |
* zero padded
|
|
|
37231 |
* @return {string}
|
|
|
37232 |
* Replacement for the matched identifier
|
|
|
37233 |
*/
|
|
|
37234 |
|
|
|
37235 |
/**
|
|
|
37236 |
* Returns a function to be used as a callback for String.prototype.replace to replace
|
|
|
37237 |
* template identifiers
|
|
|
37238 |
*
|
|
|
37239 |
* @param {Obect} values
|
|
|
37240 |
* Object containing values that shall be used to replace known identifiers
|
|
|
37241 |
* @param {number} values.RepresentationID
|
|
|
37242 |
* Value of the Representation@id attribute
|
|
|
37243 |
* @param {number} values.Number
|
|
|
37244 |
* Number of the corresponding segment
|
|
|
37245 |
* @param {number} values.Bandwidth
|
|
|
37246 |
* Value of the Representation@bandwidth attribute.
|
|
|
37247 |
* @param {number} values.Time
|
|
|
37248 |
* Timestamp value of the corresponding segment
|
|
|
37249 |
* @return {replaceCallback}
|
|
|
37250 |
* Callback to be used with String.prototype.replace to replace identifiers
|
|
|
37251 |
*/
|
|
|
37252 |
|
|
|
37253 |
const identifierReplacement = values => (match, identifier, format, width) => {
|
|
|
37254 |
if (match === '$$') {
|
|
|
37255 |
// escape sequence
|
|
|
37256 |
return '$';
|
|
|
37257 |
}
|
|
|
37258 |
if (typeof values[identifier] === 'undefined') {
|
|
|
37259 |
return match;
|
|
|
37260 |
}
|
|
|
37261 |
const value = '' + values[identifier];
|
|
|
37262 |
if (identifier === 'RepresentationID') {
|
|
|
37263 |
// Format tag shall not be present with RepresentationID
|
|
|
37264 |
return value;
|
|
|
37265 |
}
|
|
|
37266 |
if (!format) {
|
|
|
37267 |
width = 1;
|
|
|
37268 |
} else {
|
|
|
37269 |
width = parseInt(width, 10);
|
|
|
37270 |
}
|
|
|
37271 |
if (value.length >= width) {
|
|
|
37272 |
return value;
|
|
|
37273 |
}
|
|
|
37274 |
return `${new Array(width - value.length + 1).join('0')}${value}`;
|
|
|
37275 |
};
|
|
|
37276 |
/**
|
|
|
37277 |
* Constructs a segment url from a template string
|
|
|
37278 |
*
|
|
|
37279 |
* @param {string} url
|
|
|
37280 |
* Template string to construct url from
|
|
|
37281 |
* @param {Obect} values
|
|
|
37282 |
* Object containing values that shall be used to replace known identifiers
|
|
|
37283 |
* @param {number} values.RepresentationID
|
|
|
37284 |
* Value of the Representation@id attribute
|
|
|
37285 |
* @param {number} values.Number
|
|
|
37286 |
* Number of the corresponding segment
|
|
|
37287 |
* @param {number} values.Bandwidth
|
|
|
37288 |
* Value of the Representation@bandwidth attribute.
|
|
|
37289 |
* @param {number} values.Time
|
|
|
37290 |
* Timestamp value of the corresponding segment
|
|
|
37291 |
* @return {string}
|
|
|
37292 |
* Segment url with identifiers replaced
|
|
|
37293 |
*/
|
|
|
37294 |
|
|
|
37295 |
const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values));
|
|
|
37296 |
/**
|
|
|
37297 |
* Generates a list of objects containing timing and duration information about each
|
|
|
37298 |
* segment needed to generate segment uris and the complete segment object
|
|
|
37299 |
*
|
|
|
37300 |
* @param {Object} attributes
|
|
|
37301 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
37302 |
* names as keys
|
|
|
37303 |
* @param {Object[]|undefined} segmentTimeline
|
|
|
37304 |
* List of objects representing the attributes of each S element contained within
|
|
|
37305 |
* the SegmentTimeline element
|
|
|
37306 |
* @return {{number: number, duration: number, time: number, timeline: number}[]}
|
|
|
37307 |
* List of Objects with segment timing and duration info
|
|
|
37308 |
*/
|
|
|
37309 |
|
|
|
37310 |
const parseTemplateInfo = (attributes, segmentTimeline) => {
|
|
|
37311 |
if (!attributes.duration && !segmentTimeline) {
|
|
|
37312 |
// if neither @duration or SegmentTimeline are present, then there shall be exactly
|
|
|
37313 |
// one media segment
|
|
|
37314 |
return [{
|
|
|
37315 |
number: attributes.startNumber || 1,
|
|
|
37316 |
duration: attributes.sourceDuration,
|
|
|
37317 |
time: 0,
|
|
|
37318 |
timeline: attributes.periodStart
|
|
|
37319 |
}];
|
|
|
37320 |
}
|
|
|
37321 |
if (attributes.duration) {
|
|
|
37322 |
return parseByDuration(attributes);
|
|
|
37323 |
}
|
|
|
37324 |
return parseByTimeline(attributes, segmentTimeline);
|
|
|
37325 |
};
|
|
|
37326 |
/**
|
|
|
37327 |
* Generates a list of segments using information provided by the SegmentTemplate element
|
|
|
37328 |
*
|
|
|
37329 |
* @param {Object} attributes
|
|
|
37330 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
37331 |
* names as keys
|
|
|
37332 |
* @param {Object[]|undefined} segmentTimeline
|
|
|
37333 |
* List of objects representing the attributes of each S element contained within
|
|
|
37334 |
* the SegmentTimeline element
|
|
|
37335 |
* @return {Object[]}
|
|
|
37336 |
* List of segment objects
|
|
|
37337 |
*/
|
|
|
37338 |
|
|
|
37339 |
const segmentsFromTemplate = (attributes, segmentTimeline) => {
|
|
|
37340 |
const templateValues = {
|
|
|
37341 |
RepresentationID: attributes.id,
|
|
|
37342 |
Bandwidth: attributes.bandwidth || 0
|
|
|
37343 |
};
|
|
|
37344 |
const {
|
|
|
37345 |
initialization = {
|
|
|
37346 |
sourceURL: '',
|
|
|
37347 |
range: ''
|
|
|
37348 |
}
|
|
|
37349 |
} = attributes;
|
|
|
37350 |
const mapSegment = urlTypeToSegment({
|
|
|
37351 |
baseUrl: attributes.baseUrl,
|
|
|
37352 |
source: constructTemplateUrl(initialization.sourceURL, templateValues),
|
|
|
37353 |
range: initialization.range
|
|
|
37354 |
});
|
|
|
37355 |
const segments = parseTemplateInfo(attributes, segmentTimeline);
|
|
|
37356 |
return segments.map(segment => {
|
|
|
37357 |
templateValues.Number = segment.number;
|
|
|
37358 |
templateValues.Time = segment.time;
|
|
|
37359 |
const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2
|
|
|
37360 |
// - if timescale isn't present on any level, default to 1.
|
|
|
37361 |
|
|
|
37362 |
const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
|
|
|
37363 |
|
|
|
37364 |
const presentationTimeOffset = attributes.presentationTimeOffset || 0;
|
|
|
37365 |
const presentationTime =
|
|
|
37366 |
// Even if the @t attribute is not specified for the segment, segment.time is
|
|
|
37367 |
// calculated in mpd-parser prior to this, so it's assumed to be available.
|
|
|
37368 |
attributes.periodStart + (segment.time - presentationTimeOffset) / timescale;
|
|
|
37369 |
const map = {
|
|
|
37370 |
uri,
|
|
|
37371 |
timeline: segment.timeline,
|
|
|
37372 |
duration: segment.duration,
|
|
|
37373 |
resolvedUri: resolveUrl$1(attributes.baseUrl || '', uri),
|
|
|
37374 |
map: mapSegment,
|
|
|
37375 |
number: segment.number,
|
|
|
37376 |
presentationTime
|
|
|
37377 |
};
|
|
|
37378 |
return map;
|
|
|
37379 |
});
|
|
|
37380 |
};
|
|
|
37381 |
|
|
|
37382 |
/**
|
|
|
37383 |
* Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
|
|
|
37384 |
* to an object that matches the output of a segment in videojs/mpd-parser
|
|
|
37385 |
*
|
|
|
37386 |
* @param {Object} attributes
|
|
|
37387 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
37388 |
* names as keys
|
|
|
37389 |
* @param {Object} segmentUrl
|
|
|
37390 |
* <SegmentURL> node to translate into a segment object
|
|
|
37391 |
* @return {Object} translated segment object
|
|
|
37392 |
*/
|
|
|
37393 |
|
|
|
37394 |
const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
|
|
|
37395 |
const {
|
|
|
37396 |
baseUrl,
|
|
|
37397 |
initialization = {}
|
|
|
37398 |
} = attributes;
|
|
|
37399 |
const initSegment = urlTypeToSegment({
|
|
|
37400 |
baseUrl,
|
|
|
37401 |
source: initialization.sourceURL,
|
|
|
37402 |
range: initialization.range
|
|
|
37403 |
});
|
|
|
37404 |
const segment = urlTypeToSegment({
|
|
|
37405 |
baseUrl,
|
|
|
37406 |
source: segmentUrl.media,
|
|
|
37407 |
range: segmentUrl.mediaRange
|
|
|
37408 |
});
|
|
|
37409 |
segment.map = initSegment;
|
|
|
37410 |
return segment;
|
|
|
37411 |
};
|
|
|
37412 |
/**
|
|
|
37413 |
* Generates a list of segments using information provided by the SegmentList element
|
|
|
37414 |
* SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
|
|
|
37415 |
* node should be translated into segment.
|
|
|
37416 |
*
|
|
|
37417 |
* @param {Object} attributes
|
|
|
37418 |
* Object containing all inherited attributes from parent elements with attribute
|
|
|
37419 |
* names as keys
|
|
|
37420 |
* @param {Object[]|undefined} segmentTimeline
|
|
|
37421 |
* List of objects representing the attributes of each S element contained within
|
|
|
37422 |
* the SegmentTimeline element
|
|
|
37423 |
* @return {Object.<Array>} list of segments
|
|
|
37424 |
*/
|
|
|
37425 |
|
|
|
37426 |
const segmentsFromList = (attributes, segmentTimeline) => {
|
|
|
37427 |
const {
|
|
|
37428 |
duration,
|
|
|
37429 |
segmentUrls = [],
|
|
|
37430 |
periodStart
|
|
|
37431 |
} = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR
|
|
|
37432 |
// if both SegmentTimeline and @duration are defined, it is outside of spec.
|
|
|
37433 |
|
|
|
37434 |
if (!duration && !segmentTimeline || duration && segmentTimeline) {
|
|
|
37435 |
throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
|
|
|
37436 |
}
|
|
|
37437 |
const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject));
|
|
|
37438 |
let segmentTimeInfo;
|
|
|
37439 |
if (duration) {
|
|
|
37440 |
segmentTimeInfo = parseByDuration(attributes);
|
|
|
37441 |
}
|
|
|
37442 |
if (segmentTimeline) {
|
|
|
37443 |
segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
|
|
|
37444 |
}
|
|
|
37445 |
const segments = segmentTimeInfo.map((segmentTime, index) => {
|
|
|
37446 |
if (segmentUrlMap[index]) {
|
|
|
37447 |
const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2
|
|
|
37448 |
// - if timescale isn't present on any level, default to 1.
|
|
|
37449 |
|
|
|
37450 |
const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
|
|
|
37451 |
|
|
|
37452 |
const presentationTimeOffset = attributes.presentationTimeOffset || 0;
|
|
|
37453 |
segment.timeline = segmentTime.timeline;
|
|
|
37454 |
segment.duration = segmentTime.duration;
|
|
|
37455 |
segment.number = segmentTime.number;
|
|
|
37456 |
segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale;
|
|
|
37457 |
return segment;
|
|
|
37458 |
} // Since we're mapping we should get rid of any blank segments (in case
|
|
|
37459 |
// the given SegmentTimeline is handling for more elements than we have
|
|
|
37460 |
// SegmentURLs for).
|
|
|
37461 |
}).filter(segment => segment);
|
|
|
37462 |
return segments;
|
|
|
37463 |
};
|
|
|
37464 |
const generateSegments = ({
|
|
|
37465 |
attributes,
|
|
|
37466 |
segmentInfo
|
|
|
37467 |
}) => {
|
|
|
37468 |
let segmentAttributes;
|
|
|
37469 |
let segmentsFn;
|
|
|
37470 |
if (segmentInfo.template) {
|
|
|
37471 |
segmentsFn = segmentsFromTemplate;
|
|
|
37472 |
segmentAttributes = merge$1(attributes, segmentInfo.template);
|
|
|
37473 |
} else if (segmentInfo.base) {
|
|
|
37474 |
segmentsFn = segmentsFromBase;
|
|
|
37475 |
segmentAttributes = merge$1(attributes, segmentInfo.base);
|
|
|
37476 |
} else if (segmentInfo.list) {
|
|
|
37477 |
segmentsFn = segmentsFromList;
|
|
|
37478 |
segmentAttributes = merge$1(attributes, segmentInfo.list);
|
|
|
37479 |
}
|
|
|
37480 |
const segmentsInfo = {
|
|
|
37481 |
attributes
|
|
|
37482 |
};
|
|
|
37483 |
if (!segmentsFn) {
|
|
|
37484 |
return segmentsInfo;
|
|
|
37485 |
}
|
|
|
37486 |
const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which
|
|
|
37487 |
// must be in seconds. Since we've generated the segment list, we no longer need
|
|
|
37488 |
// @duration to be in @timescale units, so we can convert it here.
|
|
|
37489 |
|
|
|
37490 |
if (segmentAttributes.duration) {
|
|
|
37491 |
const {
|
|
|
37492 |
duration,
|
|
|
37493 |
timescale = 1
|
|
|
37494 |
} = segmentAttributes;
|
|
|
37495 |
segmentAttributes.duration = duration / timescale;
|
|
|
37496 |
} else if (segments.length) {
|
|
|
37497 |
// if there is no @duration attribute, use the largest segment duration as
|
|
|
37498 |
// as target duration
|
|
|
37499 |
segmentAttributes.duration = segments.reduce((max, segment) => {
|
|
|
37500 |
return Math.max(max, Math.ceil(segment.duration));
|
|
|
37501 |
}, 0);
|
|
|
37502 |
} else {
|
|
|
37503 |
segmentAttributes.duration = 0;
|
|
|
37504 |
}
|
|
|
37505 |
segmentsInfo.attributes = segmentAttributes;
|
|
|
37506 |
segmentsInfo.segments = segments; // This is a sidx box without actual segment information
|
|
|
37507 |
|
|
|
37508 |
if (segmentInfo.base && segmentAttributes.indexRange) {
|
|
|
37509 |
segmentsInfo.sidx = segments[0];
|
|
|
37510 |
segmentsInfo.segments = [];
|
|
|
37511 |
}
|
|
|
37512 |
return segmentsInfo;
|
|
|
37513 |
};
|
|
|
37514 |
const toPlaylists = representations => representations.map(generateSegments);
|
|
|
37515 |
const findChildren = (element, name) => from(element.childNodes).filter(({
|
|
|
37516 |
tagName
|
|
|
37517 |
}) => tagName === name);
|
|
|
37518 |
const getContent = element => element.textContent.trim();
|
|
|
37519 |
|
|
|
37520 |
/**
|
|
|
37521 |
* Converts the provided string that may contain a division operation to a number.
|
|
|
37522 |
*
|
|
|
37523 |
* @param {string} value - the provided string value
|
|
|
37524 |
*
|
|
|
37525 |
* @return {number} the parsed string value
|
|
|
37526 |
*/
|
|
|
37527 |
const parseDivisionValue = value => {
|
|
|
37528 |
return parseFloat(value.split('/').reduce((prev, current) => prev / current));
|
|
|
37529 |
};
|
|
|
37530 |
const parseDuration = str => {
|
|
|
37531 |
const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
|
|
|
37532 |
const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
|
|
|
37533 |
const SECONDS_IN_DAY = 24 * 60 * 60;
|
|
|
37534 |
const SECONDS_IN_HOUR = 60 * 60;
|
|
|
37535 |
const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S
|
|
|
37536 |
|
|
|
37537 |
const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
|
|
|
37538 |
const match = durationRegex.exec(str);
|
|
|
37539 |
if (!match) {
|
|
|
37540 |
return 0;
|
|
|
37541 |
}
|
|
|
37542 |
const [year, month, day, hour, minute, second] = match.slice(1);
|
|
|
37543 |
return parseFloat(year || 0) * SECONDS_IN_YEAR + parseFloat(month || 0) * SECONDS_IN_MONTH + parseFloat(day || 0) * SECONDS_IN_DAY + parseFloat(hour || 0) * SECONDS_IN_HOUR + parseFloat(minute || 0) * SECONDS_IN_MIN + parseFloat(second || 0);
|
|
|
37544 |
};
|
|
|
37545 |
const parseDate = str => {
|
|
|
37546 |
// Date format without timezone according to ISO 8601
|
|
|
37547 |
// YYY-MM-DDThh:mm:ss.ssssss
|
|
|
37548 |
const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is
|
|
|
37549 |
// expressed by ending with 'Z'
|
|
|
37550 |
|
|
|
37551 |
if (dateRegex.test(str)) {
|
|
|
37552 |
str += 'Z';
|
|
|
37553 |
}
|
|
|
37554 |
return Date.parse(str);
|
|
|
37555 |
};
|
|
|
37556 |
const parsers = {
|
|
|
37557 |
/**
|
|
|
37558 |
* Specifies the duration of the entire Media Presentation. Format is a duration string
|
|
|
37559 |
* as specified in ISO 8601
|
|
|
37560 |
*
|
|
|
37561 |
* @param {string} value
|
|
|
37562 |
* value of attribute as a string
|
|
|
37563 |
* @return {number}
|
|
|
37564 |
* The duration in seconds
|
|
|
37565 |
*/
|
|
|
37566 |
mediaPresentationDuration(value) {
|
|
|
37567 |
return parseDuration(value);
|
|
|
37568 |
},
|
|
|
37569 |
/**
|
|
|
37570 |
* Specifies the Segment availability start time for all Segments referred to in this
|
|
|
37571 |
* MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
|
|
|
37572 |
* time. Format is a date string as specified in ISO 8601
|
|
|
37573 |
*
|
|
|
37574 |
* @param {string} value
|
|
|
37575 |
* value of attribute as a string
|
|
|
37576 |
* @return {number}
|
|
|
37577 |
* The date as seconds from unix epoch
|
|
|
37578 |
*/
|
|
|
37579 |
availabilityStartTime(value) {
|
|
|
37580 |
return parseDate(value) / 1000;
|
|
|
37581 |
},
|
|
|
37582 |
/**
|
|
|
37583 |
* Specifies the smallest period between potential changes to the MPD. Format is a
|
|
|
37584 |
* duration string as specified in ISO 8601
|
|
|
37585 |
*
|
|
|
37586 |
* @param {string} value
|
|
|
37587 |
* value of attribute as a string
|
|
|
37588 |
* @return {number}
|
|
|
37589 |
* The duration in seconds
|
|
|
37590 |
*/
|
|
|
37591 |
minimumUpdatePeriod(value) {
|
|
|
37592 |
return parseDuration(value);
|
|
|
37593 |
},
|
|
|
37594 |
/**
|
|
|
37595 |
* Specifies the suggested presentation delay. Format is a
|
|
|
37596 |
* duration string as specified in ISO 8601
|
|
|
37597 |
*
|
|
|
37598 |
* @param {string} value
|
|
|
37599 |
* value of attribute as a string
|
|
|
37600 |
* @return {number}
|
|
|
37601 |
* The duration in seconds
|
|
|
37602 |
*/
|
|
|
37603 |
suggestedPresentationDelay(value) {
|
|
|
37604 |
return parseDuration(value);
|
|
|
37605 |
},
|
|
|
37606 |
/**
|
|
|
37607 |
* specifices the type of mpd. Can be either "static" or "dynamic"
|
|
|
37608 |
*
|
|
|
37609 |
* @param {string} value
|
|
|
37610 |
* value of attribute as a string
|
|
|
37611 |
*
|
|
|
37612 |
* @return {string}
|
|
|
37613 |
* The type as a string
|
|
|
37614 |
*/
|
|
|
37615 |
type(value) {
|
|
|
37616 |
return value;
|
|
|
37617 |
},
|
|
|
37618 |
/**
|
|
|
37619 |
* Specifies the duration of the smallest time shifting buffer for any Representation
|
|
|
37620 |
* in the MPD. Format is a duration string as specified in ISO 8601
|
|
|
37621 |
*
|
|
|
37622 |
* @param {string} value
|
|
|
37623 |
* value of attribute as a string
|
|
|
37624 |
* @return {number}
|
|
|
37625 |
* The duration in seconds
|
|
|
37626 |
*/
|
|
|
37627 |
timeShiftBufferDepth(value) {
|
|
|
37628 |
return parseDuration(value);
|
|
|
37629 |
},
|
|
|
37630 |
/**
|
|
|
37631 |
* Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
|
|
|
37632 |
* Format is a duration string as specified in ISO 8601
|
|
|
37633 |
*
|
|
|
37634 |
* @param {string} value
|
|
|
37635 |
* value of attribute as a string
|
|
|
37636 |
* @return {number}
|
|
|
37637 |
* The duration in seconds
|
|
|
37638 |
*/
|
|
|
37639 |
start(value) {
|
|
|
37640 |
return parseDuration(value);
|
|
|
37641 |
},
|
|
|
37642 |
/**
|
|
|
37643 |
* Specifies the width of the visual presentation
|
|
|
37644 |
*
|
|
|
37645 |
* @param {string} value
|
|
|
37646 |
* value of attribute as a string
|
|
|
37647 |
* @return {number}
|
|
|
37648 |
* The parsed width
|
|
|
37649 |
*/
|
|
|
37650 |
width(value) {
|
|
|
37651 |
return parseInt(value, 10);
|
|
|
37652 |
},
|
|
|
37653 |
/**
|
|
|
37654 |
* Specifies the height of the visual presentation
|
|
|
37655 |
*
|
|
|
37656 |
* @param {string} value
|
|
|
37657 |
* value of attribute as a string
|
|
|
37658 |
* @return {number}
|
|
|
37659 |
* The parsed height
|
|
|
37660 |
*/
|
|
|
37661 |
height(value) {
|
|
|
37662 |
return parseInt(value, 10);
|
|
|
37663 |
},
|
|
|
37664 |
/**
|
|
|
37665 |
* Specifies the bitrate of the representation
|
|
|
37666 |
*
|
|
|
37667 |
* @param {string} value
|
|
|
37668 |
* value of attribute as a string
|
|
|
37669 |
* @return {number}
|
|
|
37670 |
* The parsed bandwidth
|
|
|
37671 |
*/
|
|
|
37672 |
bandwidth(value) {
|
|
|
37673 |
return parseInt(value, 10);
|
|
|
37674 |
},
|
|
|
37675 |
/**
|
|
|
37676 |
* Specifies the frame rate of the representation
|
|
|
37677 |
*
|
|
|
37678 |
* @param {string} value
|
|
|
37679 |
* value of attribute as a string
|
|
|
37680 |
* @return {number}
|
|
|
37681 |
* The parsed frame rate
|
|
|
37682 |
*/
|
|
|
37683 |
frameRate(value) {
|
|
|
37684 |
return parseDivisionValue(value);
|
|
|
37685 |
},
|
|
|
37686 |
/**
|
|
|
37687 |
* Specifies the number of the first Media Segment in this Representation in the Period
|
|
|
37688 |
*
|
|
|
37689 |
* @param {string} value
|
|
|
37690 |
* value of attribute as a string
|
|
|
37691 |
* @return {number}
|
|
|
37692 |
* The parsed number
|
|
|
37693 |
*/
|
|
|
37694 |
startNumber(value) {
|
|
|
37695 |
return parseInt(value, 10);
|
|
|
37696 |
},
|
|
|
37697 |
/**
|
|
|
37698 |
* Specifies the timescale in units per seconds
|
|
|
37699 |
*
|
|
|
37700 |
* @param {string} value
|
|
|
37701 |
* value of attribute as a string
|
|
|
37702 |
* @return {number}
|
|
|
37703 |
* The parsed timescale
|
|
|
37704 |
*/
|
|
|
37705 |
timescale(value) {
|
|
|
37706 |
return parseInt(value, 10);
|
|
|
37707 |
},
|
|
|
37708 |
/**
|
|
|
37709 |
* Specifies the presentationTimeOffset.
|
|
|
37710 |
*
|
|
|
37711 |
* @param {string} value
|
|
|
37712 |
* value of the attribute as a string
|
|
|
37713 |
*
|
|
|
37714 |
* @return {number}
|
|
|
37715 |
* The parsed presentationTimeOffset
|
|
|
37716 |
*/
|
|
|
37717 |
presentationTimeOffset(value) {
|
|
|
37718 |
return parseInt(value, 10);
|
|
|
37719 |
},
|
|
|
37720 |
/**
|
|
|
37721 |
* Specifies the constant approximate Segment duration
|
|
|
37722 |
* NOTE: The <Period> element also contains an @duration attribute. This duration
|
|
|
37723 |
* specifies the duration of the Period. This attribute is currently not
|
|
|
37724 |
* supported by the rest of the parser, however we still check for it to prevent
|
|
|
37725 |
* errors.
|
|
|
37726 |
*
|
|
|
37727 |
* @param {string} value
|
|
|
37728 |
* value of attribute as a string
|
|
|
37729 |
* @return {number}
|
|
|
37730 |
* The parsed duration
|
|
|
37731 |
*/
|
|
|
37732 |
duration(value) {
|
|
|
37733 |
const parsedValue = parseInt(value, 10);
|
|
|
37734 |
if (isNaN(parsedValue)) {
|
|
|
37735 |
return parseDuration(value);
|
|
|
37736 |
}
|
|
|
37737 |
return parsedValue;
|
|
|
37738 |
},
|
|
|
37739 |
/**
|
|
|
37740 |
* Specifies the Segment duration, in units of the value of the @timescale.
|
|
|
37741 |
*
|
|
|
37742 |
* @param {string} value
|
|
|
37743 |
* value of attribute as a string
|
|
|
37744 |
* @return {number}
|
|
|
37745 |
* The parsed duration
|
|
|
37746 |
*/
|
|
|
37747 |
d(value) {
|
|
|
37748 |
return parseInt(value, 10);
|
|
|
37749 |
},
|
|
|
37750 |
/**
|
|
|
37751 |
* Specifies the MPD start time, in @timescale units, the first Segment in the series
|
|
|
37752 |
* starts relative to the beginning of the Period
|
|
|
37753 |
*
|
|
|
37754 |
* @param {string} value
|
|
|
37755 |
* value of attribute as a string
|
|
|
37756 |
* @return {number}
|
|
|
37757 |
* The parsed time
|
|
|
37758 |
*/
|
|
|
37759 |
t(value) {
|
|
|
37760 |
return parseInt(value, 10);
|
|
|
37761 |
},
|
|
|
37762 |
/**
|
|
|
37763 |
* Specifies the repeat count of the number of following contiguous Segments with the
|
|
|
37764 |
* same duration expressed by the value of @d
|
|
|
37765 |
*
|
|
|
37766 |
* @param {string} value
|
|
|
37767 |
* value of attribute as a string
|
|
|
37768 |
* @return {number}
|
|
|
37769 |
* The parsed number
|
|
|
37770 |
*/
|
|
|
37771 |
r(value) {
|
|
|
37772 |
return parseInt(value, 10);
|
|
|
37773 |
},
|
|
|
37774 |
/**
|
|
|
37775 |
* Specifies the presentationTime.
|
|
|
37776 |
*
|
|
|
37777 |
* @param {string} value
|
|
|
37778 |
* value of the attribute as a string
|
|
|
37779 |
*
|
|
|
37780 |
* @return {number}
|
|
|
37781 |
* The parsed presentationTime
|
|
|
37782 |
*/
|
|
|
37783 |
presentationTime(value) {
|
|
|
37784 |
return parseInt(value, 10);
|
|
|
37785 |
},
|
|
|
37786 |
/**
|
|
|
37787 |
* Default parser for all other attributes. Acts as a no-op and just returns the value
|
|
|
37788 |
* as a string
|
|
|
37789 |
*
|
|
|
37790 |
* @param {string} value
|
|
|
37791 |
* value of attribute as a string
|
|
|
37792 |
* @return {string}
|
|
|
37793 |
* Unparsed value
|
|
|
37794 |
*/
|
|
|
37795 |
DEFAULT(value) {
|
|
|
37796 |
return value;
|
|
|
37797 |
}
|
|
|
37798 |
};
|
|
|
37799 |
/**
|
|
|
37800 |
* Gets all the attributes and values of the provided node, parses attributes with known
|
|
|
37801 |
* types, and returns an object with attribute names mapped to values.
|
|
|
37802 |
*
|
|
|
37803 |
* @param {Node} el
|
|
|
37804 |
* The node to parse attributes from
|
|
|
37805 |
* @return {Object}
|
|
|
37806 |
* Object with all attributes of el parsed
|
|
|
37807 |
*/
|
|
|
37808 |
|
|
|
37809 |
const parseAttributes = el => {
|
|
|
37810 |
if (!(el && el.attributes)) {
|
|
|
37811 |
return {};
|
|
|
37812 |
}
|
|
|
37813 |
return from(el.attributes).reduce((a, e) => {
|
|
|
37814 |
const parseFn = parsers[e.name] || parsers.DEFAULT;
|
|
|
37815 |
a[e.name] = parseFn(e.value);
|
|
|
37816 |
return a;
|
|
|
37817 |
}, {});
|
|
|
37818 |
};
|
|
|
37819 |
const keySystemsMap = {
|
|
|
37820 |
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
|
|
|
37821 |
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
|
|
|
37822 |
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
|
|
|
37823 |
'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime',
|
|
|
37824 |
// ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme
|
|
|
37825 |
'urn:mpeg:dash:mp4protection:2011': 'mp4protection'
|
|
|
37826 |
};
|
|
|
37827 |
/**
|
|
|
37828 |
* Builds a list of urls that is the product of the reference urls and BaseURL values
|
|
|
37829 |
*
|
|
|
37830 |
* @param {Object[]} references
|
|
|
37831 |
* List of objects containing the reference URL as well as its attributes
|
|
|
37832 |
* @param {Node[]} baseUrlElements
|
|
|
37833 |
* List of BaseURL nodes from the mpd
|
|
|
37834 |
* @return {Object[]}
|
|
|
37835 |
* List of objects with resolved urls and attributes
|
|
|
37836 |
*/
|
|
|
37837 |
|
|
|
37838 |
const buildBaseUrls = (references, baseUrlElements) => {
|
|
|
37839 |
if (!baseUrlElements.length) {
|
|
|
37840 |
return references;
|
|
|
37841 |
}
|
|
|
37842 |
return flatten(references.map(function (reference) {
|
|
|
37843 |
return baseUrlElements.map(function (baseUrlElement) {
|
|
|
37844 |
const initialBaseUrl = getContent(baseUrlElement);
|
|
|
37845 |
const resolvedBaseUrl = resolveUrl$1(reference.baseUrl, initialBaseUrl);
|
|
|
37846 |
const finalBaseUrl = merge$1(parseAttributes(baseUrlElement), {
|
|
|
37847 |
baseUrl: resolvedBaseUrl
|
|
|
37848 |
}); // If the URL is resolved, we want to get the serviceLocation from the reference
|
|
|
37849 |
// assuming there is no serviceLocation on the initialBaseUrl
|
|
|
37850 |
|
|
|
37851 |
if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
|
|
|
37852 |
finalBaseUrl.serviceLocation = reference.serviceLocation;
|
|
|
37853 |
}
|
|
|
37854 |
return finalBaseUrl;
|
|
|
37855 |
});
|
|
|
37856 |
}));
|
|
|
37857 |
};
|
|
|
37858 |
/**
|
|
|
37859 |
* Contains all Segment information for its containing AdaptationSet
|
|
|
37860 |
*
|
|
|
37861 |
* @typedef {Object} SegmentInformation
|
|
|
37862 |
* @property {Object|undefined} template
|
|
|
37863 |
* Contains the attributes for the SegmentTemplate node
|
|
|
37864 |
* @property {Object[]|undefined} segmentTimeline
|
|
|
37865 |
* Contains a list of atrributes for each S node within the SegmentTimeline node
|
|
|
37866 |
* @property {Object|undefined} list
|
|
|
37867 |
* Contains the attributes for the SegmentList node
|
|
|
37868 |
* @property {Object|undefined} base
|
|
|
37869 |
* Contains the attributes for the SegmentBase node
|
|
|
37870 |
*/
|
|
|
37871 |
|
|
|
37872 |
/**
|
|
|
37873 |
* Returns all available Segment information contained within the AdaptationSet node
|
|
|
37874 |
*
|
|
|
37875 |
* @param {Node} adaptationSet
|
|
|
37876 |
* The AdaptationSet node to get Segment information from
|
|
|
37877 |
* @return {SegmentInformation}
|
|
|
37878 |
* The Segment information contained within the provided AdaptationSet
|
|
|
37879 |
*/
|
|
|
37880 |
|
|
|
37881 |
const getSegmentInformation = adaptationSet => {
|
|
|
37882 |
const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
|
|
|
37883 |
const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
|
|
|
37884 |
const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge$1({
|
|
|
37885 |
tag: 'SegmentURL'
|
|
|
37886 |
}, parseAttributes(s)));
|
|
|
37887 |
const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
|
|
|
37888 |
const segmentTimelineParentNode = segmentList || segmentTemplate;
|
|
|
37889 |
const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
|
|
|
37890 |
const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
|
|
|
37891 |
const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both
|
|
|
37892 |
// @initialization and an <Initialization> node. @initialization can be templated,
|
|
|
37893 |
// while the node can have a url and range specified. If the <SegmentTemplate> has
|
|
|
37894 |
// both @initialization and an <Initialization> subelement we opt to override with
|
|
|
37895 |
// the node, as this interaction is not defined in the spec.
|
|
|
37896 |
|
|
|
37897 |
const template = segmentTemplate && parseAttributes(segmentTemplate);
|
|
|
37898 |
if (template && segmentInitialization) {
|
|
|
37899 |
template.initialization = segmentInitialization && parseAttributes(segmentInitialization);
|
|
|
37900 |
} else if (template && template.initialization) {
|
|
|
37901 |
// If it is @initialization we convert it to an object since this is the format that
|
|
|
37902 |
// later functions will rely on for the initialization segment. This is only valid
|
|
|
37903 |
// for <SegmentTemplate>
|
|
|
37904 |
template.initialization = {
|
|
|
37905 |
sourceURL: template.initialization
|
|
|
37906 |
};
|
|
|
37907 |
}
|
|
|
37908 |
const segmentInfo = {
|
|
|
37909 |
template,
|
|
|
37910 |
segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)),
|
|
|
37911 |
list: segmentList && merge$1(parseAttributes(segmentList), {
|
|
|
37912 |
segmentUrls,
|
|
|
37913 |
initialization: parseAttributes(segmentInitialization)
|
|
|
37914 |
}),
|
|
|
37915 |
base: segmentBase && merge$1(parseAttributes(segmentBase), {
|
|
|
37916 |
initialization: parseAttributes(segmentInitialization)
|
|
|
37917 |
})
|
|
|
37918 |
};
|
|
|
37919 |
Object.keys(segmentInfo).forEach(key => {
|
|
|
37920 |
if (!segmentInfo[key]) {
|
|
|
37921 |
delete segmentInfo[key];
|
|
|
37922 |
}
|
|
|
37923 |
});
|
|
|
37924 |
return segmentInfo;
|
|
|
37925 |
};
|
|
|
37926 |
/**
|
|
|
37927 |
* Contains Segment information and attributes needed to construct a Playlist object
|
|
|
37928 |
* from a Representation
|
|
|
37929 |
*
|
|
|
37930 |
* @typedef {Object} RepresentationInformation
|
|
|
37931 |
* @property {SegmentInformation} segmentInfo
|
|
|
37932 |
* Segment information for this Representation
|
|
|
37933 |
* @property {Object} attributes
|
|
|
37934 |
* Inherited attributes for this Representation
|
|
|
37935 |
*/
|
|
|
37936 |
|
|
|
37937 |
/**
|
|
|
37938 |
* Maps a Representation node to an object containing Segment information and attributes
|
|
|
37939 |
*
|
|
|
37940 |
* @name inheritBaseUrlsCallback
|
|
|
37941 |
* @function
|
|
|
37942 |
* @param {Node} representation
|
|
|
37943 |
* Representation node from the mpd
|
|
|
37944 |
* @return {RepresentationInformation}
|
|
|
37945 |
* Representation information needed to construct a Playlist object
|
|
|
37946 |
*/
|
|
|
37947 |
|
|
|
37948 |
/**
|
|
|
37949 |
* Returns a callback for Array.prototype.map for mapping Representation nodes to
|
|
|
37950 |
* Segment information and attributes using inherited BaseURL nodes.
|
|
|
37951 |
*
|
|
|
37952 |
* @param {Object} adaptationSetAttributes
|
|
|
37953 |
* Contains attributes inherited by the AdaptationSet
|
|
|
37954 |
* @param {Object[]} adaptationSetBaseUrls
|
|
|
37955 |
* List of objects containing resolved base URLs and attributes
|
|
|
37956 |
* inherited by the AdaptationSet
|
|
|
37957 |
* @param {SegmentInformation} adaptationSetSegmentInfo
|
|
|
37958 |
* Contains Segment information for the AdaptationSet
|
|
|
37959 |
* @return {inheritBaseUrlsCallback}
|
|
|
37960 |
* Callback map function
|
|
|
37961 |
*/
|
|
|
37962 |
|
|
|
37963 |
const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => {
|
|
|
37964 |
const repBaseUrlElements = findChildren(representation, 'BaseURL');
|
|
|
37965 |
const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
|
|
|
37966 |
const attributes = merge$1(adaptationSetAttributes, parseAttributes(representation));
|
|
|
37967 |
const representationSegmentInfo = getSegmentInformation(representation);
|
|
|
37968 |
return repBaseUrls.map(baseUrl => {
|
|
|
37969 |
return {
|
|
|
37970 |
segmentInfo: merge$1(adaptationSetSegmentInfo, representationSegmentInfo),
|
|
|
37971 |
attributes: merge$1(attributes, baseUrl)
|
|
|
37972 |
};
|
|
|
37973 |
});
|
|
|
37974 |
};
|
|
|
37975 |
/**
|
|
|
37976 |
* Tranforms a series of content protection nodes to
|
|
|
37977 |
* an object containing pssh data by key system
|
|
|
37978 |
*
|
|
|
37979 |
* @param {Node[]} contentProtectionNodes
|
|
|
37980 |
* Content protection nodes
|
|
|
37981 |
* @return {Object}
|
|
|
37982 |
* Object containing pssh data by key system
|
|
|
37983 |
*/
|
|
|
37984 |
|
|
|
37985 |
const generateKeySystemInformation = contentProtectionNodes => {
|
|
|
37986 |
return contentProtectionNodes.reduce((acc, node) => {
|
|
|
37987 |
const attributes = parseAttributes(node); // Although it could be argued that according to the UUID RFC spec the UUID string (a-f chars) should be generated
|
|
|
37988 |
// as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system
|
|
|
37989 |
// UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do
|
|
|
37990 |
// .toLowerCase() on the input UUID string from the manifest (at least I could not think of one).
|
|
|
37991 |
|
|
|
37992 |
if (attributes.schemeIdUri) {
|
|
|
37993 |
attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase();
|
|
|
37994 |
}
|
|
|
37995 |
const keySystem = keySystemsMap[attributes.schemeIdUri];
|
|
|
37996 |
if (keySystem) {
|
|
|
37997 |
acc[keySystem] = {
|
|
|
37998 |
attributes
|
|
|
37999 |
};
|
|
|
38000 |
const psshNode = findChildren(node, 'cenc:pssh')[0];
|
|
|
38001 |
if (psshNode) {
|
|
|
38002 |
const pssh = getContent(psshNode);
|
|
|
38003 |
acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh);
|
|
|
38004 |
}
|
|
|
38005 |
}
|
|
|
38006 |
return acc;
|
|
|
38007 |
}, {});
|
|
|
38008 |
}; // defined in ANSI_SCTE 214-1 2016
|
|
|
38009 |
|
|
|
38010 |
const parseCaptionServiceMetadata = service => {
|
|
|
38011 |
// 608 captions
|
|
|
38012 |
if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') {
|
|
|
38013 |
const values = typeof service.value !== 'string' ? [] : service.value.split(';');
|
|
|
38014 |
return values.map(value => {
|
|
|
38015 |
let channel;
|
|
|
38016 |
let language; // default language to value
|
|
|
38017 |
|
|
|
38018 |
language = value;
|
|
|
38019 |
if (/^CC\d=/.test(value)) {
|
|
|
38020 |
[channel, language] = value.split('=');
|
|
|
38021 |
} else if (/^CC\d$/.test(value)) {
|
|
|
38022 |
channel = value;
|
|
|
38023 |
}
|
|
|
38024 |
return {
|
|
|
38025 |
channel,
|
|
|
38026 |
language
|
|
|
38027 |
};
|
|
|
38028 |
});
|
|
|
38029 |
} else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') {
|
|
|
38030 |
const values = typeof service.value !== 'string' ? [] : service.value.split(';');
|
|
|
38031 |
return values.map(value => {
|
|
|
38032 |
const flags = {
|
|
|
38033 |
// service or channel number 1-63
|
|
|
38034 |
'channel': undefined,
|
|
|
38035 |
// language is a 3ALPHA per ISO 639.2/B
|
|
|
38036 |
// field is required
|
|
|
38037 |
'language': undefined,
|
|
|
38038 |
// BIT 1/0 or ?
|
|
|
38039 |
// default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown
|
|
|
38040 |
'aspectRatio': 1,
|
|
|
38041 |
// BIT 1/0
|
|
|
38042 |
// easy reader flag indicated the text is tailed to the needs of beginning readers
|
|
|
38043 |
// default 0, or off
|
|
|
38044 |
'easyReader': 0,
|
|
|
38045 |
// BIT 1/0
|
|
|
38046 |
// If 3d metadata is present (CEA-708.1) then 1
|
|
|
38047 |
// default 0
|
|
|
38048 |
'3D': 0
|
|
|
38049 |
};
|
|
|
38050 |
if (/=/.test(value)) {
|
|
|
38051 |
const [channel, opts = ''] = value.split('=');
|
|
|
38052 |
flags.channel = channel;
|
|
|
38053 |
flags.language = value;
|
|
|
38054 |
opts.split(',').forEach(opt => {
|
|
|
38055 |
const [name, val] = opt.split(':');
|
|
|
38056 |
if (name === 'lang') {
|
|
|
38057 |
flags.language = val; // er for easyReadery
|
|
|
38058 |
} else if (name === 'er') {
|
|
|
38059 |
flags.easyReader = Number(val); // war for wide aspect ratio
|
|
|
38060 |
} else if (name === 'war') {
|
|
|
38061 |
flags.aspectRatio = Number(val);
|
|
|
38062 |
} else if (name === '3D') {
|
|
|
38063 |
flags['3D'] = Number(val);
|
|
|
38064 |
}
|
|
|
38065 |
});
|
|
|
38066 |
} else {
|
|
|
38067 |
flags.language = value;
|
|
|
38068 |
}
|
|
|
38069 |
if (flags.channel) {
|
|
|
38070 |
flags.channel = 'SERVICE' + flags.channel;
|
|
|
38071 |
}
|
|
|
38072 |
return flags;
|
|
|
38073 |
});
|
|
|
38074 |
}
|
|
|
38075 |
};
|
|
|
38076 |
/**
|
|
|
38077 |
* A map callback that will parse all event stream data for a collection of periods
|
|
|
38078 |
* DASH ISO_IEC_23009 5.10.2.2
|
|
|
38079 |
* https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing
|
|
|
38080 |
*
|
|
|
38081 |
* @param {PeriodInformation} period object containing necessary period information
|
|
|
38082 |
* @return a collection of parsed eventstream event objects
|
|
|
38083 |
*/
|
|
|
38084 |
|
|
|
38085 |
const toEventStream = period => {
|
|
|
38086 |
// get and flatten all EventStreams tags and parse attributes and children
|
|
|
38087 |
return flatten(findChildren(period.node, 'EventStream').map(eventStream => {
|
|
|
38088 |
const eventStreamAttributes = parseAttributes(eventStream);
|
|
|
38089 |
const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects
|
|
|
38090 |
|
|
|
38091 |
return findChildren(eventStream, 'Event').map(event => {
|
|
|
38092 |
const eventAttributes = parseAttributes(event);
|
|
|
38093 |
const presentationTime = eventAttributes.presentationTime || 0;
|
|
|
38094 |
const timescale = eventStreamAttributes.timescale || 1;
|
|
|
38095 |
const duration = eventAttributes.duration || 0;
|
|
|
38096 |
const start = presentationTime / timescale + period.attributes.start;
|
|
|
38097 |
return {
|
|
|
38098 |
schemeIdUri,
|
|
|
38099 |
value: eventStreamAttributes.value,
|
|
|
38100 |
id: eventAttributes.id,
|
|
|
38101 |
start,
|
|
|
38102 |
end: start + duration / timescale,
|
|
|
38103 |
messageData: getContent(event) || eventAttributes.messageData,
|
|
|
38104 |
contentEncoding: eventStreamAttributes.contentEncoding,
|
|
|
38105 |
presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0
|
|
|
38106 |
};
|
|
|
38107 |
});
|
|
|
38108 |
}));
|
|
|
38109 |
};
|
|
|
38110 |
/**
|
|
|
38111 |
* Maps an AdaptationSet node to a list of Representation information objects
|
|
|
38112 |
*
|
|
|
38113 |
* @name toRepresentationsCallback
|
|
|
38114 |
* @function
|
|
|
38115 |
* @param {Node} adaptationSet
|
|
|
38116 |
* AdaptationSet node from the mpd
|
|
|
38117 |
* @return {RepresentationInformation[]}
|
|
|
38118 |
* List of objects containing Representaion information
|
|
|
38119 |
*/
|
|
|
38120 |
|
|
|
38121 |
/**
|
|
|
38122 |
* Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
|
|
|
38123 |
* Representation information objects
|
|
|
38124 |
*
|
|
|
38125 |
* @param {Object} periodAttributes
|
|
|
38126 |
* Contains attributes inherited by the Period
|
|
|
38127 |
* @param {Object[]} periodBaseUrls
|
|
|
38128 |
* Contains list of objects with resolved base urls and attributes
|
|
|
38129 |
* inherited by the Period
|
|
|
38130 |
* @param {string[]} periodSegmentInfo
|
|
|
38131 |
* Contains Segment Information at the period level
|
|
|
38132 |
* @return {toRepresentationsCallback}
|
|
|
38133 |
* Callback map function
|
|
|
38134 |
*/
|
|
|
38135 |
|
|
|
38136 |
const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => {
|
|
|
38137 |
const adaptationSetAttributes = parseAttributes(adaptationSet);
|
|
|
38138 |
const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL'));
|
|
|
38139 |
const role = findChildren(adaptationSet, 'Role')[0];
|
|
|
38140 |
const roleAttributes = {
|
|
|
38141 |
role: parseAttributes(role)
|
|
|
38142 |
};
|
|
|
38143 |
let attrs = merge$1(periodAttributes, adaptationSetAttributes, roleAttributes);
|
|
|
38144 |
const accessibility = findChildren(adaptationSet, 'Accessibility')[0];
|
|
|
38145 |
const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility));
|
|
|
38146 |
if (captionServices) {
|
|
|
38147 |
attrs = merge$1(attrs, {
|
|
|
38148 |
captionServices
|
|
|
38149 |
});
|
|
|
38150 |
}
|
|
|
38151 |
const label = findChildren(adaptationSet, 'Label')[0];
|
|
|
38152 |
if (label && label.childNodes.length) {
|
|
|
38153 |
const labelVal = label.childNodes[0].nodeValue.trim();
|
|
|
38154 |
attrs = merge$1(attrs, {
|
|
|
38155 |
label: labelVal
|
|
|
38156 |
});
|
|
|
38157 |
}
|
|
|
38158 |
const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
|
|
|
38159 |
if (Object.keys(contentProtection).length) {
|
|
|
38160 |
attrs = merge$1(attrs, {
|
|
|
38161 |
contentProtection
|
|
|
38162 |
});
|
|
|
38163 |
}
|
|
|
38164 |
const segmentInfo = getSegmentInformation(adaptationSet);
|
|
|
38165 |
const representations = findChildren(adaptationSet, 'Representation');
|
|
|
38166 |
const adaptationSetSegmentInfo = merge$1(periodSegmentInfo, segmentInfo);
|
|
|
38167 |
return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
|
|
|
38168 |
};
|
|
|
38169 |
/**
|
|
|
38170 |
* Contains all period information for mapping nodes onto adaptation sets.
|
|
|
38171 |
*
|
|
|
38172 |
* @typedef {Object} PeriodInformation
|
|
|
38173 |
* @property {Node} period.node
|
|
|
38174 |
* Period node from the mpd
|
|
|
38175 |
* @property {Object} period.attributes
|
|
|
38176 |
* Parsed period attributes from node plus any added
|
|
|
38177 |
*/
|
|
|
38178 |
|
|
|
38179 |
/**
|
|
|
38180 |
* Maps a PeriodInformation object to a list of Representation information objects for all
|
|
|
38181 |
* AdaptationSet nodes contained within the Period.
|
|
|
38182 |
*
|
|
|
38183 |
* @name toAdaptationSetsCallback
|
|
|
38184 |
* @function
|
|
|
38185 |
* @param {PeriodInformation} period
|
|
|
38186 |
* Period object containing necessary period information
|
|
|
38187 |
* @param {number} periodStart
|
|
|
38188 |
* Start time of the Period within the mpd
|
|
|
38189 |
* @return {RepresentationInformation[]}
|
|
|
38190 |
* List of objects containing Representaion information
|
|
|
38191 |
*/
|
|
|
38192 |
|
|
|
38193 |
/**
|
|
|
38194 |
* Returns a callback for Array.prototype.map for mapping Period nodes to a list of
|
|
|
38195 |
* Representation information objects
|
|
|
38196 |
*
|
|
|
38197 |
* @param {Object} mpdAttributes
|
|
|
38198 |
* Contains attributes inherited by the mpd
|
|
|
38199 |
* @param {Object[]} mpdBaseUrls
|
|
|
38200 |
* Contains list of objects with resolved base urls and attributes
|
|
|
38201 |
* inherited by the mpd
|
|
|
38202 |
* @return {toAdaptationSetsCallback}
|
|
|
38203 |
* Callback map function
|
|
|
38204 |
*/
|
|
|
38205 |
|
|
|
38206 |
const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
|
|
|
38207 |
const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL'));
|
|
|
38208 |
const periodAttributes = merge$1(mpdAttributes, {
|
|
|
38209 |
periodStart: period.attributes.start
|
|
|
38210 |
});
|
|
|
38211 |
if (typeof period.attributes.duration === 'number') {
|
|
|
38212 |
periodAttributes.periodDuration = period.attributes.duration;
|
|
|
38213 |
}
|
|
|
38214 |
const adaptationSets = findChildren(period.node, 'AdaptationSet');
|
|
|
38215 |
const periodSegmentInfo = getSegmentInformation(period.node);
|
|
|
38216 |
return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
|
|
|
38217 |
};
|
|
|
38218 |
/**
|
|
|
38219 |
* Tranforms an array of content steering nodes into an object
|
|
|
38220 |
* containing CDN content steering information from the MPD manifest.
|
|
|
38221 |
*
|
|
|
38222 |
* For more information on the DASH spec for Content Steering parsing, see:
|
|
|
38223 |
* https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
|
|
|
38224 |
*
|
|
|
38225 |
* @param {Node[]} contentSteeringNodes
|
|
|
38226 |
* Content steering nodes
|
|
|
38227 |
* @param {Function} eventHandler
|
|
|
38228 |
* The event handler passed into the parser options to handle warnings
|
|
|
38229 |
* @return {Object}
|
|
|
38230 |
* Object containing content steering data
|
|
|
38231 |
*/
|
|
|
38232 |
|
|
|
38233 |
const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => {
|
|
|
38234 |
// If there are more than one ContentSteering tags, throw an error
|
|
|
38235 |
if (contentSteeringNodes.length > 1) {
|
|
|
38236 |
eventHandler({
|
|
|
38237 |
type: 'warn',
|
|
|
38238 |
message: 'The MPD manifest should contain no more than one ContentSteering tag'
|
|
|
38239 |
});
|
|
|
38240 |
} // Return a null value if there are no ContentSteering tags
|
|
|
38241 |
|
|
|
38242 |
if (!contentSteeringNodes.length) {
|
|
|
38243 |
return null;
|
|
|
38244 |
}
|
|
|
38245 |
const infoFromContentSteeringTag = merge$1({
|
|
|
38246 |
serverURL: getContent(contentSteeringNodes[0])
|
|
|
38247 |
}, parseAttributes(contentSteeringNodes[0])); // Converts `queryBeforeStart` to a boolean, as well as setting the default value
|
|
|
38248 |
// to `false` if it doesn't exist
|
|
|
38249 |
|
|
|
38250 |
infoFromContentSteeringTag.queryBeforeStart = infoFromContentSteeringTag.queryBeforeStart === 'true';
|
|
|
38251 |
return infoFromContentSteeringTag;
|
|
|
38252 |
};
|
|
|
38253 |
/**
|
|
|
38254 |
* Gets Period@start property for a given period.
|
|
|
38255 |
*
|
|
|
38256 |
* @param {Object} options
|
|
|
38257 |
* Options object
|
|
|
38258 |
* @param {Object} options.attributes
|
|
|
38259 |
* Period attributes
|
|
|
38260 |
* @param {Object} [options.priorPeriodAttributes]
|
|
|
38261 |
* Prior period attributes (if prior period is available)
|
|
|
38262 |
* @param {string} options.mpdType
|
|
|
38263 |
* The MPD@type these periods came from
|
|
|
38264 |
* @return {number|null}
|
|
|
38265 |
* The period start, or null if it's an early available period or error
|
|
|
38266 |
*/
|
|
|
38267 |
|
|
|
38268 |
const getPeriodStart = ({
|
|
|
38269 |
attributes,
|
|
|
38270 |
priorPeriodAttributes,
|
|
|
38271 |
mpdType
|
|
|
38272 |
}) => {
|
|
|
38273 |
// Summary of period start time calculation from DASH spec section 5.3.2.1
|
|
|
38274 |
//
|
|
|
38275 |
// A period's start is the first period's start + time elapsed after playing all
|
|
|
38276 |
// prior periods to this one. Periods continue one after the other in time (without
|
|
|
38277 |
// gaps) until the end of the presentation.
|
|
|
38278 |
//
|
|
|
38279 |
// The value of Period@start should be:
|
|
|
38280 |
// 1. if Period@start is present: value of Period@start
|
|
|
38281 |
// 2. if previous period exists and it has @duration: previous Period@start +
|
|
|
38282 |
// previous Period@duration
|
|
|
38283 |
// 3. if this is first period and MPD@type is 'static': 0
|
|
|
38284 |
// 4. in all other cases, consider the period an "early available period" (note: not
|
|
|
38285 |
// currently supported)
|
|
|
38286 |
// (1)
|
|
|
38287 |
if (typeof attributes.start === 'number') {
|
|
|
38288 |
return attributes.start;
|
|
|
38289 |
} // (2)
|
|
|
38290 |
|
|
|
38291 |
if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') {
|
|
|
38292 |
return priorPeriodAttributes.start + priorPeriodAttributes.duration;
|
|
|
38293 |
} // (3)
|
|
|
38294 |
|
|
|
38295 |
if (!priorPeriodAttributes && mpdType === 'static') {
|
|
|
38296 |
return 0;
|
|
|
38297 |
} // (4)
|
|
|
38298 |
// There is currently no logic for calculating the Period@start value if there is
|
|
|
38299 |
// no Period@start or prior Period@start and Period@duration available. This is not made
|
|
|
38300 |
// explicit by the DASH interop guidelines or the DASH spec, however, since there's
|
|
|
38301 |
// nothing about any other resolution strategies, it's implied. Thus, this case should
|
|
|
38302 |
// be considered an early available period, or error, and null should suffice for both
|
|
|
38303 |
// of those cases.
|
|
|
38304 |
|
|
|
38305 |
return null;
|
|
|
38306 |
};
|
|
|
38307 |
/**
|
|
|
38308 |
* Traverses the mpd xml tree to generate a list of Representation information objects
|
|
|
38309 |
* that have inherited attributes from parent nodes
|
|
|
38310 |
*
|
|
|
38311 |
* @param {Node} mpd
|
|
|
38312 |
* The root node of the mpd
|
|
|
38313 |
* @param {Object} options
|
|
|
38314 |
* Available options for inheritAttributes
|
|
|
38315 |
* @param {string} options.manifestUri
|
|
|
38316 |
* The uri source of the mpd
|
|
|
38317 |
* @param {number} options.NOW
|
|
|
38318 |
* Current time per DASH IOP. Default is current time in ms since epoch
|
|
|
38319 |
* @param {number} options.clientOffset
|
|
|
38320 |
* Client time difference from NOW (in milliseconds)
|
|
|
38321 |
* @return {RepresentationInformation[]}
|
|
|
38322 |
* List of objects containing Representation information
|
|
|
38323 |
*/
|
|
|
38324 |
|
|
|
38325 |
const inheritAttributes = (mpd, options = {}) => {
|
|
|
38326 |
const {
|
|
|
38327 |
manifestUri = '',
|
|
|
38328 |
NOW = Date.now(),
|
|
|
38329 |
clientOffset = 0,
|
|
|
38330 |
// TODO: For now, we are expecting an eventHandler callback function
|
|
|
38331 |
// to be passed into the mpd parser as an option.
|
|
|
38332 |
// In the future, we should enable stream parsing by using the Stream class from vhs-utils.
|
|
|
38333 |
// This will support new features including a standardized event handler.
|
|
|
38334 |
// See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing.
|
|
|
38335 |
// https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9
|
|
|
38336 |
eventHandler = function () {}
|
|
|
38337 |
} = options;
|
|
|
38338 |
const periodNodes = findChildren(mpd, 'Period');
|
|
|
38339 |
if (!periodNodes.length) {
|
|
|
38340 |
throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
|
|
|
38341 |
}
|
|
|
38342 |
const locations = findChildren(mpd, 'Location');
|
|
|
38343 |
const mpdAttributes = parseAttributes(mpd);
|
|
|
38344 |
const mpdBaseUrls = buildBaseUrls([{
|
|
|
38345 |
baseUrl: manifestUri
|
|
|
38346 |
}], findChildren(mpd, 'BaseURL'));
|
|
|
38347 |
const contentSteeringNodes = findChildren(mpd, 'ContentSteering'); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
|
|
|
38348 |
|
|
|
38349 |
mpdAttributes.type = mpdAttributes.type || 'static';
|
|
|
38350 |
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
|
|
|
38351 |
mpdAttributes.NOW = NOW;
|
|
|
38352 |
mpdAttributes.clientOffset = clientOffset;
|
|
|
38353 |
if (locations.length) {
|
|
|
38354 |
mpdAttributes.locations = locations.map(getContent);
|
|
|
38355 |
}
|
|
|
38356 |
const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to
|
|
|
38357 |
// adding properties that require looking at prior periods is to parse attributes and add
|
|
|
38358 |
// missing ones before toAdaptationSets is called. If more such properties are added, it
|
|
|
38359 |
// may be better to refactor toAdaptationSets.
|
|
|
38360 |
|
|
|
38361 |
periodNodes.forEach((node, index) => {
|
|
|
38362 |
const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary
|
|
|
38363 |
// for this period.
|
|
|
38364 |
|
|
|
38365 |
const priorPeriod = periods[index - 1];
|
|
|
38366 |
attributes.start = getPeriodStart({
|
|
|
38367 |
attributes,
|
|
|
38368 |
priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null,
|
|
|
38369 |
mpdType: mpdAttributes.type
|
|
|
38370 |
});
|
|
|
38371 |
periods.push({
|
|
|
38372 |
node,
|
|
|
38373 |
attributes
|
|
|
38374 |
});
|
|
|
38375 |
});
|
|
|
38376 |
return {
|
|
|
38377 |
locations: mpdAttributes.locations,
|
|
|
38378 |
contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler),
|
|
|
38379 |
// TODO: There are occurences where this `representationInfo` array contains undesired
|
|
|
38380 |
// duplicates. This generally occurs when there are multiple BaseURL nodes that are
|
|
|
38381 |
// direct children of the MPD node. When we attempt to resolve URLs from a combination of the
|
|
|
38382 |
// parent BaseURL and a child BaseURL, and the value does not resolve,
|
|
|
38383 |
// we end up returning the child BaseURL multiple times.
|
|
|
38384 |
// We need to determine a way to remove these duplicates in a safe way.
|
|
|
38385 |
// See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527
|
|
|
38386 |
representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
|
|
|
38387 |
eventStream: flatten(periods.map(toEventStream))
|
|
|
38388 |
};
|
|
|
38389 |
};
|
|
|
38390 |
const stringToMpdXml = manifestString => {
|
|
|
38391 |
if (manifestString === '') {
|
|
|
38392 |
throw new Error(errors.DASH_EMPTY_MANIFEST);
|
|
|
38393 |
}
|
|
|
38394 |
const parser = new DOMParser();
|
|
|
38395 |
let xml;
|
|
|
38396 |
let mpd;
|
|
|
38397 |
try {
|
|
|
38398 |
xml = parser.parseFromString(manifestString, 'application/xml');
|
|
|
38399 |
mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
|
|
|
38400 |
} catch (e) {// ie 11 throws on invalid xml
|
|
|
38401 |
}
|
|
|
38402 |
if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
|
|
|
38403 |
throw new Error(errors.DASH_INVALID_XML);
|
|
|
38404 |
}
|
|
|
38405 |
return mpd;
|
|
|
38406 |
};
|
|
|
38407 |
|
|
|
38408 |
/**
|
|
|
38409 |
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
|
|
|
38410 |
*
|
|
|
38411 |
* @param {string} mpd
|
|
|
38412 |
* XML string of the MPD manifest
|
|
|
38413 |
* @return {Object|null}
|
|
|
38414 |
* Attributes of UTCTiming node specified in the manifest. Null if none found
|
|
|
38415 |
*/
|
|
|
38416 |
|
|
|
38417 |
const parseUTCTimingScheme = mpd => {
|
|
|
38418 |
const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
|
|
|
38419 |
if (!UTCTimingNode) {
|
|
|
38420 |
return null;
|
|
|
38421 |
}
|
|
|
38422 |
const attributes = parseAttributes(UTCTimingNode);
|
|
|
38423 |
switch (attributes.schemeIdUri) {
|
|
|
38424 |
case 'urn:mpeg:dash:utc:http-head:2014':
|
|
|
38425 |
case 'urn:mpeg:dash:utc:http-head:2012':
|
|
|
38426 |
attributes.method = 'HEAD';
|
|
|
38427 |
break;
|
|
|
38428 |
case 'urn:mpeg:dash:utc:http-xsdate:2014':
|
|
|
38429 |
case 'urn:mpeg:dash:utc:http-iso:2014':
|
|
|
38430 |
case 'urn:mpeg:dash:utc:http-xsdate:2012':
|
|
|
38431 |
case 'urn:mpeg:dash:utc:http-iso:2012':
|
|
|
38432 |
attributes.method = 'GET';
|
|
|
38433 |
break;
|
|
|
38434 |
case 'urn:mpeg:dash:utc:direct:2014':
|
|
|
38435 |
case 'urn:mpeg:dash:utc:direct:2012':
|
|
|
38436 |
attributes.method = 'DIRECT';
|
|
|
38437 |
attributes.value = Date.parse(attributes.value);
|
|
|
38438 |
break;
|
|
|
38439 |
case 'urn:mpeg:dash:utc:http-ntp:2014':
|
|
|
38440 |
case 'urn:mpeg:dash:utc:ntp:2014':
|
|
|
38441 |
case 'urn:mpeg:dash:utc:sntp:2014':
|
|
|
38442 |
default:
|
|
|
38443 |
throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
|
|
|
38444 |
}
|
|
|
38445 |
return attributes;
|
|
|
38446 |
};
|
|
|
38447 |
/*
|
|
|
38448 |
* Given a DASH manifest string and options, parses the DASH manifest into an object in the
|
|
|
38449 |
* form outputed by m3u8-parser and accepted by videojs/http-streaming.
|
|
|
38450 |
*
|
|
|
38451 |
* For live DASH manifests, if `previousManifest` is provided in options, then the newly
|
|
|
38452 |
* parsed DASH manifest will have its media sequence and discontinuity sequence values
|
|
|
38453 |
* updated to reflect its position relative to the prior manifest.
|
|
|
38454 |
*
|
|
|
38455 |
* @param {string} manifestString - the DASH manifest as a string
|
|
|
38456 |
* @param {options} [options] - any options
|
|
|
38457 |
*
|
|
|
38458 |
* @return {Object} the manifest object
|
|
|
38459 |
*/
|
|
|
38460 |
|
|
|
38461 |
const parse = (manifestString, options = {}) => {
|
|
|
38462 |
const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
|
|
|
38463 |
const playlists = toPlaylists(parsedManifestInfo.representationInfo);
|
|
|
38464 |
return toM3u8({
|
|
|
38465 |
dashPlaylists: playlists,
|
|
|
38466 |
locations: parsedManifestInfo.locations,
|
|
|
38467 |
contentSteering: parsedManifestInfo.contentSteeringInfo,
|
|
|
38468 |
sidxMapping: options.sidxMapping,
|
|
|
38469 |
previousManifest: options.previousManifest,
|
|
|
38470 |
eventStream: parsedManifestInfo.eventStream
|
|
|
38471 |
});
|
|
|
38472 |
};
|
|
|
38473 |
/**
|
|
|
38474 |
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
|
|
|
38475 |
*
|
|
|
38476 |
* @param {string} manifestString
|
|
|
38477 |
* XML string of the MPD manifest
|
|
|
38478 |
* @return {Object|null}
|
|
|
38479 |
* Attributes of UTCTiming node specified in the manifest. Null if none found
|
|
|
38480 |
*/
|
|
|
38481 |
|
|
|
38482 |
const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString));
|
|
|
38483 |
|
|
|
38484 |
var MAX_UINT32 = Math.pow(2, 32);
|
|
|
38485 |
var getUint64$1 = function (uint8) {
|
|
|
38486 |
var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength);
|
|
|
38487 |
var value;
|
|
|
38488 |
if (dv.getBigUint64) {
|
|
|
38489 |
value = dv.getBigUint64(0);
|
|
|
38490 |
if (value < Number.MAX_SAFE_INTEGER) {
|
|
|
38491 |
return Number(value);
|
|
|
38492 |
}
|
|
|
38493 |
return value;
|
|
|
38494 |
}
|
|
|
38495 |
return dv.getUint32(0) * MAX_UINT32 + dv.getUint32(4);
|
|
|
38496 |
};
|
|
|
38497 |
var numbers = {
|
|
|
38498 |
getUint64: getUint64$1,
|
|
|
38499 |
MAX_UINT32: MAX_UINT32
|
|
|
38500 |
};
|
|
|
38501 |
|
|
|
38502 |
var getUint64 = numbers.getUint64;
|
|
|
38503 |
var parseSidx = function (data) {
|
|
|
38504 |
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
|
|
|
38505 |
result = {
|
|
|
38506 |
version: data[0],
|
|
|
38507 |
flags: new Uint8Array(data.subarray(1, 4)),
|
|
|
38508 |
references: [],
|
|
|
38509 |
referenceId: view.getUint32(4),
|
|
|
38510 |
timescale: view.getUint32(8)
|
|
|
38511 |
},
|
|
|
38512 |
i = 12;
|
|
|
38513 |
if (result.version === 0) {
|
|
|
38514 |
result.earliestPresentationTime = view.getUint32(i);
|
|
|
38515 |
result.firstOffset = view.getUint32(i + 4);
|
|
|
38516 |
i += 8;
|
|
|
38517 |
} else {
|
|
|
38518 |
// read 64 bits
|
|
|
38519 |
result.earliestPresentationTime = getUint64(data.subarray(i));
|
|
|
38520 |
result.firstOffset = getUint64(data.subarray(i + 8));
|
|
|
38521 |
i += 16;
|
|
|
38522 |
}
|
|
|
38523 |
i += 2; // reserved
|
|
|
38524 |
|
|
|
38525 |
var referenceCount = view.getUint16(i);
|
|
|
38526 |
i += 2; // start of references
|
|
|
38527 |
|
|
|
38528 |
for (; referenceCount > 0; i += 12, referenceCount--) {
|
|
|
38529 |
result.references.push({
|
|
|
38530 |
referenceType: (data[i] & 0x80) >>> 7,
|
|
|
38531 |
referencedSize: view.getUint32(i) & 0x7FFFFFFF,
|
|
|
38532 |
subsegmentDuration: view.getUint32(i + 4),
|
|
|
38533 |
startsWithSap: !!(data[i + 8] & 0x80),
|
|
|
38534 |
sapType: (data[i + 8] & 0x70) >>> 4,
|
|
|
38535 |
sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
|
|
|
38536 |
});
|
|
|
38537 |
}
|
|
|
38538 |
return result;
|
|
|
38539 |
};
|
|
|
38540 |
var parseSidx_1 = parseSidx;
|
|
|
38541 |
|
|
|
38542 |
var ID3 = toUint8([0x49, 0x44, 0x33]);
|
|
|
38543 |
var getId3Size = function getId3Size(bytes, offset) {
|
|
|
38544 |
if (offset === void 0) {
|
|
|
38545 |
offset = 0;
|
|
|
38546 |
}
|
|
|
38547 |
bytes = toUint8(bytes);
|
|
|
38548 |
var flags = bytes[offset + 5];
|
|
|
38549 |
var returnSize = bytes[offset + 6] << 21 | bytes[offset + 7] << 14 | bytes[offset + 8] << 7 | bytes[offset + 9];
|
|
|
38550 |
var footerPresent = (flags & 16) >> 4;
|
|
|
38551 |
if (footerPresent) {
|
|
|
38552 |
return returnSize + 20;
|
|
|
38553 |
}
|
|
|
38554 |
return returnSize + 10;
|
|
|
38555 |
};
|
|
|
38556 |
var getId3Offset = function getId3Offset(bytes, offset) {
|
|
|
38557 |
if (offset === void 0) {
|
|
|
38558 |
offset = 0;
|
|
|
38559 |
}
|
|
|
38560 |
bytes = toUint8(bytes);
|
|
|
38561 |
if (bytes.length - offset < 10 || !bytesMatch(bytes, ID3, {
|
|
|
38562 |
offset: offset
|
|
|
38563 |
})) {
|
|
|
38564 |
return offset;
|
|
|
38565 |
}
|
|
|
38566 |
offset += getId3Size(bytes, offset); // recursive check for id3 tags as some files
|
|
|
38567 |
// have multiple ID3 tag sections even though
|
|
|
38568 |
// they should not.
|
|
|
38569 |
|
|
|
38570 |
return getId3Offset(bytes, offset);
|
|
|
38571 |
};
|
|
|
38572 |
|
|
|
38573 |
var normalizePath$1 = function normalizePath(path) {
|
|
|
38574 |
if (typeof path === 'string') {
|
|
|
38575 |
return stringToBytes(path);
|
|
|
38576 |
}
|
|
|
38577 |
if (typeof path === 'number') {
|
|
|
38578 |
return path;
|
|
|
38579 |
}
|
|
|
38580 |
return path;
|
|
|
38581 |
};
|
|
|
38582 |
var normalizePaths$1 = function normalizePaths(paths) {
|
|
|
38583 |
if (!Array.isArray(paths)) {
|
|
|
38584 |
return [normalizePath$1(paths)];
|
|
|
38585 |
}
|
|
|
38586 |
return paths.map(function (p) {
|
|
|
38587 |
return normalizePath$1(p);
|
|
|
38588 |
});
|
|
|
38589 |
};
|
|
|
38590 |
/**
|
|
|
38591 |
* find any number of boxes by name given a path to it in an iso bmff
|
|
|
38592 |
* such as mp4.
|
|
|
38593 |
*
|
|
|
38594 |
* @param {TypedArray} bytes
|
|
|
38595 |
* bytes for the iso bmff to search for boxes in
|
|
|
38596 |
*
|
|
|
38597 |
* @param {Uint8Array[]|string[]|string|Uint8Array} name
|
|
|
38598 |
* An array of paths or a single path representing the name
|
|
|
38599 |
* of boxes to search through in bytes. Paths may be
|
|
|
38600 |
* uint8 (character codes) or strings.
|
|
|
38601 |
*
|
|
|
38602 |
* @param {boolean} [complete=false]
|
|
|
38603 |
* Should we search only for complete boxes on the final path.
|
|
|
38604 |
* This is very useful when you do not want to get back partial boxes
|
|
|
38605 |
* in the case of streaming files.
|
|
|
38606 |
*
|
|
|
38607 |
* @return {Uint8Array[]}
|
|
|
38608 |
* An array of the end paths that we found.
|
|
|
38609 |
*/
|
|
|
38610 |
|
|
|
38611 |
var findBox = function findBox(bytes, paths, complete) {
|
|
|
38612 |
if (complete === void 0) {
|
|
|
38613 |
complete = false;
|
|
|
38614 |
}
|
|
|
38615 |
paths = normalizePaths$1(paths);
|
|
|
38616 |
bytes = toUint8(bytes);
|
|
|
38617 |
var results = [];
|
|
|
38618 |
if (!paths.length) {
|
|
|
38619 |
// short-circuit the search for empty paths
|
|
|
38620 |
return results;
|
|
|
38621 |
}
|
|
|
38622 |
var i = 0;
|
|
|
38623 |
while (i < bytes.length) {
|
|
|
38624 |
var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0;
|
|
|
38625 |
var type = bytes.subarray(i + 4, i + 8); // invalid box format.
|
|
|
38626 |
|
|
|
38627 |
if (size === 0) {
|
|
|
38628 |
break;
|
|
|
38629 |
}
|
|
|
38630 |
var end = i + size;
|
|
|
38631 |
if (end > bytes.length) {
|
|
|
38632 |
// this box is bigger than the number of bytes we have
|
|
|
38633 |
// and complete is set, we cannot find any more boxes.
|
|
|
38634 |
if (complete) {
|
|
|
38635 |
break;
|
|
|
38636 |
}
|
|
|
38637 |
end = bytes.length;
|
|
|
38638 |
}
|
|
|
38639 |
var data = bytes.subarray(i + 8, end);
|
|
|
38640 |
if (bytesMatch(type, paths[0])) {
|
|
|
38641 |
if (paths.length === 1) {
|
|
|
38642 |
// this is the end of the path and we've found the box we were
|
|
|
38643 |
// looking for
|
|
|
38644 |
results.push(data);
|
|
|
38645 |
} else {
|
|
|
38646 |
// recursively search for the next box along the path
|
|
|
38647 |
results.push.apply(results, findBox(data, paths.slice(1), complete));
|
|
|
38648 |
}
|
|
|
38649 |
}
|
|
|
38650 |
i = end;
|
|
|
38651 |
} // we've finished searching all of bytes
|
|
|
38652 |
|
|
|
38653 |
return results;
|
|
|
38654 |
};
|
|
|
38655 |
|
|
|
38656 |
// https://matroska-org.github.io/libebml/specs.html
|
|
|
38657 |
// https://www.matroska.org/technical/elements.html
|
|
|
38658 |
// https://www.webmproject.org/docs/container/
|
|
|
38659 |
|
|
|
38660 |
var EBML_TAGS = {
|
|
|
38661 |
EBML: toUint8([0x1A, 0x45, 0xDF, 0xA3]),
|
|
|
38662 |
DocType: toUint8([0x42, 0x82]),
|
|
|
38663 |
Segment: toUint8([0x18, 0x53, 0x80, 0x67]),
|
|
|
38664 |
SegmentInfo: toUint8([0x15, 0x49, 0xA9, 0x66]),
|
|
|
38665 |
Tracks: toUint8([0x16, 0x54, 0xAE, 0x6B]),
|
|
|
38666 |
Track: toUint8([0xAE]),
|
|
|
38667 |
TrackNumber: toUint8([0xd7]),
|
|
|
38668 |
DefaultDuration: toUint8([0x23, 0xe3, 0x83]),
|
|
|
38669 |
TrackEntry: toUint8([0xAE]),
|
|
|
38670 |
TrackType: toUint8([0x83]),
|
|
|
38671 |
FlagDefault: toUint8([0x88]),
|
|
|
38672 |
CodecID: toUint8([0x86]),
|
|
|
38673 |
CodecPrivate: toUint8([0x63, 0xA2]),
|
|
|
38674 |
VideoTrack: toUint8([0xe0]),
|
|
|
38675 |
AudioTrack: toUint8([0xe1]),
|
|
|
38676 |
// Not used yet, but will be used for live webm/mkv
|
|
|
38677 |
// see https://www.matroska.org/technical/basics.html#block-structure
|
|
|
38678 |
// see https://www.matroska.org/technical/basics.html#simpleblock-structure
|
|
|
38679 |
Cluster: toUint8([0x1F, 0x43, 0xB6, 0x75]),
|
|
|
38680 |
Timestamp: toUint8([0xE7]),
|
|
|
38681 |
TimestampScale: toUint8([0x2A, 0xD7, 0xB1]),
|
|
|
38682 |
BlockGroup: toUint8([0xA0]),
|
|
|
38683 |
BlockDuration: toUint8([0x9B]),
|
|
|
38684 |
Block: toUint8([0xA1]),
|
|
|
38685 |
SimpleBlock: toUint8([0xA3])
|
|
|
38686 |
};
|
|
|
38687 |
/**
|
|
|
38688 |
* This is a simple table to determine the length
|
|
|
38689 |
* of things in ebml. The length is one based (starts at 1,
|
|
|
38690 |
* rather than zero) and for every zero bit before a one bit
|
|
|
38691 |
* we add one to length. We also need this table because in some
|
|
|
38692 |
* case we have to xor all the length bits from another value.
|
|
|
38693 |
*/
|
|
|
38694 |
|
|
|
38695 |
var LENGTH_TABLE = [128, 64, 32, 16, 8, 4, 2, 1];
|
|
|
38696 |
var getLength = function getLength(byte) {
|
|
|
38697 |
var len = 1;
|
|
|
38698 |
for (var i = 0; i < LENGTH_TABLE.length; i++) {
|
|
|
38699 |
if (byte & LENGTH_TABLE[i]) {
|
|
|
38700 |
break;
|
|
|
38701 |
}
|
|
|
38702 |
len++;
|
|
|
38703 |
}
|
|
|
38704 |
return len;
|
|
|
38705 |
}; // length in ebml is stored in the first 4 to 8 bits
|
|
|
38706 |
// of the first byte. 4 for the id length and 8 for the
|
|
|
38707 |
// data size length. Length is measured by converting the number to binary
|
|
|
38708 |
// then 1 + the number of zeros before a 1 is encountered starting
|
|
|
38709 |
// from the left.
|
|
|
38710 |
|
|
|
38711 |
var getvint = function getvint(bytes, offset, removeLength, signed) {
|
|
|
38712 |
if (removeLength === void 0) {
|
|
|
38713 |
removeLength = true;
|
|
|
38714 |
}
|
|
|
38715 |
if (signed === void 0) {
|
|
|
38716 |
signed = false;
|
|
|
38717 |
}
|
|
|
38718 |
var length = getLength(bytes[offset]);
|
|
|
38719 |
var valueBytes = bytes.subarray(offset, offset + length); // NOTE that we do **not** subarray here because we need to copy these bytes
|
|
|
38720 |
// as they will be modified below to remove the dataSizeLen bits and we do not
|
|
|
38721 |
// want to modify the original data. normally we could just call slice on
|
|
|
38722 |
// uint8array but ie 11 does not support that...
|
|
|
38723 |
|
|
|
38724 |
if (removeLength) {
|
|
|
38725 |
valueBytes = Array.prototype.slice.call(bytes, offset, offset + length);
|
|
|
38726 |
valueBytes[0] ^= LENGTH_TABLE[length - 1];
|
|
|
38727 |
}
|
|
|
38728 |
return {
|
|
|
38729 |
length: length,
|
|
|
38730 |
value: bytesToNumber(valueBytes, {
|
|
|
38731 |
signed: signed
|
|
|
38732 |
}),
|
|
|
38733 |
bytes: valueBytes
|
|
|
38734 |
};
|
|
|
38735 |
};
|
|
|
38736 |
var normalizePath = function normalizePath(path) {
|
|
|
38737 |
if (typeof path === 'string') {
|
|
|
38738 |
return path.match(/.{1,2}/g).map(function (p) {
|
|
|
38739 |
return normalizePath(p);
|
|
|
38740 |
});
|
|
|
38741 |
}
|
|
|
38742 |
if (typeof path === 'number') {
|
|
|
38743 |
return numberToBytes(path);
|
|
|
38744 |
}
|
|
|
38745 |
return path;
|
|
|
38746 |
};
|
|
|
38747 |
var normalizePaths = function normalizePaths(paths) {
|
|
|
38748 |
if (!Array.isArray(paths)) {
|
|
|
38749 |
return [normalizePath(paths)];
|
|
|
38750 |
}
|
|
|
38751 |
return paths.map(function (p) {
|
|
|
38752 |
return normalizePath(p);
|
|
|
38753 |
});
|
|
|
38754 |
};
|
|
|
38755 |
var getInfinityDataSize = function getInfinityDataSize(id, bytes, offset) {
|
|
|
38756 |
if (offset >= bytes.length) {
|
|
|
38757 |
return bytes.length;
|
|
|
38758 |
}
|
|
|
38759 |
var innerid = getvint(bytes, offset, false);
|
|
|
38760 |
if (bytesMatch(id.bytes, innerid.bytes)) {
|
|
|
38761 |
return offset;
|
|
|
38762 |
}
|
|
|
38763 |
var dataHeader = getvint(bytes, offset + innerid.length);
|
|
|
38764 |
return getInfinityDataSize(id, bytes, offset + dataHeader.length + dataHeader.value + innerid.length);
|
|
|
38765 |
};
|
|
|
38766 |
/**
|
|
|
38767 |
* Notes on the EBLM format.
|
|
|
38768 |
*
|
|
|
38769 |
* EBLM uses "vints" tags. Every vint tag contains
|
|
|
38770 |
* two parts
|
|
|
38771 |
*
|
|
|
38772 |
* 1. The length from the first byte. You get this by
|
|
|
38773 |
* converting the byte to binary and counting the zeros
|
|
|
38774 |
* before a 1. Then you add 1 to that. Examples
|
|
|
38775 |
* 00011111 = length 4 because there are 3 zeros before a 1.
|
|
|
38776 |
* 00100000 = length 3 because there are 2 zeros before a 1.
|
|
|
38777 |
* 00000011 = length 7 because there are 6 zeros before a 1.
|
|
|
38778 |
*
|
|
|
38779 |
* 2. The bits used for length are removed from the first byte
|
|
|
38780 |
* Then all the bytes are merged into a value. NOTE: this
|
|
|
38781 |
* is not the case for id ebml tags as there id includes
|
|
|
38782 |
* length bits.
|
|
|
38783 |
*
|
|
|
38784 |
*/
|
|
|
38785 |
|
|
|
38786 |
var findEbml = function findEbml(bytes, paths) {
|
|
|
38787 |
paths = normalizePaths(paths);
|
|
|
38788 |
bytes = toUint8(bytes);
|
|
|
38789 |
var results = [];
|
|
|
38790 |
if (!paths.length) {
|
|
|
38791 |
return results;
|
|
|
38792 |
}
|
|
|
38793 |
var i = 0;
|
|
|
38794 |
while (i < bytes.length) {
|
|
|
38795 |
var id = getvint(bytes, i, false);
|
|
|
38796 |
var dataHeader = getvint(bytes, i + id.length);
|
|
|
38797 |
var dataStart = i + id.length + dataHeader.length; // dataSize is unknown or this is a live stream
|
|
|
38798 |
|
|
|
38799 |
if (dataHeader.value === 0x7f) {
|
|
|
38800 |
dataHeader.value = getInfinityDataSize(id, bytes, dataStart);
|
|
|
38801 |
if (dataHeader.value !== bytes.length) {
|
|
|
38802 |
dataHeader.value -= dataStart;
|
|
|
38803 |
}
|
|
|
38804 |
}
|
|
|
38805 |
var dataEnd = dataStart + dataHeader.value > bytes.length ? bytes.length : dataStart + dataHeader.value;
|
|
|
38806 |
var data = bytes.subarray(dataStart, dataEnd);
|
|
|
38807 |
if (bytesMatch(paths[0], id.bytes)) {
|
|
|
38808 |
if (paths.length === 1) {
|
|
|
38809 |
// this is the end of the paths and we've found the tag we were
|
|
|
38810 |
// looking for
|
|
|
38811 |
results.push(data);
|
|
|
38812 |
} else {
|
|
|
38813 |
// recursively search for the next tag inside of the data
|
|
|
38814 |
// of this one
|
|
|
38815 |
results = results.concat(findEbml(data, paths.slice(1)));
|
|
|
38816 |
}
|
|
|
38817 |
}
|
|
|
38818 |
var totalLength = id.length + dataHeader.length + data.length; // move past this tag entirely, we are not looking for it
|
|
|
38819 |
|
|
|
38820 |
i += totalLength;
|
|
|
38821 |
}
|
|
|
38822 |
return results;
|
|
|
38823 |
}; // see https://www.matroska.org/technical/basics.html#block-structure
|
|
|
38824 |
|
|
|
38825 |
var NAL_TYPE_ONE = toUint8([0x00, 0x00, 0x00, 0x01]);
|
|
|
38826 |
var NAL_TYPE_TWO = toUint8([0x00, 0x00, 0x01]);
|
|
|
38827 |
var EMULATION_PREVENTION = toUint8([0x00, 0x00, 0x03]);
|
|
|
38828 |
/**
|
|
|
38829 |
* Expunge any "Emulation Prevention" bytes from a "Raw Byte
|
|
|
38830 |
* Sequence Payload"
|
|
|
38831 |
*
|
|
|
38832 |
* @param data {Uint8Array} the bytes of a RBSP from a NAL
|
|
|
38833 |
* unit
|
|
|
38834 |
* @return {Uint8Array} the RBSP without any Emulation
|
|
|
38835 |
* Prevention Bytes
|
|
|
38836 |
*/
|
|
|
38837 |
|
|
|
38838 |
var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(bytes) {
|
|
|
38839 |
var positions = [];
|
|
|
38840 |
var i = 1; // Find all `Emulation Prevention Bytes`
|
|
|
38841 |
|
|
|
38842 |
while (i < bytes.length - 2) {
|
|
|
38843 |
if (bytesMatch(bytes.subarray(i, i + 3), EMULATION_PREVENTION)) {
|
|
|
38844 |
positions.push(i + 2);
|
|
|
38845 |
i++;
|
|
|
38846 |
}
|
|
|
38847 |
i++;
|
|
|
38848 |
} // If no Emulation Prevention Bytes were found just return the original
|
|
|
38849 |
// array
|
|
|
38850 |
|
|
|
38851 |
if (positions.length === 0) {
|
|
|
38852 |
return bytes;
|
|
|
38853 |
} // Create a new array to hold the NAL unit data
|
|
|
38854 |
|
|
|
38855 |
var newLength = bytes.length - positions.length;
|
|
|
38856 |
var newData = new Uint8Array(newLength);
|
|
|
38857 |
var sourceIndex = 0;
|
|
|
38858 |
for (i = 0; i < newLength; sourceIndex++, i++) {
|
|
|
38859 |
if (sourceIndex === positions[0]) {
|
|
|
38860 |
// Skip this byte
|
|
|
38861 |
sourceIndex++; // Remove this position index
|
|
|
38862 |
|
|
|
38863 |
positions.shift();
|
|
|
38864 |
}
|
|
|
38865 |
newData[i] = bytes[sourceIndex];
|
|
|
38866 |
}
|
|
|
38867 |
return newData;
|
|
|
38868 |
};
|
|
|
38869 |
var findNal = function findNal(bytes, dataType, types, nalLimit) {
|
|
|
38870 |
if (nalLimit === void 0) {
|
|
|
38871 |
nalLimit = Infinity;
|
|
|
38872 |
}
|
|
|
38873 |
bytes = toUint8(bytes);
|
|
|
38874 |
types = [].concat(types);
|
|
|
38875 |
var i = 0;
|
|
|
38876 |
var nalStart;
|
|
|
38877 |
var nalsFound = 0; // keep searching until:
|
|
|
38878 |
// we reach the end of bytes
|
|
|
38879 |
// we reach the maximum number of nals they want to seach
|
|
|
38880 |
// NOTE: that we disregard nalLimit when we have found the start
|
|
|
38881 |
// of the nal we want so that we can find the end of the nal we want.
|
|
|
38882 |
|
|
|
38883 |
while (i < bytes.length && (nalsFound < nalLimit || nalStart)) {
|
|
|
38884 |
var nalOffset = void 0;
|
|
|
38885 |
if (bytesMatch(bytes.subarray(i), NAL_TYPE_ONE)) {
|
|
|
38886 |
nalOffset = 4;
|
|
|
38887 |
} else if (bytesMatch(bytes.subarray(i), NAL_TYPE_TWO)) {
|
|
|
38888 |
nalOffset = 3;
|
|
|
38889 |
} // we are unsynced,
|
|
|
38890 |
// find the next nal unit
|
|
|
38891 |
|
|
|
38892 |
if (!nalOffset) {
|
|
|
38893 |
i++;
|
|
|
38894 |
continue;
|
|
|
38895 |
}
|
|
|
38896 |
nalsFound++;
|
|
|
38897 |
if (nalStart) {
|
|
|
38898 |
return discardEmulationPreventionBytes(bytes.subarray(nalStart, i));
|
|
|
38899 |
}
|
|
|
38900 |
var nalType = void 0;
|
|
|
38901 |
if (dataType === 'h264') {
|
|
|
38902 |
nalType = bytes[i + nalOffset] & 0x1f;
|
|
|
38903 |
} else if (dataType === 'h265') {
|
|
|
38904 |
nalType = bytes[i + nalOffset] >> 1 & 0x3f;
|
|
|
38905 |
}
|
|
|
38906 |
if (types.indexOf(nalType) !== -1) {
|
|
|
38907 |
nalStart = i + nalOffset;
|
|
|
38908 |
} // nal header is 1 length for h264, and 2 for h265
|
|
|
38909 |
|
|
|
38910 |
i += nalOffset + (dataType === 'h264' ? 1 : 2);
|
|
|
38911 |
}
|
|
|
38912 |
return bytes.subarray(0, 0);
|
|
|
38913 |
};
|
|
|
38914 |
var findH264Nal = function findH264Nal(bytes, type, nalLimit) {
|
|
|
38915 |
return findNal(bytes, 'h264', type, nalLimit);
|
|
|
38916 |
};
|
|
|
38917 |
var findH265Nal = function findH265Nal(bytes, type, nalLimit) {
|
|
|
38918 |
return findNal(bytes, 'h265', type, nalLimit);
|
|
|
38919 |
};
|
|
|
38920 |
|
|
|
38921 |
var CONSTANTS = {
|
|
|
38922 |
// "webm" string literal in hex
|
|
|
38923 |
'webm': toUint8([0x77, 0x65, 0x62, 0x6d]),
|
|
|
38924 |
// "matroska" string literal in hex
|
|
|
38925 |
'matroska': toUint8([0x6d, 0x61, 0x74, 0x72, 0x6f, 0x73, 0x6b, 0x61]),
|
|
|
38926 |
// "fLaC" string literal in hex
|
|
|
38927 |
'flac': toUint8([0x66, 0x4c, 0x61, 0x43]),
|
|
|
38928 |
// "OggS" string literal in hex
|
|
|
38929 |
'ogg': toUint8([0x4f, 0x67, 0x67, 0x53]),
|
|
|
38930 |
// ac-3 sync byte, also works for ec-3 as that is simply a codec
|
|
|
38931 |
// of ac-3
|
|
|
38932 |
'ac3': toUint8([0x0b, 0x77]),
|
|
|
38933 |
// "RIFF" string literal in hex used for wav and avi
|
|
|
38934 |
'riff': toUint8([0x52, 0x49, 0x46, 0x46]),
|
|
|
38935 |
// "AVI" string literal in hex
|
|
|
38936 |
'avi': toUint8([0x41, 0x56, 0x49]),
|
|
|
38937 |
// "WAVE" string literal in hex
|
|
|
38938 |
'wav': toUint8([0x57, 0x41, 0x56, 0x45]),
|
|
|
38939 |
// "ftyp3g" string literal in hex
|
|
|
38940 |
'3gp': toUint8([0x66, 0x74, 0x79, 0x70, 0x33, 0x67]),
|
|
|
38941 |
// "ftyp" string literal in hex
|
|
|
38942 |
'mp4': toUint8([0x66, 0x74, 0x79, 0x70]),
|
|
|
38943 |
// "styp" string literal in hex
|
|
|
38944 |
'fmp4': toUint8([0x73, 0x74, 0x79, 0x70]),
|
|
|
38945 |
// "ftypqt" string literal in hex
|
|
|
38946 |
'mov': toUint8([0x66, 0x74, 0x79, 0x70, 0x71, 0x74]),
|
|
|
38947 |
// moov string literal in hex
|
|
|
38948 |
'moov': toUint8([0x6D, 0x6F, 0x6F, 0x76]),
|
|
|
38949 |
// moof string literal in hex
|
|
|
38950 |
'moof': toUint8([0x6D, 0x6F, 0x6F, 0x66])
|
|
|
38951 |
};
|
|
|
38952 |
var _isLikely = {
|
|
|
38953 |
aac: function aac(bytes) {
|
|
|
38954 |
var offset = getId3Offset(bytes);
|
|
|
38955 |
return bytesMatch(bytes, [0xFF, 0x10], {
|
|
|
38956 |
offset: offset,
|
|
|
38957 |
mask: [0xFF, 0x16]
|
|
|
38958 |
});
|
|
|
38959 |
},
|
|
|
38960 |
mp3: function mp3(bytes) {
|
|
|
38961 |
var offset = getId3Offset(bytes);
|
|
|
38962 |
return bytesMatch(bytes, [0xFF, 0x02], {
|
|
|
38963 |
offset: offset,
|
|
|
38964 |
mask: [0xFF, 0x06]
|
|
|
38965 |
});
|
|
|
38966 |
},
|
|
|
38967 |
webm: function webm(bytes) {
|
|
|
38968 |
var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is webm
|
|
|
38969 |
|
|
|
38970 |
return bytesMatch(docType, CONSTANTS.webm);
|
|
|
38971 |
},
|
|
|
38972 |
mkv: function mkv(bytes) {
|
|
|
38973 |
var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is matroska
|
|
|
38974 |
|
|
|
38975 |
return bytesMatch(docType, CONSTANTS.matroska);
|
|
|
38976 |
},
|
|
|
38977 |
mp4: function mp4(bytes) {
|
|
|
38978 |
// if this file is another base media file format, it is not mp4
|
|
|
38979 |
if (_isLikely['3gp'](bytes) || _isLikely.mov(bytes)) {
|
|
|
38980 |
return false;
|
|
|
38981 |
} // if this file starts with a ftyp or styp box its mp4
|
|
|
38982 |
|
|
|
38983 |
if (bytesMatch(bytes, CONSTANTS.mp4, {
|
|
|
38984 |
offset: 4
|
|
|
38985 |
}) || bytesMatch(bytes, CONSTANTS.fmp4, {
|
|
|
38986 |
offset: 4
|
|
|
38987 |
})) {
|
|
|
38988 |
return true;
|
|
|
38989 |
} // if this file starts with a moof/moov box its mp4
|
|
|
38990 |
|
|
|
38991 |
if (bytesMatch(bytes, CONSTANTS.moof, {
|
|
|
38992 |
offset: 4
|
|
|
38993 |
}) || bytesMatch(bytes, CONSTANTS.moov, {
|
|
|
38994 |
offset: 4
|
|
|
38995 |
})) {
|
|
|
38996 |
return true;
|
|
|
38997 |
}
|
|
|
38998 |
},
|
|
|
38999 |
mov: function mov(bytes) {
|
|
|
39000 |
return bytesMatch(bytes, CONSTANTS.mov, {
|
|
|
39001 |
offset: 4
|
|
|
39002 |
});
|
|
|
39003 |
},
|
|
|
39004 |
'3gp': function gp(bytes) {
|
|
|
39005 |
return bytesMatch(bytes, CONSTANTS['3gp'], {
|
|
|
39006 |
offset: 4
|
|
|
39007 |
});
|
|
|
39008 |
},
|
|
|
39009 |
ac3: function ac3(bytes) {
|
|
|
39010 |
var offset = getId3Offset(bytes);
|
|
|
39011 |
return bytesMatch(bytes, CONSTANTS.ac3, {
|
|
|
39012 |
offset: offset
|
|
|
39013 |
});
|
|
|
39014 |
},
|
|
|
39015 |
ts: function ts(bytes) {
|
|
|
39016 |
if (bytes.length < 189 && bytes.length >= 1) {
|
|
|
39017 |
return bytes[0] === 0x47;
|
|
|
39018 |
}
|
|
|
39019 |
var i = 0; // check the first 376 bytes for two matching sync bytes
|
|
|
39020 |
|
|
|
39021 |
while (i + 188 < bytes.length && i < 188) {
|
|
|
39022 |
if (bytes[i] === 0x47 && bytes[i + 188] === 0x47) {
|
|
|
39023 |
return true;
|
|
|
39024 |
}
|
|
|
39025 |
i += 1;
|
|
|
39026 |
}
|
|
|
39027 |
return false;
|
|
|
39028 |
},
|
|
|
39029 |
flac: function flac(bytes) {
|
|
|
39030 |
var offset = getId3Offset(bytes);
|
|
|
39031 |
return bytesMatch(bytes, CONSTANTS.flac, {
|
|
|
39032 |
offset: offset
|
|
|
39033 |
});
|
|
|
39034 |
},
|
|
|
39035 |
ogg: function ogg(bytes) {
|
|
|
39036 |
return bytesMatch(bytes, CONSTANTS.ogg);
|
|
|
39037 |
},
|
|
|
39038 |
avi: function avi(bytes) {
|
|
|
39039 |
return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.avi, {
|
|
|
39040 |
offset: 8
|
|
|
39041 |
});
|
|
|
39042 |
},
|
|
|
39043 |
wav: function wav(bytes) {
|
|
|
39044 |
return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.wav, {
|
|
|
39045 |
offset: 8
|
|
|
39046 |
});
|
|
|
39047 |
},
|
|
|
39048 |
'h264': function h264(bytes) {
|
|
|
39049 |
// find seq_parameter_set_rbsp
|
|
|
39050 |
return findH264Nal(bytes, 7, 3).length;
|
|
|
39051 |
},
|
|
|
39052 |
'h265': function h265(bytes) {
|
|
|
39053 |
// find video_parameter_set_rbsp or seq_parameter_set_rbsp
|
|
|
39054 |
return findH265Nal(bytes, [32, 33], 3).length;
|
|
|
39055 |
}
|
|
|
39056 |
}; // get all the isLikely functions
|
|
|
39057 |
// but make sure 'ts' is above h264 and h265
|
|
|
39058 |
// but below everything else as it is the least specific
|
|
|
39059 |
|
|
|
39060 |
var isLikelyTypes = Object.keys(_isLikely) // remove ts, h264, h265
|
|
|
39061 |
.filter(function (t) {
|
|
|
39062 |
return t !== 'ts' && t !== 'h264' && t !== 'h265';
|
|
|
39063 |
}) // add it back to the bottom
|
|
|
39064 |
.concat(['ts', 'h264', 'h265']); // make sure we are dealing with uint8 data.
|
|
|
39065 |
|
|
|
39066 |
isLikelyTypes.forEach(function (type) {
|
|
|
39067 |
var isLikelyFn = _isLikely[type];
|
|
|
39068 |
_isLikely[type] = function (bytes) {
|
|
|
39069 |
return isLikelyFn(toUint8(bytes));
|
|
|
39070 |
};
|
|
|
39071 |
}); // export after wrapping
|
|
|
39072 |
|
|
|
39073 |
var isLikely = _isLikely; // A useful list of file signatures can be found here
|
|
|
39074 |
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
|
39075 |
|
|
|
39076 |
var detectContainerForBytes = function detectContainerForBytes(bytes) {
|
|
|
39077 |
bytes = toUint8(bytes);
|
|
|
39078 |
for (var i = 0; i < isLikelyTypes.length; i++) {
|
|
|
39079 |
var type = isLikelyTypes[i];
|
|
|
39080 |
if (isLikely[type](bytes)) {
|
|
|
39081 |
return type;
|
|
|
39082 |
}
|
|
|
39083 |
}
|
|
|
39084 |
return '';
|
|
|
39085 |
}; // fmp4 is not a container
|
|
|
39086 |
|
|
|
39087 |
var isLikelyFmp4MediaSegment = function isLikelyFmp4MediaSegment(bytes) {
|
|
|
39088 |
return findBox(bytes, ['moof']).length > 0;
|
|
|
39089 |
};
|
|
|
39090 |
|
|
|
39091 |
/**
|
|
|
39092 |
* mux.js
|
|
|
39093 |
*
|
|
|
39094 |
* Copyright (c) Brightcove
|
|
|
39095 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
39096 |
*/
|
|
|
39097 |
var ONE_SECOND_IN_TS = 90000,
|
|
|
39098 |
// 90kHz clock
|
|
|
39099 |
secondsToVideoTs,
|
|
|
39100 |
secondsToAudioTs,
|
|
|
39101 |
videoTsToSeconds,
|
|
|
39102 |
audioTsToSeconds,
|
|
|
39103 |
audioTsToVideoTs,
|
|
|
39104 |
videoTsToAudioTs,
|
|
|
39105 |
metadataTsToSeconds;
|
|
|
39106 |
secondsToVideoTs = function (seconds) {
|
|
|
39107 |
return seconds * ONE_SECOND_IN_TS;
|
|
|
39108 |
};
|
|
|
39109 |
secondsToAudioTs = function (seconds, sampleRate) {
|
|
|
39110 |
return seconds * sampleRate;
|
|
|
39111 |
};
|
|
|
39112 |
videoTsToSeconds = function (timestamp) {
|
|
|
39113 |
return timestamp / ONE_SECOND_IN_TS;
|
|
|
39114 |
};
|
|
|
39115 |
audioTsToSeconds = function (timestamp, sampleRate) {
|
|
|
39116 |
return timestamp / sampleRate;
|
|
|
39117 |
};
|
|
|
39118 |
audioTsToVideoTs = function (timestamp, sampleRate) {
|
|
|
39119 |
return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate));
|
|
|
39120 |
};
|
|
|
39121 |
videoTsToAudioTs = function (timestamp, sampleRate) {
|
|
|
39122 |
return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate);
|
|
|
39123 |
};
|
|
|
39124 |
|
|
|
39125 |
/**
|
|
|
39126 |
* Adjust ID3 tag or caption timing information by the timeline pts values
|
|
|
39127 |
* (if keepOriginalTimestamps is false) and convert to seconds
|
|
|
39128 |
*/
|
|
|
39129 |
metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) {
|
|
|
39130 |
return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts);
|
|
|
39131 |
};
|
|
|
39132 |
var clock = {
|
|
|
39133 |
ONE_SECOND_IN_TS: ONE_SECOND_IN_TS,
|
|
|
39134 |
secondsToVideoTs: secondsToVideoTs,
|
|
|
39135 |
secondsToAudioTs: secondsToAudioTs,
|
|
|
39136 |
videoTsToSeconds: videoTsToSeconds,
|
|
|
39137 |
audioTsToSeconds: audioTsToSeconds,
|
|
|
39138 |
audioTsToVideoTs: audioTsToVideoTs,
|
|
|
39139 |
videoTsToAudioTs: videoTsToAudioTs,
|
|
|
39140 |
metadataTsToSeconds: metadataTsToSeconds
|
|
|
39141 |
};
|
|
|
39142 |
var clock_1 = clock.ONE_SECOND_IN_TS;
|
|
|
39143 |
|
|
|
39144 |
/*! @name @videojs/http-streaming @version 3.10.0 @license Apache-2.0 */
|
|
|
39145 |
|
|
|
39146 |
/**
|
|
|
39147 |
* @file resolve-url.js - Handling how URLs are resolved and manipulated
|
|
|
39148 |
*/
|
|
|
39149 |
const resolveUrl = resolveUrl$1;
|
|
|
39150 |
/**
|
|
|
39151 |
* If the xhr request was redirected, return the responseURL, otherwise,
|
|
|
39152 |
* return the original url.
|
|
|
39153 |
*
|
|
|
39154 |
* @api private
|
|
|
39155 |
*
|
|
|
39156 |
* @param {string} url - an url being requested
|
|
|
39157 |
* @param {XMLHttpRequest} req - xhr request result
|
|
|
39158 |
*
|
|
|
39159 |
* @return {string}
|
|
|
39160 |
*/
|
|
|
39161 |
|
|
|
39162 |
const resolveManifestRedirect = (url, req) => {
|
|
|
39163 |
// To understand how the responseURL below is set and generated:
|
|
|
39164 |
// - https://fetch.spec.whatwg.org/#concept-response-url
|
|
|
39165 |
// - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
|
|
|
39166 |
if (req && req.responseURL && url !== req.responseURL) {
|
|
|
39167 |
return req.responseURL;
|
|
|
39168 |
}
|
|
|
39169 |
return url;
|
|
|
39170 |
};
|
|
|
39171 |
const logger = source => {
|
|
|
39172 |
if (videojs.log.debug) {
|
|
|
39173 |
return videojs.log.debug.bind(videojs, 'VHS:', `${source} >`);
|
|
|
39174 |
}
|
|
|
39175 |
return function () {};
|
|
|
39176 |
};
|
|
|
39177 |
|
|
|
39178 |
/**
|
|
|
39179 |
* Provides a compatibility layer between Video.js 7 and 8 API changes for VHS.
|
|
|
39180 |
*/
|
|
|
39181 |
/**
|
|
|
39182 |
* Delegates to videojs.obj.merge (Video.js 8) or
|
|
|
39183 |
* videojs.mergeOptions (Video.js 7).
|
|
|
39184 |
*/
|
|
|
39185 |
|
|
|
39186 |
function merge(...args) {
|
|
|
39187 |
const context = videojs.obj || videojs;
|
|
|
39188 |
const fn = context.merge || context.mergeOptions;
|
|
|
39189 |
return fn.apply(context, args);
|
|
|
39190 |
}
|
|
|
39191 |
/**
|
|
|
39192 |
* Delegates to videojs.time.createTimeRanges (Video.js 8) or
|
|
|
39193 |
* videojs.createTimeRanges (Video.js 7).
|
|
|
39194 |
*/
|
|
|
39195 |
|
|
|
39196 |
function createTimeRanges(...args) {
|
|
|
39197 |
const context = videojs.time || videojs;
|
|
|
39198 |
const fn = context.createTimeRanges || context.createTimeRanges;
|
|
|
39199 |
return fn.apply(context, args);
|
|
|
39200 |
}
|
|
|
39201 |
|
|
|
39202 |
/**
|
|
|
39203 |
* ranges
|
|
|
39204 |
*
|
|
|
39205 |
* Utilities for working with TimeRanges.
|
|
|
39206 |
*
|
|
|
39207 |
*/
|
|
|
39208 |
|
|
|
39209 |
const TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range
|
|
|
39210 |
// can be misleading because of precision differences or when the current media has poorly
|
|
|
39211 |
// aligned audio and video, which can cause values to be slightly off from what you would
|
|
|
39212 |
// expect. This value is what we consider to be safe to use in such comparisons to account
|
|
|
39213 |
// for these scenarios.
|
|
|
39214 |
|
|
|
39215 |
const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
|
|
|
39216 |
const filterRanges = function (timeRanges, predicate) {
|
|
|
39217 |
const results = [];
|
|
|
39218 |
let i;
|
|
|
39219 |
if (timeRanges && timeRanges.length) {
|
|
|
39220 |
// Search for ranges that match the predicate
|
|
|
39221 |
for (i = 0; i < timeRanges.length; i++) {
|
|
|
39222 |
if (predicate(timeRanges.start(i), timeRanges.end(i))) {
|
|
|
39223 |
results.push([timeRanges.start(i), timeRanges.end(i)]);
|
|
|
39224 |
}
|
|
|
39225 |
}
|
|
|
39226 |
}
|
|
|
39227 |
return createTimeRanges(results);
|
|
|
39228 |
};
|
|
|
39229 |
/**
|
|
|
39230 |
* Attempts to find the buffered TimeRange that contains the specified
|
|
|
39231 |
* time.
|
|
|
39232 |
*
|
|
|
39233 |
* @param {TimeRanges} buffered - the TimeRanges object to query
|
|
|
39234 |
* @param {number} time - the time to filter on.
|
|
|
39235 |
* @return {TimeRanges} a new TimeRanges object
|
|
|
39236 |
*/
|
|
|
39237 |
|
|
|
39238 |
const findRange = function (buffered, time) {
|
|
|
39239 |
return filterRanges(buffered, function (start, end) {
|
|
|
39240 |
return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time;
|
|
|
39241 |
});
|
|
|
39242 |
};
|
|
|
39243 |
/**
|
|
|
39244 |
* Returns the TimeRanges that begin later than the specified time.
|
|
|
39245 |
*
|
|
|
39246 |
* @param {TimeRanges} timeRanges - the TimeRanges object to query
|
|
|
39247 |
* @param {number} time - the time to filter on.
|
|
|
39248 |
* @return {TimeRanges} a new TimeRanges object.
|
|
|
39249 |
*/
|
|
|
39250 |
|
|
|
39251 |
const findNextRange = function (timeRanges, time) {
|
|
|
39252 |
return filterRanges(timeRanges, function (start) {
|
|
|
39253 |
return start - TIME_FUDGE_FACTOR >= time;
|
|
|
39254 |
});
|
|
|
39255 |
};
|
|
|
39256 |
/**
|
|
|
39257 |
* Returns gaps within a list of TimeRanges
|
|
|
39258 |
*
|
|
|
39259 |
* @param {TimeRanges} buffered - the TimeRanges object
|
|
|
39260 |
* @return {TimeRanges} a TimeRanges object of gaps
|
|
|
39261 |
*/
|
|
|
39262 |
|
|
|
39263 |
const findGaps = function (buffered) {
|
|
|
39264 |
if (buffered.length < 2) {
|
|
|
39265 |
return createTimeRanges();
|
|
|
39266 |
}
|
|
|
39267 |
const ranges = [];
|
|
|
39268 |
for (let i = 1; i < buffered.length; i++) {
|
|
|
39269 |
const start = buffered.end(i - 1);
|
|
|
39270 |
const end = buffered.start(i);
|
|
|
39271 |
ranges.push([start, end]);
|
|
|
39272 |
}
|
|
|
39273 |
return createTimeRanges(ranges);
|
|
|
39274 |
};
|
|
|
39275 |
/**
|
|
|
39276 |
* Calculate the intersection of two TimeRanges
|
|
|
39277 |
*
|
|
|
39278 |
* @param {TimeRanges} bufferA
|
|
|
39279 |
* @param {TimeRanges} bufferB
|
|
|
39280 |
* @return {TimeRanges} The interesection of `bufferA` with `bufferB`
|
|
|
39281 |
*/
|
|
|
39282 |
|
|
|
39283 |
const bufferIntersection = function (bufferA, bufferB) {
|
|
|
39284 |
let start = null;
|
|
|
39285 |
let end = null;
|
|
|
39286 |
let arity = 0;
|
|
|
39287 |
const extents = [];
|
|
|
39288 |
const ranges = [];
|
|
|
39289 |
if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
|
|
|
39290 |
return createTimeRanges();
|
|
|
39291 |
} // Handle the case where we have both buffers and create an
|
|
|
39292 |
// intersection of the two
|
|
|
39293 |
|
|
|
39294 |
let count = bufferA.length; // A) Gather up all start and end times
|
|
|
39295 |
|
|
|
39296 |
while (count--) {
|
|
|
39297 |
extents.push({
|
|
|
39298 |
time: bufferA.start(count),
|
|
|
39299 |
type: 'start'
|
|
|
39300 |
});
|
|
|
39301 |
extents.push({
|
|
|
39302 |
time: bufferA.end(count),
|
|
|
39303 |
type: 'end'
|
|
|
39304 |
});
|
|
|
39305 |
}
|
|
|
39306 |
count = bufferB.length;
|
|
|
39307 |
while (count--) {
|
|
|
39308 |
extents.push({
|
|
|
39309 |
time: bufferB.start(count),
|
|
|
39310 |
type: 'start'
|
|
|
39311 |
});
|
|
|
39312 |
extents.push({
|
|
|
39313 |
time: bufferB.end(count),
|
|
|
39314 |
type: 'end'
|
|
|
39315 |
});
|
|
|
39316 |
} // B) Sort them by time
|
|
|
39317 |
|
|
|
39318 |
extents.sort(function (a, b) {
|
|
|
39319 |
return a.time - b.time;
|
|
|
39320 |
}); // C) Go along one by one incrementing arity for start and decrementing
|
|
|
39321 |
// arity for ends
|
|
|
39322 |
|
|
|
39323 |
for (count = 0; count < extents.length; count++) {
|
|
|
39324 |
if (extents[count].type === 'start') {
|
|
|
39325 |
arity++; // D) If arity is ever incremented to 2 we are entering an
|
|
|
39326 |
// overlapping range
|
|
|
39327 |
|
|
|
39328 |
if (arity === 2) {
|
|
|
39329 |
start = extents[count].time;
|
|
|
39330 |
}
|
|
|
39331 |
} else if (extents[count].type === 'end') {
|
|
|
39332 |
arity--; // E) If arity is ever decremented to 1 we leaving an
|
|
|
39333 |
// overlapping range
|
|
|
39334 |
|
|
|
39335 |
if (arity === 1) {
|
|
|
39336 |
end = extents[count].time;
|
|
|
39337 |
}
|
|
|
39338 |
} // F) Record overlapping ranges
|
|
|
39339 |
|
|
|
39340 |
if (start !== null && end !== null) {
|
|
|
39341 |
ranges.push([start, end]);
|
|
|
39342 |
start = null;
|
|
|
39343 |
end = null;
|
|
|
39344 |
}
|
|
|
39345 |
}
|
|
|
39346 |
return createTimeRanges(ranges);
|
|
|
39347 |
};
|
|
|
39348 |
/**
|
|
|
39349 |
* Gets a human readable string for a TimeRange
|
|
|
39350 |
*
|
|
|
39351 |
* @param {TimeRange} range
|
|
|
39352 |
* @return {string} a human readable string
|
|
|
39353 |
*/
|
|
|
39354 |
|
|
|
39355 |
const printableRange = range => {
|
|
|
39356 |
const strArr = [];
|
|
|
39357 |
if (!range || !range.length) {
|
|
|
39358 |
return '';
|
|
|
39359 |
}
|
|
|
39360 |
for (let i = 0; i < range.length; i++) {
|
|
|
39361 |
strArr.push(range.start(i) + ' => ' + range.end(i));
|
|
|
39362 |
}
|
|
|
39363 |
return strArr.join(', ');
|
|
|
39364 |
};
|
|
|
39365 |
/**
|
|
|
39366 |
* Calculates the amount of time left in seconds until the player hits the end of the
|
|
|
39367 |
* buffer and causes a rebuffer
|
|
|
39368 |
*
|
|
|
39369 |
* @param {TimeRange} buffered
|
|
|
39370 |
* The state of the buffer
|
|
|
39371 |
* @param {Numnber} currentTime
|
|
|
39372 |
* The current time of the player
|
|
|
39373 |
* @param {number} playbackRate
|
|
|
39374 |
* The current playback rate of the player. Defaults to 1.
|
|
|
39375 |
* @return {number}
|
|
|
39376 |
* Time until the player has to start rebuffering in seconds.
|
|
|
39377 |
* @function timeUntilRebuffer
|
|
|
39378 |
*/
|
|
|
39379 |
|
|
|
39380 |
const timeUntilRebuffer = function (buffered, currentTime, playbackRate = 1) {
|
|
|
39381 |
const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
|
|
|
39382 |
return (bufferedEnd - currentTime) / playbackRate;
|
|
|
39383 |
};
|
|
|
39384 |
/**
|
|
|
39385 |
* Converts a TimeRanges object into an array representation
|
|
|
39386 |
*
|
|
|
39387 |
* @param {TimeRanges} timeRanges
|
|
|
39388 |
* @return {Array}
|
|
|
39389 |
*/
|
|
|
39390 |
|
|
|
39391 |
const timeRangesToArray = timeRanges => {
|
|
|
39392 |
const timeRangesList = [];
|
|
|
39393 |
for (let i = 0; i < timeRanges.length; i++) {
|
|
|
39394 |
timeRangesList.push({
|
|
|
39395 |
start: timeRanges.start(i),
|
|
|
39396 |
end: timeRanges.end(i)
|
|
|
39397 |
});
|
|
|
39398 |
}
|
|
|
39399 |
return timeRangesList;
|
|
|
39400 |
};
|
|
|
39401 |
/**
|
|
|
39402 |
* Determines if two time range objects are different.
|
|
|
39403 |
*
|
|
|
39404 |
* @param {TimeRange} a
|
|
|
39405 |
* the first time range object to check
|
|
|
39406 |
*
|
|
|
39407 |
* @param {TimeRange} b
|
|
|
39408 |
* the second time range object to check
|
|
|
39409 |
*
|
|
|
39410 |
* @return {Boolean}
|
|
|
39411 |
* Whether the time range objects differ
|
|
|
39412 |
*/
|
|
|
39413 |
|
|
|
39414 |
const isRangeDifferent = function (a, b) {
|
|
|
39415 |
// same object
|
|
|
39416 |
if (a === b) {
|
|
|
39417 |
return false;
|
|
|
39418 |
} // one or the other is undefined
|
|
|
39419 |
|
|
|
39420 |
if (!a && b || !b && a) {
|
|
|
39421 |
return true;
|
|
|
39422 |
} // length is different
|
|
|
39423 |
|
|
|
39424 |
if (a.length !== b.length) {
|
|
|
39425 |
return true;
|
|
|
39426 |
} // see if any start/end pair is different
|
|
|
39427 |
|
|
|
39428 |
for (let i = 0; i < a.length; i++) {
|
|
|
39429 |
if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) {
|
|
|
39430 |
return true;
|
|
|
39431 |
}
|
|
|
39432 |
} // if the length and every pair is the same
|
|
|
39433 |
// this is the same time range
|
|
|
39434 |
|
|
|
39435 |
return false;
|
|
|
39436 |
};
|
|
|
39437 |
const lastBufferedEnd = function (a) {
|
|
|
39438 |
if (!a || !a.length || !a.end) {
|
|
|
39439 |
return;
|
|
|
39440 |
}
|
|
|
39441 |
return a.end(a.length - 1);
|
|
|
39442 |
};
|
|
|
39443 |
/**
|
|
|
39444 |
* A utility function to add up the amount of time in a timeRange
|
|
|
39445 |
* after a specified startTime.
|
|
|
39446 |
* ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0
|
|
|
39447 |
* would return 40 as there are 40s seconds after 0 in the timeRange
|
|
|
39448 |
*
|
|
|
39449 |
* @param {TimeRange} range
|
|
|
39450 |
* The range to check against
|
|
|
39451 |
* @param {number} startTime
|
|
|
39452 |
* The time in the time range that you should start counting from
|
|
|
39453 |
*
|
|
|
39454 |
* @return {number}
|
|
|
39455 |
* The number of seconds in the buffer passed the specified time.
|
|
|
39456 |
*/
|
|
|
39457 |
|
|
|
39458 |
const timeAheadOf = function (range, startTime) {
|
|
|
39459 |
let time = 0;
|
|
|
39460 |
if (!range || !range.length) {
|
|
|
39461 |
return time;
|
|
|
39462 |
}
|
|
|
39463 |
for (let i = 0; i < range.length; i++) {
|
|
|
39464 |
const start = range.start(i);
|
|
|
39465 |
const end = range.end(i); // startTime is after this range entirely
|
|
|
39466 |
|
|
|
39467 |
if (startTime > end) {
|
|
|
39468 |
continue;
|
|
|
39469 |
} // startTime is within this range
|
|
|
39470 |
|
|
|
39471 |
if (startTime > start && startTime <= end) {
|
|
|
39472 |
time += end - startTime;
|
|
|
39473 |
continue;
|
|
|
39474 |
} // startTime is before this range.
|
|
|
39475 |
|
|
|
39476 |
time += end - start;
|
|
|
39477 |
}
|
|
|
39478 |
return time;
|
|
|
39479 |
};
|
|
|
39480 |
|
|
|
39481 |
/**
|
|
|
39482 |
* @file playlist.js
|
|
|
39483 |
*
|
|
|
39484 |
* Playlist related utilities.
|
|
|
39485 |
*/
|
|
|
39486 |
/**
|
|
|
39487 |
* Get the duration of a segment, with special cases for
|
|
|
39488 |
* llhls segments that do not have a duration yet.
|
|
|
39489 |
*
|
|
|
39490 |
* @param {Object} playlist
|
|
|
39491 |
* the playlist that the segment belongs to.
|
|
|
39492 |
* @param {Object} segment
|
|
|
39493 |
* the segment to get a duration for.
|
|
|
39494 |
*
|
|
|
39495 |
* @return {number}
|
|
|
39496 |
* the segment duration
|
|
|
39497 |
*/
|
|
|
39498 |
|
|
|
39499 |
const segmentDurationWithParts = (playlist, segment) => {
|
|
|
39500 |
// if this isn't a preload segment
|
|
|
39501 |
// then we will have a segment duration that is accurate.
|
|
|
39502 |
if (!segment.preload) {
|
|
|
39503 |
return segment.duration;
|
|
|
39504 |
} // otherwise we have to add up parts and preload hints
|
|
|
39505 |
// to get an up to date duration.
|
|
|
39506 |
|
|
|
39507 |
let result = 0;
|
|
|
39508 |
(segment.parts || []).forEach(function (p) {
|
|
|
39509 |
result += p.duration;
|
|
|
39510 |
}); // for preload hints we have to use partTargetDuration
|
|
|
39511 |
// as they won't even have a duration yet.
|
|
|
39512 |
|
|
|
39513 |
(segment.preloadHints || []).forEach(function (p) {
|
|
|
39514 |
if (p.type === 'PART') {
|
|
|
39515 |
result += playlist.partTargetDuration;
|
|
|
39516 |
}
|
|
|
39517 |
});
|
|
|
39518 |
return result;
|
|
|
39519 |
};
|
|
|
39520 |
/**
|
|
|
39521 |
* A function to get a combined list of parts and segments with durations
|
|
|
39522 |
* and indexes.
|
|
|
39523 |
*
|
|
|
39524 |
* @param {Playlist} playlist the playlist to get the list for.
|
|
|
39525 |
*
|
|
|
39526 |
* @return {Array} The part/segment list.
|
|
|
39527 |
*/
|
|
|
39528 |
|
|
|
39529 |
const getPartsAndSegments = playlist => (playlist.segments || []).reduce((acc, segment, si) => {
|
|
|
39530 |
if (segment.parts) {
|
|
|
39531 |
segment.parts.forEach(function (part, pi) {
|
|
|
39532 |
acc.push({
|
|
|
39533 |
duration: part.duration,
|
|
|
39534 |
segmentIndex: si,
|
|
|
39535 |
partIndex: pi,
|
|
|
39536 |
part,
|
|
|
39537 |
segment
|
|
|
39538 |
});
|
|
|
39539 |
});
|
|
|
39540 |
} else {
|
|
|
39541 |
acc.push({
|
|
|
39542 |
duration: segment.duration,
|
|
|
39543 |
segmentIndex: si,
|
|
|
39544 |
partIndex: null,
|
|
|
39545 |
segment,
|
|
|
39546 |
part: null
|
|
|
39547 |
});
|
|
|
39548 |
}
|
|
|
39549 |
return acc;
|
|
|
39550 |
}, []);
|
|
|
39551 |
const getLastParts = media => {
|
|
|
39552 |
const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1];
|
|
|
39553 |
return lastSegment && lastSegment.parts || [];
|
|
|
39554 |
};
|
|
|
39555 |
const getKnownPartCount = ({
|
|
|
39556 |
preloadSegment
|
|
|
39557 |
}) => {
|
|
|
39558 |
if (!preloadSegment) {
|
|
|
39559 |
return;
|
|
|
39560 |
}
|
|
|
39561 |
const {
|
|
|
39562 |
parts,
|
|
|
39563 |
preloadHints
|
|
|
39564 |
} = preloadSegment;
|
|
|
39565 |
let partCount = (preloadHints || []).reduce((count, hint) => count + (hint.type === 'PART' ? 1 : 0), 0);
|
|
|
39566 |
partCount += parts && parts.length ? parts.length : 0;
|
|
|
39567 |
return partCount;
|
|
|
39568 |
};
|
|
|
39569 |
/**
|
|
|
39570 |
* Get the number of seconds to delay from the end of a
|
|
|
39571 |
* live playlist.
|
|
|
39572 |
*
|
|
|
39573 |
* @param {Playlist} main the main playlist
|
|
|
39574 |
* @param {Playlist} media the media playlist
|
|
|
39575 |
* @return {number} the hold back in seconds.
|
|
|
39576 |
*/
|
|
|
39577 |
|
|
|
39578 |
const liveEdgeDelay = (main, media) => {
|
|
|
39579 |
if (media.endList) {
|
|
|
39580 |
return 0;
|
|
|
39581 |
} // dash suggestedPresentationDelay trumps everything
|
|
|
39582 |
|
|
|
39583 |
if (main && main.suggestedPresentationDelay) {
|
|
|
39584 |
return main.suggestedPresentationDelay;
|
|
|
39585 |
}
|
|
|
39586 |
const hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first
|
|
|
39587 |
|
|
|
39588 |
if (hasParts && media.serverControl && media.serverControl.partHoldBack) {
|
|
|
39589 |
return media.serverControl.partHoldBack;
|
|
|
39590 |
} else if (hasParts && media.partTargetDuration) {
|
|
|
39591 |
return media.partTargetDuration * 3; // finally look for full segment delays
|
|
|
39592 |
} else if (media.serverControl && media.serverControl.holdBack) {
|
|
|
39593 |
return media.serverControl.holdBack;
|
|
|
39594 |
} else if (media.targetDuration) {
|
|
|
39595 |
return media.targetDuration * 3;
|
|
|
39596 |
}
|
|
|
39597 |
return 0;
|
|
|
39598 |
};
|
|
|
39599 |
/**
|
|
|
39600 |
* walk backward until we find a duration we can use
|
|
|
39601 |
* or return a failure
|
|
|
39602 |
*
|
|
|
39603 |
* @param {Playlist} playlist the playlist to walk through
|
|
|
39604 |
* @param {Number} endSequence the mediaSequence to stop walking on
|
|
|
39605 |
*/
|
|
|
39606 |
|
|
|
39607 |
const backwardDuration = function (playlist, endSequence) {
|
|
|
39608 |
let result = 0;
|
|
|
39609 |
let i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following
|
|
|
39610 |
// the interval, use it
|
|
|
39611 |
|
|
|
39612 |
let segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline
|
|
|
39613 |
// information that is earlier than endSequence
|
|
|
39614 |
|
|
|
39615 |
if (segment) {
|
|
|
39616 |
if (typeof segment.start !== 'undefined') {
|
|
|
39617 |
return {
|
|
|
39618 |
result: segment.start,
|
|
|
39619 |
precise: true
|
|
|
39620 |
};
|
|
|
39621 |
}
|
|
|
39622 |
if (typeof segment.end !== 'undefined') {
|
|
|
39623 |
return {
|
|
|
39624 |
result: segment.end - segment.duration,
|
|
|
39625 |
precise: true
|
|
|
39626 |
};
|
|
|
39627 |
}
|
|
|
39628 |
}
|
|
|
39629 |
while (i--) {
|
|
|
39630 |
segment = playlist.segments[i];
|
|
|
39631 |
if (typeof segment.end !== 'undefined') {
|
|
|
39632 |
return {
|
|
|
39633 |
result: result + segment.end,
|
|
|
39634 |
precise: true
|
|
|
39635 |
};
|
|
|
39636 |
}
|
|
|
39637 |
result += segmentDurationWithParts(playlist, segment);
|
|
|
39638 |
if (typeof segment.start !== 'undefined') {
|
|
|
39639 |
return {
|
|
|
39640 |
result: result + segment.start,
|
|
|
39641 |
precise: true
|
|
|
39642 |
};
|
|
|
39643 |
}
|
|
|
39644 |
}
|
|
|
39645 |
return {
|
|
|
39646 |
result,
|
|
|
39647 |
precise: false
|
|
|
39648 |
};
|
|
|
39649 |
};
|
|
|
39650 |
/**
|
|
|
39651 |
* walk forward until we find a duration we can use
|
|
|
39652 |
* or return a failure
|
|
|
39653 |
*
|
|
|
39654 |
* @param {Playlist} playlist the playlist to walk through
|
|
|
39655 |
* @param {number} endSequence the mediaSequence to stop walking on
|
|
|
39656 |
*/
|
|
|
39657 |
|
|
|
39658 |
const forwardDuration = function (playlist, endSequence) {
|
|
|
39659 |
let result = 0;
|
|
|
39660 |
let segment;
|
|
|
39661 |
let i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline
|
|
|
39662 |
// information
|
|
|
39663 |
|
|
|
39664 |
for (; i < playlist.segments.length; i++) {
|
|
|
39665 |
segment = playlist.segments[i];
|
|
|
39666 |
if (typeof segment.start !== 'undefined') {
|
|
|
39667 |
return {
|
|
|
39668 |
result: segment.start - result,
|
|
|
39669 |
precise: true
|
|
|
39670 |
};
|
|
|
39671 |
}
|
|
|
39672 |
result += segmentDurationWithParts(playlist, segment);
|
|
|
39673 |
if (typeof segment.end !== 'undefined') {
|
|
|
39674 |
return {
|
|
|
39675 |
result: segment.end - result,
|
|
|
39676 |
precise: true
|
|
|
39677 |
};
|
|
|
39678 |
}
|
|
|
39679 |
} // indicate we didn't find a useful duration estimate
|
|
|
39680 |
|
|
|
39681 |
return {
|
|
|
39682 |
result: -1,
|
|
|
39683 |
precise: false
|
|
|
39684 |
};
|
|
|
39685 |
};
|
|
|
39686 |
/**
|
|
|
39687 |
* Calculate the media duration from the segments associated with a
|
|
|
39688 |
* playlist. The duration of a subinterval of the available segments
|
|
|
39689 |
* may be calculated by specifying an end index.
|
|
|
39690 |
*
|
|
|
39691 |
* @param {Object} playlist a media playlist object
|
|
|
39692 |
* @param {number=} endSequence an exclusive upper boundary
|
|
|
39693 |
* for the playlist. Defaults to playlist length.
|
|
|
39694 |
* @param {number} expired the amount of time that has dropped
|
|
|
39695 |
* off the front of the playlist in a live scenario
|
|
|
39696 |
* @return {number} the duration between the first available segment
|
|
|
39697 |
* and end index.
|
|
|
39698 |
*/
|
|
|
39699 |
|
|
|
39700 |
const intervalDuration = function (playlist, endSequence, expired) {
|
|
|
39701 |
if (typeof endSequence === 'undefined') {
|
|
|
39702 |
endSequence = playlist.mediaSequence + playlist.segments.length;
|
|
|
39703 |
}
|
|
|
39704 |
if (endSequence < playlist.mediaSequence) {
|
|
|
39705 |
return 0;
|
|
|
39706 |
} // do a backward walk to estimate the duration
|
|
|
39707 |
|
|
|
39708 |
const backward = backwardDuration(playlist, endSequence);
|
|
|
39709 |
if (backward.precise) {
|
|
|
39710 |
// if we were able to base our duration estimate on timing
|
|
|
39711 |
// information provided directly from the Media Source, return
|
|
|
39712 |
// it
|
|
|
39713 |
return backward.result;
|
|
|
39714 |
} // walk forward to see if a precise duration estimate can be made
|
|
|
39715 |
// that way
|
|
|
39716 |
|
|
|
39717 |
const forward = forwardDuration(playlist, endSequence);
|
|
|
39718 |
if (forward.precise) {
|
|
|
39719 |
// we found a segment that has been buffered and so it's
|
|
|
39720 |
// position is known precisely
|
|
|
39721 |
return forward.result;
|
|
|
39722 |
} // return the less-precise, playlist-based duration estimate
|
|
|
39723 |
|
|
|
39724 |
return backward.result + expired;
|
|
|
39725 |
};
|
|
|
39726 |
/**
|
|
|
39727 |
* Calculates the duration of a playlist. If a start and end index
|
|
|
39728 |
* are specified, the duration will be for the subset of the media
|
|
|
39729 |
* timeline between those two indices. The total duration for live
|
|
|
39730 |
* playlists is always Infinity.
|
|
|
39731 |
*
|
|
|
39732 |
* @param {Object} playlist a media playlist object
|
|
|
39733 |
* @param {number=} endSequence an exclusive upper
|
|
|
39734 |
* boundary for the playlist. Defaults to the playlist media
|
|
|
39735 |
* sequence number plus its length.
|
|
|
39736 |
* @param {number=} expired the amount of time that has
|
|
|
39737 |
* dropped off the front of the playlist in a live scenario
|
|
|
39738 |
* @return {number} the duration between the start index and end
|
|
|
39739 |
* index.
|
|
|
39740 |
*/
|
|
|
39741 |
|
|
|
39742 |
const duration = function (playlist, endSequence, expired) {
|
|
|
39743 |
if (!playlist) {
|
|
|
39744 |
return 0;
|
|
|
39745 |
}
|
|
|
39746 |
if (typeof expired !== 'number') {
|
|
|
39747 |
expired = 0;
|
|
|
39748 |
} // if a slice of the total duration is not requested, use
|
|
|
39749 |
// playlist-level duration indicators when they're present
|
|
|
39750 |
|
|
|
39751 |
if (typeof endSequence === 'undefined') {
|
|
|
39752 |
// if present, use the duration specified in the playlist
|
|
|
39753 |
if (playlist.totalDuration) {
|
|
|
39754 |
return playlist.totalDuration;
|
|
|
39755 |
} // duration should be Infinity for live playlists
|
|
|
39756 |
|
|
|
39757 |
if (!playlist.endList) {
|
|
|
39758 |
return window.Infinity;
|
|
|
39759 |
}
|
|
|
39760 |
} // calculate the total duration based on the segment durations
|
|
|
39761 |
|
|
|
39762 |
return intervalDuration(playlist, endSequence, expired);
|
|
|
39763 |
};
|
|
|
39764 |
/**
|
|
|
39765 |
* Calculate the time between two indexes in the current playlist
|
|
|
39766 |
* neight the start- nor the end-index need to be within the current
|
|
|
39767 |
* playlist in which case, the targetDuration of the playlist is used
|
|
|
39768 |
* to approximate the durations of the segments
|
|
|
39769 |
*
|
|
|
39770 |
* @param {Array} options.durationList list to iterate over for durations.
|
|
|
39771 |
* @param {number} options.defaultDuration duration to use for elements before or after the durationList
|
|
|
39772 |
* @param {number} options.startIndex partsAndSegments index to start
|
|
|
39773 |
* @param {number} options.endIndex partsAndSegments index to end.
|
|
|
39774 |
* @return {number} the number of seconds between startIndex and endIndex
|
|
|
39775 |
*/
|
|
|
39776 |
|
|
|
39777 |
const sumDurations = function ({
|
|
|
39778 |
defaultDuration,
|
|
|
39779 |
durationList,
|
|
|
39780 |
startIndex,
|
|
|
39781 |
endIndex
|
|
|
39782 |
}) {
|
|
|
39783 |
let durations = 0;
|
|
|
39784 |
if (startIndex > endIndex) {
|
|
|
39785 |
[startIndex, endIndex] = [endIndex, startIndex];
|
|
|
39786 |
}
|
|
|
39787 |
if (startIndex < 0) {
|
|
|
39788 |
for (let i = startIndex; i < Math.min(0, endIndex); i++) {
|
|
|
39789 |
durations += defaultDuration;
|
|
|
39790 |
}
|
|
|
39791 |
startIndex = 0;
|
|
|
39792 |
}
|
|
|
39793 |
for (let i = startIndex; i < endIndex; i++) {
|
|
|
39794 |
durations += durationList[i].duration;
|
|
|
39795 |
}
|
|
|
39796 |
return durations;
|
|
|
39797 |
};
|
|
|
39798 |
/**
|
|
|
39799 |
* Calculates the playlist end time
|
|
|
39800 |
*
|
|
|
39801 |
* @param {Object} playlist a media playlist object
|
|
|
39802 |
* @param {number=} expired the amount of time that has
|
|
|
39803 |
* dropped off the front of the playlist in a live scenario
|
|
|
39804 |
* @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the
|
|
|
39805 |
* playlist end calculation should consider the safe live end
|
|
|
39806 |
* (truncate the playlist end by three segments). This is normally
|
|
|
39807 |
* used for calculating the end of the playlist's seekable range.
|
|
|
39808 |
* This takes into account the value of liveEdgePadding.
|
|
|
39809 |
* Setting liveEdgePadding to 0 is equivalent to setting this to false.
|
|
|
39810 |
* @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds.
|
|
|
39811 |
* If this is provided, it is used in the safe live end calculation.
|
|
|
39812 |
* Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent.
|
|
|
39813 |
* Corresponds to suggestedPresentationDelay in DASH manifests.
|
|
|
39814 |
* @return {number} the end time of playlist
|
|
|
39815 |
* @function playlistEnd
|
|
|
39816 |
*/
|
|
|
39817 |
|
|
|
39818 |
const playlistEnd = function (playlist, expired, useSafeLiveEnd, liveEdgePadding) {
|
|
|
39819 |
if (!playlist || !playlist.segments) {
|
|
|
39820 |
return null;
|
|
|
39821 |
}
|
|
|
39822 |
if (playlist.endList) {
|
|
|
39823 |
return duration(playlist);
|
|
|
39824 |
}
|
|
|
39825 |
if (expired === null) {
|
|
|
39826 |
return null;
|
|
|
39827 |
}
|
|
|
39828 |
expired = expired || 0;
|
|
|
39829 |
let lastSegmentEndTime = intervalDuration(playlist, playlist.mediaSequence + playlist.segments.length, expired);
|
|
|
39830 |
if (useSafeLiveEnd) {
|
|
|
39831 |
liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist);
|
|
|
39832 |
lastSegmentEndTime -= liveEdgePadding;
|
|
|
39833 |
} // don't return a time less than zero
|
|
|
39834 |
|
|
|
39835 |
return Math.max(0, lastSegmentEndTime);
|
|
|
39836 |
};
|
|
|
39837 |
/**
|
|
|
39838 |
* Calculates the interval of time that is currently seekable in a
|
|
|
39839 |
* playlist. The returned time ranges are relative to the earliest
|
|
|
39840 |
* moment in the specified playlist that is still available. A full
|
|
|
39841 |
* seekable implementation for live streams would need to offset
|
|
|
39842 |
* these values by the duration of content that has expired from the
|
|
|
39843 |
* stream.
|
|
|
39844 |
*
|
|
|
39845 |
* @param {Object} playlist a media playlist object
|
|
|
39846 |
* dropped off the front of the playlist in a live scenario
|
|
|
39847 |
* @param {number=} expired the amount of time that has
|
|
|
39848 |
* dropped off the front of the playlist in a live scenario
|
|
|
39849 |
* @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds.
|
|
|
39850 |
* Corresponds to suggestedPresentationDelay in DASH manifests.
|
|
|
39851 |
* @return {TimeRanges} the periods of time that are valid targets
|
|
|
39852 |
* for seeking
|
|
|
39853 |
*/
|
|
|
39854 |
|
|
|
39855 |
const seekable = function (playlist, expired, liveEdgePadding) {
|
|
|
39856 |
const useSafeLiveEnd = true;
|
|
|
39857 |
const seekableStart = expired || 0;
|
|
|
39858 |
let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding);
|
|
|
39859 |
if (seekableEnd === null) {
|
|
|
39860 |
return createTimeRanges();
|
|
|
39861 |
} // Clamp seekable end since it can not be less than the seekable start
|
|
|
39862 |
|
|
|
39863 |
if (seekableEnd < seekableStart) {
|
|
|
39864 |
seekableEnd = seekableStart;
|
|
|
39865 |
}
|
|
|
39866 |
return createTimeRanges(seekableStart, seekableEnd);
|
|
|
39867 |
};
|
|
|
39868 |
/**
|
|
|
39869 |
* Determine the index and estimated starting time of the segment that
|
|
|
39870 |
* contains a specified playback position in a media playlist.
|
|
|
39871 |
*
|
|
|
39872 |
* @param {Object} options.playlist the media playlist to query
|
|
|
39873 |
* @param {number} options.currentTime The number of seconds since the earliest
|
|
|
39874 |
* possible position to determine the containing segment for
|
|
|
39875 |
* @param {number} options.startTime the time when the segment/part starts
|
|
|
39876 |
* @param {number} options.startingSegmentIndex the segment index to start looking at.
|
|
|
39877 |
* @param {number?} [options.startingPartIndex] the part index to look at within the segment.
|
|
|
39878 |
*
|
|
|
39879 |
* @return {Object} an object with partIndex, segmentIndex, and startTime.
|
|
|
39880 |
*/
|
|
|
39881 |
|
|
|
39882 |
const getMediaInfoForTime = function ({
|
|
|
39883 |
playlist,
|
|
|
39884 |
currentTime,
|
|
|
39885 |
startingSegmentIndex,
|
|
|
39886 |
startingPartIndex,
|
|
|
39887 |
startTime,
|
|
|
39888 |
exactManifestTimings
|
|
|
39889 |
}) {
|
|
|
39890 |
let time = currentTime - startTime;
|
|
|
39891 |
const partsAndSegments = getPartsAndSegments(playlist);
|
|
|
39892 |
let startIndex = 0;
|
|
|
39893 |
for (let i = 0; i < partsAndSegments.length; i++) {
|
|
|
39894 |
const partAndSegment = partsAndSegments[i];
|
|
|
39895 |
if (startingSegmentIndex !== partAndSegment.segmentIndex) {
|
|
|
39896 |
continue;
|
|
|
39897 |
} // skip this if part index does not match.
|
|
|
39898 |
|
|
|
39899 |
if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) {
|
|
|
39900 |
continue;
|
|
|
39901 |
}
|
|
|
39902 |
startIndex = i;
|
|
|
39903 |
break;
|
|
|
39904 |
}
|
|
|
39905 |
if (time < 0) {
|
|
|
39906 |
// Walk backward from startIndex in the playlist, adding durations
|
|
|
39907 |
// until we find a segment that contains `time` and return it
|
|
|
39908 |
if (startIndex > 0) {
|
|
|
39909 |
for (let i = startIndex - 1; i >= 0; i--) {
|
|
|
39910 |
const partAndSegment = partsAndSegments[i];
|
|
|
39911 |
time += partAndSegment.duration;
|
|
|
39912 |
if (exactManifestTimings) {
|
|
|
39913 |
if (time < 0) {
|
|
|
39914 |
continue;
|
|
|
39915 |
}
|
|
|
39916 |
} else if (time + TIME_FUDGE_FACTOR <= 0) {
|
|
|
39917 |
continue;
|
|
|
39918 |
}
|
|
|
39919 |
return {
|
|
|
39920 |
partIndex: partAndSegment.partIndex,
|
|
|
39921 |
segmentIndex: partAndSegment.segmentIndex,
|
|
|
39922 |
startTime: startTime - sumDurations({
|
|
|
39923 |
defaultDuration: playlist.targetDuration,
|
|
|
39924 |
durationList: partsAndSegments,
|
|
|
39925 |
startIndex,
|
|
|
39926 |
endIndex: i
|
|
|
39927 |
})
|
|
|
39928 |
};
|
|
|
39929 |
}
|
|
|
39930 |
} // We were unable to find a good segment within the playlist
|
|
|
39931 |
// so select the first segment
|
|
|
39932 |
|
|
|
39933 |
return {
|
|
|
39934 |
partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
|
|
|
39935 |
segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
|
|
|
39936 |
startTime: currentTime
|
|
|
39937 |
};
|
|
|
39938 |
} // When startIndex is negative, we first walk forward to first segment
|
|
|
39939 |
// adding target durations. If we "run out of time" before getting to
|
|
|
39940 |
// the first segment, return the first segment
|
|
|
39941 |
|
|
|
39942 |
if (startIndex < 0) {
|
|
|
39943 |
for (let i = startIndex; i < 0; i++) {
|
|
|
39944 |
time -= playlist.targetDuration;
|
|
|
39945 |
if (time < 0) {
|
|
|
39946 |
return {
|
|
|
39947 |
partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
|
|
|
39948 |
segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
|
|
|
39949 |
startTime: currentTime
|
|
|
39950 |
};
|
|
|
39951 |
}
|
|
|
39952 |
}
|
|
|
39953 |
startIndex = 0;
|
|
|
39954 |
} // Walk forward from startIndex in the playlist, subtracting durations
|
|
|
39955 |
// until we find a segment that contains `time` and return it
|
|
|
39956 |
|
|
|
39957 |
for (let i = startIndex; i < partsAndSegments.length; i++) {
|
|
|
39958 |
const partAndSegment = partsAndSegments[i];
|
|
|
39959 |
time -= partAndSegment.duration;
|
|
|
39960 |
const canUseFudgeFactor = partAndSegment.duration > TIME_FUDGE_FACTOR;
|
|
|
39961 |
const isExactlyAtTheEnd = time === 0;
|
|
|
39962 |
const isExtremelyCloseToTheEnd = canUseFudgeFactor && time + TIME_FUDGE_FACTOR >= 0;
|
|
|
39963 |
if (isExactlyAtTheEnd || isExtremelyCloseToTheEnd) {
|
|
|
39964 |
// 1) We are exactly at the end of the current segment.
|
|
|
39965 |
// 2) We are extremely close to the end of the current segment (The difference is less than 1 / 30).
|
|
|
39966 |
// We may encounter this situation when
|
|
|
39967 |
// we don't have exact match between segment duration info in the manifest and the actual duration of the segment
|
|
|
39968 |
// For example:
|
|
|
39969 |
// We appended 3 segments 10 seconds each, meaning we should have 30 sec buffered,
|
|
|
39970 |
// but we the actual buffered is 29.99999
|
|
|
39971 |
//
|
|
|
39972 |
// In both cases:
|
|
|
39973 |
// if we passed current time -> it means that we already played current segment
|
|
|
39974 |
// if we passed buffered.end -> it means that this segment is already loaded and buffered
|
|
|
39975 |
// we should select the next segment if we have one:
|
|
|
39976 |
if (i !== partsAndSegments.length - 1) {
|
|
|
39977 |
continue;
|
|
|
39978 |
}
|
|
|
39979 |
}
|
|
|
39980 |
if (exactManifestTimings) {
|
|
|
39981 |
if (time > 0) {
|
|
|
39982 |
continue;
|
|
|
39983 |
}
|
|
|
39984 |
} else if (time - TIME_FUDGE_FACTOR >= 0) {
|
|
|
39985 |
continue;
|
|
|
39986 |
}
|
|
|
39987 |
return {
|
|
|
39988 |
partIndex: partAndSegment.partIndex,
|
|
|
39989 |
segmentIndex: partAndSegment.segmentIndex,
|
|
|
39990 |
startTime: startTime + sumDurations({
|
|
|
39991 |
defaultDuration: playlist.targetDuration,
|
|
|
39992 |
durationList: partsAndSegments,
|
|
|
39993 |
startIndex,
|
|
|
39994 |
endIndex: i
|
|
|
39995 |
})
|
|
|
39996 |
};
|
|
|
39997 |
} // We are out of possible candidates so load the last one...
|
|
|
39998 |
|
|
|
39999 |
return {
|
|
|
40000 |
segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex,
|
|
|
40001 |
partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex,
|
|
|
40002 |
startTime: currentTime
|
|
|
40003 |
};
|
|
|
40004 |
};
|
|
|
40005 |
/**
|
|
|
40006 |
* Check whether the playlist is excluded or not.
|
|
|
40007 |
*
|
|
|
40008 |
* @param {Object} playlist the media playlist object
|
|
|
40009 |
* @return {boolean} whether the playlist is excluded or not
|
|
|
40010 |
* @function isExcluded
|
|
|
40011 |
*/
|
|
|
40012 |
|
|
|
40013 |
const isExcluded = function (playlist) {
|
|
|
40014 |
return playlist.excludeUntil && playlist.excludeUntil > Date.now();
|
|
|
40015 |
};
|
|
|
40016 |
/**
|
|
|
40017 |
* Check whether the playlist is compatible with current playback configuration or has
|
|
|
40018 |
* been excluded permanently for being incompatible.
|
|
|
40019 |
*
|
|
|
40020 |
* @param {Object} playlist the media playlist object
|
|
|
40021 |
* @return {boolean} whether the playlist is incompatible or not
|
|
|
40022 |
* @function isIncompatible
|
|
|
40023 |
*/
|
|
|
40024 |
|
|
|
40025 |
const isIncompatible = function (playlist) {
|
|
|
40026 |
return playlist.excludeUntil && playlist.excludeUntil === Infinity;
|
|
|
40027 |
};
|
|
|
40028 |
/**
|
|
|
40029 |
* Check whether the playlist is enabled or not.
|
|
|
40030 |
*
|
|
|
40031 |
* @param {Object} playlist the media playlist object
|
|
|
40032 |
* @return {boolean} whether the playlist is enabled or not
|
|
|
40033 |
* @function isEnabled
|
|
|
40034 |
*/
|
|
|
40035 |
|
|
|
40036 |
const isEnabled = function (playlist) {
|
|
|
40037 |
const excluded = isExcluded(playlist);
|
|
|
40038 |
return !playlist.disabled && !excluded;
|
|
|
40039 |
};
|
|
|
40040 |
/**
|
|
|
40041 |
* Check whether the playlist has been manually disabled through the representations api.
|
|
|
40042 |
*
|
|
|
40043 |
* @param {Object} playlist the media playlist object
|
|
|
40044 |
* @return {boolean} whether the playlist is disabled manually or not
|
|
|
40045 |
* @function isDisabled
|
|
|
40046 |
*/
|
|
|
40047 |
|
|
|
40048 |
const isDisabled = function (playlist) {
|
|
|
40049 |
return playlist.disabled;
|
|
|
40050 |
};
|
|
|
40051 |
/**
|
|
|
40052 |
* Returns whether the current playlist is an AES encrypted HLS stream
|
|
|
40053 |
*
|
|
|
40054 |
* @return {boolean} true if it's an AES encrypted HLS stream
|
|
|
40055 |
*/
|
|
|
40056 |
|
|
|
40057 |
const isAes = function (media) {
|
|
|
40058 |
for (let i = 0; i < media.segments.length; i++) {
|
|
|
40059 |
if (media.segments[i].key) {
|
|
|
40060 |
return true;
|
|
|
40061 |
}
|
|
|
40062 |
}
|
|
|
40063 |
return false;
|
|
|
40064 |
};
|
|
|
40065 |
/**
|
|
|
40066 |
* Checks if the playlist has a value for the specified attribute
|
|
|
40067 |
*
|
|
|
40068 |
* @param {string} attr
|
|
|
40069 |
* Attribute to check for
|
|
|
40070 |
* @param {Object} playlist
|
|
|
40071 |
* The media playlist object
|
|
|
40072 |
* @return {boolean}
|
|
|
40073 |
* Whether the playlist contains a value for the attribute or not
|
|
|
40074 |
* @function hasAttribute
|
|
|
40075 |
*/
|
|
|
40076 |
|
|
|
40077 |
const hasAttribute = function (attr, playlist) {
|
|
|
40078 |
return playlist.attributes && playlist.attributes[attr];
|
|
|
40079 |
};
|
|
|
40080 |
/**
|
|
|
40081 |
* Estimates the time required to complete a segment download from the specified playlist
|
|
|
40082 |
*
|
|
|
40083 |
* @param {number} segmentDuration
|
|
|
40084 |
* Duration of requested segment
|
|
|
40085 |
* @param {number} bandwidth
|
|
|
40086 |
* Current measured bandwidth of the player
|
|
|
40087 |
* @param {Object} playlist
|
|
|
40088 |
* The media playlist object
|
|
|
40089 |
* @param {number=} bytesReceived
|
|
|
40090 |
* Number of bytes already received for the request. Defaults to 0
|
|
|
40091 |
* @return {number|NaN}
|
|
|
40092 |
* The estimated time to request the segment. NaN if bandwidth information for
|
|
|
40093 |
* the given playlist is unavailable
|
|
|
40094 |
* @function estimateSegmentRequestTime
|
|
|
40095 |
*/
|
|
|
40096 |
|
|
|
40097 |
const estimateSegmentRequestTime = function (segmentDuration, bandwidth, playlist, bytesReceived = 0) {
|
|
|
40098 |
if (!hasAttribute('BANDWIDTH', playlist)) {
|
|
|
40099 |
return NaN;
|
|
|
40100 |
}
|
|
|
40101 |
const size = segmentDuration * playlist.attributes.BANDWIDTH;
|
|
|
40102 |
return (size - bytesReceived * 8) / bandwidth;
|
|
|
40103 |
};
|
|
|
40104 |
/*
|
|
|
40105 |
* Returns whether the current playlist is the lowest rendition
|
|
|
40106 |
*
|
|
|
40107 |
* @return {Boolean} true if on lowest rendition
|
|
|
40108 |
*/
|
|
|
40109 |
|
|
|
40110 |
const isLowestEnabledRendition = (main, media) => {
|
|
|
40111 |
if (main.playlists.length === 1) {
|
|
|
40112 |
return true;
|
|
|
40113 |
}
|
|
|
40114 |
const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE;
|
|
|
40115 |
return main.playlists.filter(playlist => {
|
|
|
40116 |
if (!isEnabled(playlist)) {
|
|
|
40117 |
return false;
|
|
|
40118 |
}
|
|
|
40119 |
return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth;
|
|
|
40120 |
}).length === 0;
|
|
|
40121 |
};
|
|
|
40122 |
const playlistMatch = (a, b) => {
|
|
|
40123 |
// both playlits are null
|
|
|
40124 |
// or only one playlist is non-null
|
|
|
40125 |
// no match
|
|
|
40126 |
if (!a && !b || !a && b || a && !b) {
|
|
|
40127 |
return false;
|
|
|
40128 |
} // playlist objects are the same, match
|
|
|
40129 |
|
|
|
40130 |
if (a === b) {
|
|
|
40131 |
return true;
|
|
|
40132 |
} // first try to use id as it should be the most
|
|
|
40133 |
// accurate
|
|
|
40134 |
|
|
|
40135 |
if (a.id && b.id && a.id === b.id) {
|
|
|
40136 |
return true;
|
|
|
40137 |
} // next try to use reslovedUri as it should be the
|
|
|
40138 |
// second most accurate.
|
|
|
40139 |
|
|
|
40140 |
if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) {
|
|
|
40141 |
return true;
|
|
|
40142 |
} // finally try to use uri as it should be accurate
|
|
|
40143 |
// but might miss a few cases for relative uris
|
|
|
40144 |
|
|
|
40145 |
if (a.uri && b.uri && a.uri === b.uri) {
|
|
|
40146 |
return true;
|
|
|
40147 |
}
|
|
|
40148 |
return false;
|
|
|
40149 |
};
|
|
|
40150 |
const someAudioVariant = function (main, callback) {
|
|
|
40151 |
const AUDIO = main && main.mediaGroups && main.mediaGroups.AUDIO || {};
|
|
|
40152 |
let found = false;
|
|
|
40153 |
for (const groupName in AUDIO) {
|
|
|
40154 |
for (const label in AUDIO[groupName]) {
|
|
|
40155 |
found = callback(AUDIO[groupName][label]);
|
|
|
40156 |
if (found) {
|
|
|
40157 |
break;
|
|
|
40158 |
}
|
|
|
40159 |
}
|
|
|
40160 |
if (found) {
|
|
|
40161 |
break;
|
|
|
40162 |
}
|
|
|
40163 |
}
|
|
|
40164 |
return !!found;
|
|
|
40165 |
};
|
|
|
40166 |
const isAudioOnly = main => {
|
|
|
40167 |
// we are audio only if we have no main playlists but do
|
|
|
40168 |
// have media group playlists.
|
|
|
40169 |
if (!main || !main.playlists || !main.playlists.length) {
|
|
|
40170 |
// without audio variants or playlists this
|
|
|
40171 |
// is not an audio only main.
|
|
|
40172 |
const found = someAudioVariant(main, variant => variant.playlists && variant.playlists.length || variant.uri);
|
|
|
40173 |
return found;
|
|
|
40174 |
} // if every playlist has only an audio codec it is audio only
|
|
|
40175 |
|
|
|
40176 |
for (let i = 0; i < main.playlists.length; i++) {
|
|
|
40177 |
const playlist = main.playlists[i];
|
|
|
40178 |
const CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist.
|
|
|
40179 |
|
|
|
40180 |
if (CODECS && CODECS.split(',').every(c => isAudioCodec(c))) {
|
|
|
40181 |
continue;
|
|
|
40182 |
} // playlist is in an audio group it is audio only
|
|
|
40183 |
|
|
|
40184 |
const found = someAudioVariant(main, variant => playlistMatch(playlist, variant));
|
|
|
40185 |
if (found) {
|
|
|
40186 |
continue;
|
|
|
40187 |
} // if we make it here this playlist isn't audio and we
|
|
|
40188 |
// are not audio only
|
|
|
40189 |
|
|
|
40190 |
return false;
|
|
|
40191 |
} // if we make it past every playlist without returning, then
|
|
|
40192 |
// this is an audio only playlist.
|
|
|
40193 |
|
|
|
40194 |
return true;
|
|
|
40195 |
}; // exports
|
|
|
40196 |
|
|
|
40197 |
var Playlist = {
|
|
|
40198 |
liveEdgeDelay,
|
|
|
40199 |
duration,
|
|
|
40200 |
seekable,
|
|
|
40201 |
getMediaInfoForTime,
|
|
|
40202 |
isEnabled,
|
|
|
40203 |
isDisabled,
|
|
|
40204 |
isExcluded,
|
|
|
40205 |
isIncompatible,
|
|
|
40206 |
playlistEnd,
|
|
|
40207 |
isAes,
|
|
|
40208 |
hasAttribute,
|
|
|
40209 |
estimateSegmentRequestTime,
|
|
|
40210 |
isLowestEnabledRendition,
|
|
|
40211 |
isAudioOnly,
|
|
|
40212 |
playlistMatch,
|
|
|
40213 |
segmentDurationWithParts
|
|
|
40214 |
};
|
|
|
40215 |
const {
|
|
|
40216 |
log
|
|
|
40217 |
} = videojs;
|
|
|
40218 |
const createPlaylistID = (index, uri) => {
|
|
|
40219 |
return `${index}-${uri}`;
|
|
|
40220 |
}; // default function for creating a group id
|
|
|
40221 |
|
|
|
40222 |
const groupID = (type, group, label) => {
|
|
|
40223 |
return `placeholder-uri-${type}-${group}-${label}`;
|
|
|
40224 |
};
|
|
|
40225 |
/**
|
|
|
40226 |
* Parses a given m3u8 playlist
|
|
|
40227 |
*
|
|
|
40228 |
* @param {Function} [onwarn]
|
|
|
40229 |
* a function to call when the parser triggers a warning event.
|
|
|
40230 |
* @param {Function} [oninfo]
|
|
|
40231 |
* a function to call when the parser triggers an info event.
|
|
|
40232 |
* @param {string} manifestString
|
|
|
40233 |
* The downloaded manifest string
|
|
|
40234 |
* @param {Object[]} [customTagParsers]
|
|
|
40235 |
* An array of custom tag parsers for the m3u8-parser instance
|
|
|
40236 |
* @param {Object[]} [customTagMappers]
|
|
|
40237 |
* An array of custom tag mappers for the m3u8-parser instance
|
|
|
40238 |
* @param {boolean} [llhls]
|
|
|
40239 |
* Whether to keep ll-hls features in the manifest after parsing.
|
|
|
40240 |
* @return {Object}
|
|
|
40241 |
* The manifest object
|
|
|
40242 |
*/
|
|
|
40243 |
|
|
|
40244 |
const parseManifest = ({
|
|
|
40245 |
onwarn,
|
|
|
40246 |
oninfo,
|
|
|
40247 |
manifestString,
|
|
|
40248 |
customTagParsers = [],
|
|
|
40249 |
customTagMappers = [],
|
|
|
40250 |
llhls
|
|
|
40251 |
}) => {
|
|
|
40252 |
const parser = new Parser();
|
|
|
40253 |
if (onwarn) {
|
|
|
40254 |
parser.on('warn', onwarn);
|
|
|
40255 |
}
|
|
|
40256 |
if (oninfo) {
|
|
|
40257 |
parser.on('info', oninfo);
|
|
|
40258 |
}
|
|
|
40259 |
customTagParsers.forEach(customParser => parser.addParser(customParser));
|
|
|
40260 |
customTagMappers.forEach(mapper => parser.addTagMapper(mapper));
|
|
|
40261 |
parser.push(manifestString);
|
|
|
40262 |
parser.end();
|
|
|
40263 |
const manifest = parser.manifest; // remove llhls features from the parsed manifest
|
|
|
40264 |
// if we don't want llhls support.
|
|
|
40265 |
|
|
|
40266 |
if (!llhls) {
|
|
|
40267 |
['preloadSegment', 'skip', 'serverControl', 'renditionReports', 'partInf', 'partTargetDuration'].forEach(function (k) {
|
|
|
40268 |
if (manifest.hasOwnProperty(k)) {
|
|
|
40269 |
delete manifest[k];
|
|
|
40270 |
}
|
|
|
40271 |
});
|
|
|
40272 |
if (manifest.segments) {
|
|
|
40273 |
manifest.segments.forEach(function (segment) {
|
|
|
40274 |
['parts', 'preloadHints'].forEach(function (k) {
|
|
|
40275 |
if (segment.hasOwnProperty(k)) {
|
|
|
40276 |
delete segment[k];
|
|
|
40277 |
}
|
|
|
40278 |
});
|
|
|
40279 |
});
|
|
|
40280 |
}
|
|
|
40281 |
}
|
|
|
40282 |
if (!manifest.targetDuration) {
|
|
|
40283 |
let targetDuration = 10;
|
|
|
40284 |
if (manifest.segments && manifest.segments.length) {
|
|
|
40285 |
targetDuration = manifest.segments.reduce((acc, s) => Math.max(acc, s.duration), 0);
|
|
|
40286 |
}
|
|
|
40287 |
if (onwarn) {
|
|
|
40288 |
onwarn({
|
|
|
40289 |
message: `manifest has no targetDuration defaulting to ${targetDuration}`
|
|
|
40290 |
});
|
|
|
40291 |
}
|
|
|
40292 |
manifest.targetDuration = targetDuration;
|
|
|
40293 |
}
|
|
|
40294 |
const parts = getLastParts(manifest);
|
|
|
40295 |
if (parts.length && !manifest.partTargetDuration) {
|
|
|
40296 |
const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0);
|
|
|
40297 |
if (onwarn) {
|
|
|
40298 |
onwarn({
|
|
|
40299 |
message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}`
|
|
|
40300 |
});
|
|
|
40301 |
log.error('LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.');
|
|
|
40302 |
}
|
|
|
40303 |
manifest.partTargetDuration = partTargetDuration;
|
|
|
40304 |
}
|
|
|
40305 |
return manifest;
|
|
|
40306 |
};
|
|
|
40307 |
/**
|
|
|
40308 |
* Loops through all supported media groups in main and calls the provided
|
|
|
40309 |
* callback for each group
|
|
|
40310 |
*
|
|
|
40311 |
* @param {Object} main
|
|
|
40312 |
* The parsed main manifest object
|
|
|
40313 |
* @param {Function} callback
|
|
|
40314 |
* Callback to call for each media group
|
|
|
40315 |
*/
|
|
|
40316 |
|
|
|
40317 |
const forEachMediaGroup = (main, callback) => {
|
|
|
40318 |
if (!main.mediaGroups) {
|
|
|
40319 |
return;
|
|
|
40320 |
}
|
|
|
40321 |
['AUDIO', 'SUBTITLES'].forEach(mediaType => {
|
|
|
40322 |
if (!main.mediaGroups[mediaType]) {
|
|
|
40323 |
return;
|
|
|
40324 |
}
|
|
|
40325 |
for (const groupKey in main.mediaGroups[mediaType]) {
|
|
|
40326 |
for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
|
|
|
40327 |
const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey];
|
|
|
40328 |
callback(mediaProperties, mediaType, groupKey, labelKey);
|
|
|
40329 |
}
|
|
|
40330 |
}
|
|
|
40331 |
});
|
|
|
40332 |
};
|
|
|
40333 |
/**
|
|
|
40334 |
* Adds properties and attributes to the playlist to keep consistent functionality for
|
|
|
40335 |
* playlists throughout VHS.
|
|
|
40336 |
*
|
|
|
40337 |
* @param {Object} config
|
|
|
40338 |
* Arguments object
|
|
|
40339 |
* @param {Object} config.playlist
|
|
|
40340 |
* The media playlist
|
|
|
40341 |
* @param {string} [config.uri]
|
|
|
40342 |
* The uri to the media playlist (if media playlist is not from within a main
|
|
|
40343 |
* playlist)
|
|
|
40344 |
* @param {string} id
|
|
|
40345 |
* ID to use for the playlist
|
|
|
40346 |
*/
|
|
|
40347 |
|
|
|
40348 |
const setupMediaPlaylist = ({
|
|
|
40349 |
playlist,
|
|
|
40350 |
uri,
|
|
|
40351 |
id
|
|
|
40352 |
}) => {
|
|
|
40353 |
playlist.id = id;
|
|
|
40354 |
playlist.playlistErrors_ = 0;
|
|
|
40355 |
if (uri) {
|
|
|
40356 |
// For media playlists, m3u8-parser does not have access to a URI, as HLS media
|
|
|
40357 |
// playlists do not contain their own source URI, but one is needed for consistency in
|
|
|
40358 |
// VHS.
|
|
|
40359 |
playlist.uri = uri;
|
|
|
40360 |
} // For HLS main playlists, even though certain attributes MUST be defined, the
|
|
|
40361 |
// stream may still be played without them.
|
|
|
40362 |
// For HLS media playlists, m3u8-parser does not attach an attributes object to the
|
|
|
40363 |
// manifest.
|
|
|
40364 |
//
|
|
|
40365 |
// To avoid undefined reference errors through the project, and make the code easier
|
|
|
40366 |
// to write/read, add an empty attributes object for these cases.
|
|
|
40367 |
|
|
|
40368 |
playlist.attributes = playlist.attributes || {};
|
|
|
40369 |
};
|
|
|
40370 |
/**
|
|
|
40371 |
* Adds ID, resolvedUri, and attributes properties to each playlist of the main, where
|
|
|
40372 |
* necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to
|
|
|
40373 |
* playlist references to the playlists array.
|
|
|
40374 |
*
|
|
|
40375 |
* @param {Object} main
|
|
|
40376 |
* The main playlist
|
|
|
40377 |
*/
|
|
|
40378 |
|
|
|
40379 |
const setupMediaPlaylists = main => {
|
|
|
40380 |
let i = main.playlists.length;
|
|
|
40381 |
while (i--) {
|
|
|
40382 |
const playlist = main.playlists[i];
|
|
|
40383 |
setupMediaPlaylist({
|
|
|
40384 |
playlist,
|
|
|
40385 |
id: createPlaylistID(i, playlist.uri)
|
|
|
40386 |
});
|
|
|
40387 |
playlist.resolvedUri = resolveUrl(main.uri, playlist.uri);
|
|
|
40388 |
main.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility
|
|
|
40389 |
|
|
|
40390 |
main.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute,
|
|
|
40391 |
// the stream can be played without it. Although an attributes property may have been
|
|
|
40392 |
// added to the playlist to prevent undefined references, issue a warning to fix the
|
|
|
40393 |
// manifest.
|
|
|
40394 |
|
|
|
40395 |
if (!playlist.attributes.BANDWIDTH) {
|
|
|
40396 |
log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
|
|
|
40397 |
}
|
|
|
40398 |
}
|
|
|
40399 |
};
|
|
|
40400 |
/**
|
|
|
40401 |
* Adds resolvedUri properties to each media group.
|
|
|
40402 |
*
|
|
|
40403 |
* @param {Object} main
|
|
|
40404 |
* The main playlist
|
|
|
40405 |
*/
|
|
|
40406 |
|
|
|
40407 |
const resolveMediaGroupUris = main => {
|
|
|
40408 |
forEachMediaGroup(main, properties => {
|
|
|
40409 |
if (properties.uri) {
|
|
|
40410 |
properties.resolvedUri = resolveUrl(main.uri, properties.uri);
|
|
|
40411 |
}
|
|
|
40412 |
});
|
|
|
40413 |
};
|
|
|
40414 |
/**
|
|
|
40415 |
* Creates a main playlist wrapper to insert a sole media playlist into.
|
|
|
40416 |
*
|
|
|
40417 |
* @param {Object} media
|
|
|
40418 |
* Media playlist
|
|
|
40419 |
* @param {string} uri
|
|
|
40420 |
* The media URI
|
|
|
40421 |
*
|
|
|
40422 |
* @return {Object}
|
|
|
40423 |
* main playlist
|
|
|
40424 |
*/
|
|
|
40425 |
|
|
|
40426 |
const mainForMedia = (media, uri) => {
|
|
|
40427 |
const id = createPlaylistID(0, uri);
|
|
|
40428 |
const main = {
|
|
|
40429 |
mediaGroups: {
|
|
|
40430 |
'AUDIO': {},
|
|
|
40431 |
'VIDEO': {},
|
|
|
40432 |
'CLOSED-CAPTIONS': {},
|
|
|
40433 |
'SUBTITLES': {}
|
|
|
40434 |
},
|
|
|
40435 |
uri: window.location.href,
|
|
|
40436 |
resolvedUri: window.location.href,
|
|
|
40437 |
playlists: [{
|
|
|
40438 |
uri,
|
|
|
40439 |
id,
|
|
|
40440 |
resolvedUri: uri,
|
|
|
40441 |
// m3u8-parser does not attach an attributes property to media playlists so make
|
|
|
40442 |
// sure that the property is attached to avoid undefined reference errors
|
|
|
40443 |
attributes: {}
|
|
|
40444 |
}]
|
|
|
40445 |
}; // set up ID reference
|
|
|
40446 |
|
|
|
40447 |
main.playlists[id] = main.playlists[0]; // URI reference added for backwards compatibility
|
|
|
40448 |
|
|
|
40449 |
main.playlists[uri] = main.playlists[0];
|
|
|
40450 |
return main;
|
|
|
40451 |
};
|
|
|
40452 |
/**
|
|
|
40453 |
* Does an in-place update of the main manifest to add updated playlist URI references
|
|
|
40454 |
* as well as other properties needed by VHS that aren't included by the parser.
|
|
|
40455 |
*
|
|
|
40456 |
* @param {Object} main
|
|
|
40457 |
* main manifest object
|
|
|
40458 |
* @param {string} uri
|
|
|
40459 |
* The source URI
|
|
|
40460 |
* @param {function} createGroupID
|
|
|
40461 |
* A function to determine how to create the groupID for mediaGroups
|
|
|
40462 |
*/
|
|
|
40463 |
|
|
|
40464 |
const addPropertiesToMain = (main, uri, createGroupID = groupID) => {
|
|
|
40465 |
main.uri = uri;
|
|
|
40466 |
for (let i = 0; i < main.playlists.length; i++) {
|
|
|
40467 |
if (!main.playlists[i].uri) {
|
|
|
40468 |
// Set up phony URIs for the playlists since playlists are referenced by their URIs
|
|
|
40469 |
// throughout VHS, but some formats (e.g., DASH) don't have external URIs
|
|
|
40470 |
// TODO: consider adding dummy URIs in mpd-parser
|
|
|
40471 |
const phonyUri = `placeholder-uri-${i}`;
|
|
|
40472 |
main.playlists[i].uri = phonyUri;
|
|
|
40473 |
}
|
|
|
40474 |
}
|
|
|
40475 |
const audioOnlyMain = isAudioOnly(main);
|
|
|
40476 |
forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => {
|
|
|
40477 |
// add a playlist array under properties
|
|
|
40478 |
if (!properties.playlists || !properties.playlists.length) {
|
|
|
40479 |
// If the manifest is audio only and this media group does not have a uri, check
|
|
|
40480 |
// if the media group is located in the main list of playlists. If it is, don't add
|
|
|
40481 |
// placeholder properties as it shouldn't be considered an alternate audio track.
|
|
|
40482 |
if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) {
|
|
|
40483 |
for (let i = 0; i < main.playlists.length; i++) {
|
|
|
40484 |
const p = main.playlists[i];
|
|
|
40485 |
if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) {
|
|
|
40486 |
return;
|
|
|
40487 |
}
|
|
|
40488 |
}
|
|
|
40489 |
}
|
|
|
40490 |
properties.playlists = [_extends$1({}, properties)];
|
|
|
40491 |
}
|
|
|
40492 |
properties.playlists.forEach(function (p, i) {
|
|
|
40493 |
const groupId = createGroupID(mediaType, groupKey, labelKey, p);
|
|
|
40494 |
const id = createPlaylistID(i, groupId);
|
|
|
40495 |
if (p.uri) {
|
|
|
40496 |
p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri);
|
|
|
40497 |
} else {
|
|
|
40498 |
// DEPRECATED, this has been added to prevent a breaking change.
|
|
|
40499 |
// previously we only ever had a single media group playlist, so
|
|
|
40500 |
// we mark the first playlist uri without prepending the index as we used to
|
|
|
40501 |
// ideally we would do all of the playlists the same way.
|
|
|
40502 |
p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use
|
|
|
40503 |
// the placeholder again
|
|
|
40504 |
|
|
|
40505 |
p.resolvedUri = p.uri;
|
|
|
40506 |
}
|
|
|
40507 |
p.id = p.id || id; // add an empty attributes object, all playlists are
|
|
|
40508 |
// expected to have this.
|
|
|
40509 |
|
|
|
40510 |
p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility)
|
|
|
40511 |
|
|
|
40512 |
main.playlists[p.id] = p;
|
|
|
40513 |
main.playlists[p.uri] = p;
|
|
|
40514 |
});
|
|
|
40515 |
});
|
|
|
40516 |
setupMediaPlaylists(main);
|
|
|
40517 |
resolveMediaGroupUris(main);
|
|
|
40518 |
};
|
|
|
40519 |
class DateRangesStorage {
|
|
|
40520 |
constructor() {
|
|
|
40521 |
this.offset_ = null;
|
|
|
40522 |
this.pendingDateRanges_ = new Map();
|
|
|
40523 |
this.processedDateRanges_ = new Map();
|
|
|
40524 |
}
|
|
|
40525 |
setOffset(segments = []) {
|
|
|
40526 |
// already set
|
|
|
40527 |
if (this.offset_ !== null) {
|
|
|
40528 |
return;
|
|
|
40529 |
} // no segment to process
|
|
|
40530 |
|
|
|
40531 |
if (!segments.length) {
|
|
|
40532 |
return;
|
|
|
40533 |
}
|
|
|
40534 |
const [firstSegment] = segments; // no program date time
|
|
|
40535 |
|
|
|
40536 |
if (firstSegment.programDateTime === undefined) {
|
|
|
40537 |
return;
|
|
|
40538 |
} // Set offset as ProgramDateTime for the very first segment of the very first playlist load:
|
|
|
40539 |
|
|
|
40540 |
this.offset_ = firstSegment.programDateTime / 1000;
|
|
|
40541 |
}
|
|
|
40542 |
setPendingDateRanges(dateRanges = []) {
|
|
|
40543 |
if (!dateRanges.length) {
|
|
|
40544 |
return;
|
|
|
40545 |
}
|
|
|
40546 |
const [dateRange] = dateRanges;
|
|
|
40547 |
const startTime = dateRange.startDate.getTime();
|
|
|
40548 |
this.trimProcessedDateRanges_(startTime);
|
|
|
40549 |
this.pendingDateRanges_ = dateRanges.reduce((map, pendingDateRange) => {
|
|
|
40550 |
map.set(pendingDateRange.id, pendingDateRange);
|
|
|
40551 |
return map;
|
|
|
40552 |
}, new Map());
|
|
|
40553 |
}
|
|
|
40554 |
processDateRange(dateRange) {
|
|
|
40555 |
this.pendingDateRanges_.delete(dateRange.id);
|
|
|
40556 |
this.processedDateRanges_.set(dateRange.id, dateRange);
|
|
|
40557 |
}
|
|
|
40558 |
getDateRangesToProcess() {
|
|
|
40559 |
if (this.offset_ === null) {
|
|
|
40560 |
return [];
|
|
|
40561 |
}
|
|
|
40562 |
const dateRangeClasses = {};
|
|
|
40563 |
const dateRangesToProcess = [];
|
|
|
40564 |
this.pendingDateRanges_.forEach((dateRange, id) => {
|
|
|
40565 |
if (this.processedDateRanges_.has(id)) {
|
|
|
40566 |
return;
|
|
|
40567 |
}
|
|
|
40568 |
dateRange.startTime = dateRange.startDate.getTime() / 1000 - this.offset_;
|
|
|
40569 |
dateRange.processDateRange = () => this.processDateRange(dateRange);
|
|
|
40570 |
dateRangesToProcess.push(dateRange);
|
|
|
40571 |
if (!dateRange.class) {
|
|
|
40572 |
return;
|
|
|
40573 |
}
|
|
|
40574 |
if (dateRangeClasses[dateRange.class]) {
|
|
|
40575 |
const length = dateRangeClasses[dateRange.class].push(dateRange);
|
|
|
40576 |
dateRange.classListIndex = length - 1;
|
|
|
40577 |
} else {
|
|
|
40578 |
dateRangeClasses[dateRange.class] = [dateRange];
|
|
|
40579 |
dateRange.classListIndex = 0;
|
|
|
40580 |
}
|
|
|
40581 |
});
|
|
|
40582 |
for (const dateRange of dateRangesToProcess) {
|
|
|
40583 |
const classList = dateRangeClasses[dateRange.class] || [];
|
|
|
40584 |
if (dateRange.endDate) {
|
|
|
40585 |
dateRange.endTime = dateRange.endDate.getTime() / 1000 - this.offset_;
|
|
|
40586 |
} else if (dateRange.endOnNext && classList[dateRange.classListIndex + 1]) {
|
|
|
40587 |
dateRange.endTime = classList[dateRange.classListIndex + 1].startTime;
|
|
|
40588 |
} else if (dateRange.duration) {
|
|
|
40589 |
dateRange.endTime = dateRange.startTime + dateRange.duration;
|
|
|
40590 |
} else if (dateRange.plannedDuration) {
|
|
|
40591 |
dateRange.endTime = dateRange.startTime + dateRange.plannedDuration;
|
|
|
40592 |
} else {
|
|
|
40593 |
dateRange.endTime = dateRange.startTime;
|
|
|
40594 |
}
|
|
|
40595 |
}
|
|
|
40596 |
return dateRangesToProcess;
|
|
|
40597 |
}
|
|
|
40598 |
trimProcessedDateRanges_(startTime) {
|
|
|
40599 |
const copy = new Map(this.processedDateRanges_);
|
|
|
40600 |
copy.forEach((dateRange, id) => {
|
|
|
40601 |
if (dateRange.startDate.getTime() < startTime) {
|
|
|
40602 |
this.processedDateRanges_.delete(id);
|
|
|
40603 |
}
|
|
|
40604 |
});
|
|
|
40605 |
}
|
|
|
40606 |
}
|
|
|
40607 |
const {
|
|
|
40608 |
EventTarget: EventTarget$1
|
|
|
40609 |
} = videojs;
|
|
|
40610 |
const addLLHLSQueryDirectives = (uri, media) => {
|
|
|
40611 |
if (media.endList || !media.serverControl) {
|
|
|
40612 |
return uri;
|
|
|
40613 |
}
|
|
|
40614 |
const parameters = {};
|
|
|
40615 |
if (media.serverControl.canBlockReload) {
|
|
|
40616 |
const {
|
|
|
40617 |
preloadSegment
|
|
|
40618 |
} = media; // next msn is a zero based value, length is not.
|
|
|
40619 |
|
|
|
40620 |
let nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely
|
|
|
40621 |
// that we are going to request a part of that preload segment.
|
|
|
40622 |
// the logic below is used to determine that.
|
|
|
40623 |
|
|
|
40624 |
if (preloadSegment) {
|
|
|
40625 |
const parts = preloadSegment.parts || []; // _HLS_part is a zero based index
|
|
|
40626 |
|
|
|
40627 |
const nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the
|
|
|
40628 |
// length of parts, then we know we had part preload hints
|
|
|
40629 |
// and we need to add the _HLS_part= query
|
|
|
40630 |
|
|
|
40631 |
if (nextPart > -1 && nextPart !== parts.length - 1) {
|
|
|
40632 |
// add existing parts to our preload hints
|
|
|
40633 |
// eslint-disable-next-line
|
|
|
40634 |
parameters._HLS_part = nextPart;
|
|
|
40635 |
} // this if statement makes sure that we request the msn
|
|
|
40636 |
// of the preload segment if:
|
|
|
40637 |
// 1. the preload segment had parts (and was not yet a full segment)
|
|
|
40638 |
// but was added to our segments array
|
|
|
40639 |
// 2. the preload segment had preload hints for parts that are not in
|
|
|
40640 |
// the manifest yet.
|
|
|
40641 |
// in all other cases we want the segment after the preload segment
|
|
|
40642 |
// which will be given by using media.segments.length because it is 1 based
|
|
|
40643 |
// rather than 0 based.
|
|
|
40644 |
|
|
|
40645 |
if (nextPart > -1 || parts.length) {
|
|
|
40646 |
nextMSN--;
|
|
|
40647 |
}
|
|
|
40648 |
} // add _HLS_msn= in front of any _HLS_part query
|
|
|
40649 |
// eslint-disable-next-line
|
|
|
40650 |
|
|
|
40651 |
parameters._HLS_msn = nextMSN;
|
|
|
40652 |
}
|
|
|
40653 |
if (media.serverControl && media.serverControl.canSkipUntil) {
|
|
|
40654 |
// add _HLS_skip= infront of all other queries.
|
|
|
40655 |
// eslint-disable-next-line
|
|
|
40656 |
parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES';
|
|
|
40657 |
}
|
|
|
40658 |
if (Object.keys(parameters).length) {
|
|
|
40659 |
const parsedUri = new window.URL(uri);
|
|
|
40660 |
['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) {
|
|
|
40661 |
if (!parameters.hasOwnProperty(name)) {
|
|
|
40662 |
return;
|
|
|
40663 |
}
|
|
|
40664 |
parsedUri.searchParams.set(name, parameters[name]);
|
|
|
40665 |
});
|
|
|
40666 |
uri = parsedUri.toString();
|
|
|
40667 |
}
|
|
|
40668 |
return uri;
|
|
|
40669 |
};
|
|
|
40670 |
/**
|
|
|
40671 |
* Returns a new segment object with properties and
|
|
|
40672 |
* the parts array merged.
|
|
|
40673 |
*
|
|
|
40674 |
* @param {Object} a the old segment
|
|
|
40675 |
* @param {Object} b the new segment
|
|
|
40676 |
*
|
|
|
40677 |
* @return {Object} the merged segment
|
|
|
40678 |
*/
|
|
|
40679 |
|
|
|
40680 |
const updateSegment = (a, b) => {
|
|
|
40681 |
if (!a) {
|
|
|
40682 |
return b;
|
|
|
40683 |
}
|
|
|
40684 |
const result = merge(a, b); // if only the old segment has preload hints
|
|
|
40685 |
// and the new one does not, remove preload hints.
|
|
|
40686 |
|
|
|
40687 |
if (a.preloadHints && !b.preloadHints) {
|
|
|
40688 |
delete result.preloadHints;
|
|
|
40689 |
} // if only the old segment has parts
|
|
|
40690 |
// then the parts are no longer valid
|
|
|
40691 |
|
|
|
40692 |
if (a.parts && !b.parts) {
|
|
|
40693 |
delete result.parts; // if both segments have parts
|
|
|
40694 |
// copy part propeties from the old segment
|
|
|
40695 |
// to the new one.
|
|
|
40696 |
} else if (a.parts && b.parts) {
|
|
|
40697 |
for (let i = 0; i < b.parts.length; i++) {
|
|
|
40698 |
if (a.parts && a.parts[i]) {
|
|
|
40699 |
result.parts[i] = merge(a.parts[i], b.parts[i]);
|
|
|
40700 |
}
|
|
|
40701 |
}
|
|
|
40702 |
} // set skipped to false for segments that have
|
|
|
40703 |
// have had information merged from the old segment.
|
|
|
40704 |
|
|
|
40705 |
if (!a.skipped && b.skipped) {
|
|
|
40706 |
result.skipped = false;
|
|
|
40707 |
} // set preload to false for segments that have
|
|
|
40708 |
// had information added in the new segment.
|
|
|
40709 |
|
|
|
40710 |
if (a.preload && !b.preload) {
|
|
|
40711 |
result.preload = false;
|
|
|
40712 |
}
|
|
|
40713 |
return result;
|
|
|
40714 |
};
|
|
|
40715 |
/**
|
|
|
40716 |
* Returns a new array of segments that is the result of merging
|
|
|
40717 |
* properties from an older list of segments onto an updated
|
|
|
40718 |
* list. No properties on the updated playlist will be ovewritten.
|
|
|
40719 |
*
|
|
|
40720 |
* @param {Array} original the outdated list of segments
|
|
|
40721 |
* @param {Array} update the updated list of segments
|
|
|
40722 |
* @param {number=} offset the index of the first update
|
|
|
40723 |
* segment in the original segment list. For non-live playlists,
|
|
|
40724 |
* this should always be zero and does not need to be
|
|
|
40725 |
* specified. For live playlists, it should be the difference
|
|
|
40726 |
* between the media sequence numbers in the original and updated
|
|
|
40727 |
* playlists.
|
|
|
40728 |
* @return {Array} a list of merged segment objects
|
|
|
40729 |
*/
|
|
|
40730 |
|
|
|
40731 |
const updateSegments = (original, update, offset) => {
|
|
|
40732 |
const oldSegments = original.slice();
|
|
|
40733 |
const newSegments = update.slice();
|
|
|
40734 |
offset = offset || 0;
|
|
|
40735 |
const result = [];
|
|
|
40736 |
let currentMap;
|
|
|
40737 |
for (let newIndex = 0; newIndex < newSegments.length; newIndex++) {
|
|
|
40738 |
const oldSegment = oldSegments[newIndex + offset];
|
|
|
40739 |
const newSegment = newSegments[newIndex];
|
|
|
40740 |
if (oldSegment) {
|
|
|
40741 |
currentMap = oldSegment.map || currentMap;
|
|
|
40742 |
result.push(updateSegment(oldSegment, newSegment));
|
|
|
40743 |
} else {
|
|
|
40744 |
// carry over map to new segment if it is missing
|
|
|
40745 |
if (currentMap && !newSegment.map) {
|
|
|
40746 |
newSegment.map = currentMap;
|
|
|
40747 |
}
|
|
|
40748 |
result.push(newSegment);
|
|
|
40749 |
}
|
|
|
40750 |
}
|
|
|
40751 |
return result;
|
|
|
40752 |
};
|
|
|
40753 |
const resolveSegmentUris = (segment, baseUri) => {
|
|
|
40754 |
// preloadSegment will not have a uri at all
|
|
|
40755 |
// as the segment isn't actually in the manifest yet, only parts
|
|
|
40756 |
if (!segment.resolvedUri && segment.uri) {
|
|
|
40757 |
segment.resolvedUri = resolveUrl(baseUri, segment.uri);
|
|
|
40758 |
}
|
|
|
40759 |
if (segment.key && !segment.key.resolvedUri) {
|
|
|
40760 |
segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri);
|
|
|
40761 |
}
|
|
|
40762 |
if (segment.map && !segment.map.resolvedUri) {
|
|
|
40763 |
segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri);
|
|
|
40764 |
}
|
|
|
40765 |
if (segment.map && segment.map.key && !segment.map.key.resolvedUri) {
|
|
|
40766 |
segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri);
|
|
|
40767 |
}
|
|
|
40768 |
if (segment.parts && segment.parts.length) {
|
|
|
40769 |
segment.parts.forEach(p => {
|
|
|
40770 |
if (p.resolvedUri) {
|
|
|
40771 |
return;
|
|
|
40772 |
}
|
|
|
40773 |
p.resolvedUri = resolveUrl(baseUri, p.uri);
|
|
|
40774 |
});
|
|
|
40775 |
}
|
|
|
40776 |
if (segment.preloadHints && segment.preloadHints.length) {
|
|
|
40777 |
segment.preloadHints.forEach(p => {
|
|
|
40778 |
if (p.resolvedUri) {
|
|
|
40779 |
return;
|
|
|
40780 |
}
|
|
|
40781 |
p.resolvedUri = resolveUrl(baseUri, p.uri);
|
|
|
40782 |
});
|
|
|
40783 |
}
|
|
|
40784 |
};
|
|
|
40785 |
const getAllSegments = function (media) {
|
|
|
40786 |
const segments = media.segments || [];
|
|
|
40787 |
const preloadSegment = media.preloadSegment; // a preloadSegment with only preloadHints is not currently
|
|
|
40788 |
// a usable segment, only include a preloadSegment that has
|
|
|
40789 |
// parts.
|
|
|
40790 |
|
|
|
40791 |
if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) {
|
|
|
40792 |
// if preloadHints has a MAP that means that the
|
|
|
40793 |
// init segment is going to change. We cannot use any of the parts
|
|
|
40794 |
// from this preload segment.
|
|
|
40795 |
if (preloadSegment.preloadHints) {
|
|
|
40796 |
for (let i = 0; i < preloadSegment.preloadHints.length; i++) {
|
|
|
40797 |
if (preloadSegment.preloadHints[i].type === 'MAP') {
|
|
|
40798 |
return segments;
|
|
|
40799 |
}
|
|
|
40800 |
}
|
|
|
40801 |
} // set the duration for our preload segment to target duration.
|
|
|
40802 |
|
|
|
40803 |
preloadSegment.duration = media.targetDuration;
|
|
|
40804 |
preloadSegment.preload = true;
|
|
|
40805 |
segments.push(preloadSegment);
|
|
|
40806 |
}
|
|
|
40807 |
return segments;
|
|
|
40808 |
}; // consider the playlist unchanged if the playlist object is the same or
|
|
|
40809 |
// the number of segments is equal, the media sequence number is unchanged,
|
|
|
40810 |
// and this playlist hasn't become the end of the playlist
|
|
|
40811 |
|
|
|
40812 |
const isPlaylistUnchanged = (a, b) => a === b || a.segments && b.segments && a.segments.length === b.segments.length && a.endList === b.endList && a.mediaSequence === b.mediaSequence && a.preloadSegment === b.preloadSegment;
|
|
|
40813 |
/**
|
|
|
40814 |
* Returns a new main playlist that is the result of merging an
|
|
|
40815 |
* updated media playlist into the original version. If the
|
|
|
40816 |
* updated media playlist does not match any of the playlist
|
|
|
40817 |
* entries in the original main playlist, null is returned.
|
|
|
40818 |
*
|
|
|
40819 |
* @param {Object} main a parsed main M3U8 object
|
|
|
40820 |
* @param {Object} media a parsed media M3U8 object
|
|
|
40821 |
* @return {Object} a new object that represents the original
|
|
|
40822 |
* main playlist with the updated media playlist merged in, or
|
|
|
40823 |
* null if the merge produced no change.
|
|
|
40824 |
*/
|
|
|
40825 |
|
|
|
40826 |
const updateMain$1 = (main, newMedia, unchangedCheck = isPlaylistUnchanged) => {
|
|
|
40827 |
const result = merge(main, {});
|
|
|
40828 |
const oldMedia = result.playlists[newMedia.id];
|
|
|
40829 |
if (!oldMedia) {
|
|
|
40830 |
return null;
|
|
|
40831 |
}
|
|
|
40832 |
if (unchangedCheck(oldMedia, newMedia)) {
|
|
|
40833 |
return null;
|
|
|
40834 |
}
|
|
|
40835 |
newMedia.segments = getAllSegments(newMedia);
|
|
|
40836 |
const mergedPlaylist = merge(oldMedia, newMedia); // always use the new media's preload segment
|
|
|
40837 |
|
|
|
40838 |
if (mergedPlaylist.preloadSegment && !newMedia.preloadSegment) {
|
|
|
40839 |
delete mergedPlaylist.preloadSegment;
|
|
|
40840 |
} // if the update could overlap existing segment information, merge the two segment lists
|
|
|
40841 |
|
|
|
40842 |
if (oldMedia.segments) {
|
|
|
40843 |
if (newMedia.skip) {
|
|
|
40844 |
newMedia.segments = newMedia.segments || []; // add back in objects for skipped segments, so that we merge
|
|
|
40845 |
// old properties into the new segments
|
|
|
40846 |
|
|
|
40847 |
for (let i = 0; i < newMedia.skip.skippedSegments; i++) {
|
|
|
40848 |
newMedia.segments.unshift({
|
|
|
40849 |
skipped: true
|
|
|
40850 |
});
|
|
|
40851 |
}
|
|
|
40852 |
}
|
|
|
40853 |
mergedPlaylist.segments = updateSegments(oldMedia.segments, newMedia.segments, newMedia.mediaSequence - oldMedia.mediaSequence);
|
|
|
40854 |
} // resolve any segment URIs to prevent us from having to do it later
|
|
|
40855 |
|
|
|
40856 |
mergedPlaylist.segments.forEach(segment => {
|
|
|
40857 |
resolveSegmentUris(segment, mergedPlaylist.resolvedUri);
|
|
|
40858 |
}); // TODO Right now in the playlists array there are two references to each playlist, one
|
|
|
40859 |
// that is referenced by index, and one by URI. The index reference may no longer be
|
|
|
40860 |
// necessary.
|
|
|
40861 |
|
|
|
40862 |
for (let i = 0; i < result.playlists.length; i++) {
|
|
|
40863 |
if (result.playlists[i].id === newMedia.id) {
|
|
|
40864 |
result.playlists[i] = mergedPlaylist;
|
|
|
40865 |
}
|
|
|
40866 |
}
|
|
|
40867 |
result.playlists[newMedia.id] = mergedPlaylist; // URI reference added for backwards compatibility
|
|
|
40868 |
|
|
|
40869 |
result.playlists[newMedia.uri] = mergedPlaylist; // update media group playlist references.
|
|
|
40870 |
|
|
|
40871 |
forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => {
|
|
|
40872 |
if (!properties.playlists) {
|
|
|
40873 |
return;
|
|
|
40874 |
}
|
|
|
40875 |
for (let i = 0; i < properties.playlists.length; i++) {
|
|
|
40876 |
if (newMedia.id === properties.playlists[i].id) {
|
|
|
40877 |
properties.playlists[i] = mergedPlaylist;
|
|
|
40878 |
}
|
|
|
40879 |
}
|
|
|
40880 |
});
|
|
|
40881 |
return result;
|
|
|
40882 |
};
|
|
|
40883 |
/**
|
|
|
40884 |
* Calculates the time to wait before refreshing a live playlist
|
|
|
40885 |
*
|
|
|
40886 |
* @param {Object} media
|
|
|
40887 |
* The current media
|
|
|
40888 |
* @param {boolean} update
|
|
|
40889 |
* True if there were any updates from the last refresh, false otherwise
|
|
|
40890 |
* @return {number}
|
|
|
40891 |
* The time in ms to wait before refreshing the live playlist
|
|
|
40892 |
*/
|
|
|
40893 |
|
|
|
40894 |
const refreshDelay = (media, update) => {
|
|
|
40895 |
const segments = media.segments || [];
|
|
|
40896 |
const lastSegment = segments[segments.length - 1];
|
|
|
40897 |
const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1];
|
|
|
40898 |
const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration;
|
|
|
40899 |
if (update && lastDuration) {
|
|
|
40900 |
return lastDuration * 1000;
|
|
|
40901 |
} // if the playlist is unchanged since the last reload or last segment duration
|
|
|
40902 |
// cannot be determined, try again after half the target duration
|
|
|
40903 |
|
|
|
40904 |
return (media.partTargetDuration || media.targetDuration || 10) * 500;
|
|
|
40905 |
};
|
|
|
40906 |
/**
|
|
|
40907 |
* Load a playlist from a remote location
|
|
|
40908 |
*
|
|
|
40909 |
* @class PlaylistLoader
|
|
|
40910 |
* @extends Stream
|
|
|
40911 |
* @param {string|Object} src url or object of manifest
|
|
|
40912 |
* @param {boolean} withCredentials the withCredentials xhr option
|
|
|
40913 |
* @class
|
|
|
40914 |
*/
|
|
|
40915 |
|
|
|
40916 |
class PlaylistLoader extends EventTarget$1 {
|
|
|
40917 |
constructor(src, vhs, options = {}) {
|
|
|
40918 |
super();
|
|
|
40919 |
if (!src) {
|
|
|
40920 |
throw new Error('A non-empty playlist URL or object is required');
|
|
|
40921 |
}
|
|
|
40922 |
this.logger_ = logger('PlaylistLoader');
|
|
|
40923 |
const {
|
|
|
40924 |
withCredentials = false
|
|
|
40925 |
} = options;
|
|
|
40926 |
this.src = src;
|
|
|
40927 |
this.vhs_ = vhs;
|
|
|
40928 |
this.withCredentials = withCredentials;
|
|
|
40929 |
this.addDateRangesToTextTrack_ = options.addDateRangesToTextTrack;
|
|
|
40930 |
const vhsOptions = vhs.options_;
|
|
|
40931 |
this.customTagParsers = vhsOptions && vhsOptions.customTagParsers || [];
|
|
|
40932 |
this.customTagMappers = vhsOptions && vhsOptions.customTagMappers || [];
|
|
|
40933 |
this.llhls = vhsOptions && vhsOptions.llhls;
|
|
|
40934 |
this.dateRangesStorage_ = new DateRangesStorage(); // initialize the loader state
|
|
|
40935 |
|
|
|
40936 |
this.state = 'HAVE_NOTHING'; // live playlist staleness timeout
|
|
|
40937 |
|
|
|
40938 |
this.handleMediaupdatetimeout_ = this.handleMediaupdatetimeout_.bind(this);
|
|
|
40939 |
this.on('mediaupdatetimeout', this.handleMediaupdatetimeout_);
|
|
|
40940 |
this.on('loadedplaylist', this.handleLoadedPlaylist_.bind(this));
|
|
|
40941 |
}
|
|
|
40942 |
handleLoadedPlaylist_() {
|
|
|
40943 |
const mediaPlaylist = this.media();
|
|
|
40944 |
if (!mediaPlaylist) {
|
|
|
40945 |
return;
|
|
|
40946 |
}
|
|
|
40947 |
this.dateRangesStorage_.setOffset(mediaPlaylist.segments);
|
|
|
40948 |
this.dateRangesStorage_.setPendingDateRanges(mediaPlaylist.dateRanges);
|
|
|
40949 |
const availableDateRanges = this.dateRangesStorage_.getDateRangesToProcess();
|
|
|
40950 |
if (!availableDateRanges.length || !this.addDateRangesToTextTrack_) {
|
|
|
40951 |
return;
|
|
|
40952 |
}
|
|
|
40953 |
this.addDateRangesToTextTrack_(availableDateRanges);
|
|
|
40954 |
}
|
|
|
40955 |
handleMediaupdatetimeout_() {
|
|
|
40956 |
if (this.state !== 'HAVE_METADATA') {
|
|
|
40957 |
// only refresh the media playlist if no other activity is going on
|
|
|
40958 |
return;
|
|
|
40959 |
}
|
|
|
40960 |
const media = this.media();
|
|
|
40961 |
let uri = resolveUrl(this.main.uri, media.uri);
|
|
|
40962 |
if (this.llhls) {
|
|
|
40963 |
uri = addLLHLSQueryDirectives(uri, media);
|
|
|
40964 |
}
|
|
|
40965 |
this.state = 'HAVE_CURRENT_METADATA';
|
|
|
40966 |
this.request = this.vhs_.xhr({
|
|
|
40967 |
uri,
|
|
|
40968 |
withCredentials: this.withCredentials
|
|
|
40969 |
}, (error, req) => {
|
|
|
40970 |
// disposed
|
|
|
40971 |
if (!this.request) {
|
|
|
40972 |
return;
|
|
|
40973 |
}
|
|
|
40974 |
if (error) {
|
|
|
40975 |
return this.playlistRequestError(this.request, this.media(), 'HAVE_METADATA');
|
|
|
40976 |
}
|
|
|
40977 |
this.haveMetadata({
|
|
|
40978 |
playlistString: this.request.responseText,
|
|
|
40979 |
url: this.media().uri,
|
|
|
40980 |
id: this.media().id
|
|
|
40981 |
});
|
|
|
40982 |
});
|
|
|
40983 |
}
|
|
|
40984 |
playlistRequestError(xhr, playlist, startingState) {
|
|
|
40985 |
const {
|
|
|
40986 |
uri,
|
|
|
40987 |
id
|
|
|
40988 |
} = playlist; // any in-flight request is now finished
|
|
|
40989 |
|
|
|
40990 |
this.request = null;
|
|
|
40991 |
if (startingState) {
|
|
|
40992 |
this.state = startingState;
|
|
|
40993 |
}
|
|
|
40994 |
this.error = {
|
|
|
40995 |
playlist: this.main.playlists[id],
|
|
|
40996 |
status: xhr.status,
|
|
|
40997 |
message: `HLS playlist request error at URL: ${uri}.`,
|
|
|
40998 |
responseText: xhr.responseText,
|
|
|
40999 |
code: xhr.status >= 500 ? 4 : 2
|
|
|
41000 |
};
|
|
|
41001 |
this.trigger('error');
|
|
|
41002 |
}
|
|
|
41003 |
parseManifest_({
|
|
|
41004 |
url,
|
|
|
41005 |
manifestString
|
|
|
41006 |
}) {
|
|
|
41007 |
return parseManifest({
|
|
|
41008 |
onwarn: ({
|
|
|
41009 |
message
|
|
|
41010 |
}) => this.logger_(`m3u8-parser warn for ${url}: ${message}`),
|
|
|
41011 |
oninfo: ({
|
|
|
41012 |
message
|
|
|
41013 |
}) => this.logger_(`m3u8-parser info for ${url}: ${message}`),
|
|
|
41014 |
manifestString,
|
|
|
41015 |
customTagParsers: this.customTagParsers,
|
|
|
41016 |
customTagMappers: this.customTagMappers,
|
|
|
41017 |
llhls: this.llhls
|
|
|
41018 |
});
|
|
|
41019 |
}
|
|
|
41020 |
/**
|
|
|
41021 |
* Update the playlist loader's state in response to a new or updated playlist.
|
|
|
41022 |
*
|
|
|
41023 |
* @param {string} [playlistString]
|
|
|
41024 |
* Playlist string (if playlistObject is not provided)
|
|
|
41025 |
* @param {Object} [playlistObject]
|
|
|
41026 |
* Playlist object (if playlistString is not provided)
|
|
|
41027 |
* @param {string} url
|
|
|
41028 |
* URL of playlist
|
|
|
41029 |
* @param {string} id
|
|
|
41030 |
* ID to use for playlist
|
|
|
41031 |
*/
|
|
|
41032 |
|
|
|
41033 |
haveMetadata({
|
|
|
41034 |
playlistString,
|
|
|
41035 |
playlistObject,
|
|
|
41036 |
url,
|
|
|
41037 |
id
|
|
|
41038 |
}) {
|
|
|
41039 |
// any in-flight request is now finished
|
|
|
41040 |
this.request = null;
|
|
|
41041 |
this.state = 'HAVE_METADATA';
|
|
|
41042 |
const playlist = playlistObject || this.parseManifest_({
|
|
|
41043 |
url,
|
|
|
41044 |
manifestString: playlistString
|
|
|
41045 |
});
|
|
|
41046 |
playlist.lastRequest = Date.now();
|
|
|
41047 |
setupMediaPlaylist({
|
|
|
41048 |
playlist,
|
|
|
41049 |
uri: url,
|
|
|
41050 |
id
|
|
|
41051 |
}); // merge this playlist into the main manifest
|
|
|
41052 |
|
|
|
41053 |
const update = updateMain$1(this.main, playlist);
|
|
|
41054 |
this.targetDuration = playlist.partTargetDuration || playlist.targetDuration;
|
|
|
41055 |
this.pendingMedia_ = null;
|
|
|
41056 |
if (update) {
|
|
|
41057 |
this.main = update;
|
|
|
41058 |
this.media_ = this.main.playlists[id];
|
|
|
41059 |
} else {
|
|
|
41060 |
this.trigger('playlistunchanged');
|
|
|
41061 |
}
|
|
|
41062 |
this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update));
|
|
|
41063 |
this.trigger('loadedplaylist');
|
|
|
41064 |
}
|
|
|
41065 |
/**
|
|
|
41066 |
* Abort any outstanding work and clean up.
|
|
|
41067 |
*/
|
|
|
41068 |
|
|
|
41069 |
dispose() {
|
|
|
41070 |
this.trigger('dispose');
|
|
|
41071 |
this.stopRequest();
|
|
|
41072 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
41073 |
window.clearTimeout(this.finalRenditionTimeout);
|
|
|
41074 |
this.dateRangesStorage_ = new DateRangesStorage();
|
|
|
41075 |
this.off();
|
|
|
41076 |
}
|
|
|
41077 |
stopRequest() {
|
|
|
41078 |
if (this.request) {
|
|
|
41079 |
const oldRequest = this.request;
|
|
|
41080 |
this.request = null;
|
|
|
41081 |
oldRequest.onreadystatechange = null;
|
|
|
41082 |
oldRequest.abort();
|
|
|
41083 |
}
|
|
|
41084 |
}
|
|
|
41085 |
/**
|
|
|
41086 |
* When called without any arguments, returns the currently
|
|
|
41087 |
* active media playlist. When called with a single argument,
|
|
|
41088 |
* triggers the playlist loader to asynchronously switch to the
|
|
|
41089 |
* specified media playlist. Calling this method while the
|
|
|
41090 |
* loader is in the HAVE_NOTHING causes an error to be emitted
|
|
|
41091 |
* but otherwise has no effect.
|
|
|
41092 |
*
|
|
|
41093 |
* @param {Object=} playlist the parsed media playlist
|
|
|
41094 |
* object to switch to
|
|
|
41095 |
* @param {boolean=} shouldDelay whether we should delay the request by half target duration
|
|
|
41096 |
*
|
|
|
41097 |
* @return {Playlist} the current loaded media
|
|
|
41098 |
*/
|
|
|
41099 |
|
|
|
41100 |
media(playlist, shouldDelay) {
|
|
|
41101 |
// getter
|
|
|
41102 |
if (!playlist) {
|
|
|
41103 |
return this.media_;
|
|
|
41104 |
} // setter
|
|
|
41105 |
|
|
|
41106 |
if (this.state === 'HAVE_NOTHING') {
|
|
|
41107 |
throw new Error('Cannot switch media playlist from ' + this.state);
|
|
|
41108 |
} // find the playlist object if the target playlist has been
|
|
|
41109 |
// specified by URI
|
|
|
41110 |
|
|
|
41111 |
if (typeof playlist === 'string') {
|
|
|
41112 |
if (!this.main.playlists[playlist]) {
|
|
|
41113 |
throw new Error('Unknown playlist URI: ' + playlist);
|
|
|
41114 |
}
|
|
|
41115 |
playlist = this.main.playlists[playlist];
|
|
|
41116 |
}
|
|
|
41117 |
window.clearTimeout(this.finalRenditionTimeout);
|
|
|
41118 |
if (shouldDelay) {
|
|
|
41119 |
const delay = (playlist.partTargetDuration || playlist.targetDuration) / 2 * 1000 || 5 * 1000;
|
|
|
41120 |
this.finalRenditionTimeout = window.setTimeout(this.media.bind(this, playlist, false), delay);
|
|
|
41121 |
return;
|
|
|
41122 |
}
|
|
|
41123 |
const startingState = this.state;
|
|
|
41124 |
const mediaChange = !this.media_ || playlist.id !== this.media_.id;
|
|
|
41125 |
const mainPlaylistRef = this.main.playlists[playlist.id]; // switch to fully loaded playlists immediately
|
|
|
41126 |
|
|
|
41127 |
if (mainPlaylistRef && mainPlaylistRef.endList ||
|
|
|
41128 |
// handle the case of a playlist object (e.g., if using vhs-json with a resolved
|
|
|
41129 |
// media playlist or, for the case of demuxed audio, a resolved audio media group)
|
|
|
41130 |
playlist.endList && playlist.segments.length) {
|
|
|
41131 |
// abort outstanding playlist requests
|
|
|
41132 |
if (this.request) {
|
|
|
41133 |
this.request.onreadystatechange = null;
|
|
|
41134 |
this.request.abort();
|
|
|
41135 |
this.request = null;
|
|
|
41136 |
}
|
|
|
41137 |
this.state = 'HAVE_METADATA';
|
|
|
41138 |
this.media_ = playlist; // trigger media change if the active media has been updated
|
|
|
41139 |
|
|
|
41140 |
if (mediaChange) {
|
|
|
41141 |
this.trigger('mediachanging');
|
|
|
41142 |
if (startingState === 'HAVE_MAIN_MANIFEST') {
|
|
|
41143 |
// The initial playlist was a main manifest, and the first media selected was
|
|
|
41144 |
// also provided (in the form of a resolved playlist object) as part of the
|
|
|
41145 |
// source object (rather than just a URL). Therefore, since the media playlist
|
|
|
41146 |
// doesn't need to be requested, loadedmetadata won't trigger as part of the
|
|
|
41147 |
// normal flow, and needs an explicit trigger here.
|
|
|
41148 |
this.trigger('loadedmetadata');
|
|
|
41149 |
} else {
|
|
|
41150 |
this.trigger('mediachange');
|
|
|
41151 |
}
|
|
|
41152 |
}
|
|
|
41153 |
return;
|
|
|
41154 |
} // We update/set the timeout here so that live playlists
|
|
|
41155 |
// that are not a media change will "start" the loader as expected.
|
|
|
41156 |
// We expect that this function will start the media update timeout
|
|
|
41157 |
// cycle again. This also prevents a playlist switch failure from
|
|
|
41158 |
// causing us to stall during live.
|
|
|
41159 |
|
|
|
41160 |
this.updateMediaUpdateTimeout_(refreshDelay(playlist, true)); // switching to the active playlist is a no-op
|
|
|
41161 |
|
|
|
41162 |
if (!mediaChange) {
|
|
|
41163 |
return;
|
|
|
41164 |
}
|
|
|
41165 |
this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request
|
|
|
41166 |
|
|
|
41167 |
if (this.request) {
|
|
|
41168 |
if (playlist.resolvedUri === this.request.url) {
|
|
|
41169 |
// requesting to switch to the same playlist multiple times
|
|
|
41170 |
// has no effect after the first
|
|
|
41171 |
return;
|
|
|
41172 |
}
|
|
|
41173 |
this.request.onreadystatechange = null;
|
|
|
41174 |
this.request.abort();
|
|
|
41175 |
this.request = null;
|
|
|
41176 |
} // request the new playlist
|
|
|
41177 |
|
|
|
41178 |
if (this.media_) {
|
|
|
41179 |
this.trigger('mediachanging');
|
|
|
41180 |
}
|
|
|
41181 |
this.pendingMedia_ = playlist;
|
|
|
41182 |
this.request = this.vhs_.xhr({
|
|
|
41183 |
uri: playlist.resolvedUri,
|
|
|
41184 |
withCredentials: this.withCredentials
|
|
|
41185 |
}, (error, req) => {
|
|
|
41186 |
// disposed
|
|
|
41187 |
if (!this.request) {
|
|
|
41188 |
return;
|
|
|
41189 |
}
|
|
|
41190 |
playlist.lastRequest = Date.now();
|
|
|
41191 |
playlist.resolvedUri = resolveManifestRedirect(playlist.resolvedUri, req);
|
|
|
41192 |
if (error) {
|
|
|
41193 |
return this.playlistRequestError(this.request, playlist, startingState);
|
|
|
41194 |
}
|
|
|
41195 |
this.haveMetadata({
|
|
|
41196 |
playlistString: req.responseText,
|
|
|
41197 |
url: playlist.uri,
|
|
|
41198 |
id: playlist.id
|
|
|
41199 |
}); // fire loadedmetadata the first time a media playlist is loaded
|
|
|
41200 |
|
|
|
41201 |
if (startingState === 'HAVE_MAIN_MANIFEST') {
|
|
|
41202 |
this.trigger('loadedmetadata');
|
|
|
41203 |
} else {
|
|
|
41204 |
this.trigger('mediachange');
|
|
|
41205 |
}
|
|
|
41206 |
});
|
|
|
41207 |
}
|
|
|
41208 |
/**
|
|
|
41209 |
* pause loading of the playlist
|
|
|
41210 |
*/
|
|
|
41211 |
|
|
|
41212 |
pause() {
|
|
|
41213 |
if (this.mediaUpdateTimeout) {
|
|
|
41214 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
41215 |
this.mediaUpdateTimeout = null;
|
|
|
41216 |
}
|
|
|
41217 |
this.stopRequest();
|
|
|
41218 |
if (this.state === 'HAVE_NOTHING') {
|
|
|
41219 |
// If we pause the loader before any data has been retrieved, its as if we never
|
|
|
41220 |
// started, so reset to an unstarted state.
|
|
|
41221 |
this.started = false;
|
|
|
41222 |
} // Need to restore state now that no activity is happening
|
|
|
41223 |
|
|
|
41224 |
if (this.state === 'SWITCHING_MEDIA') {
|
|
|
41225 |
// if the loader was in the process of switching media, it should either return to
|
|
|
41226 |
// HAVE_MAIN_MANIFEST or HAVE_METADATA depending on if the loader has loaded a media
|
|
|
41227 |
// playlist yet. This is determined by the existence of loader.media_
|
|
|
41228 |
if (this.media_) {
|
|
|
41229 |
this.state = 'HAVE_METADATA';
|
|
|
41230 |
} else {
|
|
|
41231 |
this.state = 'HAVE_MAIN_MANIFEST';
|
|
|
41232 |
}
|
|
|
41233 |
} else if (this.state === 'HAVE_CURRENT_METADATA') {
|
|
|
41234 |
this.state = 'HAVE_METADATA';
|
|
|
41235 |
}
|
|
|
41236 |
}
|
|
|
41237 |
/**
|
|
|
41238 |
* start loading of the playlist
|
|
|
41239 |
*/
|
|
|
41240 |
|
|
|
41241 |
load(shouldDelay) {
|
|
|
41242 |
if (this.mediaUpdateTimeout) {
|
|
|
41243 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
41244 |
this.mediaUpdateTimeout = null;
|
|
|
41245 |
}
|
|
|
41246 |
const media = this.media();
|
|
|
41247 |
if (shouldDelay) {
|
|
|
41248 |
const delay = media ? (media.partTargetDuration || media.targetDuration) / 2 * 1000 : 5 * 1000;
|
|
|
41249 |
this.mediaUpdateTimeout = window.setTimeout(() => {
|
|
|
41250 |
this.mediaUpdateTimeout = null;
|
|
|
41251 |
this.load();
|
|
|
41252 |
}, delay);
|
|
|
41253 |
return;
|
|
|
41254 |
}
|
|
|
41255 |
if (!this.started) {
|
|
|
41256 |
this.start();
|
|
|
41257 |
return;
|
|
|
41258 |
}
|
|
|
41259 |
if (media && !media.endList) {
|
|
|
41260 |
this.trigger('mediaupdatetimeout');
|
|
|
41261 |
} else {
|
|
|
41262 |
this.trigger('loadedplaylist');
|
|
|
41263 |
}
|
|
|
41264 |
}
|
|
|
41265 |
updateMediaUpdateTimeout_(delay) {
|
|
|
41266 |
if (this.mediaUpdateTimeout) {
|
|
|
41267 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
41268 |
this.mediaUpdateTimeout = null;
|
|
|
41269 |
} // we only have use mediaupdatetimeout for live playlists.
|
|
|
41270 |
|
|
|
41271 |
if (!this.media() || this.media().endList) {
|
|
|
41272 |
return;
|
|
|
41273 |
}
|
|
|
41274 |
this.mediaUpdateTimeout = window.setTimeout(() => {
|
|
|
41275 |
this.mediaUpdateTimeout = null;
|
|
|
41276 |
this.trigger('mediaupdatetimeout');
|
|
|
41277 |
this.updateMediaUpdateTimeout_(delay);
|
|
|
41278 |
}, delay);
|
|
|
41279 |
}
|
|
|
41280 |
/**
|
|
|
41281 |
* start loading of the playlist
|
|
|
41282 |
*/
|
|
|
41283 |
|
|
|
41284 |
start() {
|
|
|
41285 |
this.started = true;
|
|
|
41286 |
if (typeof this.src === 'object') {
|
|
|
41287 |
// in the case of an entirely constructed manifest object (meaning there's no actual
|
|
|
41288 |
// manifest on a server), default the uri to the page's href
|
|
|
41289 |
if (!this.src.uri) {
|
|
|
41290 |
this.src.uri = window.location.href;
|
|
|
41291 |
} // resolvedUri is added on internally after the initial request. Since there's no
|
|
|
41292 |
// request for pre-resolved manifests, add on resolvedUri here.
|
|
|
41293 |
|
|
|
41294 |
this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first
|
|
|
41295 |
// request can be skipped (since the top level of the manifest, at a minimum, is
|
|
|
41296 |
// already available as a parsed manifest object). However, if the manifest object
|
|
|
41297 |
// represents a main playlist, some media playlists may need to be resolved before
|
|
|
41298 |
// the starting segment list is available. Therefore, go directly to setup of the
|
|
|
41299 |
// initial playlist, and let the normal flow continue from there.
|
|
|
41300 |
//
|
|
|
41301 |
// Note that the call to setup is asynchronous, as other sections of VHS may assume
|
|
|
41302 |
// that the first request is asynchronous.
|
|
|
41303 |
|
|
|
41304 |
setTimeout(() => {
|
|
|
41305 |
this.setupInitialPlaylist(this.src);
|
|
|
41306 |
}, 0);
|
|
|
41307 |
return;
|
|
|
41308 |
} // request the specified URL
|
|
|
41309 |
|
|
|
41310 |
this.request = this.vhs_.xhr({
|
|
|
41311 |
uri: this.src,
|
|
|
41312 |
withCredentials: this.withCredentials
|
|
|
41313 |
}, (error, req) => {
|
|
|
41314 |
// disposed
|
|
|
41315 |
if (!this.request) {
|
|
|
41316 |
return;
|
|
|
41317 |
} // clear the loader's request reference
|
|
|
41318 |
|
|
|
41319 |
this.request = null;
|
|
|
41320 |
if (error) {
|
|
|
41321 |
this.error = {
|
|
|
41322 |
status: req.status,
|
|
|
41323 |
message: `HLS playlist request error at URL: ${this.src}.`,
|
|
|
41324 |
responseText: req.responseText,
|
|
|
41325 |
// MEDIA_ERR_NETWORK
|
|
|
41326 |
code: 2
|
|
|
41327 |
};
|
|
|
41328 |
if (this.state === 'HAVE_NOTHING') {
|
|
|
41329 |
this.started = false;
|
|
|
41330 |
}
|
|
|
41331 |
return this.trigger('error');
|
|
|
41332 |
}
|
|
|
41333 |
this.src = resolveManifestRedirect(this.src, req);
|
|
|
41334 |
const manifest = this.parseManifest_({
|
|
|
41335 |
manifestString: req.responseText,
|
|
|
41336 |
url: this.src
|
|
|
41337 |
});
|
|
|
41338 |
this.setupInitialPlaylist(manifest);
|
|
|
41339 |
});
|
|
|
41340 |
}
|
|
|
41341 |
srcUri() {
|
|
|
41342 |
return typeof this.src === 'string' ? this.src : this.src.uri;
|
|
|
41343 |
}
|
|
|
41344 |
/**
|
|
|
41345 |
* Given a manifest object that's either a main or media playlist, trigger the proper
|
|
|
41346 |
* events and set the state of the playlist loader.
|
|
|
41347 |
*
|
|
|
41348 |
* If the manifest object represents a main playlist, `loadedplaylist` will be
|
|
|
41349 |
* triggered to allow listeners to select a playlist. If none is selected, the loader
|
|
|
41350 |
* will default to the first one in the playlists array.
|
|
|
41351 |
*
|
|
|
41352 |
* If the manifest object represents a media playlist, `loadedplaylist` will be
|
|
|
41353 |
* triggered followed by `loadedmetadata`, as the only available playlist is loaded.
|
|
|
41354 |
*
|
|
|
41355 |
* In the case of a media playlist, a main playlist object wrapper with one playlist
|
|
|
41356 |
* will be created so that all logic can handle playlists in the same fashion (as an
|
|
|
41357 |
* assumed manifest object schema).
|
|
|
41358 |
*
|
|
|
41359 |
* @param {Object} manifest
|
|
|
41360 |
* The parsed manifest object
|
|
|
41361 |
*/
|
|
|
41362 |
|
|
|
41363 |
setupInitialPlaylist(manifest) {
|
|
|
41364 |
this.state = 'HAVE_MAIN_MANIFEST';
|
|
|
41365 |
if (manifest.playlists) {
|
|
|
41366 |
this.main = manifest;
|
|
|
41367 |
addPropertiesToMain(this.main, this.srcUri()); // If the initial main playlist has playlists wtih segments already resolved,
|
|
|
41368 |
// then resolve URIs in advance, as they are usually done after a playlist request,
|
|
|
41369 |
// which may not happen if the playlist is resolved.
|
|
|
41370 |
|
|
|
41371 |
manifest.playlists.forEach(playlist => {
|
|
|
41372 |
playlist.segments = getAllSegments(playlist);
|
|
|
41373 |
playlist.segments.forEach(segment => {
|
|
|
41374 |
resolveSegmentUris(segment, playlist.resolvedUri);
|
|
|
41375 |
});
|
|
|
41376 |
});
|
|
|
41377 |
this.trigger('loadedplaylist');
|
|
|
41378 |
if (!this.request) {
|
|
|
41379 |
// no media playlist was specifically selected so start
|
|
|
41380 |
// from the first listed one
|
|
|
41381 |
this.media(this.main.playlists[0]);
|
|
|
41382 |
}
|
|
|
41383 |
return;
|
|
|
41384 |
} // In order to support media playlists passed in as vhs-json, the case where the uri
|
|
|
41385 |
// is not provided as part of the manifest should be considered, and an appropriate
|
|
|
41386 |
// default used.
|
|
|
41387 |
|
|
|
41388 |
const uri = this.srcUri() || window.location.href;
|
|
|
41389 |
this.main = mainForMedia(manifest, uri);
|
|
|
41390 |
this.haveMetadata({
|
|
|
41391 |
playlistObject: manifest,
|
|
|
41392 |
url: uri,
|
|
|
41393 |
id: this.main.playlists[0].id
|
|
|
41394 |
});
|
|
|
41395 |
this.trigger('loadedmetadata');
|
|
|
41396 |
}
|
|
|
41397 |
/**
|
|
|
41398 |
* Updates or deletes a preexisting pathway clone.
|
|
|
41399 |
* Ensures that all playlists related to the old pathway clone are
|
|
|
41400 |
* either updated or deleted.
|
|
|
41401 |
*
|
|
|
41402 |
* @param {Object} clone On update, the pathway clone object for the newly updated pathway clone.
|
|
|
41403 |
* On delete, the old pathway clone object to be deleted.
|
|
|
41404 |
* @param {boolean} isUpdate True if the pathway is to be updated,
|
|
|
41405 |
* false if it is meant to be deleted.
|
|
|
41406 |
*/
|
|
|
41407 |
|
|
|
41408 |
updateOrDeleteClone(clone, isUpdate) {
|
|
|
41409 |
const main = this.main;
|
|
|
41410 |
const pathway = clone.ID;
|
|
|
41411 |
let i = main.playlists.length; // Iterate backwards through the playlist so we can remove playlists if necessary.
|
|
|
41412 |
|
|
|
41413 |
while (i--) {
|
|
|
41414 |
const p = main.playlists[i];
|
|
|
41415 |
if (p.attributes['PATHWAY-ID'] === pathway) {
|
|
|
41416 |
const oldPlaylistUri = p.resolvedUri;
|
|
|
41417 |
const oldPlaylistId = p.id; // update the indexed playlist and add new playlists by ID and URI
|
|
|
41418 |
|
|
|
41419 |
if (isUpdate) {
|
|
|
41420 |
const newPlaylistUri = this.createCloneURI_(p.resolvedUri, clone);
|
|
|
41421 |
const newPlaylistId = createPlaylistID(pathway, newPlaylistUri);
|
|
|
41422 |
const attributes = this.createCloneAttributes_(pathway, p.attributes);
|
|
|
41423 |
const updatedPlaylist = this.createClonePlaylist_(p, newPlaylistId, clone, attributes);
|
|
|
41424 |
main.playlists[i] = updatedPlaylist;
|
|
|
41425 |
main.playlists[newPlaylistId] = updatedPlaylist;
|
|
|
41426 |
main.playlists[newPlaylistUri] = updatedPlaylist;
|
|
|
41427 |
} else {
|
|
|
41428 |
// Remove the indexed playlist.
|
|
|
41429 |
main.playlists.splice(i, 1);
|
|
|
41430 |
} // Remove playlists by the old ID and URI.
|
|
|
41431 |
|
|
|
41432 |
delete main.playlists[oldPlaylistId];
|
|
|
41433 |
delete main.playlists[oldPlaylistUri];
|
|
|
41434 |
}
|
|
|
41435 |
}
|
|
|
41436 |
this.updateOrDeleteCloneMedia(clone, isUpdate);
|
|
|
41437 |
}
|
|
|
41438 |
/**
|
|
|
41439 |
* Updates or deletes media data based on the pathway clone object.
|
|
|
41440 |
* Due to the complexity of the media groups and playlists, in all cases
|
|
|
41441 |
* we remove all of the old media groups and playlists.
|
|
|
41442 |
* On updates, we then create new media groups and playlists based on the
|
|
|
41443 |
* new pathway clone object.
|
|
|
41444 |
*
|
|
|
41445 |
* @param {Object} clone The pathway clone object for the newly updated pathway clone.
|
|
|
41446 |
* @param {boolean} isUpdate True if the pathway is to be updated,
|
|
|
41447 |
* false if it is meant to be deleted.
|
|
|
41448 |
*/
|
|
|
41449 |
|
|
|
41450 |
updateOrDeleteCloneMedia(clone, isUpdate) {
|
|
|
41451 |
const main = this.main;
|
|
|
41452 |
const id = clone.ID;
|
|
|
41453 |
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => {
|
|
|
41454 |
if (!main.mediaGroups[mediaType] || !main.mediaGroups[mediaType][id]) {
|
|
|
41455 |
return;
|
|
|
41456 |
}
|
|
|
41457 |
for (const groupKey in main.mediaGroups[mediaType]) {
|
|
|
41458 |
// Remove all media playlists for the media group for this pathway clone.
|
|
|
41459 |
if (groupKey === id) {
|
|
|
41460 |
for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
|
|
|
41461 |
const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey];
|
|
|
41462 |
oldMedia.playlists.forEach((p, i) => {
|
|
|
41463 |
const oldMediaPlaylist = main.playlists[p.id];
|
|
|
41464 |
const oldPlaylistId = oldMediaPlaylist.id;
|
|
|
41465 |
const oldPlaylistUri = oldMediaPlaylist.resolvedUri;
|
|
|
41466 |
delete main.playlists[oldPlaylistId];
|
|
|
41467 |
delete main.playlists[oldPlaylistUri];
|
|
|
41468 |
});
|
|
|
41469 |
} // Delete the old media group.
|
|
|
41470 |
|
|
|
41471 |
delete main.mediaGroups[mediaType][groupKey];
|
|
|
41472 |
}
|
|
|
41473 |
}
|
|
|
41474 |
}); // Create the new media groups and playlists if there is an update.
|
|
|
41475 |
|
|
|
41476 |
if (isUpdate) {
|
|
|
41477 |
this.createClonedMediaGroups_(clone);
|
|
|
41478 |
}
|
|
|
41479 |
}
|
|
|
41480 |
/**
|
|
|
41481 |
* Given a pathway clone object, clones all necessary playlists.
|
|
|
41482 |
*
|
|
|
41483 |
* @param {Object} clone The pathway clone object.
|
|
|
41484 |
* @param {Object} basePlaylist The original playlist to clone from.
|
|
|
41485 |
*/
|
|
|
41486 |
|
|
|
41487 |
addClonePathway(clone, basePlaylist = {}) {
|
|
|
41488 |
const main = this.main;
|
|
|
41489 |
const index = main.playlists.length;
|
|
|
41490 |
const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone);
|
|
|
41491 |
const playlistId = createPlaylistID(clone.ID, uri);
|
|
|
41492 |
const attributes = this.createCloneAttributes_(clone.ID, basePlaylist.attributes);
|
|
|
41493 |
const playlist = this.createClonePlaylist_(basePlaylist, playlistId, clone, attributes);
|
|
|
41494 |
main.playlists[index] = playlist; // add playlist by ID and URI
|
|
|
41495 |
|
|
|
41496 |
main.playlists[playlistId] = playlist;
|
|
|
41497 |
main.playlists[uri] = playlist;
|
|
|
41498 |
this.createClonedMediaGroups_(clone);
|
|
|
41499 |
}
|
|
|
41500 |
/**
|
|
|
41501 |
* Given a pathway clone object we create clones of all media.
|
|
|
41502 |
* In this function, all necessary information and updated playlists
|
|
|
41503 |
* are added to the `mediaGroup` object.
|
|
|
41504 |
* Playlists are also added to the `playlists` array so the media groups
|
|
|
41505 |
* will be properly linked.
|
|
|
41506 |
*
|
|
|
41507 |
* @param {Object} clone The pathway clone object.
|
|
|
41508 |
*/
|
|
|
41509 |
|
|
|
41510 |
createClonedMediaGroups_(clone) {
|
|
|
41511 |
const id = clone.ID;
|
|
|
41512 |
const baseID = clone['BASE-ID'];
|
|
|
41513 |
const main = this.main;
|
|
|
41514 |
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => {
|
|
|
41515 |
// If the media type doesn't exist, or there is already a clone, skip
|
|
|
41516 |
// to the next media type.
|
|
|
41517 |
if (!main.mediaGroups[mediaType] || main.mediaGroups[mediaType][id]) {
|
|
|
41518 |
return;
|
|
|
41519 |
}
|
|
|
41520 |
for (const groupKey in main.mediaGroups[mediaType]) {
|
|
|
41521 |
if (groupKey === baseID) {
|
|
|
41522 |
// Create the group.
|
|
|
41523 |
main.mediaGroups[mediaType][id] = {};
|
|
|
41524 |
} else {
|
|
|
41525 |
// There is no need to iterate over label keys in this case.
|
|
|
41526 |
continue;
|
|
|
41527 |
}
|
|
|
41528 |
for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
|
|
|
41529 |
const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey];
|
|
|
41530 |
main.mediaGroups[mediaType][id][labelKey] = _extends$1({}, oldMedia);
|
|
|
41531 |
const newMedia = main.mediaGroups[mediaType][id][labelKey]; // update URIs on the media
|
|
|
41532 |
|
|
|
41533 |
const newUri = this.createCloneURI_(oldMedia.resolvedUri, clone);
|
|
|
41534 |
newMedia.resolvedUri = newUri;
|
|
|
41535 |
newMedia.uri = newUri; // Reset playlists in the new media group.
|
|
|
41536 |
|
|
|
41537 |
newMedia.playlists = []; // Create new playlists in the newly cloned media group.
|
|
|
41538 |
|
|
|
41539 |
oldMedia.playlists.forEach((p, i) => {
|
|
|
41540 |
const oldMediaPlaylist = main.playlists[p.id];
|
|
|
41541 |
const group = groupID(mediaType, id, labelKey);
|
|
|
41542 |
const newPlaylistID = createPlaylistID(id, group); // Check to see if it already exists
|
|
|
41543 |
|
|
|
41544 |
if (oldMediaPlaylist && !main.playlists[newPlaylistID]) {
|
|
|
41545 |
const newMediaPlaylist = this.createClonePlaylist_(oldMediaPlaylist, newPlaylistID, clone);
|
|
|
41546 |
const newPlaylistUri = newMediaPlaylist.resolvedUri;
|
|
|
41547 |
main.playlists[newPlaylistID] = newMediaPlaylist;
|
|
|
41548 |
main.playlists[newPlaylistUri] = newMediaPlaylist;
|
|
|
41549 |
}
|
|
|
41550 |
newMedia.playlists[i] = this.createClonePlaylist_(p, newPlaylistID, clone);
|
|
|
41551 |
});
|
|
|
41552 |
}
|
|
|
41553 |
}
|
|
|
41554 |
});
|
|
|
41555 |
}
|
|
|
41556 |
/**
|
|
|
41557 |
* Using the original playlist to be cloned, and the pathway clone object
|
|
|
41558 |
* information, we create a new playlist.
|
|
|
41559 |
*
|
|
|
41560 |
* @param {Object} basePlaylist The original playlist to be cloned from.
|
|
|
41561 |
* @param {string} id The desired id of the newly cloned playlist.
|
|
|
41562 |
* @param {Object} clone The pathway clone object.
|
|
|
41563 |
* @param {Object} attributes An optional object to populate the `attributes` property in the playlist.
|
|
|
41564 |
*
|
|
|
41565 |
* @return {Object} The combined cloned playlist.
|
|
|
41566 |
*/
|
|
|
41567 |
|
|
|
41568 |
createClonePlaylist_(basePlaylist, id, clone, attributes) {
|
|
|
41569 |
const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone);
|
|
|
41570 |
const newProps = {
|
|
|
41571 |
resolvedUri: uri,
|
|
|
41572 |
uri,
|
|
|
41573 |
id
|
|
|
41574 |
}; // Remove all segments from previous playlist in the clone.
|
|
|
41575 |
|
|
|
41576 |
if (basePlaylist.segments) {
|
|
|
41577 |
newProps.segments = [];
|
|
|
41578 |
}
|
|
|
41579 |
if (attributes) {
|
|
|
41580 |
newProps.attributes = attributes;
|
|
|
41581 |
}
|
|
|
41582 |
return merge(basePlaylist, newProps);
|
|
|
41583 |
}
|
|
|
41584 |
/**
|
|
|
41585 |
* Generates an updated URI for a cloned pathway based on the original
|
|
|
41586 |
* pathway's URI and the paramaters from the pathway clone object in the
|
|
|
41587 |
* content steering server response.
|
|
|
41588 |
*
|
|
|
41589 |
* @param {string} baseUri URI to be updated in the cloned pathway.
|
|
|
41590 |
* @param {Object} clone The pathway clone object.
|
|
|
41591 |
*
|
|
|
41592 |
* @return {string} The updated URI for the cloned pathway.
|
|
|
41593 |
*/
|
|
|
41594 |
|
|
|
41595 |
createCloneURI_(baseURI, clone) {
|
|
|
41596 |
const uri = new URL(baseURI);
|
|
|
41597 |
uri.hostname = clone['URI-REPLACEMENT'].HOST;
|
|
|
41598 |
const params = clone['URI-REPLACEMENT'].PARAMS; // Add params to the cloned URL.
|
|
|
41599 |
|
|
|
41600 |
for (const key of Object.keys(params)) {
|
|
|
41601 |
uri.searchParams.set(key, params[key]);
|
|
|
41602 |
}
|
|
|
41603 |
return uri.href;
|
|
|
41604 |
}
|
|
|
41605 |
/**
|
|
|
41606 |
* Helper function to create the attributes needed for the new clone.
|
|
|
41607 |
* This mainly adds the necessary media attributes.
|
|
|
41608 |
*
|
|
|
41609 |
* @param {string} id The pathway clone object ID.
|
|
|
41610 |
* @param {Object} oldAttributes The old attributes to compare to.
|
|
|
41611 |
* @return {Object} The new attributes to add to the playlist.
|
|
|
41612 |
*/
|
|
|
41613 |
|
|
|
41614 |
createCloneAttributes_(id, oldAttributes) {
|
|
|
41615 |
const attributes = {
|
|
|
41616 |
['PATHWAY-ID']: id
|
|
|
41617 |
};
|
|
|
41618 |
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => {
|
|
|
41619 |
if (oldAttributes[mediaType]) {
|
|
|
41620 |
attributes[mediaType] = id;
|
|
|
41621 |
}
|
|
|
41622 |
});
|
|
|
41623 |
return attributes;
|
|
|
41624 |
}
|
|
|
41625 |
/**
|
|
|
41626 |
* Returns the key ID set from a playlist
|
|
|
41627 |
*
|
|
|
41628 |
* @param {playlist} playlist to fetch the key ID set from.
|
|
|
41629 |
* @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist.
|
|
|
41630 |
*/
|
|
|
41631 |
|
|
|
41632 |
getKeyIdSet(playlist) {
|
|
|
41633 |
if (playlist.contentProtection) {
|
|
|
41634 |
const keyIds = new Set();
|
|
|
41635 |
for (const keysystem in playlist.contentProtection) {
|
|
|
41636 |
const keyId = playlist.contentProtection[keysystem].attributes.keyId;
|
|
|
41637 |
if (keyId) {
|
|
|
41638 |
keyIds.add(keyId.toLowerCase());
|
|
|
41639 |
}
|
|
|
41640 |
}
|
|
|
41641 |
return keyIds;
|
|
|
41642 |
}
|
|
|
41643 |
}
|
|
|
41644 |
}
|
|
|
41645 |
|
|
|
41646 |
/**
|
|
|
41647 |
* @file xhr.js
|
|
|
41648 |
*/
|
|
|
41649 |
const {
|
|
|
41650 |
xhr: videojsXHR
|
|
|
41651 |
} = videojs;
|
|
|
41652 |
const callbackWrapper = function (request, error, response, callback) {
|
|
|
41653 |
const reqResponse = request.responseType === 'arraybuffer' ? request.response : request.responseText;
|
|
|
41654 |
if (!error && reqResponse) {
|
|
|
41655 |
request.responseTime = Date.now();
|
|
|
41656 |
request.roundTripTime = request.responseTime - request.requestTime;
|
|
|
41657 |
request.bytesReceived = reqResponse.byteLength || reqResponse.length;
|
|
|
41658 |
if (!request.bandwidth) {
|
|
|
41659 |
request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000);
|
|
|
41660 |
}
|
|
|
41661 |
}
|
|
|
41662 |
if (response.headers) {
|
|
|
41663 |
request.responseHeaders = response.headers;
|
|
|
41664 |
} // videojs.xhr now uses a specific code on the error
|
|
|
41665 |
// object to signal that a request has timed out instead
|
|
|
41666 |
// of setting a boolean on the request object
|
|
|
41667 |
|
|
|
41668 |
if (error && error.code === 'ETIMEDOUT') {
|
|
|
41669 |
request.timedout = true;
|
|
|
41670 |
} // videojs.xhr no longer considers status codes outside of 200 and 0
|
|
|
41671 |
// (for file uris) to be errors, but the old XHR did, so emulate that
|
|
|
41672 |
// behavior. Status 206 may be used in response to byterange requests.
|
|
|
41673 |
|
|
|
41674 |
if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) {
|
|
|
41675 |
error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText)));
|
|
|
41676 |
}
|
|
|
41677 |
callback(error, request);
|
|
|
41678 |
};
|
|
|
41679 |
/**
|
|
|
41680 |
* Iterates over the request hooks Set and calls them in order
|
|
|
41681 |
*
|
|
|
41682 |
* @param {Set} hooks the hook Set to iterate over
|
|
|
41683 |
* @param {Object} options the request options to pass to the xhr wrapper
|
|
|
41684 |
* @return the callback hook function return value, the modified or new options Object.
|
|
|
41685 |
*/
|
|
|
41686 |
|
|
|
41687 |
const callAllRequestHooks = (requestSet, options) => {
|
|
|
41688 |
if (!requestSet || !requestSet.size) {
|
|
|
41689 |
return;
|
|
|
41690 |
}
|
|
|
41691 |
let newOptions = options;
|
|
|
41692 |
requestSet.forEach(requestCallback => {
|
|
|
41693 |
newOptions = requestCallback(newOptions);
|
|
|
41694 |
});
|
|
|
41695 |
return newOptions;
|
|
|
41696 |
};
|
|
|
41697 |
/**
|
|
|
41698 |
* Iterates over the response hooks Set and calls them in order.
|
|
|
41699 |
*
|
|
|
41700 |
* @param {Set} hooks the hook Set to iterate over
|
|
|
41701 |
* @param {Object} request the xhr request object
|
|
|
41702 |
* @param {Object} error the xhr error object
|
|
|
41703 |
* @param {Object} response the xhr response object
|
|
|
41704 |
*/
|
|
|
41705 |
|
|
|
41706 |
const callAllResponseHooks = (responseSet, request, error, response) => {
|
|
|
41707 |
if (!responseSet || !responseSet.size) {
|
|
|
41708 |
return;
|
|
|
41709 |
}
|
|
|
41710 |
responseSet.forEach(responseCallback => {
|
|
|
41711 |
responseCallback(request, error, response);
|
|
|
41712 |
});
|
|
|
41713 |
};
|
|
|
41714 |
const xhrFactory = function () {
|
|
|
41715 |
const xhr = function XhrFunction(options, callback) {
|
|
|
41716 |
// Add a default timeout
|
|
|
41717 |
options = merge({
|
|
|
41718 |
timeout: 45e3
|
|
|
41719 |
}, options); // Allow an optional user-specified function to modify the option
|
|
|
41720 |
// object before we construct the xhr request
|
|
|
41721 |
// TODO: Remove beforeRequest in the next major release.
|
|
|
41722 |
|
|
|
41723 |
const beforeRequest = XhrFunction.beforeRequest || videojs.Vhs.xhr.beforeRequest; // onRequest and onResponse hooks as a Set, at either the player or global level.
|
|
|
41724 |
// TODO: new Set added here for beforeRequest alias. Remove this when beforeRequest is removed.
|
|
|
41725 |
|
|
|
41726 |
const _requestCallbackSet = XhrFunction._requestCallbackSet || videojs.Vhs.xhr._requestCallbackSet || new Set();
|
|
|
41727 |
const _responseCallbackSet = XhrFunction._responseCallbackSet || videojs.Vhs.xhr._responseCallbackSet;
|
|
|
41728 |
if (beforeRequest && typeof beforeRequest === 'function') {
|
|
|
41729 |
videojs.log.warn('beforeRequest is deprecated, use onRequest instead.');
|
|
|
41730 |
_requestCallbackSet.add(beforeRequest);
|
|
|
41731 |
} // Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden
|
|
|
41732 |
// TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11
|
|
|
41733 |
|
|
|
41734 |
const xhrMethod = videojs.Vhs.xhr.original === true ? videojsXHR : videojs.Vhs.xhr; // call all registered onRequest hooks, assign new options.
|
|
|
41735 |
|
|
|
41736 |
const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options); // Remove the beforeRequest function from the hooks set so stale beforeRequest functions are not called.
|
|
|
41737 |
|
|
|
41738 |
_requestCallbackSet.delete(beforeRequest); // xhrMethod will call XMLHttpRequest.open and XMLHttpRequest.send
|
|
|
41739 |
|
|
|
41740 |
const request = xhrMethod(beforeRequestOptions || options, function (error, response) {
|
|
|
41741 |
// call all registered onResponse hooks
|
|
|
41742 |
callAllResponseHooks(_responseCallbackSet, request, error, response);
|
|
|
41743 |
return callbackWrapper(request, error, response, callback);
|
|
|
41744 |
});
|
|
|
41745 |
const originalAbort = request.abort;
|
|
|
41746 |
request.abort = function () {
|
|
|
41747 |
request.aborted = true;
|
|
|
41748 |
return originalAbort.apply(request, arguments);
|
|
|
41749 |
};
|
|
|
41750 |
request.uri = options.uri;
|
|
|
41751 |
request.requestTime = Date.now();
|
|
|
41752 |
return request;
|
|
|
41753 |
};
|
|
|
41754 |
xhr.original = true;
|
|
|
41755 |
return xhr;
|
|
|
41756 |
};
|
|
|
41757 |
/**
|
|
|
41758 |
* Turns segment byterange into a string suitable for use in
|
|
|
41759 |
* HTTP Range requests
|
|
|
41760 |
*
|
|
|
41761 |
* @param {Object} byterange - an object with two values defining the start and end
|
|
|
41762 |
* of a byte-range
|
|
|
41763 |
*/
|
|
|
41764 |
|
|
|
41765 |
const byterangeStr = function (byterange) {
|
|
|
41766 |
// `byterangeEnd` is one less than `offset + length` because the HTTP range
|
|
|
41767 |
// header uses inclusive ranges
|
|
|
41768 |
let byterangeEnd;
|
|
|
41769 |
const byterangeStart = byterange.offset;
|
|
|
41770 |
if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
|
|
|
41771 |
byterangeEnd = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
|
|
|
41772 |
} else {
|
|
|
41773 |
byterangeEnd = byterange.offset + byterange.length - 1;
|
|
|
41774 |
}
|
|
|
41775 |
return 'bytes=' + byterangeStart + '-' + byterangeEnd;
|
|
|
41776 |
};
|
|
|
41777 |
/**
|
|
|
41778 |
* Defines headers for use in the xhr request for a particular segment.
|
|
|
41779 |
*
|
|
|
41780 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
41781 |
* from SegmentLoader
|
|
|
41782 |
*/
|
|
|
41783 |
|
|
|
41784 |
const segmentXhrHeaders = function (segment) {
|
|
|
41785 |
const headers = {};
|
|
|
41786 |
if (segment.byterange) {
|
|
|
41787 |
headers.Range = byterangeStr(segment.byterange);
|
|
|
41788 |
}
|
|
|
41789 |
return headers;
|
|
|
41790 |
};
|
|
|
41791 |
|
|
|
41792 |
/**
|
|
|
41793 |
* @file bin-utils.js
|
|
|
41794 |
*/
|
|
|
41795 |
|
|
|
41796 |
/**
|
|
|
41797 |
* convert a TimeRange to text
|
|
|
41798 |
*
|
|
|
41799 |
* @param {TimeRange} range the timerange to use for conversion
|
|
|
41800 |
* @param {number} i the iterator on the range to convert
|
|
|
41801 |
* @return {string} the range in string format
|
|
|
41802 |
*/
|
|
|
41803 |
|
|
|
41804 |
const textRange = function (range, i) {
|
|
|
41805 |
return range.start(i) + '-' + range.end(i);
|
|
|
41806 |
};
|
|
|
41807 |
/**
|
|
|
41808 |
* format a number as hex string
|
|
|
41809 |
*
|
|
|
41810 |
* @param {number} e The number
|
|
|
41811 |
* @param {number} i the iterator
|
|
|
41812 |
* @return {string} the hex formatted number as a string
|
|
|
41813 |
*/
|
|
|
41814 |
|
|
|
41815 |
const formatHexString = function (e, i) {
|
|
|
41816 |
const value = e.toString(16);
|
|
|
41817 |
return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
|
|
|
41818 |
};
|
|
|
41819 |
const formatAsciiString = function (e) {
|
|
|
41820 |
if (e >= 0x20 && e < 0x7e) {
|
|
|
41821 |
return String.fromCharCode(e);
|
|
|
41822 |
}
|
|
|
41823 |
return '.';
|
|
|
41824 |
};
|
|
|
41825 |
/**
|
|
|
41826 |
* Creates an object for sending to a web worker modifying properties that are TypedArrays
|
|
|
41827 |
* into a new object with seperated properties for the buffer, byteOffset, and byteLength.
|
|
|
41828 |
*
|
|
|
41829 |
* @param {Object} message
|
|
|
41830 |
* Object of properties and values to send to the web worker
|
|
|
41831 |
* @return {Object}
|
|
|
41832 |
* Modified message with TypedArray values expanded
|
|
|
41833 |
* @function createTransferableMessage
|
|
|
41834 |
*/
|
|
|
41835 |
|
|
|
41836 |
const createTransferableMessage = function (message) {
|
|
|
41837 |
const transferable = {};
|
|
|
41838 |
Object.keys(message).forEach(key => {
|
|
|
41839 |
const value = message[key];
|
|
|
41840 |
if (isArrayBufferView(value)) {
|
|
|
41841 |
transferable[key] = {
|
|
|
41842 |
bytes: value.buffer,
|
|
|
41843 |
byteOffset: value.byteOffset,
|
|
|
41844 |
byteLength: value.byteLength
|
|
|
41845 |
};
|
|
|
41846 |
} else {
|
|
|
41847 |
transferable[key] = value;
|
|
|
41848 |
}
|
|
|
41849 |
});
|
|
|
41850 |
return transferable;
|
|
|
41851 |
};
|
|
|
41852 |
/**
|
|
|
41853 |
* Returns a unique string identifier for a media initialization
|
|
|
41854 |
* segment.
|
|
|
41855 |
*
|
|
|
41856 |
* @param {Object} initSegment
|
|
|
41857 |
* the init segment object.
|
|
|
41858 |
*
|
|
|
41859 |
* @return {string} the generated init segment id
|
|
|
41860 |
*/
|
|
|
41861 |
|
|
|
41862 |
const initSegmentId = function (initSegment) {
|
|
|
41863 |
const byterange = initSegment.byterange || {
|
|
|
41864 |
length: Infinity,
|
|
|
41865 |
offset: 0
|
|
|
41866 |
};
|
|
|
41867 |
return [byterange.length, byterange.offset, initSegment.resolvedUri].join(',');
|
|
|
41868 |
};
|
|
|
41869 |
/**
|
|
|
41870 |
* Returns a unique string identifier for a media segment key.
|
|
|
41871 |
*
|
|
|
41872 |
* @param {Object} key the encryption key
|
|
|
41873 |
* @return {string} the unique id for the media segment key.
|
|
|
41874 |
*/
|
|
|
41875 |
|
|
|
41876 |
const segmentKeyId = function (key) {
|
|
|
41877 |
return key.resolvedUri;
|
|
|
41878 |
};
|
|
|
41879 |
/**
|
|
|
41880 |
* utils to help dump binary data to the console
|
|
|
41881 |
*
|
|
|
41882 |
* @param {Array|TypedArray} data
|
|
|
41883 |
* data to dump to a string
|
|
|
41884 |
*
|
|
|
41885 |
* @return {string} the data as a hex string.
|
|
|
41886 |
*/
|
|
|
41887 |
|
|
|
41888 |
const hexDump = data => {
|
|
|
41889 |
const bytes = Array.prototype.slice.call(data);
|
|
|
41890 |
const step = 16;
|
|
|
41891 |
let result = '';
|
|
|
41892 |
let hex;
|
|
|
41893 |
let ascii;
|
|
|
41894 |
for (let j = 0; j < bytes.length / step; j++) {
|
|
|
41895 |
hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
|
|
|
41896 |
ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
|
|
|
41897 |
result += hex + ' ' + ascii + '\n';
|
|
|
41898 |
}
|
|
|
41899 |
return result;
|
|
|
41900 |
};
|
|
|
41901 |
const tagDump = ({
|
|
|
41902 |
bytes
|
|
|
41903 |
}) => hexDump(bytes);
|
|
|
41904 |
const textRanges = ranges => {
|
|
|
41905 |
let result = '';
|
|
|
41906 |
let i;
|
|
|
41907 |
for (i = 0; i < ranges.length; i++) {
|
|
|
41908 |
result += textRange(ranges, i) + ' ';
|
|
|
41909 |
}
|
|
|
41910 |
return result;
|
|
|
41911 |
};
|
|
|
41912 |
var utils = /*#__PURE__*/Object.freeze({
|
|
|
41913 |
__proto__: null,
|
|
|
41914 |
createTransferableMessage: createTransferableMessage,
|
|
|
41915 |
initSegmentId: initSegmentId,
|
|
|
41916 |
segmentKeyId: segmentKeyId,
|
|
|
41917 |
hexDump: hexDump,
|
|
|
41918 |
tagDump: tagDump,
|
|
|
41919 |
textRanges: textRanges
|
|
|
41920 |
});
|
|
|
41921 |
|
|
|
41922 |
// TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux
|
|
|
41923 |
// 25% was arbitrarily chosen, and may need to be refined over time.
|
|
|
41924 |
|
|
|
41925 |
const SEGMENT_END_FUDGE_PERCENT = 0.25;
|
|
|
41926 |
/**
|
|
|
41927 |
* Converts a player time (any time that can be gotten/set from player.currentTime(),
|
|
|
41928 |
* e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a
|
|
|
41929 |
* program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)).
|
|
|
41930 |
*
|
|
|
41931 |
* The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor
|
|
|
41932 |
* point" (a point where we have a mapping from program time to player time, with player
|
|
|
41933 |
* time being the post transmux start of the segment).
|
|
|
41934 |
*
|
|
|
41935 |
* For more details, see [this doc](../../docs/program-time-from-player-time.md).
|
|
|
41936 |
*
|
|
|
41937 |
* @param {number} playerTime the player time
|
|
|
41938 |
* @param {Object} segment the segment which contains the player time
|
|
|
41939 |
* @return {Date} program time
|
|
|
41940 |
*/
|
|
|
41941 |
|
|
|
41942 |
const playerTimeToProgramTime = (playerTime, segment) => {
|
|
|
41943 |
if (!segment.dateTimeObject) {
|
|
|
41944 |
// Can't convert without an "anchor point" for the program time (i.e., a time that can
|
|
|
41945 |
// be used to map the start of a segment with a real world time).
|
|
|
41946 |
return null;
|
|
|
41947 |
}
|
|
|
41948 |
const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds;
|
|
|
41949 |
const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended
|
|
|
41950 |
|
|
|
41951 |
const startOfSegment = transmuxedStart + transmuxerPrependedSeconds;
|
|
|
41952 |
const offsetFromSegmentStart = playerTime - startOfSegment;
|
|
|
41953 |
return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000);
|
|
|
41954 |
};
|
|
|
41955 |
const originalSegmentVideoDuration = videoTimingInfo => {
|
|
|
41956 |
return videoTimingInfo.transmuxedPresentationEnd - videoTimingInfo.transmuxedPresentationStart - videoTimingInfo.transmuxerPrependedSeconds;
|
|
|
41957 |
};
|
|
|
41958 |
/**
|
|
|
41959 |
* Finds a segment that contains the time requested given as an ISO-8601 string. The
|
|
|
41960 |
* returned segment might be an estimate or an accurate match.
|
|
|
41961 |
*
|
|
|
41962 |
* @param {string} programTime The ISO-8601 programTime to find a match for
|
|
|
41963 |
* @param {Object} playlist A playlist object to search within
|
|
|
41964 |
*/
|
|
|
41965 |
|
|
|
41966 |
const findSegmentForProgramTime = (programTime, playlist) => {
|
|
|
41967 |
// Assumptions:
|
|
|
41968 |
// - verifyProgramDateTimeTags has already been run
|
|
|
41969 |
// - live streams have been started
|
|
|
41970 |
let dateTimeObject;
|
|
|
41971 |
try {
|
|
|
41972 |
dateTimeObject = new Date(programTime);
|
|
|
41973 |
} catch (e) {
|
|
|
41974 |
return null;
|
|
|
41975 |
}
|
|
|
41976 |
if (!playlist || !playlist.segments || playlist.segments.length === 0) {
|
|
|
41977 |
return null;
|
|
|
41978 |
}
|
|
|
41979 |
let segment = playlist.segments[0];
|
|
|
41980 |
if (dateTimeObject < new Date(segment.dateTimeObject)) {
|
|
|
41981 |
// Requested time is before stream start.
|
|
|
41982 |
return null;
|
|
|
41983 |
}
|
|
|
41984 |
for (let i = 0; i < playlist.segments.length - 1; i++) {
|
|
|
41985 |
segment = playlist.segments[i];
|
|
|
41986 |
const nextSegmentStart = new Date(playlist.segments[i + 1].dateTimeObject);
|
|
|
41987 |
if (dateTimeObject < nextSegmentStart) {
|
|
|
41988 |
break;
|
|
|
41989 |
}
|
|
|
41990 |
}
|
|
|
41991 |
const lastSegment = playlist.segments[playlist.segments.length - 1];
|
|
|
41992 |
const lastSegmentStart = lastSegment.dateTimeObject;
|
|
|
41993 |
const lastSegmentDuration = lastSegment.videoTimingInfo ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT;
|
|
|
41994 |
const lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000);
|
|
|
41995 |
if (dateTimeObject > lastSegmentEnd) {
|
|
|
41996 |
// Beyond the end of the stream, or our best guess of the end of the stream.
|
|
|
41997 |
return null;
|
|
|
41998 |
}
|
|
|
41999 |
if (dateTimeObject > new Date(lastSegmentStart)) {
|
|
|
42000 |
segment = lastSegment;
|
|
|
42001 |
}
|
|
|
42002 |
return {
|
|
|
42003 |
segment,
|
|
|
42004 |
estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)),
|
|
|
42005 |
// Although, given that all segments have accurate date time objects, the segment
|
|
|
42006 |
// selected should be accurate, unless the video has been transmuxed at some point
|
|
|
42007 |
// (determined by the presence of the videoTimingInfo object), the segment's "player
|
|
|
42008 |
// time" (the start time in the player) can't be considered accurate.
|
|
|
42009 |
type: segment.videoTimingInfo ? 'accurate' : 'estimate'
|
|
|
42010 |
};
|
|
|
42011 |
};
|
|
|
42012 |
/**
|
|
|
42013 |
* Finds a segment that contains the given player time(in seconds).
|
|
|
42014 |
*
|
|
|
42015 |
* @param {number} time The player time to find a match for
|
|
|
42016 |
* @param {Object} playlist A playlist object to search within
|
|
|
42017 |
*/
|
|
|
42018 |
|
|
|
42019 |
const findSegmentForPlayerTime = (time, playlist) => {
|
|
|
42020 |
// Assumptions:
|
|
|
42021 |
// - there will always be a segment.duration
|
|
|
42022 |
// - we can start from zero
|
|
|
42023 |
// - segments are in time order
|
|
|
42024 |
if (!playlist || !playlist.segments || playlist.segments.length === 0) {
|
|
|
42025 |
return null;
|
|
|
42026 |
}
|
|
|
42027 |
let segmentEnd = 0;
|
|
|
42028 |
let segment;
|
|
|
42029 |
for (let i = 0; i < playlist.segments.length; i++) {
|
|
|
42030 |
segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and
|
|
|
42031 |
// should contain the most accurate values we have for the segment's player times.
|
|
|
42032 |
//
|
|
|
42033 |
// Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall
|
|
|
42034 |
// back to an estimate based on the manifest derived (inaccurate) segment.duration, to
|
|
|
42035 |
// calculate an end value.
|
|
|
42036 |
|
|
|
42037 |
segmentEnd = segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration;
|
|
|
42038 |
if (time <= segmentEnd) {
|
|
|
42039 |
break;
|
|
|
42040 |
}
|
|
|
42041 |
}
|
|
|
42042 |
const lastSegment = playlist.segments[playlist.segments.length - 1];
|
|
|
42043 |
if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) {
|
|
|
42044 |
// The time requested is beyond the stream end.
|
|
|
42045 |
return null;
|
|
|
42046 |
}
|
|
|
42047 |
if (time > segmentEnd) {
|
|
|
42048 |
// The time is within or beyond the last segment.
|
|
|
42049 |
//
|
|
|
42050 |
// Check to see if the time is beyond a reasonable guess of the end of the stream.
|
|
|
42051 |
if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) {
|
|
|
42052 |
// Technically, because the duration value is only an estimate, the time may still
|
|
|
42053 |
// exist in the last segment, however, there isn't enough information to make even
|
|
|
42054 |
// a reasonable estimate.
|
|
|
42055 |
return null;
|
|
|
42056 |
}
|
|
|
42057 |
segment = lastSegment;
|
|
|
42058 |
}
|
|
|
42059 |
return {
|
|
|
42060 |
segment,
|
|
|
42061 |
estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration,
|
|
|
42062 |
// Because videoTimingInfo is only set after transmux, it is the only way to get
|
|
|
42063 |
// accurate timing values.
|
|
|
42064 |
type: segment.videoTimingInfo ? 'accurate' : 'estimate'
|
|
|
42065 |
};
|
|
|
42066 |
};
|
|
|
42067 |
/**
|
|
|
42068 |
* Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds.
|
|
|
42069 |
* If the offset returned is positive, the programTime occurs after the
|
|
|
42070 |
* comparisonTimestamp.
|
|
|
42071 |
* If the offset is negative, the programTime occurs before the comparisonTimestamp.
|
|
|
42072 |
*
|
|
|
42073 |
* @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against
|
|
|
42074 |
* @param {string} programTime The programTime as an ISO-8601 string
|
|
|
42075 |
* @return {number} offset
|
|
|
42076 |
*/
|
|
|
42077 |
|
|
|
42078 |
const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => {
|
|
|
42079 |
let segmentDateTime;
|
|
|
42080 |
let programDateTime;
|
|
|
42081 |
try {
|
|
|
42082 |
segmentDateTime = new Date(comparisonTimeStamp);
|
|
|
42083 |
programDateTime = new Date(programTime);
|
|
|
42084 |
} catch (e) {// TODO handle error
|
|
|
42085 |
}
|
|
|
42086 |
const segmentTimeEpoch = segmentDateTime.getTime();
|
|
|
42087 |
const programTimeEpoch = programDateTime.getTime();
|
|
|
42088 |
return (programTimeEpoch - segmentTimeEpoch) / 1000;
|
|
|
42089 |
};
|
|
|
42090 |
/**
|
|
|
42091 |
* Checks that all segments in this playlist have programDateTime tags.
|
|
|
42092 |
*
|
|
|
42093 |
* @param {Object} playlist A playlist object
|
|
|
42094 |
*/
|
|
|
42095 |
|
|
|
42096 |
const verifyProgramDateTimeTags = playlist => {
|
|
|
42097 |
if (!playlist.segments || playlist.segments.length === 0) {
|
|
|
42098 |
return false;
|
|
|
42099 |
}
|
|
|
42100 |
for (let i = 0; i < playlist.segments.length; i++) {
|
|
|
42101 |
const segment = playlist.segments[i];
|
|
|
42102 |
if (!segment.dateTimeObject) {
|
|
|
42103 |
return false;
|
|
|
42104 |
}
|
|
|
42105 |
}
|
|
|
42106 |
return true;
|
|
|
42107 |
};
|
|
|
42108 |
/**
|
|
|
42109 |
* Returns the programTime of the media given a playlist and a playerTime.
|
|
|
42110 |
* The playlist must have programDateTime tags for a programDateTime tag to be returned.
|
|
|
42111 |
* If the segments containing the time requested have not been buffered yet, an estimate
|
|
|
42112 |
* may be returned to the callback.
|
|
|
42113 |
*
|
|
|
42114 |
* @param {Object} args
|
|
|
42115 |
* @param {Object} args.playlist A playlist object to search within
|
|
|
42116 |
* @param {number} time A playerTime in seconds
|
|
|
42117 |
* @param {Function} callback(err, programTime)
|
|
|
42118 |
* @return {string} err.message A detailed error message
|
|
|
42119 |
* @return {Object} programTime
|
|
|
42120 |
* @return {number} programTime.mediaSeconds The streamTime in seconds
|
|
|
42121 |
* @return {string} programTime.programDateTime The programTime as an ISO-8601 String
|
|
|
42122 |
*/
|
|
|
42123 |
|
|
|
42124 |
const getProgramTime = ({
|
|
|
42125 |
playlist,
|
|
|
42126 |
time = undefined,
|
|
|
42127 |
callback
|
|
|
42128 |
}) => {
|
|
|
42129 |
if (!callback) {
|
|
|
42130 |
throw new Error('getProgramTime: callback must be provided');
|
|
|
42131 |
}
|
|
|
42132 |
if (!playlist || time === undefined) {
|
|
|
42133 |
return callback({
|
|
|
42134 |
message: 'getProgramTime: playlist and time must be provided'
|
|
|
42135 |
});
|
|
|
42136 |
}
|
|
|
42137 |
const matchedSegment = findSegmentForPlayerTime(time, playlist);
|
|
|
42138 |
if (!matchedSegment) {
|
|
|
42139 |
return callback({
|
|
|
42140 |
message: 'valid programTime was not found'
|
|
|
42141 |
});
|
|
|
42142 |
}
|
|
|
42143 |
if (matchedSegment.type === 'estimate') {
|
|
|
42144 |
return callback({
|
|
|
42145 |
message: 'Accurate programTime could not be determined.' + ' Please seek to e.seekTime and try again',
|
|
|
42146 |
seekTime: matchedSegment.estimatedStart
|
|
|
42147 |
});
|
|
|
42148 |
}
|
|
|
42149 |
const programTimeObject = {
|
|
|
42150 |
mediaSeconds: time
|
|
|
42151 |
};
|
|
|
42152 |
const programTime = playerTimeToProgramTime(time, matchedSegment.segment);
|
|
|
42153 |
if (programTime) {
|
|
|
42154 |
programTimeObject.programDateTime = programTime.toISOString();
|
|
|
42155 |
}
|
|
|
42156 |
return callback(null, programTimeObject);
|
|
|
42157 |
};
|
|
|
42158 |
/**
|
|
|
42159 |
* Seeks in the player to a time that matches the given programTime ISO-8601 string.
|
|
|
42160 |
*
|
|
|
42161 |
* @param {Object} args
|
|
|
42162 |
* @param {string} args.programTime A programTime to seek to as an ISO-8601 String
|
|
|
42163 |
* @param {Object} args.playlist A playlist to look within
|
|
|
42164 |
* @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2.
|
|
|
42165 |
* @param {Function} args.seekTo A method to perform a seek
|
|
|
42166 |
* @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true.
|
|
|
42167 |
* @param {Object} args.tech The tech to seek on
|
|
|
42168 |
* @param {Function} args.callback(err, newTime) A callback to return the new time to
|
|
|
42169 |
* @return {string} err.message A detailed error message
|
|
|
42170 |
* @return {number} newTime The exact time that was seeked to in seconds
|
|
|
42171 |
*/
|
|
|
42172 |
|
|
|
42173 |
const seekToProgramTime = ({
|
|
|
42174 |
programTime,
|
|
|
42175 |
playlist,
|
|
|
42176 |
retryCount = 2,
|
|
|
42177 |
seekTo,
|
|
|
42178 |
pauseAfterSeek = true,
|
|
|
42179 |
tech,
|
|
|
42180 |
callback
|
|
|
42181 |
}) => {
|
|
|
42182 |
if (!callback) {
|
|
|
42183 |
throw new Error('seekToProgramTime: callback must be provided');
|
|
|
42184 |
}
|
|
|
42185 |
if (typeof programTime === 'undefined' || !playlist || !seekTo) {
|
|
|
42186 |
return callback({
|
|
|
42187 |
message: 'seekToProgramTime: programTime, seekTo and playlist must be provided'
|
|
|
42188 |
});
|
|
|
42189 |
}
|
|
|
42190 |
if (!playlist.endList && !tech.hasStarted_) {
|
|
|
42191 |
return callback({
|
|
|
42192 |
message: 'player must be playing a live stream to start buffering'
|
|
|
42193 |
});
|
|
|
42194 |
}
|
|
|
42195 |
if (!verifyProgramDateTimeTags(playlist)) {
|
|
|
42196 |
return callback({
|
|
|
42197 |
message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri
|
|
|
42198 |
});
|
|
|
42199 |
}
|
|
|
42200 |
const matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match
|
|
|
42201 |
|
|
|
42202 |
if (!matchedSegment) {
|
|
|
42203 |
return callback({
|
|
|
42204 |
message: `${programTime} was not found in the stream`
|
|
|
42205 |
});
|
|
|
42206 |
}
|
|
|
42207 |
const segment = matchedSegment.segment;
|
|
|
42208 |
const mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime);
|
|
|
42209 |
if (matchedSegment.type === 'estimate') {
|
|
|
42210 |
// we've run out of retries
|
|
|
42211 |
if (retryCount === 0) {
|
|
|
42212 |
return callback({
|
|
|
42213 |
message: `${programTime} is not buffered yet. Try again`
|
|
|
42214 |
});
|
|
|
42215 |
}
|
|
|
42216 |
seekTo(matchedSegment.estimatedStart + mediaOffset);
|
|
|
42217 |
tech.one('seeked', () => {
|
|
|
42218 |
seekToProgramTime({
|
|
|
42219 |
programTime,
|
|
|
42220 |
playlist,
|
|
|
42221 |
retryCount: retryCount - 1,
|
|
|
42222 |
seekTo,
|
|
|
42223 |
pauseAfterSeek,
|
|
|
42224 |
tech,
|
|
|
42225 |
callback
|
|
|
42226 |
});
|
|
|
42227 |
});
|
|
|
42228 |
return;
|
|
|
42229 |
} // Since the segment.start value is determined from the buffered end or ending time
|
|
|
42230 |
// of the prior segment, the seekToTime doesn't need to account for any transmuxer
|
|
|
42231 |
// modifications.
|
|
|
42232 |
|
|
|
42233 |
const seekToTime = segment.start + mediaOffset;
|
|
|
42234 |
const seekedCallback = () => {
|
|
|
42235 |
return callback(null, tech.currentTime());
|
|
|
42236 |
}; // listen for seeked event
|
|
|
42237 |
|
|
|
42238 |
tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state
|
|
|
42239 |
|
|
|
42240 |
if (pauseAfterSeek) {
|
|
|
42241 |
tech.pause();
|
|
|
42242 |
}
|
|
|
42243 |
seekTo(seekToTime);
|
|
|
42244 |
};
|
|
|
42245 |
|
|
|
42246 |
// which will only happen if the request is complete.
|
|
|
42247 |
|
|
|
42248 |
const callbackOnCompleted = (request, cb) => {
|
|
|
42249 |
if (request.readyState === 4) {
|
|
|
42250 |
return cb();
|
|
|
42251 |
}
|
|
|
42252 |
return;
|
|
|
42253 |
};
|
|
|
42254 |
const containerRequest = (uri, xhr, cb) => {
|
|
|
42255 |
let bytes = [];
|
|
|
42256 |
let id3Offset;
|
|
|
42257 |
let finished = false;
|
|
|
42258 |
const endRequestAndCallback = function (err, req, type, _bytes) {
|
|
|
42259 |
req.abort();
|
|
|
42260 |
finished = true;
|
|
|
42261 |
return cb(err, req, type, _bytes);
|
|
|
42262 |
};
|
|
|
42263 |
const progressListener = function (error, request) {
|
|
|
42264 |
if (finished) {
|
|
|
42265 |
return;
|
|
|
42266 |
}
|
|
|
42267 |
if (error) {
|
|
|
42268 |
return endRequestAndCallback(error, request, '', bytes);
|
|
|
42269 |
} // grap the new part of content that was just downloaded
|
|
|
42270 |
|
|
|
42271 |
const newPart = request.responseText.substring(bytes && bytes.byteLength || 0, request.responseText.length); // add that onto bytes
|
|
|
42272 |
|
|
|
42273 |
bytes = concatTypedArrays(bytes, stringToBytes(newPart, true));
|
|
|
42274 |
id3Offset = id3Offset || getId3Offset(bytes); // we need at least 10 bytes to determine a type
|
|
|
42275 |
// or we need at least two bytes after an id3Offset
|
|
|
42276 |
|
|
|
42277 |
if (bytes.length < 10 || id3Offset && bytes.length < id3Offset + 2) {
|
|
|
42278 |
return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes));
|
|
|
42279 |
}
|
|
|
42280 |
const type = detectContainerForBytes(bytes); // if this looks like a ts segment but we don't have enough data
|
|
|
42281 |
// to see the second sync byte, wait until we have enough data
|
|
|
42282 |
// before declaring it ts
|
|
|
42283 |
|
|
|
42284 |
if (type === 'ts' && bytes.length < 188) {
|
|
|
42285 |
return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes));
|
|
|
42286 |
} // this may be an unsynced ts segment
|
|
|
42287 |
// wait for 376 bytes before detecting no container
|
|
|
42288 |
|
|
|
42289 |
if (!type && bytes.length < 376) {
|
|
|
42290 |
return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes));
|
|
|
42291 |
}
|
|
|
42292 |
return endRequestAndCallback(null, request, type, bytes);
|
|
|
42293 |
};
|
|
|
42294 |
const options = {
|
|
|
42295 |
uri,
|
|
|
42296 |
beforeSend(request) {
|
|
|
42297 |
// this forces the browser to pass the bytes to us unprocessed
|
|
|
42298 |
request.overrideMimeType('text/plain; charset=x-user-defined');
|
|
|
42299 |
request.addEventListener('progress', function ({
|
|
|
42300 |
total,
|
|
|
42301 |
loaded
|
|
|
42302 |
}) {
|
|
|
42303 |
return callbackWrapper(request, null, {
|
|
|
42304 |
statusCode: request.status
|
|
|
42305 |
}, progressListener);
|
|
|
42306 |
});
|
|
|
42307 |
}
|
|
|
42308 |
};
|
|
|
42309 |
const request = xhr(options, function (error, response) {
|
|
|
42310 |
return callbackWrapper(request, error, response, progressListener);
|
|
|
42311 |
});
|
|
|
42312 |
return request;
|
|
|
42313 |
};
|
|
|
42314 |
const {
|
|
|
42315 |
EventTarget
|
|
|
42316 |
} = videojs;
|
|
|
42317 |
const dashPlaylistUnchanged = function (a, b) {
|
|
|
42318 |
if (!isPlaylistUnchanged(a, b)) {
|
|
|
42319 |
return false;
|
|
|
42320 |
} // for dash the above check will often return true in scenarios where
|
|
|
42321 |
// the playlist actually has changed because mediaSequence isn't a
|
|
|
42322 |
// dash thing, and we often set it to 1. So if the playlists have the same amount
|
|
|
42323 |
// of segments we return true.
|
|
|
42324 |
// So for dash we need to make sure that the underlying segments are different.
|
|
|
42325 |
// if sidx changed then the playlists are different.
|
|
|
42326 |
|
|
|
42327 |
if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) {
|
|
|
42328 |
return false;
|
|
|
42329 |
} else if (!a.sidx && b.sidx || a.sidx && !b.sidx) {
|
|
|
42330 |
return false;
|
|
|
42331 |
} // one or the other does not have segments
|
|
|
42332 |
// there was a change.
|
|
|
42333 |
|
|
|
42334 |
if (a.segments && !b.segments || !a.segments && b.segments) {
|
|
|
42335 |
return false;
|
|
|
42336 |
} // neither has segments nothing changed
|
|
|
42337 |
|
|
|
42338 |
if (!a.segments && !b.segments) {
|
|
|
42339 |
return true;
|
|
|
42340 |
} // check segments themselves
|
|
|
42341 |
|
|
|
42342 |
for (let i = 0; i < a.segments.length; i++) {
|
|
|
42343 |
const aSegment = a.segments[i];
|
|
|
42344 |
const bSegment = b.segments[i]; // if uris are different between segments there was a change
|
|
|
42345 |
|
|
|
42346 |
if (aSegment.uri !== bSegment.uri) {
|
|
|
42347 |
return false;
|
|
|
42348 |
} // neither segment has a byterange, there will be no byterange change.
|
|
|
42349 |
|
|
|
42350 |
if (!aSegment.byterange && !bSegment.byterange) {
|
|
|
42351 |
continue;
|
|
|
42352 |
}
|
|
|
42353 |
const aByterange = aSegment.byterange;
|
|
|
42354 |
const bByterange = bSegment.byterange; // if byterange only exists on one of the segments, there was a change.
|
|
|
42355 |
|
|
|
42356 |
if (aByterange && !bByterange || !aByterange && bByterange) {
|
|
|
42357 |
return false;
|
|
|
42358 |
} // if both segments have byterange with different offsets, there was a change.
|
|
|
42359 |
|
|
|
42360 |
if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) {
|
|
|
42361 |
return false;
|
|
|
42362 |
}
|
|
|
42363 |
} // if everything was the same with segments, this is the same playlist.
|
|
|
42364 |
|
|
|
42365 |
return true;
|
|
|
42366 |
};
|
|
|
42367 |
/**
|
|
|
42368 |
* Use the representation IDs from the mpd object to create groupIDs, the NAME is set to mandatory representation
|
|
|
42369 |
* ID in the parser. This allows for continuous playout across periods with the same representation IDs
|
|
|
42370 |
* (continuous periods as defined in DASH-IF 3.2.12). This is assumed in the mpd-parser as well. If we want to support
|
|
|
42371 |
* periods without continuous playback this function may need modification as well as the parser.
|
|
|
42372 |
*/
|
|
|
42373 |
|
|
|
42374 |
const dashGroupId = (type, group, label, playlist) => {
|
|
|
42375 |
// If the manifest somehow does not have an ID (non-dash compliant), use the label.
|
|
|
42376 |
const playlistId = playlist.attributes.NAME || label;
|
|
|
42377 |
return `placeholder-uri-${type}-${group}-${playlistId}`;
|
|
|
42378 |
};
|
|
|
42379 |
/**
|
|
|
42380 |
* Parses the main XML string and updates playlist URI references.
|
|
|
42381 |
*
|
|
|
42382 |
* @param {Object} config
|
|
|
42383 |
* Object of arguments
|
|
|
42384 |
* @param {string} config.mainXml
|
|
|
42385 |
* The mpd XML
|
|
|
42386 |
* @param {string} config.srcUrl
|
|
|
42387 |
* The mpd URL
|
|
|
42388 |
* @param {Date} config.clientOffset
|
|
|
42389 |
* A time difference between server and client
|
|
|
42390 |
* @param {Object} config.sidxMapping
|
|
|
42391 |
* SIDX mappings for moof/mdat URIs and byte ranges
|
|
|
42392 |
* @return {Object}
|
|
|
42393 |
* The parsed mpd manifest object
|
|
|
42394 |
*/
|
|
|
42395 |
|
|
|
42396 |
const parseMainXml = ({
|
|
|
42397 |
mainXml,
|
|
|
42398 |
srcUrl,
|
|
|
42399 |
clientOffset,
|
|
|
42400 |
sidxMapping,
|
|
|
42401 |
previousManifest
|
|
|
42402 |
}) => {
|
|
|
42403 |
const manifest = parse(mainXml, {
|
|
|
42404 |
manifestUri: srcUrl,
|
|
|
42405 |
clientOffset,
|
|
|
42406 |
sidxMapping,
|
|
|
42407 |
previousManifest
|
|
|
42408 |
});
|
|
|
42409 |
addPropertiesToMain(manifest, srcUrl, dashGroupId);
|
|
|
42410 |
return manifest;
|
|
|
42411 |
};
|
|
|
42412 |
/**
|
|
|
42413 |
* Removes any mediaGroup labels that no longer exist in the newMain
|
|
|
42414 |
*
|
|
|
42415 |
* @param {Object} update
|
|
|
42416 |
* The previous mpd object being updated
|
|
|
42417 |
* @param {Object} newMain
|
|
|
42418 |
* The new mpd object
|
|
|
42419 |
*/
|
|
|
42420 |
|
|
|
42421 |
const removeOldMediaGroupLabels = (update, newMain) => {
|
|
|
42422 |
forEachMediaGroup(update, (properties, type, group, label) => {
|
|
|
42423 |
if (!(label in newMain.mediaGroups[type][group])) {
|
|
|
42424 |
delete update.mediaGroups[type][group][label];
|
|
|
42425 |
}
|
|
|
42426 |
});
|
|
|
42427 |
};
|
|
|
42428 |
/**
|
|
|
42429 |
* Returns a new main manifest that is the result of merging an updated main manifest
|
|
|
42430 |
* into the original version.
|
|
|
42431 |
*
|
|
|
42432 |
* @param {Object} oldMain
|
|
|
42433 |
* The old parsed mpd object
|
|
|
42434 |
* @param {Object} newMain
|
|
|
42435 |
* The updated parsed mpd object
|
|
|
42436 |
* @return {Object}
|
|
|
42437 |
* A new object representing the original main manifest with the updated media
|
|
|
42438 |
* playlists merged in
|
|
|
42439 |
*/
|
|
|
42440 |
|
|
|
42441 |
const updateMain = (oldMain, newMain, sidxMapping) => {
|
|
|
42442 |
let noChanges = true;
|
|
|
42443 |
let update = merge(oldMain, {
|
|
|
42444 |
// These are top level properties that can be updated
|
|
|
42445 |
duration: newMain.duration,
|
|
|
42446 |
minimumUpdatePeriod: newMain.minimumUpdatePeriod,
|
|
|
42447 |
timelineStarts: newMain.timelineStarts
|
|
|
42448 |
}); // First update the playlists in playlist list
|
|
|
42449 |
|
|
|
42450 |
for (let i = 0; i < newMain.playlists.length; i++) {
|
|
|
42451 |
const playlist = newMain.playlists[i];
|
|
|
42452 |
if (playlist.sidx) {
|
|
|
42453 |
const sidxKey = generateSidxKey(playlist.sidx); // add sidx segments to the playlist if we have all the sidx info already
|
|
|
42454 |
|
|
|
42455 |
if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) {
|
|
|
42456 |
addSidxSegmentsToPlaylist$1(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri);
|
|
|
42457 |
}
|
|
|
42458 |
}
|
|
|
42459 |
const playlistUpdate = updateMain$1(update, playlist, dashPlaylistUnchanged);
|
|
|
42460 |
if (playlistUpdate) {
|
|
|
42461 |
update = playlistUpdate;
|
|
|
42462 |
noChanges = false;
|
|
|
42463 |
}
|
|
|
42464 |
} // Then update media group playlists
|
|
|
42465 |
|
|
|
42466 |
forEachMediaGroup(newMain, (properties, type, group, label) => {
|
|
|
42467 |
if (properties.playlists && properties.playlists.length) {
|
|
|
42468 |
const id = properties.playlists[0].id;
|
|
|
42469 |
const playlistUpdate = updateMain$1(update, properties.playlists[0], dashPlaylistUnchanged);
|
|
|
42470 |
if (playlistUpdate) {
|
|
|
42471 |
update = playlistUpdate; // add new mediaGroup label if it doesn't exist and assign the new mediaGroup.
|
|
|
42472 |
|
|
|
42473 |
if (!(label in update.mediaGroups[type][group])) {
|
|
|
42474 |
update.mediaGroups[type][group][label] = properties;
|
|
|
42475 |
} // update the playlist reference within media groups
|
|
|
42476 |
|
|
|
42477 |
update.mediaGroups[type][group][label].playlists[0] = update.playlists[id];
|
|
|
42478 |
noChanges = false;
|
|
|
42479 |
}
|
|
|
42480 |
}
|
|
|
42481 |
}); // remove mediaGroup labels and references that no longer exist in the newMain
|
|
|
42482 |
|
|
|
42483 |
removeOldMediaGroupLabels(update, newMain);
|
|
|
42484 |
if (newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) {
|
|
|
42485 |
noChanges = false;
|
|
|
42486 |
}
|
|
|
42487 |
if (noChanges) {
|
|
|
42488 |
return null;
|
|
|
42489 |
}
|
|
|
42490 |
return update;
|
|
|
42491 |
}; // SIDX should be equivalent if the URI and byteranges of the SIDX match.
|
|
|
42492 |
// If the SIDXs have maps, the two maps should match,
|
|
|
42493 |
// both `a` and `b` missing SIDXs is considered matching.
|
|
|
42494 |
// If `a` or `b` but not both have a map, they aren't matching.
|
|
|
42495 |
|
|
|
42496 |
const equivalentSidx = (a, b) => {
|
|
|
42497 |
const neitherMap = Boolean(!a.map && !b.map);
|
|
|
42498 |
const equivalentMap = neitherMap || Boolean(a.map && b.map && a.map.byterange.offset === b.map.byterange.offset && a.map.byterange.length === b.map.byterange.length);
|
|
|
42499 |
return equivalentMap && a.uri === b.uri && a.byterange.offset === b.byterange.offset && a.byterange.length === b.byterange.length;
|
|
|
42500 |
}; // exported for testing
|
|
|
42501 |
|
|
|
42502 |
const compareSidxEntry = (playlists, oldSidxMapping) => {
|
|
|
42503 |
const newSidxMapping = {};
|
|
|
42504 |
for (const id in playlists) {
|
|
|
42505 |
const playlist = playlists[id];
|
|
|
42506 |
const currentSidxInfo = playlist.sidx;
|
|
|
42507 |
if (currentSidxInfo) {
|
|
|
42508 |
const key = generateSidxKey(currentSidxInfo);
|
|
|
42509 |
if (!oldSidxMapping[key]) {
|
|
|
42510 |
break;
|
|
|
42511 |
}
|
|
|
42512 |
const savedSidxInfo = oldSidxMapping[key].sidxInfo;
|
|
|
42513 |
if (equivalentSidx(savedSidxInfo, currentSidxInfo)) {
|
|
|
42514 |
newSidxMapping[key] = oldSidxMapping[key];
|
|
|
42515 |
}
|
|
|
42516 |
}
|
|
|
42517 |
}
|
|
|
42518 |
return newSidxMapping;
|
|
|
42519 |
};
|
|
|
42520 |
/**
|
|
|
42521 |
* A function that filters out changed items as they need to be requested separately.
|
|
|
42522 |
*
|
|
|
42523 |
* The method is exported for testing
|
|
|
42524 |
*
|
|
|
42525 |
* @param {Object} main the parsed mpd XML returned via mpd-parser
|
|
|
42526 |
* @param {Object} oldSidxMapping the SIDX to compare against
|
|
|
42527 |
*/
|
|
|
42528 |
|
|
|
42529 |
const filterChangedSidxMappings = (main, oldSidxMapping) => {
|
|
|
42530 |
const videoSidx = compareSidxEntry(main.playlists, oldSidxMapping);
|
|
|
42531 |
let mediaGroupSidx = videoSidx;
|
|
|
42532 |
forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => {
|
|
|
42533 |
if (properties.playlists && properties.playlists.length) {
|
|
|
42534 |
const playlists = properties.playlists;
|
|
|
42535 |
mediaGroupSidx = merge(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping));
|
|
|
42536 |
}
|
|
|
42537 |
});
|
|
|
42538 |
return mediaGroupSidx;
|
|
|
42539 |
};
|
|
|
42540 |
class DashPlaylistLoader extends EventTarget {
|
|
|
42541 |
// DashPlaylistLoader must accept either a src url or a playlist because subsequent
|
|
|
42542 |
// playlist loader setups from media groups will expect to be able to pass a playlist
|
|
|
42543 |
// (since there aren't external URLs to media playlists with DASH)
|
|
|
42544 |
constructor(srcUrlOrPlaylist, vhs, options = {}, mainPlaylistLoader) {
|
|
|
42545 |
super();
|
|
|
42546 |
this.mainPlaylistLoader_ = mainPlaylistLoader || this;
|
|
|
42547 |
if (!mainPlaylistLoader) {
|
|
|
42548 |
this.isMain_ = true;
|
|
|
42549 |
}
|
|
|
42550 |
const {
|
|
|
42551 |
withCredentials = false
|
|
|
42552 |
} = options;
|
|
|
42553 |
this.vhs_ = vhs;
|
|
|
42554 |
this.withCredentials = withCredentials;
|
|
|
42555 |
this.addMetadataToTextTrack = options.addMetadataToTextTrack;
|
|
|
42556 |
if (!srcUrlOrPlaylist) {
|
|
|
42557 |
throw new Error('A non-empty playlist URL or object is required');
|
|
|
42558 |
} // event naming?
|
|
|
42559 |
|
|
|
42560 |
this.on('minimumUpdatePeriod', () => {
|
|
|
42561 |
this.refreshXml_();
|
|
|
42562 |
}); // live playlist staleness timeout
|
|
|
42563 |
|
|
|
42564 |
this.on('mediaupdatetimeout', () => {
|
|
|
42565 |
this.refreshMedia_(this.media().id);
|
|
|
42566 |
});
|
|
|
42567 |
this.state = 'HAVE_NOTHING';
|
|
|
42568 |
this.loadedPlaylists_ = {};
|
|
|
42569 |
this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state
|
|
|
42570 |
// The mainPlaylistLoader will be created with a string
|
|
|
42571 |
|
|
|
42572 |
if (this.isMain_) {
|
|
|
42573 |
this.mainPlaylistLoader_.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes
|
|
|
42574 |
// once multi-period is refactored
|
|
|
42575 |
|
|
|
42576 |
this.mainPlaylistLoader_.sidxMapping_ = {};
|
|
|
42577 |
} else {
|
|
|
42578 |
this.childPlaylist_ = srcUrlOrPlaylist;
|
|
|
42579 |
}
|
|
|
42580 |
}
|
|
|
42581 |
requestErrored_(err, request, startingState) {
|
|
|
42582 |
// disposed
|
|
|
42583 |
if (!this.request) {
|
|
|
42584 |
return true;
|
|
|
42585 |
} // pending request is cleared
|
|
|
42586 |
|
|
|
42587 |
this.request = null;
|
|
|
42588 |
if (err) {
|
|
|
42589 |
// use the provided error object or create one
|
|
|
42590 |
// based on the request/response
|
|
|
42591 |
this.error = typeof err === 'object' && !(err instanceof Error) ? err : {
|
|
|
42592 |
status: request.status,
|
|
|
42593 |
message: 'DASH request error at URL: ' + request.uri,
|
|
|
42594 |
response: request.response,
|
|
|
42595 |
// MEDIA_ERR_NETWORK
|
|
|
42596 |
code: 2
|
|
|
42597 |
};
|
|
|
42598 |
if (startingState) {
|
|
|
42599 |
this.state = startingState;
|
|
|
42600 |
}
|
|
|
42601 |
this.trigger('error');
|
|
|
42602 |
return true;
|
|
|
42603 |
}
|
|
|
42604 |
}
|
|
|
42605 |
/**
|
|
|
42606 |
* Verify that the container of the sidx segment can be parsed
|
|
|
42607 |
* and if it can, get and parse that segment.
|
|
|
42608 |
*/
|
|
|
42609 |
|
|
|
42610 |
addSidxSegments_(playlist, startingState, cb) {
|
|
|
42611 |
const sidxKey = playlist.sidx && generateSidxKey(playlist.sidx); // playlist lacks sidx or sidx segments were added to this playlist already.
|
|
|
42612 |
|
|
|
42613 |
if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) {
|
|
|
42614 |
// keep this function async
|
|
|
42615 |
this.mediaRequest_ = window.setTimeout(() => cb(false), 0);
|
|
|
42616 |
return;
|
|
|
42617 |
} // resolve the segment URL relative to the playlist
|
|
|
42618 |
|
|
|
42619 |
const uri = resolveManifestRedirect(playlist.sidx.resolvedUri);
|
|
|
42620 |
const fin = (err, request) => {
|
|
|
42621 |
if (this.requestErrored_(err, request, startingState)) {
|
|
|
42622 |
return;
|
|
|
42623 |
}
|
|
|
42624 |
const sidxMapping = this.mainPlaylistLoader_.sidxMapping_;
|
|
|
42625 |
let sidx;
|
|
|
42626 |
try {
|
|
|
42627 |
sidx = parseSidx_1(toUint8(request.response).subarray(8));
|
|
|
42628 |
} catch (e) {
|
|
|
42629 |
// sidx parsing failed.
|
|
|
42630 |
this.requestErrored_(e, request, startingState);
|
|
|
42631 |
return;
|
|
|
42632 |
}
|
|
|
42633 |
sidxMapping[sidxKey] = {
|
|
|
42634 |
sidxInfo: playlist.sidx,
|
|
|
42635 |
sidx
|
|
|
42636 |
};
|
|
|
42637 |
addSidxSegmentsToPlaylist$1(playlist, sidx, playlist.sidx.resolvedUri);
|
|
|
42638 |
return cb(true);
|
|
|
42639 |
};
|
|
|
42640 |
this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => {
|
|
|
42641 |
if (err) {
|
|
|
42642 |
return fin(err, request);
|
|
|
42643 |
}
|
|
|
42644 |
if (!container || container !== 'mp4') {
|
|
|
42645 |
return fin({
|
|
|
42646 |
status: request.status,
|
|
|
42647 |
message: `Unsupported ${container || 'unknown'} container type for sidx segment at URL: ${uri}`,
|
|
|
42648 |
// response is just bytes in this case
|
|
|
42649 |
// but we really don't want to return that.
|
|
|
42650 |
response: '',
|
|
|
42651 |
playlist,
|
|
|
42652 |
internal: true,
|
|
|
42653 |
playlistExclusionDuration: Infinity,
|
|
|
42654 |
// MEDIA_ERR_NETWORK
|
|
|
42655 |
code: 2
|
|
|
42656 |
}, request);
|
|
|
42657 |
} // if we already downloaded the sidx bytes in the container request, use them
|
|
|
42658 |
|
|
|
42659 |
const {
|
|
|
42660 |
offset,
|
|
|
42661 |
length
|
|
|
42662 |
} = playlist.sidx.byterange;
|
|
|
42663 |
if (bytes.length >= length + offset) {
|
|
|
42664 |
return fin(err, {
|
|
|
42665 |
response: bytes.subarray(offset, offset + length),
|
|
|
42666 |
status: request.status,
|
|
|
42667 |
uri: request.uri
|
|
|
42668 |
});
|
|
|
42669 |
} // otherwise request sidx bytes
|
|
|
42670 |
|
|
|
42671 |
this.request = this.vhs_.xhr({
|
|
|
42672 |
uri,
|
|
|
42673 |
responseType: 'arraybuffer',
|
|
|
42674 |
headers: segmentXhrHeaders({
|
|
|
42675 |
byterange: playlist.sidx.byterange
|
|
|
42676 |
})
|
|
|
42677 |
}, fin);
|
|
|
42678 |
});
|
|
|
42679 |
}
|
|
|
42680 |
dispose() {
|
|
|
42681 |
this.trigger('dispose');
|
|
|
42682 |
this.stopRequest();
|
|
|
42683 |
this.loadedPlaylists_ = {};
|
|
|
42684 |
window.clearTimeout(this.minimumUpdatePeriodTimeout_);
|
|
|
42685 |
window.clearTimeout(this.mediaRequest_);
|
|
|
42686 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
42687 |
this.mediaUpdateTimeout = null;
|
|
|
42688 |
this.mediaRequest_ = null;
|
|
|
42689 |
this.minimumUpdatePeriodTimeout_ = null;
|
|
|
42690 |
if (this.mainPlaylistLoader_.createMupOnMedia_) {
|
|
|
42691 |
this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_);
|
|
|
42692 |
this.mainPlaylistLoader_.createMupOnMedia_ = null;
|
|
|
42693 |
}
|
|
|
42694 |
this.off();
|
|
|
42695 |
}
|
|
|
42696 |
hasPendingRequest() {
|
|
|
42697 |
return this.request || this.mediaRequest_;
|
|
|
42698 |
}
|
|
|
42699 |
stopRequest() {
|
|
|
42700 |
if (this.request) {
|
|
|
42701 |
const oldRequest = this.request;
|
|
|
42702 |
this.request = null;
|
|
|
42703 |
oldRequest.onreadystatechange = null;
|
|
|
42704 |
oldRequest.abort();
|
|
|
42705 |
}
|
|
|
42706 |
}
|
|
|
42707 |
media(playlist) {
|
|
|
42708 |
// getter
|
|
|
42709 |
if (!playlist) {
|
|
|
42710 |
return this.media_;
|
|
|
42711 |
} // setter
|
|
|
42712 |
|
|
|
42713 |
if (this.state === 'HAVE_NOTHING') {
|
|
|
42714 |
throw new Error('Cannot switch media playlist from ' + this.state);
|
|
|
42715 |
}
|
|
|
42716 |
const startingState = this.state; // find the playlist object if the target playlist has been specified by URI
|
|
|
42717 |
|
|
|
42718 |
if (typeof playlist === 'string') {
|
|
|
42719 |
if (!this.mainPlaylistLoader_.main.playlists[playlist]) {
|
|
|
42720 |
throw new Error('Unknown playlist URI: ' + playlist);
|
|
|
42721 |
}
|
|
|
42722 |
playlist = this.mainPlaylistLoader_.main.playlists[playlist];
|
|
|
42723 |
}
|
|
|
42724 |
const mediaChange = !this.media_ || playlist.id !== this.media_.id; // switch to previously loaded playlists immediately
|
|
|
42725 |
|
|
|
42726 |
if (mediaChange && this.loadedPlaylists_[playlist.id] && this.loadedPlaylists_[playlist.id].endList) {
|
|
|
42727 |
this.state = 'HAVE_METADATA';
|
|
|
42728 |
this.media_ = playlist; // trigger media change if the active media has been updated
|
|
|
42729 |
|
|
|
42730 |
if (mediaChange) {
|
|
|
42731 |
this.trigger('mediachanging');
|
|
|
42732 |
this.trigger('mediachange');
|
|
|
42733 |
}
|
|
|
42734 |
return;
|
|
|
42735 |
} // switching to the active playlist is a no-op
|
|
|
42736 |
|
|
|
42737 |
if (!mediaChange) {
|
|
|
42738 |
return;
|
|
|
42739 |
} // switching from an already loaded playlist
|
|
|
42740 |
|
|
|
42741 |
if (this.media_) {
|
|
|
42742 |
this.trigger('mediachanging');
|
|
|
42743 |
}
|
|
|
42744 |
this.addSidxSegments_(playlist, startingState, sidxChanged => {
|
|
|
42745 |
// everything is ready just continue to haveMetadata
|
|
|
42746 |
this.haveMetadata({
|
|
|
42747 |
startingState,
|
|
|
42748 |
playlist
|
|
|
42749 |
});
|
|
|
42750 |
});
|
|
|
42751 |
}
|
|
|
42752 |
haveMetadata({
|
|
|
42753 |
startingState,
|
|
|
42754 |
playlist
|
|
|
42755 |
}) {
|
|
|
42756 |
this.state = 'HAVE_METADATA';
|
|
|
42757 |
this.loadedPlaylists_[playlist.id] = playlist;
|
|
|
42758 |
this.mediaRequest_ = null; // This will trigger loadedplaylist
|
|
|
42759 |
|
|
|
42760 |
this.refreshMedia_(playlist.id); // fire loadedmetadata the first time a media playlist is loaded
|
|
|
42761 |
// to resolve setup of media groups
|
|
|
42762 |
|
|
|
42763 |
if (startingState === 'HAVE_MAIN_MANIFEST') {
|
|
|
42764 |
this.trigger('loadedmetadata');
|
|
|
42765 |
} else {
|
|
|
42766 |
// trigger media change if the active media has been updated
|
|
|
42767 |
this.trigger('mediachange');
|
|
|
42768 |
}
|
|
|
42769 |
}
|
|
|
42770 |
pause() {
|
|
|
42771 |
if (this.mainPlaylistLoader_.createMupOnMedia_) {
|
|
|
42772 |
this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_);
|
|
|
42773 |
this.mainPlaylistLoader_.createMupOnMedia_ = null;
|
|
|
42774 |
}
|
|
|
42775 |
this.stopRequest();
|
|
|
42776 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
42777 |
this.mediaUpdateTimeout = null;
|
|
|
42778 |
if (this.isMain_) {
|
|
|
42779 |
window.clearTimeout(this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_);
|
|
|
42780 |
this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_ = null;
|
|
|
42781 |
}
|
|
|
42782 |
if (this.state === 'HAVE_NOTHING') {
|
|
|
42783 |
// If we pause the loader before any data has been retrieved, its as if we never
|
|
|
42784 |
// started, so reset to an unstarted state.
|
|
|
42785 |
this.started = false;
|
|
|
42786 |
}
|
|
|
42787 |
}
|
|
|
42788 |
load(isFinalRendition) {
|
|
|
42789 |
window.clearTimeout(this.mediaUpdateTimeout);
|
|
|
42790 |
this.mediaUpdateTimeout = null;
|
|
|
42791 |
const media = this.media();
|
|
|
42792 |
if (isFinalRendition) {
|
|
|
42793 |
const delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000;
|
|
|
42794 |
this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay);
|
|
|
42795 |
return;
|
|
|
42796 |
} // because the playlists are internal to the manifest, load should either load the
|
|
|
42797 |
// main manifest, or do nothing but trigger an event
|
|
|
42798 |
|
|
|
42799 |
if (!this.started) {
|
|
|
42800 |
this.start();
|
|
|
42801 |
return;
|
|
|
42802 |
}
|
|
|
42803 |
if (media && !media.endList) {
|
|
|
42804 |
// Check to see if this is the main loader and the MUP was cleared (this happens
|
|
|
42805 |
// when the loader was paused). `media` should be set at this point since one is always
|
|
|
42806 |
// set during `start()`.
|
|
|
42807 |
if (this.isMain_ && !this.minimumUpdatePeriodTimeout_) {
|
|
|
42808 |
// Trigger minimumUpdatePeriod to refresh the main manifest
|
|
|
42809 |
this.trigger('minimumUpdatePeriod'); // Since there was no prior minimumUpdatePeriodTimeout it should be recreated
|
|
|
42810 |
|
|
|
42811 |
this.updateMinimumUpdatePeriodTimeout_();
|
|
|
42812 |
}
|
|
|
42813 |
this.trigger('mediaupdatetimeout');
|
|
|
42814 |
} else {
|
|
|
42815 |
this.trigger('loadedplaylist');
|
|
|
42816 |
}
|
|
|
42817 |
}
|
|
|
42818 |
start() {
|
|
|
42819 |
this.started = true; // We don't need to request the main manifest again
|
|
|
42820 |
// Call this asynchronously to match the xhr request behavior below
|
|
|
42821 |
|
|
|
42822 |
if (!this.isMain_) {
|
|
|
42823 |
this.mediaRequest_ = window.setTimeout(() => this.haveMain_(), 0);
|
|
|
42824 |
return;
|
|
|
42825 |
}
|
|
|
42826 |
this.requestMain_((req, mainChanged) => {
|
|
|
42827 |
this.haveMain_();
|
|
|
42828 |
if (!this.hasPendingRequest() && !this.media_) {
|
|
|
42829 |
this.media(this.mainPlaylistLoader_.main.playlists[0]);
|
|
|
42830 |
}
|
|
|
42831 |
});
|
|
|
42832 |
}
|
|
|
42833 |
requestMain_(cb) {
|
|
|
42834 |
this.request = this.vhs_.xhr({
|
|
|
42835 |
uri: this.mainPlaylistLoader_.srcUrl,
|
|
|
42836 |
withCredentials: this.withCredentials
|
|
|
42837 |
}, (error, req) => {
|
|
|
42838 |
if (this.requestErrored_(error, req)) {
|
|
|
42839 |
if (this.state === 'HAVE_NOTHING') {
|
|
|
42840 |
this.started = false;
|
|
|
42841 |
}
|
|
|
42842 |
return;
|
|
|
42843 |
}
|
|
|
42844 |
const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_;
|
|
|
42845 |
this.mainPlaylistLoader_.mainXml_ = req.responseText;
|
|
|
42846 |
if (req.responseHeaders && req.responseHeaders.date) {
|
|
|
42847 |
this.mainLoaded_ = Date.parse(req.responseHeaders.date);
|
|
|
42848 |
} else {
|
|
|
42849 |
this.mainLoaded_ = Date.now();
|
|
|
42850 |
}
|
|
|
42851 |
this.mainPlaylistLoader_.srcUrl = resolveManifestRedirect(this.mainPlaylistLoader_.srcUrl, req);
|
|
|
42852 |
if (mainChanged) {
|
|
|
42853 |
this.handleMain_();
|
|
|
42854 |
this.syncClientServerClock_(() => {
|
|
|
42855 |
return cb(req, mainChanged);
|
|
|
42856 |
});
|
|
|
42857 |
return;
|
|
|
42858 |
}
|
|
|
42859 |
return cb(req, mainChanged);
|
|
|
42860 |
});
|
|
|
42861 |
}
|
|
|
42862 |
/**
|
|
|
42863 |
* Parses the main xml for UTCTiming node to sync the client clock to the server
|
|
|
42864 |
* clock. If the UTCTiming node requires a HEAD or GET request, that request is made.
|
|
|
42865 |
*
|
|
|
42866 |
* @param {Function} done
|
|
|
42867 |
* Function to call when clock sync has completed
|
|
|
42868 |
*/
|
|
|
42869 |
|
|
|
42870 |
syncClientServerClock_(done) {
|
|
|
42871 |
const utcTiming = parseUTCTiming(this.mainPlaylistLoader_.mainXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the
|
|
|
42872 |
// server clock
|
|
|
42873 |
|
|
|
42874 |
if (utcTiming === null) {
|
|
|
42875 |
this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now();
|
|
|
42876 |
return done();
|
|
|
42877 |
}
|
|
|
42878 |
if (utcTiming.method === 'DIRECT') {
|
|
|
42879 |
this.mainPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now();
|
|
|
42880 |
return done();
|
|
|
42881 |
}
|
|
|
42882 |
this.request = this.vhs_.xhr({
|
|
|
42883 |
uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value),
|
|
|
42884 |
method: utcTiming.method,
|
|
|
42885 |
withCredentials: this.withCredentials
|
|
|
42886 |
}, (error, req) => {
|
|
|
42887 |
// disposed
|
|
|
42888 |
if (!this.request) {
|
|
|
42889 |
return;
|
|
|
42890 |
}
|
|
|
42891 |
if (error) {
|
|
|
42892 |
// sync request failed, fall back to using date header from mpd
|
|
|
42893 |
// TODO: log warning
|
|
|
42894 |
this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now();
|
|
|
42895 |
return done();
|
|
|
42896 |
}
|
|
|
42897 |
let serverTime;
|
|
|
42898 |
if (utcTiming.method === 'HEAD') {
|
|
|
42899 |
if (!req.responseHeaders || !req.responseHeaders.date) {
|
|
|
42900 |
// expected date header not preset, fall back to using date header from mpd
|
|
|
42901 |
// TODO: log warning
|
|
|
42902 |
serverTime = this.mainLoaded_;
|
|
|
42903 |
} else {
|
|
|
42904 |
serverTime = Date.parse(req.responseHeaders.date);
|
|
|
42905 |
}
|
|
|
42906 |
} else {
|
|
|
42907 |
serverTime = Date.parse(req.responseText);
|
|
|
42908 |
}
|
|
|
42909 |
this.mainPlaylistLoader_.clientOffset_ = serverTime - Date.now();
|
|
|
42910 |
done();
|
|
|
42911 |
});
|
|
|
42912 |
}
|
|
|
42913 |
haveMain_() {
|
|
|
42914 |
this.state = 'HAVE_MAIN_MANIFEST';
|
|
|
42915 |
if (this.isMain_) {
|
|
|
42916 |
// We have the main playlist at this point, so
|
|
|
42917 |
// trigger this to allow PlaylistController
|
|
|
42918 |
// to make an initial playlist selection
|
|
|
42919 |
this.trigger('loadedplaylist');
|
|
|
42920 |
} else if (!this.media_) {
|
|
|
42921 |
// no media playlist was specifically selected so select
|
|
|
42922 |
// the one the child playlist loader was created with
|
|
|
42923 |
this.media(this.childPlaylist_);
|
|
|
42924 |
}
|
|
|
42925 |
}
|
|
|
42926 |
handleMain_() {
|
|
|
42927 |
// clear media request
|
|
|
42928 |
this.mediaRequest_ = null;
|
|
|
42929 |
const oldMain = this.mainPlaylistLoader_.main;
|
|
|
42930 |
let newMain = parseMainXml({
|
|
|
42931 |
mainXml: this.mainPlaylistLoader_.mainXml_,
|
|
|
42932 |
srcUrl: this.mainPlaylistLoader_.srcUrl,
|
|
|
42933 |
clientOffset: this.mainPlaylistLoader_.clientOffset_,
|
|
|
42934 |
sidxMapping: this.mainPlaylistLoader_.sidxMapping_,
|
|
|
42935 |
previousManifest: oldMain
|
|
|
42936 |
}); // if we have an old main to compare the new main against
|
|
|
42937 |
|
|
|
42938 |
if (oldMain) {
|
|
|
42939 |
newMain = updateMain(oldMain, newMain, this.mainPlaylistLoader_.sidxMapping_);
|
|
|
42940 |
} // only update main if we have a new main
|
|
|
42941 |
|
|
|
42942 |
this.mainPlaylistLoader_.main = newMain ? newMain : oldMain;
|
|
|
42943 |
const location = this.mainPlaylistLoader_.main.locations && this.mainPlaylistLoader_.main.locations[0];
|
|
|
42944 |
if (location && location !== this.mainPlaylistLoader_.srcUrl) {
|
|
|
42945 |
this.mainPlaylistLoader_.srcUrl = location;
|
|
|
42946 |
}
|
|
|
42947 |
if (!oldMain || newMain && newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) {
|
|
|
42948 |
this.updateMinimumUpdatePeriodTimeout_();
|
|
|
42949 |
}
|
|
|
42950 |
this.addEventStreamToMetadataTrack_(newMain);
|
|
|
42951 |
return Boolean(newMain);
|
|
|
42952 |
}
|
|
|
42953 |
updateMinimumUpdatePeriodTimeout_() {
|
|
|
42954 |
const mpl = this.mainPlaylistLoader_; // cancel any pending creation of mup on media
|
|
|
42955 |
// a new one will be added if needed.
|
|
|
42956 |
|
|
|
42957 |
if (mpl.createMupOnMedia_) {
|
|
|
42958 |
mpl.off('loadedmetadata', mpl.createMupOnMedia_);
|
|
|
42959 |
mpl.createMupOnMedia_ = null;
|
|
|
42960 |
} // clear any pending timeouts
|
|
|
42961 |
|
|
|
42962 |
if (mpl.minimumUpdatePeriodTimeout_) {
|
|
|
42963 |
window.clearTimeout(mpl.minimumUpdatePeriodTimeout_);
|
|
|
42964 |
mpl.minimumUpdatePeriodTimeout_ = null;
|
|
|
42965 |
}
|
|
|
42966 |
let mup = mpl.main && mpl.main.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current
|
|
|
42967 |
// MPD has no future validity, so a new one will need to be acquired when new
|
|
|
42968 |
// media segments are to be made available. Thus, we use the target duration
|
|
|
42969 |
// in this case
|
|
|
42970 |
|
|
|
42971 |
if (mup === 0) {
|
|
|
42972 |
if (mpl.media()) {
|
|
|
42973 |
mup = mpl.media().targetDuration * 1000;
|
|
|
42974 |
} else {
|
|
|
42975 |
mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_;
|
|
|
42976 |
mpl.one('loadedmetadata', mpl.createMupOnMedia_);
|
|
|
42977 |
}
|
|
|
42978 |
} // if minimumUpdatePeriod is invalid or <= zero, which
|
|
|
42979 |
// can happen when a live video becomes VOD. skip timeout
|
|
|
42980 |
// creation.
|
|
|
42981 |
|
|
|
42982 |
if (typeof mup !== 'number' || mup <= 0) {
|
|
|
42983 |
if (mup < 0) {
|
|
|
42984 |
this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`);
|
|
|
42985 |
}
|
|
|
42986 |
return;
|
|
|
42987 |
}
|
|
|
42988 |
this.createMUPTimeout_(mup);
|
|
|
42989 |
}
|
|
|
42990 |
createMUPTimeout_(mup) {
|
|
|
42991 |
const mpl = this.mainPlaylistLoader_;
|
|
|
42992 |
mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => {
|
|
|
42993 |
mpl.minimumUpdatePeriodTimeout_ = null;
|
|
|
42994 |
mpl.trigger('minimumUpdatePeriod');
|
|
|
42995 |
mpl.createMUPTimeout_(mup);
|
|
|
42996 |
}, mup);
|
|
|
42997 |
}
|
|
|
42998 |
/**
|
|
|
42999 |
* Sends request to refresh the main xml and updates the parsed main manifest
|
|
|
43000 |
*/
|
|
|
43001 |
|
|
|
43002 |
refreshXml_() {
|
|
|
43003 |
this.requestMain_((req, mainChanged) => {
|
|
|
43004 |
if (!mainChanged) {
|
|
|
43005 |
return;
|
|
|
43006 |
}
|
|
|
43007 |
if (this.media_) {
|
|
|
43008 |
this.media_ = this.mainPlaylistLoader_.main.playlists[this.media_.id];
|
|
|
43009 |
} // This will filter out updated sidx info from the mapping
|
|
|
43010 |
|
|
|
43011 |
this.mainPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.sidxMapping_);
|
|
|
43012 |
this.addSidxSegments_(this.media(), this.state, sidxChanged => {
|
|
|
43013 |
// TODO: do we need to reload the current playlist?
|
|
|
43014 |
this.refreshMedia_(this.media().id);
|
|
|
43015 |
});
|
|
|
43016 |
});
|
|
|
43017 |
}
|
|
|
43018 |
/**
|
|
|
43019 |
* Refreshes the media playlist by re-parsing the main xml and updating playlist
|
|
|
43020 |
* references. If this is an alternate loader, the updated parsed manifest is retrieved
|
|
|
43021 |
* from the main loader.
|
|
|
43022 |
*/
|
|
|
43023 |
|
|
|
43024 |
refreshMedia_(mediaID) {
|
|
|
43025 |
if (!mediaID) {
|
|
|
43026 |
throw new Error('refreshMedia_ must take a media id');
|
|
|
43027 |
} // for main we have to reparse the main xml
|
|
|
43028 |
// to re-create segments based on current timing values
|
|
|
43029 |
// which may change media. We only skip updating the main manifest
|
|
|
43030 |
// if this is the first time this.media_ is being set.
|
|
|
43031 |
// as main was just parsed in that case.
|
|
|
43032 |
|
|
|
43033 |
if (this.media_ && this.isMain_) {
|
|
|
43034 |
this.handleMain_();
|
|
|
43035 |
}
|
|
|
43036 |
const playlists = this.mainPlaylistLoader_.main.playlists;
|
|
|
43037 |
const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID];
|
|
|
43038 |
if (mediaChanged) {
|
|
|
43039 |
this.media_ = playlists[mediaID];
|
|
|
43040 |
} else {
|
|
|
43041 |
this.trigger('playlistunchanged');
|
|
|
43042 |
}
|
|
|
43043 |
if (!this.mediaUpdateTimeout) {
|
|
|
43044 |
const createMediaUpdateTimeout = () => {
|
|
|
43045 |
if (this.media().endList) {
|
|
|
43046 |
return;
|
|
|
43047 |
}
|
|
|
43048 |
this.mediaUpdateTimeout = window.setTimeout(() => {
|
|
|
43049 |
this.trigger('mediaupdatetimeout');
|
|
|
43050 |
createMediaUpdateTimeout();
|
|
|
43051 |
}, refreshDelay(this.media(), Boolean(mediaChanged)));
|
|
|
43052 |
};
|
|
|
43053 |
createMediaUpdateTimeout();
|
|
|
43054 |
}
|
|
|
43055 |
this.trigger('loadedplaylist');
|
|
|
43056 |
}
|
|
|
43057 |
/**
|
|
|
43058 |
* Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track.
|
|
|
43059 |
*
|
|
|
43060 |
* @param {manifest} newMain the newly parsed manifest
|
|
|
43061 |
*/
|
|
|
43062 |
|
|
|
43063 |
addEventStreamToMetadataTrack_(newMain) {
|
|
|
43064 |
// Only add new event stream metadata if we have a new manifest.
|
|
|
43065 |
if (newMain && this.mainPlaylistLoader_.main.eventStream) {
|
|
|
43066 |
// convert EventStream to ID3-like data.
|
|
|
43067 |
const metadataArray = this.mainPlaylistLoader_.main.eventStream.map(eventStreamNode => {
|
|
|
43068 |
return {
|
|
|
43069 |
cueTime: eventStreamNode.start,
|
|
|
43070 |
frames: [{
|
|
|
43071 |
data: eventStreamNode.messageData
|
|
|
43072 |
}]
|
|
|
43073 |
};
|
|
|
43074 |
});
|
|
|
43075 |
this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration);
|
|
|
43076 |
}
|
|
|
43077 |
}
|
|
|
43078 |
/**
|
|
|
43079 |
* Returns the key ID set from a playlist
|
|
|
43080 |
*
|
|
|
43081 |
* @param {playlist} playlist to fetch the key ID set from.
|
|
|
43082 |
* @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist.
|
|
|
43083 |
*/
|
|
|
43084 |
|
|
|
43085 |
getKeyIdSet(playlist) {
|
|
|
43086 |
if (playlist.contentProtection) {
|
|
|
43087 |
const keyIds = new Set();
|
|
|
43088 |
for (const keysystem in playlist.contentProtection) {
|
|
|
43089 |
const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID'];
|
|
|
43090 |
if (defaultKID) {
|
|
|
43091 |
// DASH keyIds are separated by dashes.
|
|
|
43092 |
keyIds.add(defaultKID.replace(/-/g, '').toLowerCase());
|
|
|
43093 |
}
|
|
|
43094 |
}
|
|
|
43095 |
return keyIds;
|
|
|
43096 |
}
|
|
|
43097 |
}
|
|
|
43098 |
}
|
|
|
43099 |
var Config = {
|
|
|
43100 |
GOAL_BUFFER_LENGTH: 30,
|
|
|
43101 |
MAX_GOAL_BUFFER_LENGTH: 60,
|
|
|
43102 |
BACK_BUFFER_LENGTH: 30,
|
|
|
43103 |
GOAL_BUFFER_LENGTH_RATE: 1,
|
|
|
43104 |
// 0.5 MB/s
|
|
|
43105 |
INITIAL_BANDWIDTH: 4194304,
|
|
|
43106 |
// A fudge factor to apply to advertised playlist bitrates to account for
|
|
|
43107 |
// temporary flucations in client bandwidth
|
|
|
43108 |
BANDWIDTH_VARIANCE: 1.2,
|
|
|
43109 |
// How much of the buffer must be filled before we consider upswitching
|
|
|
43110 |
BUFFER_LOW_WATER_LINE: 0,
|
|
|
43111 |
MAX_BUFFER_LOW_WATER_LINE: 30,
|
|
|
43112 |
// TODO: Remove this when experimentalBufferBasedABR is removed
|
|
|
43113 |
EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16,
|
|
|
43114 |
BUFFER_LOW_WATER_LINE_RATE: 1,
|
|
|
43115 |
// If the buffer is greater than the high water line, we won't switch down
|
|
|
43116 |
BUFFER_HIGH_WATER_LINE: 30
|
|
|
43117 |
};
|
|
|
43118 |
const stringToArrayBuffer = string => {
|
|
|
43119 |
const view = new Uint8Array(new ArrayBuffer(string.length));
|
|
|
43120 |
for (let i = 0; i < string.length; i++) {
|
|
|
43121 |
view[i] = string.charCodeAt(i);
|
|
|
43122 |
}
|
|
|
43123 |
return view.buffer;
|
|
|
43124 |
};
|
|
|
43125 |
|
|
|
43126 |
/* global Blob, BlobBuilder, Worker */
|
|
|
43127 |
// unify worker interface
|
|
|
43128 |
const browserWorkerPolyFill = function (workerObj) {
|
|
|
43129 |
// node only supports on/off
|
|
|
43130 |
workerObj.on = workerObj.addEventListener;
|
|
|
43131 |
workerObj.off = workerObj.removeEventListener;
|
|
|
43132 |
return workerObj;
|
|
|
43133 |
};
|
|
|
43134 |
const createObjectURL = function (str) {
|
|
|
43135 |
try {
|
|
|
43136 |
return URL.createObjectURL(new Blob([str], {
|
|
|
43137 |
type: 'application/javascript'
|
|
|
43138 |
}));
|
|
|
43139 |
} catch (e) {
|
|
|
43140 |
const blob = new BlobBuilder();
|
|
|
43141 |
blob.append(str);
|
|
|
43142 |
return URL.createObjectURL(blob.getBlob());
|
|
|
43143 |
}
|
|
|
43144 |
};
|
|
|
43145 |
const factory = function (code) {
|
|
|
43146 |
return function () {
|
|
|
43147 |
const objectUrl = createObjectURL(code);
|
|
|
43148 |
const worker = browserWorkerPolyFill(new Worker(objectUrl));
|
|
|
43149 |
worker.objURL = objectUrl;
|
|
|
43150 |
const terminate = worker.terminate;
|
|
|
43151 |
worker.on = worker.addEventListener;
|
|
|
43152 |
worker.off = worker.removeEventListener;
|
|
|
43153 |
worker.terminate = function () {
|
|
|
43154 |
URL.revokeObjectURL(objectUrl);
|
|
|
43155 |
return terminate.call(this);
|
|
|
43156 |
};
|
|
|
43157 |
return worker;
|
|
|
43158 |
};
|
|
|
43159 |
};
|
|
|
43160 |
const transform = function (code) {
|
|
|
43161 |
return `var browserWorkerPolyFill = ${browserWorkerPolyFill.toString()};\n` + 'browserWorkerPolyFill(self);\n' + code;
|
|
|
43162 |
};
|
|
|
43163 |
const getWorkerString = function (fn) {
|
|
|
43164 |
return fn.toString().replace(/^function.+?{/, '').slice(0, -1);
|
|
|
43165 |
};
|
|
|
43166 |
|
|
|
43167 |
/* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */
|
|
|
43168 |
const workerCode$1 = transform(getWorkerString(function () {
|
|
|
43169 |
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
|
43170 |
/**
|
|
|
43171 |
* mux.js
|
|
|
43172 |
*
|
|
|
43173 |
* Copyright (c) Brightcove
|
|
|
43174 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
43175 |
*
|
|
|
43176 |
* A lightweight readable stream implemention that handles event dispatching.
|
|
|
43177 |
* Objects that inherit from streams should call init in their constructors.
|
|
|
43178 |
*/
|
|
|
43179 |
|
|
|
43180 |
var Stream$8 = function () {
|
|
|
43181 |
this.init = function () {
|
|
|
43182 |
var listeners = {};
|
|
|
43183 |
/**
|
|
|
43184 |
* Add a listener for a specified event type.
|
|
|
43185 |
* @param type {string} the event name
|
|
|
43186 |
* @param listener {function} the callback to be invoked when an event of
|
|
|
43187 |
* the specified type occurs
|
|
|
43188 |
*/
|
|
|
43189 |
|
|
|
43190 |
this.on = function (type, listener) {
|
|
|
43191 |
if (!listeners[type]) {
|
|
|
43192 |
listeners[type] = [];
|
|
|
43193 |
}
|
|
|
43194 |
listeners[type] = listeners[type].concat(listener);
|
|
|
43195 |
};
|
|
|
43196 |
/**
|
|
|
43197 |
* Remove a listener for a specified event type.
|
|
|
43198 |
* @param type {string} the event name
|
|
|
43199 |
* @param listener {function} a function previously registered for this
|
|
|
43200 |
* type of event through `on`
|
|
|
43201 |
*/
|
|
|
43202 |
|
|
|
43203 |
this.off = function (type, listener) {
|
|
|
43204 |
var index;
|
|
|
43205 |
if (!listeners[type]) {
|
|
|
43206 |
return false;
|
|
|
43207 |
}
|
|
|
43208 |
index = listeners[type].indexOf(listener);
|
|
|
43209 |
listeners[type] = listeners[type].slice();
|
|
|
43210 |
listeners[type].splice(index, 1);
|
|
|
43211 |
return index > -1;
|
|
|
43212 |
};
|
|
|
43213 |
/**
|
|
|
43214 |
* Trigger an event of the specified type on this stream. Any additional
|
|
|
43215 |
* arguments to this function are passed as parameters to event listeners.
|
|
|
43216 |
* @param type {string} the event name
|
|
|
43217 |
*/
|
|
|
43218 |
|
|
|
43219 |
this.trigger = function (type) {
|
|
|
43220 |
var callbacks, i, length, args;
|
|
|
43221 |
callbacks = listeners[type];
|
|
|
43222 |
if (!callbacks) {
|
|
|
43223 |
return;
|
|
|
43224 |
} // Slicing the arguments on every invocation of this method
|
|
|
43225 |
// can add a significant amount of overhead. Avoid the
|
|
|
43226 |
// intermediate object creation for the common case of a
|
|
|
43227 |
// single callback argument
|
|
|
43228 |
|
|
|
43229 |
if (arguments.length === 2) {
|
|
|
43230 |
length = callbacks.length;
|
|
|
43231 |
for (i = 0; i < length; ++i) {
|
|
|
43232 |
callbacks[i].call(this, arguments[1]);
|
|
|
43233 |
}
|
|
|
43234 |
} else {
|
|
|
43235 |
args = [];
|
|
|
43236 |
i = arguments.length;
|
|
|
43237 |
for (i = 1; i < arguments.length; ++i) {
|
|
|
43238 |
args.push(arguments[i]);
|
|
|
43239 |
}
|
|
|
43240 |
length = callbacks.length;
|
|
|
43241 |
for (i = 0; i < length; ++i) {
|
|
|
43242 |
callbacks[i].apply(this, args);
|
|
|
43243 |
}
|
|
|
43244 |
}
|
|
|
43245 |
};
|
|
|
43246 |
/**
|
|
|
43247 |
* Destroys the stream and cleans up.
|
|
|
43248 |
*/
|
|
|
43249 |
|
|
|
43250 |
this.dispose = function () {
|
|
|
43251 |
listeners = {};
|
|
|
43252 |
};
|
|
|
43253 |
};
|
|
|
43254 |
};
|
|
|
43255 |
/**
|
|
|
43256 |
* Forwards all `data` events on this stream to the destination stream. The
|
|
|
43257 |
* destination stream should provide a method `push` to receive the data
|
|
|
43258 |
* events as they arrive.
|
|
|
43259 |
* @param destination {stream} the stream that will receive all `data` events
|
|
|
43260 |
* @param autoFlush {boolean} if false, we will not call `flush` on the destination
|
|
|
43261 |
* when the current stream emits a 'done' event
|
|
|
43262 |
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
|
|
|
43263 |
*/
|
|
|
43264 |
|
|
|
43265 |
Stream$8.prototype.pipe = function (destination) {
|
|
|
43266 |
this.on('data', function (data) {
|
|
|
43267 |
destination.push(data);
|
|
|
43268 |
});
|
|
|
43269 |
this.on('done', function (flushSource) {
|
|
|
43270 |
destination.flush(flushSource);
|
|
|
43271 |
});
|
|
|
43272 |
this.on('partialdone', function (flushSource) {
|
|
|
43273 |
destination.partialFlush(flushSource);
|
|
|
43274 |
});
|
|
|
43275 |
this.on('endedtimeline', function (flushSource) {
|
|
|
43276 |
destination.endTimeline(flushSource);
|
|
|
43277 |
});
|
|
|
43278 |
this.on('reset', function (flushSource) {
|
|
|
43279 |
destination.reset(flushSource);
|
|
|
43280 |
});
|
|
|
43281 |
return destination;
|
|
|
43282 |
}; // Default stream functions that are expected to be overridden to perform
|
|
|
43283 |
// actual work. These are provided by the prototype as a sort of no-op
|
|
|
43284 |
// implementation so that we don't have to check for their existence in the
|
|
|
43285 |
// `pipe` function above.
|
|
|
43286 |
|
|
|
43287 |
Stream$8.prototype.push = function (data) {
|
|
|
43288 |
this.trigger('data', data);
|
|
|
43289 |
};
|
|
|
43290 |
Stream$8.prototype.flush = function (flushSource) {
|
|
|
43291 |
this.trigger('done', flushSource);
|
|
|
43292 |
};
|
|
|
43293 |
Stream$8.prototype.partialFlush = function (flushSource) {
|
|
|
43294 |
this.trigger('partialdone', flushSource);
|
|
|
43295 |
};
|
|
|
43296 |
Stream$8.prototype.endTimeline = function (flushSource) {
|
|
|
43297 |
this.trigger('endedtimeline', flushSource);
|
|
|
43298 |
};
|
|
|
43299 |
Stream$8.prototype.reset = function (flushSource) {
|
|
|
43300 |
this.trigger('reset', flushSource);
|
|
|
43301 |
};
|
|
|
43302 |
var stream = Stream$8;
|
|
|
43303 |
var MAX_UINT32$1 = Math.pow(2, 32);
|
|
|
43304 |
var getUint64$3 = function (uint8) {
|
|
|
43305 |
var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength);
|
|
|
43306 |
var value;
|
|
|
43307 |
if (dv.getBigUint64) {
|
|
|
43308 |
value = dv.getBigUint64(0);
|
|
|
43309 |
if (value < Number.MAX_SAFE_INTEGER) {
|
|
|
43310 |
return Number(value);
|
|
|
43311 |
}
|
|
|
43312 |
return value;
|
|
|
43313 |
}
|
|
|
43314 |
return dv.getUint32(0) * MAX_UINT32$1 + dv.getUint32(4);
|
|
|
43315 |
};
|
|
|
43316 |
var numbers = {
|
|
|
43317 |
getUint64: getUint64$3,
|
|
|
43318 |
MAX_UINT32: MAX_UINT32$1
|
|
|
43319 |
};
|
|
|
43320 |
/**
|
|
|
43321 |
* mux.js
|
|
|
43322 |
*
|
|
|
43323 |
* Copyright (c) Brightcove
|
|
|
43324 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
43325 |
*
|
|
|
43326 |
* Functions that generate fragmented MP4s suitable for use with Media
|
|
|
43327 |
* Source Extensions.
|
|
|
43328 |
*/
|
|
|
43329 |
|
|
|
43330 |
var MAX_UINT32 = numbers.MAX_UINT32;
|
|
|
43331 |
var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun$1, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS; // pre-calculate constants
|
|
|
43332 |
|
|
|
43333 |
(function () {
|
|
|
43334 |
var i;
|
|
|
43335 |
types = {
|
|
|
43336 |
avc1: [],
|
|
|
43337 |
// codingname
|
|
|
43338 |
avcC: [],
|
|
|
43339 |
btrt: [],
|
|
|
43340 |
dinf: [],
|
|
|
43341 |
dref: [],
|
|
|
43342 |
esds: [],
|
|
|
43343 |
ftyp: [],
|
|
|
43344 |
hdlr: [],
|
|
|
43345 |
mdat: [],
|
|
|
43346 |
mdhd: [],
|
|
|
43347 |
mdia: [],
|
|
|
43348 |
mfhd: [],
|
|
|
43349 |
minf: [],
|
|
|
43350 |
moof: [],
|
|
|
43351 |
moov: [],
|
|
|
43352 |
mp4a: [],
|
|
|
43353 |
// codingname
|
|
|
43354 |
mvex: [],
|
|
|
43355 |
mvhd: [],
|
|
|
43356 |
pasp: [],
|
|
|
43357 |
sdtp: [],
|
|
|
43358 |
smhd: [],
|
|
|
43359 |
stbl: [],
|
|
|
43360 |
stco: [],
|
|
|
43361 |
stsc: [],
|
|
|
43362 |
stsd: [],
|
|
|
43363 |
stsz: [],
|
|
|
43364 |
stts: [],
|
|
|
43365 |
styp: [],
|
|
|
43366 |
tfdt: [],
|
|
|
43367 |
tfhd: [],
|
|
|
43368 |
traf: [],
|
|
|
43369 |
trak: [],
|
|
|
43370 |
trun: [],
|
|
|
43371 |
trex: [],
|
|
|
43372 |
tkhd: [],
|
|
|
43373 |
vmhd: []
|
|
|
43374 |
}; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we
|
|
|
43375 |
// don't throw an error
|
|
|
43376 |
|
|
|
43377 |
if (typeof Uint8Array === 'undefined') {
|
|
|
43378 |
return;
|
|
|
43379 |
}
|
|
|
43380 |
for (i in types) {
|
|
|
43381 |
if (types.hasOwnProperty(i)) {
|
|
|
43382 |
types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)];
|
|
|
43383 |
}
|
|
|
43384 |
}
|
|
|
43385 |
MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]);
|
|
|
43386 |
AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]);
|
|
|
43387 |
MINOR_VERSION = new Uint8Array([0, 0, 0, 1]);
|
|
|
43388 |
VIDEO_HDLR = new Uint8Array([0x00,
|
|
|
43389 |
// version 0
|
|
|
43390 |
0x00, 0x00, 0x00,
|
|
|
43391 |
// flags
|
|
|
43392 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43393 |
// pre_defined
|
|
|
43394 |
0x76, 0x69, 0x64, 0x65,
|
|
|
43395 |
// handler_type: 'vide'
|
|
|
43396 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43397 |
// reserved
|
|
|
43398 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43399 |
// reserved
|
|
|
43400 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43401 |
// reserved
|
|
|
43402 |
0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler'
|
|
|
43403 |
]);
|
|
|
43404 |
|
|
|
43405 |
AUDIO_HDLR = new Uint8Array([0x00,
|
|
|
43406 |
// version 0
|
|
|
43407 |
0x00, 0x00, 0x00,
|
|
|
43408 |
// flags
|
|
|
43409 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43410 |
// pre_defined
|
|
|
43411 |
0x73, 0x6f, 0x75, 0x6e,
|
|
|
43412 |
// handler_type: 'soun'
|
|
|
43413 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43414 |
// reserved
|
|
|
43415 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43416 |
// reserved
|
|
|
43417 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43418 |
// reserved
|
|
|
43419 |
0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler'
|
|
|
43420 |
]);
|
|
|
43421 |
|
|
|
43422 |
HDLR_TYPES = {
|
|
|
43423 |
video: VIDEO_HDLR,
|
|
|
43424 |
audio: AUDIO_HDLR
|
|
|
43425 |
};
|
|
|
43426 |
DREF = new Uint8Array([0x00,
|
|
|
43427 |
// version 0
|
|
|
43428 |
0x00, 0x00, 0x00,
|
|
|
43429 |
// flags
|
|
|
43430 |
0x00, 0x00, 0x00, 0x01,
|
|
|
43431 |
// entry_count
|
|
|
43432 |
0x00, 0x00, 0x00, 0x0c,
|
|
|
43433 |
// entry_size
|
|
|
43434 |
0x75, 0x72, 0x6c, 0x20,
|
|
|
43435 |
// 'url' type
|
|
|
43436 |
0x00,
|
|
|
43437 |
// version 0
|
|
|
43438 |
0x00, 0x00, 0x01 // entry_flags
|
|
|
43439 |
]);
|
|
|
43440 |
|
|
|
43441 |
SMHD = new Uint8Array([0x00,
|
|
|
43442 |
// version
|
|
|
43443 |
0x00, 0x00, 0x00,
|
|
|
43444 |
// flags
|
|
|
43445 |
0x00, 0x00,
|
|
|
43446 |
// balance, 0 means centered
|
|
|
43447 |
0x00, 0x00 // reserved
|
|
|
43448 |
]);
|
|
|
43449 |
|
|
|
43450 |
STCO = new Uint8Array([0x00,
|
|
|
43451 |
// version
|
|
|
43452 |
0x00, 0x00, 0x00,
|
|
|
43453 |
// flags
|
|
|
43454 |
0x00, 0x00, 0x00, 0x00 // entry_count
|
|
|
43455 |
]);
|
|
|
43456 |
|
|
|
43457 |
STSC = STCO;
|
|
|
43458 |
STSZ = new Uint8Array([0x00,
|
|
|
43459 |
// version
|
|
|
43460 |
0x00, 0x00, 0x00,
|
|
|
43461 |
// flags
|
|
|
43462 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43463 |
// sample_size
|
|
|
43464 |
0x00, 0x00, 0x00, 0x00 // sample_count
|
|
|
43465 |
]);
|
|
|
43466 |
|
|
|
43467 |
STTS = STCO;
|
|
|
43468 |
VMHD = new Uint8Array([0x00,
|
|
|
43469 |
// version
|
|
|
43470 |
0x00, 0x00, 0x01,
|
|
|
43471 |
// flags
|
|
|
43472 |
0x00, 0x00,
|
|
|
43473 |
// graphicsmode
|
|
|
43474 |
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor
|
|
|
43475 |
]);
|
|
|
43476 |
})();
|
|
|
43477 |
|
|
|
43478 |
box = function (type) {
|
|
|
43479 |
var payload = [],
|
|
|
43480 |
size = 0,
|
|
|
43481 |
i,
|
|
|
43482 |
result,
|
|
|
43483 |
view;
|
|
|
43484 |
for (i = 1; i < arguments.length; i++) {
|
|
|
43485 |
payload.push(arguments[i]);
|
|
|
43486 |
}
|
|
|
43487 |
i = payload.length; // calculate the total size we need to allocate
|
|
|
43488 |
|
|
|
43489 |
while (i--) {
|
|
|
43490 |
size += payload[i].byteLength;
|
|
|
43491 |
}
|
|
|
43492 |
result = new Uint8Array(size + 8);
|
|
|
43493 |
view = new DataView(result.buffer, result.byteOffset, result.byteLength);
|
|
|
43494 |
view.setUint32(0, result.byteLength);
|
|
|
43495 |
result.set(type, 4); // copy the payload into the result
|
|
|
43496 |
|
|
|
43497 |
for (i = 0, size = 8; i < payload.length; i++) {
|
|
|
43498 |
result.set(payload[i], size);
|
|
|
43499 |
size += payload[i].byteLength;
|
|
|
43500 |
}
|
|
|
43501 |
return result;
|
|
|
43502 |
};
|
|
|
43503 |
dinf = function () {
|
|
|
43504 |
return box(types.dinf, box(types.dref, DREF));
|
|
|
43505 |
};
|
|
|
43506 |
esds = function (track) {
|
|
|
43507 |
return box(types.esds, new Uint8Array([0x00,
|
|
|
43508 |
// version
|
|
|
43509 |
0x00, 0x00, 0x00,
|
|
|
43510 |
// flags
|
|
|
43511 |
// ES_Descriptor
|
|
|
43512 |
0x03,
|
|
|
43513 |
// tag, ES_DescrTag
|
|
|
43514 |
0x19,
|
|
|
43515 |
// length
|
|
|
43516 |
0x00, 0x00,
|
|
|
43517 |
// ES_ID
|
|
|
43518 |
0x00,
|
|
|
43519 |
// streamDependenceFlag, URL_flag, reserved, streamPriority
|
|
|
43520 |
// DecoderConfigDescriptor
|
|
|
43521 |
0x04,
|
|
|
43522 |
// tag, DecoderConfigDescrTag
|
|
|
43523 |
0x11,
|
|
|
43524 |
// length
|
|
|
43525 |
0x40,
|
|
|
43526 |
// object type
|
|
|
43527 |
0x15,
|
|
|
43528 |
// streamType
|
|
|
43529 |
0x00, 0x06, 0x00,
|
|
|
43530 |
// bufferSizeDB
|
|
|
43531 |
0x00, 0x00, 0xda, 0xc0,
|
|
|
43532 |
// maxBitrate
|
|
|
43533 |
0x00, 0x00, 0xda, 0xc0,
|
|
|
43534 |
// avgBitrate
|
|
|
43535 |
// DecoderSpecificInfo
|
|
|
43536 |
0x05,
|
|
|
43537 |
// tag, DecoderSpecificInfoTag
|
|
|
43538 |
0x02,
|
|
|
43539 |
// length
|
|
|
43540 |
// ISO/IEC 14496-3, AudioSpecificConfig
|
|
|
43541 |
// for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35
|
|
|
43542 |
track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig
|
|
|
43543 |
]));
|
|
|
43544 |
};
|
|
|
43545 |
|
|
|
43546 |
ftyp = function () {
|
|
|
43547 |
return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND);
|
|
|
43548 |
};
|
|
|
43549 |
hdlr = function (type) {
|
|
|
43550 |
return box(types.hdlr, HDLR_TYPES[type]);
|
|
|
43551 |
};
|
|
|
43552 |
mdat = function (data) {
|
|
|
43553 |
return box(types.mdat, data);
|
|
|
43554 |
};
|
|
|
43555 |
mdhd = function (track) {
|
|
|
43556 |
var result = new Uint8Array([0x00,
|
|
|
43557 |
// version 0
|
|
|
43558 |
0x00, 0x00, 0x00,
|
|
|
43559 |
// flags
|
|
|
43560 |
0x00, 0x00, 0x00, 0x02,
|
|
|
43561 |
// creation_time
|
|
|
43562 |
0x00, 0x00, 0x00, 0x03,
|
|
|
43563 |
// modification_time
|
|
|
43564 |
0x00, 0x01, 0x5f, 0x90,
|
|
|
43565 |
// timescale, 90,000 "ticks" per second
|
|
|
43566 |
track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF,
|
|
|
43567 |
// duration
|
|
|
43568 |
0x55, 0xc4,
|
|
|
43569 |
// 'und' language (undetermined)
|
|
|
43570 |
0x00, 0x00]); // Use the sample rate from the track metadata, when it is
|
|
|
43571 |
// defined. The sample rate can be parsed out of an ADTS header, for
|
|
|
43572 |
// instance.
|
|
|
43573 |
|
|
|
43574 |
if (track.samplerate) {
|
|
|
43575 |
result[12] = track.samplerate >>> 24 & 0xFF;
|
|
|
43576 |
result[13] = track.samplerate >>> 16 & 0xFF;
|
|
|
43577 |
result[14] = track.samplerate >>> 8 & 0xFF;
|
|
|
43578 |
result[15] = track.samplerate & 0xFF;
|
|
|
43579 |
}
|
|
|
43580 |
return box(types.mdhd, result);
|
|
|
43581 |
};
|
|
|
43582 |
mdia = function (track) {
|
|
|
43583 |
return box(types.mdia, mdhd(track), hdlr(track.type), minf(track));
|
|
|
43584 |
};
|
|
|
43585 |
mfhd = function (sequenceNumber) {
|
|
|
43586 |
return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00,
|
|
|
43587 |
// flags
|
|
|
43588 |
(sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number
|
|
|
43589 |
]));
|
|
|
43590 |
};
|
|
|
43591 |
|
|
|
43592 |
minf = function (track) {
|
|
|
43593 |
return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track));
|
|
|
43594 |
};
|
|
|
43595 |
moof = function (sequenceNumber, tracks) {
|
|
|
43596 |
var trackFragments = [],
|
|
|
43597 |
i = tracks.length; // build traf boxes for each track fragment
|
|
|
43598 |
|
|
|
43599 |
while (i--) {
|
|
|
43600 |
trackFragments[i] = traf(tracks[i]);
|
|
|
43601 |
}
|
|
|
43602 |
return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments));
|
|
|
43603 |
};
|
|
|
43604 |
/**
|
|
|
43605 |
* Returns a movie box.
|
|
|
43606 |
* @param tracks {array} the tracks associated with this movie
|
|
|
43607 |
* @see ISO/IEC 14496-12:2012(E), section 8.2.1
|
|
|
43608 |
*/
|
|
|
43609 |
|
|
|
43610 |
moov = function (tracks) {
|
|
|
43611 |
var i = tracks.length,
|
|
|
43612 |
boxes = [];
|
|
|
43613 |
while (i--) {
|
|
|
43614 |
boxes[i] = trak(tracks[i]);
|
|
|
43615 |
}
|
|
|
43616 |
return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks)));
|
|
|
43617 |
};
|
|
|
43618 |
mvex = function (tracks) {
|
|
|
43619 |
var i = tracks.length,
|
|
|
43620 |
boxes = [];
|
|
|
43621 |
while (i--) {
|
|
|
43622 |
boxes[i] = trex(tracks[i]);
|
|
|
43623 |
}
|
|
|
43624 |
return box.apply(null, [types.mvex].concat(boxes));
|
|
|
43625 |
};
|
|
|
43626 |
mvhd = function (duration) {
|
|
|
43627 |
var bytes = new Uint8Array([0x00,
|
|
|
43628 |
// version 0
|
|
|
43629 |
0x00, 0x00, 0x00,
|
|
|
43630 |
// flags
|
|
|
43631 |
0x00, 0x00, 0x00, 0x01,
|
|
|
43632 |
// creation_time
|
|
|
43633 |
0x00, 0x00, 0x00, 0x02,
|
|
|
43634 |
// modification_time
|
|
|
43635 |
0x00, 0x01, 0x5f, 0x90,
|
|
|
43636 |
// timescale, 90,000 "ticks" per second
|
|
|
43637 |
(duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF,
|
|
|
43638 |
// duration
|
|
|
43639 |
0x00, 0x01, 0x00, 0x00,
|
|
|
43640 |
// 1.0 rate
|
|
|
43641 |
0x01, 0x00,
|
|
|
43642 |
// 1.0 volume
|
|
|
43643 |
0x00, 0x00,
|
|
|
43644 |
// reserved
|
|
|
43645 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43646 |
// reserved
|
|
|
43647 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43648 |
// reserved
|
|
|
43649 |
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
|
|
|
43650 |
// transformation: unity matrix
|
|
|
43651 |
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
43652 |
// pre_defined
|
|
|
43653 |
0xff, 0xff, 0xff, 0xff // next_track_ID
|
|
|
43654 |
]);
|
|
|
43655 |
|
|
|
43656 |
return box(types.mvhd, bytes);
|
|
|
43657 |
};
|
|
|
43658 |
sdtp = function (track) {
|
|
|
43659 |
var samples = track.samples || [],
|
|
|
43660 |
bytes = new Uint8Array(4 + samples.length),
|
|
|
43661 |
flags,
|
|
|
43662 |
i; // leave the full box header (4 bytes) all zero
|
|
|
43663 |
// write the sample table
|
|
|
43664 |
|
|
|
43665 |
for (i = 0; i < samples.length; i++) {
|
|
|
43666 |
flags = samples[i].flags;
|
|
|
43667 |
bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy;
|
|
|
43668 |
}
|
|
|
43669 |
return box(types.sdtp, bytes);
|
|
|
43670 |
};
|
|
|
43671 |
stbl = function (track) {
|
|
|
43672 |
return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO));
|
|
|
43673 |
};
|
|
|
43674 |
(function () {
|
|
|
43675 |
var videoSample, audioSample;
|
|
|
43676 |
stsd = function (track) {
|
|
|
43677 |
return box(types.stsd, new Uint8Array([0x00,
|
|
|
43678 |
// version 0
|
|
|
43679 |
0x00, 0x00, 0x00,
|
|
|
43680 |
// flags
|
|
|
43681 |
0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track));
|
|
|
43682 |
};
|
|
|
43683 |
videoSample = function (track) {
|
|
|
43684 |
var sps = track.sps || [],
|
|
|
43685 |
pps = track.pps || [],
|
|
|
43686 |
sequenceParameterSets = [],
|
|
|
43687 |
pictureParameterSets = [],
|
|
|
43688 |
i,
|
|
|
43689 |
avc1Box; // assemble the SPSs
|
|
|
43690 |
|
|
|
43691 |
for (i = 0; i < sps.length; i++) {
|
|
|
43692 |
sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8);
|
|
|
43693 |
sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength
|
|
|
43694 |
|
|
|
43695 |
sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS
|
|
|
43696 |
} // assemble the PPSs
|
|
|
43697 |
|
|
|
43698 |
for (i = 0; i < pps.length; i++) {
|
|
|
43699 |
pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8);
|
|
|
43700 |
pictureParameterSets.push(pps[i].byteLength & 0xFF);
|
|
|
43701 |
pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i]));
|
|
|
43702 |
}
|
|
|
43703 |
avc1Box = [types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
43704 |
// reserved
|
|
|
43705 |
0x00, 0x01,
|
|
|
43706 |
// data_reference_index
|
|
|
43707 |
0x00, 0x00,
|
|
|
43708 |
// pre_defined
|
|
|
43709 |
0x00, 0x00,
|
|
|
43710 |
// reserved
|
|
|
43711 |
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
43712 |
// pre_defined
|
|
|
43713 |
(track.width & 0xff00) >> 8, track.width & 0xff,
|
|
|
43714 |
// width
|
|
|
43715 |
(track.height & 0xff00) >> 8, track.height & 0xff,
|
|
|
43716 |
// height
|
|
|
43717 |
0x00, 0x48, 0x00, 0x00,
|
|
|
43718 |
// horizresolution
|
|
|
43719 |
0x00, 0x48, 0x00, 0x00,
|
|
|
43720 |
// vertresolution
|
|
|
43721 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43722 |
// reserved
|
|
|
43723 |
0x00, 0x01,
|
|
|
43724 |
// frame_count
|
|
|
43725 |
0x13, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6a, 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x69, 0x62, 0x2d, 0x68, 0x6c, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
43726 |
// compressorname
|
|
|
43727 |
0x00, 0x18,
|
|
|
43728 |
// depth = 24
|
|
|
43729 |
0x11, 0x11 // pre_defined = -1
|
|
|
43730 |
]), box(types.avcC, new Uint8Array([0x01,
|
|
|
43731 |
// configurationVersion
|
|
|
43732 |
track.profileIdc,
|
|
|
43733 |
// AVCProfileIndication
|
|
|
43734 |
track.profileCompatibility,
|
|
|
43735 |
// profile_compatibility
|
|
|
43736 |
track.levelIdc,
|
|
|
43737 |
// AVCLevelIndication
|
|
|
43738 |
0xff // lengthSizeMinusOne, hard-coded to 4 bytes
|
|
|
43739 |
].concat([sps.length],
|
|
|
43740 |
// numOfSequenceParameterSets
|
|
|
43741 |
sequenceParameterSets,
|
|
|
43742 |
// "SPS"
|
|
|
43743 |
[pps.length],
|
|
|
43744 |
// numOfPictureParameterSets
|
|
|
43745 |
pictureParameterSets // "PPS"
|
|
|
43746 |
))), box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80,
|
|
|
43747 |
// bufferSizeDB
|
|
|
43748 |
0x00, 0x2d, 0xc6, 0xc0,
|
|
|
43749 |
// maxBitrate
|
|
|
43750 |
0x00, 0x2d, 0xc6, 0xc0 // avgBitrate
|
|
|
43751 |
]))];
|
|
|
43752 |
|
|
|
43753 |
if (track.sarRatio) {
|
|
|
43754 |
var hSpacing = track.sarRatio[0],
|
|
|
43755 |
vSpacing = track.sarRatio[1];
|
|
|
43756 |
avc1Box.push(box(types.pasp, new Uint8Array([(hSpacing & 0xFF000000) >> 24, (hSpacing & 0xFF0000) >> 16, (hSpacing & 0xFF00) >> 8, hSpacing & 0xFF, (vSpacing & 0xFF000000) >> 24, (vSpacing & 0xFF0000) >> 16, (vSpacing & 0xFF00) >> 8, vSpacing & 0xFF])));
|
|
|
43757 |
}
|
|
|
43758 |
return box.apply(null, avc1Box);
|
|
|
43759 |
};
|
|
|
43760 |
audioSample = function (track) {
|
|
|
43761 |
return box(types.mp4a, new Uint8Array([
|
|
|
43762 |
// SampleEntry, ISO/IEC 14496-12
|
|
|
43763 |
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
43764 |
// reserved
|
|
|
43765 |
0x00, 0x01,
|
|
|
43766 |
// data_reference_index
|
|
|
43767 |
// AudioSampleEntry, ISO/IEC 14496-12
|
|
|
43768 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43769 |
// reserved
|
|
|
43770 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43771 |
// reserved
|
|
|
43772 |
(track.channelcount & 0xff00) >> 8, track.channelcount & 0xff,
|
|
|
43773 |
// channelcount
|
|
|
43774 |
(track.samplesize & 0xff00) >> 8, track.samplesize & 0xff,
|
|
|
43775 |
// samplesize
|
|
|
43776 |
0x00, 0x00,
|
|
|
43777 |
// pre_defined
|
|
|
43778 |
0x00, 0x00,
|
|
|
43779 |
// reserved
|
|
|
43780 |
(track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16
|
|
|
43781 |
// MP4AudioSampleEntry, ISO/IEC 14496-14
|
|
|
43782 |
]), esds(track));
|
|
|
43783 |
};
|
|
|
43784 |
})();
|
|
|
43785 |
tkhd = function (track) {
|
|
|
43786 |
var result = new Uint8Array([0x00,
|
|
|
43787 |
// version 0
|
|
|
43788 |
0x00, 0x00, 0x07,
|
|
|
43789 |
// flags
|
|
|
43790 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43791 |
// creation_time
|
|
|
43792 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43793 |
// modification_time
|
|
|
43794 |
(track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF,
|
|
|
43795 |
// track_ID
|
|
|
43796 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43797 |
// reserved
|
|
|
43798 |
(track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF,
|
|
|
43799 |
// duration
|
|
|
43800 |
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
43801 |
// reserved
|
|
|
43802 |
0x00, 0x00,
|
|
|
43803 |
// layer
|
|
|
43804 |
0x00, 0x00,
|
|
|
43805 |
// alternate_group
|
|
|
43806 |
0x01, 0x00,
|
|
|
43807 |
// non-audio track volume
|
|
|
43808 |
0x00, 0x00,
|
|
|
43809 |
// reserved
|
|
|
43810 |
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
|
|
|
43811 |
// transformation: unity matrix
|
|
|
43812 |
(track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00,
|
|
|
43813 |
// width
|
|
|
43814 |
(track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height
|
|
|
43815 |
]);
|
|
|
43816 |
|
|
|
43817 |
return box(types.tkhd, result);
|
|
|
43818 |
};
|
|
|
43819 |
/**
|
|
|
43820 |
* Generate a track fragment (traf) box. A traf box collects metadata
|
|
|
43821 |
* about tracks in a movie fragment (moof) box.
|
|
|
43822 |
*/
|
|
|
43823 |
|
|
|
43824 |
traf = function (track) {
|
|
|
43825 |
var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime;
|
|
|
43826 |
trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00,
|
|
|
43827 |
// version 0
|
|
|
43828 |
0x00, 0x00, 0x3a,
|
|
|
43829 |
// flags
|
|
|
43830 |
(track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF,
|
|
|
43831 |
// track_ID
|
|
|
43832 |
0x00, 0x00, 0x00, 0x01,
|
|
|
43833 |
// sample_description_index
|
|
|
43834 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43835 |
// default_sample_duration
|
|
|
43836 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43837 |
// default_sample_size
|
|
|
43838 |
0x00, 0x00, 0x00, 0x00 // default_sample_flags
|
|
|
43839 |
]));
|
|
|
43840 |
|
|
|
43841 |
upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / MAX_UINT32);
|
|
|
43842 |
lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % MAX_UINT32);
|
|
|
43843 |
trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01,
|
|
|
43844 |
// version 1
|
|
|
43845 |
0x00, 0x00, 0x00,
|
|
|
43846 |
// flags
|
|
|
43847 |
// baseMediaDecodeTime
|
|
|
43848 |
upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF])); // the data offset specifies the number of bytes from the start of
|
|
|
43849 |
// the containing moof to the first payload byte of the associated
|
|
|
43850 |
// mdat
|
|
|
43851 |
|
|
|
43852 |
dataOffset = 32 +
|
|
|
43853 |
// tfhd
|
|
|
43854 |
20 +
|
|
|
43855 |
// tfdt
|
|
|
43856 |
8 +
|
|
|
43857 |
// traf header
|
|
|
43858 |
16 +
|
|
|
43859 |
// mfhd
|
|
|
43860 |
8 +
|
|
|
43861 |
// moof header
|
|
|
43862 |
8; // mdat header
|
|
|
43863 |
// audio tracks require less metadata
|
|
|
43864 |
|
|
|
43865 |
if (track.type === 'audio') {
|
|
|
43866 |
trackFragmentRun = trun$1(track, dataOffset);
|
|
|
43867 |
return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun);
|
|
|
43868 |
} // video tracks should contain an independent and disposable samples
|
|
|
43869 |
// box (sdtp)
|
|
|
43870 |
// generate one and adjust offsets to match
|
|
|
43871 |
|
|
|
43872 |
sampleDependencyTable = sdtp(track);
|
|
|
43873 |
trackFragmentRun = trun$1(track, sampleDependencyTable.length + dataOffset);
|
|
|
43874 |
return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable);
|
|
|
43875 |
};
|
|
|
43876 |
/**
|
|
|
43877 |
* Generate a track box.
|
|
|
43878 |
* @param track {object} a track definition
|
|
|
43879 |
* @return {Uint8Array} the track box
|
|
|
43880 |
*/
|
|
|
43881 |
|
|
|
43882 |
trak = function (track) {
|
|
|
43883 |
track.duration = track.duration || 0xffffffff;
|
|
|
43884 |
return box(types.trak, tkhd(track), mdia(track));
|
|
|
43885 |
};
|
|
|
43886 |
trex = function (track) {
|
|
|
43887 |
var result = new Uint8Array([0x00,
|
|
|
43888 |
// version 0
|
|
|
43889 |
0x00, 0x00, 0x00,
|
|
|
43890 |
// flags
|
|
|
43891 |
(track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF,
|
|
|
43892 |
// track_ID
|
|
|
43893 |
0x00, 0x00, 0x00, 0x01,
|
|
|
43894 |
// default_sample_description_index
|
|
|
43895 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43896 |
// default_sample_duration
|
|
|
43897 |
0x00, 0x00, 0x00, 0x00,
|
|
|
43898 |
// default_sample_size
|
|
|
43899 |
0x00, 0x01, 0x00, 0x01 // default_sample_flags
|
|
|
43900 |
]); // the last two bytes of default_sample_flags is the sample
|
|
|
43901 |
// degradation priority, a hint about the importance of this sample
|
|
|
43902 |
// relative to others. Lower the degradation priority for all sample
|
|
|
43903 |
// types other than video.
|
|
|
43904 |
|
|
|
43905 |
if (track.type !== 'video') {
|
|
|
43906 |
result[result.length - 1] = 0x00;
|
|
|
43907 |
}
|
|
|
43908 |
return box(types.trex, result);
|
|
|
43909 |
};
|
|
|
43910 |
(function () {
|
|
|
43911 |
var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a
|
|
|
43912 |
// duration is present for the first sample, it will be present for
|
|
|
43913 |
// all subsequent samples.
|
|
|
43914 |
// see ISO/IEC 14496-12:2012, Section 8.8.8.1
|
|
|
43915 |
|
|
|
43916 |
trunHeader = function (samples, offset) {
|
|
|
43917 |
var durationPresent = 0,
|
|
|
43918 |
sizePresent = 0,
|
|
|
43919 |
flagsPresent = 0,
|
|
|
43920 |
compositionTimeOffset = 0; // trun flag constants
|
|
|
43921 |
|
|
|
43922 |
if (samples.length) {
|
|
|
43923 |
if (samples[0].duration !== undefined) {
|
|
|
43924 |
durationPresent = 0x1;
|
|
|
43925 |
}
|
|
|
43926 |
if (samples[0].size !== undefined) {
|
|
|
43927 |
sizePresent = 0x2;
|
|
|
43928 |
}
|
|
|
43929 |
if (samples[0].flags !== undefined) {
|
|
|
43930 |
flagsPresent = 0x4;
|
|
|
43931 |
}
|
|
|
43932 |
if (samples[0].compositionTimeOffset !== undefined) {
|
|
|
43933 |
compositionTimeOffset = 0x8;
|
|
|
43934 |
}
|
|
|
43935 |
}
|
|
|
43936 |
return [0x00,
|
|
|
43937 |
// version 0
|
|
|
43938 |
0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01,
|
|
|
43939 |
// flags
|
|
|
43940 |
(samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF,
|
|
|
43941 |
// sample_count
|
|
|
43942 |
(offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset
|
|
|
43943 |
];
|
|
|
43944 |
};
|
|
|
43945 |
|
|
|
43946 |
videoTrun = function (track, offset) {
|
|
|
43947 |
var bytesOffest, bytes, header, samples, sample, i;
|
|
|
43948 |
samples = track.samples || [];
|
|
|
43949 |
offset += 8 + 12 + 16 * samples.length;
|
|
|
43950 |
header = trunHeader(samples, offset);
|
|
|
43951 |
bytes = new Uint8Array(header.length + samples.length * 16);
|
|
|
43952 |
bytes.set(header);
|
|
|
43953 |
bytesOffest = header.length;
|
|
|
43954 |
for (i = 0; i < samples.length; i++) {
|
|
|
43955 |
sample = samples[i];
|
|
|
43956 |
bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24;
|
|
|
43957 |
bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16;
|
|
|
43958 |
bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8;
|
|
|
43959 |
bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration
|
|
|
43960 |
|
|
|
43961 |
bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24;
|
|
|
43962 |
bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16;
|
|
|
43963 |
bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8;
|
|
|
43964 |
bytes[bytesOffest++] = sample.size & 0xFF; // sample_size
|
|
|
43965 |
|
|
|
43966 |
bytes[bytesOffest++] = sample.flags.isLeading << 2 | sample.flags.dependsOn;
|
|
|
43967 |
bytes[bytesOffest++] = sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample;
|
|
|
43968 |
bytes[bytesOffest++] = sample.flags.degradationPriority & 0xF0 << 8;
|
|
|
43969 |
bytes[bytesOffest++] = sample.flags.degradationPriority & 0x0F; // sample_flags
|
|
|
43970 |
|
|
|
43971 |
bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF000000) >>> 24;
|
|
|
43972 |
bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF0000) >>> 16;
|
|
|
43973 |
bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF00) >>> 8;
|
|
|
43974 |
bytes[bytesOffest++] = sample.compositionTimeOffset & 0xFF; // sample_composition_time_offset
|
|
|
43975 |
}
|
|
|
43976 |
|
|
|
43977 |
return box(types.trun, bytes);
|
|
|
43978 |
};
|
|
|
43979 |
audioTrun = function (track, offset) {
|
|
|
43980 |
var bytes, bytesOffest, header, samples, sample, i;
|
|
|
43981 |
samples = track.samples || [];
|
|
|
43982 |
offset += 8 + 12 + 8 * samples.length;
|
|
|
43983 |
header = trunHeader(samples, offset);
|
|
|
43984 |
bytes = new Uint8Array(header.length + samples.length * 8);
|
|
|
43985 |
bytes.set(header);
|
|
|
43986 |
bytesOffest = header.length;
|
|
|
43987 |
for (i = 0; i < samples.length; i++) {
|
|
|
43988 |
sample = samples[i];
|
|
|
43989 |
bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24;
|
|
|
43990 |
bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16;
|
|
|
43991 |
bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8;
|
|
|
43992 |
bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration
|
|
|
43993 |
|
|
|
43994 |
bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24;
|
|
|
43995 |
bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16;
|
|
|
43996 |
bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8;
|
|
|
43997 |
bytes[bytesOffest++] = sample.size & 0xFF; // sample_size
|
|
|
43998 |
}
|
|
|
43999 |
|
|
|
44000 |
return box(types.trun, bytes);
|
|
|
44001 |
};
|
|
|
44002 |
trun$1 = function (track, offset) {
|
|
|
44003 |
if (track.type === 'audio') {
|
|
|
44004 |
return audioTrun(track, offset);
|
|
|
44005 |
}
|
|
|
44006 |
return videoTrun(track, offset);
|
|
|
44007 |
};
|
|
|
44008 |
})();
|
|
|
44009 |
var mp4Generator = {
|
|
|
44010 |
ftyp: ftyp,
|
|
|
44011 |
mdat: mdat,
|
|
|
44012 |
moof: moof,
|
|
|
44013 |
moov: moov,
|
|
|
44014 |
initSegment: function (tracks) {
|
|
|
44015 |
var fileType = ftyp(),
|
|
|
44016 |
movie = moov(tracks),
|
|
|
44017 |
result;
|
|
|
44018 |
result = new Uint8Array(fileType.byteLength + movie.byteLength);
|
|
|
44019 |
result.set(fileType);
|
|
|
44020 |
result.set(movie, fileType.byteLength);
|
|
|
44021 |
return result;
|
|
|
44022 |
}
|
|
|
44023 |
};
|
|
|
44024 |
/**
|
|
|
44025 |
* mux.js
|
|
|
44026 |
*
|
|
|
44027 |
* Copyright (c) Brightcove
|
|
|
44028 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44029 |
*/
|
|
|
44030 |
// composed of the nal units that make up that frame
|
|
|
44031 |
// Also keep track of cummulative data about the frame from the nal units such
|
|
|
44032 |
// as the frame duration, starting pts, etc.
|
|
|
44033 |
|
|
|
44034 |
var groupNalsIntoFrames = function (nalUnits) {
|
|
|
44035 |
var i,
|
|
|
44036 |
currentNal,
|
|
|
44037 |
currentFrame = [],
|
|
|
44038 |
frames = []; // TODO added for LHLS, make sure this is OK
|
|
|
44039 |
|
|
|
44040 |
frames.byteLength = 0;
|
|
|
44041 |
frames.nalCount = 0;
|
|
|
44042 |
frames.duration = 0;
|
|
|
44043 |
currentFrame.byteLength = 0;
|
|
|
44044 |
for (i = 0; i < nalUnits.length; i++) {
|
|
|
44045 |
currentNal = nalUnits[i]; // Split on 'aud'-type nal units
|
|
|
44046 |
|
|
|
44047 |
if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
|
|
|
44048 |
// Since the very first nal unit is expected to be an AUD
|
|
|
44049 |
// only push to the frames array when currentFrame is not empty
|
|
|
44050 |
if (currentFrame.length) {
|
|
|
44051 |
currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK
|
|
|
44052 |
|
|
|
44053 |
frames.byteLength += currentFrame.byteLength;
|
|
|
44054 |
frames.nalCount += currentFrame.length;
|
|
|
44055 |
frames.duration += currentFrame.duration;
|
|
|
44056 |
frames.push(currentFrame);
|
|
|
44057 |
}
|
|
|
44058 |
currentFrame = [currentNal];
|
|
|
44059 |
currentFrame.byteLength = currentNal.data.byteLength;
|
|
|
44060 |
currentFrame.pts = currentNal.pts;
|
|
|
44061 |
currentFrame.dts = currentNal.dts;
|
|
|
44062 |
} else {
|
|
|
44063 |
// Specifically flag key frames for ease of use later
|
|
|
44064 |
if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
|
|
|
44065 |
currentFrame.keyFrame = true;
|
|
|
44066 |
}
|
|
|
44067 |
currentFrame.duration = currentNal.dts - currentFrame.dts;
|
|
|
44068 |
currentFrame.byteLength += currentNal.data.byteLength;
|
|
|
44069 |
currentFrame.push(currentNal);
|
|
|
44070 |
}
|
|
|
44071 |
} // For the last frame, use the duration of the previous frame if we
|
|
|
44072 |
// have nothing better to go on
|
|
|
44073 |
|
|
|
44074 |
if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) {
|
|
|
44075 |
currentFrame.duration = frames[frames.length - 1].duration;
|
|
|
44076 |
} // Push the final frame
|
|
|
44077 |
// TODO added for LHLS, make sure this is OK
|
|
|
44078 |
|
|
|
44079 |
frames.byteLength += currentFrame.byteLength;
|
|
|
44080 |
frames.nalCount += currentFrame.length;
|
|
|
44081 |
frames.duration += currentFrame.duration;
|
|
|
44082 |
frames.push(currentFrame);
|
|
|
44083 |
return frames;
|
|
|
44084 |
}; // Convert an array of frames into an array of Gop with each Gop being composed
|
|
|
44085 |
// of the frames that make up that Gop
|
|
|
44086 |
// Also keep track of cummulative data about the Gop from the frames such as the
|
|
|
44087 |
// Gop duration, starting pts, etc.
|
|
|
44088 |
|
|
|
44089 |
var groupFramesIntoGops = function (frames) {
|
|
|
44090 |
var i,
|
|
|
44091 |
currentFrame,
|
|
|
44092 |
currentGop = [],
|
|
|
44093 |
gops = []; // We must pre-set some of the values on the Gop since we
|
|
|
44094 |
// keep running totals of these values
|
|
|
44095 |
|
|
|
44096 |
currentGop.byteLength = 0;
|
|
|
44097 |
currentGop.nalCount = 0;
|
|
|
44098 |
currentGop.duration = 0;
|
|
|
44099 |
currentGop.pts = frames[0].pts;
|
|
|
44100 |
currentGop.dts = frames[0].dts; // store some metadata about all the Gops
|
|
|
44101 |
|
|
|
44102 |
gops.byteLength = 0;
|
|
|
44103 |
gops.nalCount = 0;
|
|
|
44104 |
gops.duration = 0;
|
|
|
44105 |
gops.pts = frames[0].pts;
|
|
|
44106 |
gops.dts = frames[0].dts;
|
|
|
44107 |
for (i = 0; i < frames.length; i++) {
|
|
|
44108 |
currentFrame = frames[i];
|
|
|
44109 |
if (currentFrame.keyFrame) {
|
|
|
44110 |
// Since the very first frame is expected to be an keyframe
|
|
|
44111 |
// only push to the gops array when currentGop is not empty
|
|
|
44112 |
if (currentGop.length) {
|
|
|
44113 |
gops.push(currentGop);
|
|
|
44114 |
gops.byteLength += currentGop.byteLength;
|
|
|
44115 |
gops.nalCount += currentGop.nalCount;
|
|
|
44116 |
gops.duration += currentGop.duration;
|
|
|
44117 |
}
|
|
|
44118 |
currentGop = [currentFrame];
|
|
|
44119 |
currentGop.nalCount = currentFrame.length;
|
|
|
44120 |
currentGop.byteLength = currentFrame.byteLength;
|
|
|
44121 |
currentGop.pts = currentFrame.pts;
|
|
|
44122 |
currentGop.dts = currentFrame.dts;
|
|
|
44123 |
currentGop.duration = currentFrame.duration;
|
|
|
44124 |
} else {
|
|
|
44125 |
currentGop.duration += currentFrame.duration;
|
|
|
44126 |
currentGop.nalCount += currentFrame.length;
|
|
|
44127 |
currentGop.byteLength += currentFrame.byteLength;
|
|
|
44128 |
currentGop.push(currentFrame);
|
|
|
44129 |
}
|
|
|
44130 |
}
|
|
|
44131 |
if (gops.length && currentGop.duration <= 0) {
|
|
|
44132 |
currentGop.duration = gops[gops.length - 1].duration;
|
|
|
44133 |
}
|
|
|
44134 |
gops.byteLength += currentGop.byteLength;
|
|
|
44135 |
gops.nalCount += currentGop.nalCount;
|
|
|
44136 |
gops.duration += currentGop.duration; // push the final Gop
|
|
|
44137 |
|
|
|
44138 |
gops.push(currentGop);
|
|
|
44139 |
return gops;
|
|
|
44140 |
};
|
|
|
44141 |
/*
|
|
|
44142 |
* Search for the first keyframe in the GOPs and throw away all frames
|
|
|
44143 |
* until that keyframe. Then extend the duration of the pulled keyframe
|
|
|
44144 |
* and pull the PTS and DTS of the keyframe so that it covers the time
|
|
|
44145 |
* range of the frames that were disposed.
|
|
|
44146 |
*
|
|
|
44147 |
* @param {Array} gops video GOPs
|
|
|
44148 |
* @returns {Array} modified video GOPs
|
|
|
44149 |
*/
|
|
|
44150 |
|
|
|
44151 |
var extendFirstKeyFrame = function (gops) {
|
|
|
44152 |
var currentGop;
|
|
|
44153 |
if (!gops[0][0].keyFrame && gops.length > 1) {
|
|
|
44154 |
// Remove the first GOP
|
|
|
44155 |
currentGop = gops.shift();
|
|
|
44156 |
gops.byteLength -= currentGop.byteLength;
|
|
|
44157 |
gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the
|
|
|
44158 |
// first gop to cover the time period of the
|
|
|
44159 |
// frames we just removed
|
|
|
44160 |
|
|
|
44161 |
gops[0][0].dts = currentGop.dts;
|
|
|
44162 |
gops[0][0].pts = currentGop.pts;
|
|
|
44163 |
gops[0][0].duration += currentGop.duration;
|
|
|
44164 |
}
|
|
|
44165 |
return gops;
|
|
|
44166 |
};
|
|
|
44167 |
/**
|
|
|
44168 |
* Default sample object
|
|
|
44169 |
* see ISO/IEC 14496-12:2012, section 8.6.4.3
|
|
|
44170 |
*/
|
|
|
44171 |
|
|
|
44172 |
var createDefaultSample = function () {
|
|
|
44173 |
return {
|
|
|
44174 |
size: 0,
|
|
|
44175 |
flags: {
|
|
|
44176 |
isLeading: 0,
|
|
|
44177 |
dependsOn: 1,
|
|
|
44178 |
isDependedOn: 0,
|
|
|
44179 |
hasRedundancy: 0,
|
|
|
44180 |
degradationPriority: 0,
|
|
|
44181 |
isNonSyncSample: 1
|
|
|
44182 |
}
|
|
|
44183 |
};
|
|
|
44184 |
};
|
|
|
44185 |
/*
|
|
|
44186 |
* Collates information from a video frame into an object for eventual
|
|
|
44187 |
* entry into an MP4 sample table.
|
|
|
44188 |
*
|
|
|
44189 |
* @param {Object} frame the video frame
|
|
|
44190 |
* @param {Number} dataOffset the byte offset to position the sample
|
|
|
44191 |
* @return {Object} object containing sample table info for a frame
|
|
|
44192 |
*/
|
|
|
44193 |
|
|
|
44194 |
var sampleForFrame = function (frame, dataOffset) {
|
|
|
44195 |
var sample = createDefaultSample();
|
|
|
44196 |
sample.dataOffset = dataOffset;
|
|
|
44197 |
sample.compositionTimeOffset = frame.pts - frame.dts;
|
|
|
44198 |
sample.duration = frame.duration;
|
|
|
44199 |
sample.size = 4 * frame.length; // Space for nal unit size
|
|
|
44200 |
|
|
|
44201 |
sample.size += frame.byteLength;
|
|
|
44202 |
if (frame.keyFrame) {
|
|
|
44203 |
sample.flags.dependsOn = 2;
|
|
|
44204 |
sample.flags.isNonSyncSample = 0;
|
|
|
44205 |
}
|
|
|
44206 |
return sample;
|
|
|
44207 |
}; // generate the track's sample table from an array of gops
|
|
|
44208 |
|
|
|
44209 |
var generateSampleTable$1 = function (gops, baseDataOffset) {
|
|
|
44210 |
var h,
|
|
|
44211 |
i,
|
|
|
44212 |
sample,
|
|
|
44213 |
currentGop,
|
|
|
44214 |
currentFrame,
|
|
|
44215 |
dataOffset = baseDataOffset || 0,
|
|
|
44216 |
samples = [];
|
|
|
44217 |
for (h = 0; h < gops.length; h++) {
|
|
|
44218 |
currentGop = gops[h];
|
|
|
44219 |
for (i = 0; i < currentGop.length; i++) {
|
|
|
44220 |
currentFrame = currentGop[i];
|
|
|
44221 |
sample = sampleForFrame(currentFrame, dataOffset);
|
|
|
44222 |
dataOffset += sample.size;
|
|
|
44223 |
samples.push(sample);
|
|
|
44224 |
}
|
|
|
44225 |
}
|
|
|
44226 |
return samples;
|
|
|
44227 |
}; // generate the track's raw mdat data from an array of gops
|
|
|
44228 |
|
|
|
44229 |
var concatenateNalData = function (gops) {
|
|
|
44230 |
var h,
|
|
|
44231 |
i,
|
|
|
44232 |
j,
|
|
|
44233 |
currentGop,
|
|
|
44234 |
currentFrame,
|
|
|
44235 |
currentNal,
|
|
|
44236 |
dataOffset = 0,
|
|
|
44237 |
nalsByteLength = gops.byteLength,
|
|
|
44238 |
numberOfNals = gops.nalCount,
|
|
|
44239 |
totalByteLength = nalsByteLength + 4 * numberOfNals,
|
|
|
44240 |
data = new Uint8Array(totalByteLength),
|
|
|
44241 |
view = new DataView(data.buffer); // For each Gop..
|
|
|
44242 |
|
|
|
44243 |
for (h = 0; h < gops.length; h++) {
|
|
|
44244 |
currentGop = gops[h]; // For each Frame..
|
|
|
44245 |
|
|
|
44246 |
for (i = 0; i < currentGop.length; i++) {
|
|
|
44247 |
currentFrame = currentGop[i]; // For each NAL..
|
|
|
44248 |
|
|
|
44249 |
for (j = 0; j < currentFrame.length; j++) {
|
|
|
44250 |
currentNal = currentFrame[j];
|
|
|
44251 |
view.setUint32(dataOffset, currentNal.data.byteLength);
|
|
|
44252 |
dataOffset += 4;
|
|
|
44253 |
data.set(currentNal.data, dataOffset);
|
|
|
44254 |
dataOffset += currentNal.data.byteLength;
|
|
|
44255 |
}
|
|
|
44256 |
}
|
|
|
44257 |
}
|
|
|
44258 |
return data;
|
|
|
44259 |
}; // generate the track's sample table from a frame
|
|
|
44260 |
|
|
|
44261 |
var generateSampleTableForFrame = function (frame, baseDataOffset) {
|
|
|
44262 |
var sample,
|
|
|
44263 |
dataOffset = baseDataOffset || 0,
|
|
|
44264 |
samples = [];
|
|
|
44265 |
sample = sampleForFrame(frame, dataOffset);
|
|
|
44266 |
samples.push(sample);
|
|
|
44267 |
return samples;
|
|
|
44268 |
}; // generate the track's raw mdat data from a frame
|
|
|
44269 |
|
|
|
44270 |
var concatenateNalDataForFrame = function (frame) {
|
|
|
44271 |
var i,
|
|
|
44272 |
currentNal,
|
|
|
44273 |
dataOffset = 0,
|
|
|
44274 |
nalsByteLength = frame.byteLength,
|
|
|
44275 |
numberOfNals = frame.length,
|
|
|
44276 |
totalByteLength = nalsByteLength + 4 * numberOfNals,
|
|
|
44277 |
data = new Uint8Array(totalByteLength),
|
|
|
44278 |
view = new DataView(data.buffer); // For each NAL..
|
|
|
44279 |
|
|
|
44280 |
for (i = 0; i < frame.length; i++) {
|
|
|
44281 |
currentNal = frame[i];
|
|
|
44282 |
view.setUint32(dataOffset, currentNal.data.byteLength);
|
|
|
44283 |
dataOffset += 4;
|
|
|
44284 |
data.set(currentNal.data, dataOffset);
|
|
|
44285 |
dataOffset += currentNal.data.byteLength;
|
|
|
44286 |
}
|
|
|
44287 |
return data;
|
|
|
44288 |
};
|
|
|
44289 |
var frameUtils$1 = {
|
|
|
44290 |
groupNalsIntoFrames: groupNalsIntoFrames,
|
|
|
44291 |
groupFramesIntoGops: groupFramesIntoGops,
|
|
|
44292 |
extendFirstKeyFrame: extendFirstKeyFrame,
|
|
|
44293 |
generateSampleTable: generateSampleTable$1,
|
|
|
44294 |
concatenateNalData: concatenateNalData,
|
|
|
44295 |
generateSampleTableForFrame: generateSampleTableForFrame,
|
|
|
44296 |
concatenateNalDataForFrame: concatenateNalDataForFrame
|
|
|
44297 |
};
|
|
|
44298 |
/**
|
|
|
44299 |
* mux.js
|
|
|
44300 |
*
|
|
|
44301 |
* Copyright (c) Brightcove
|
|
|
44302 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44303 |
*/
|
|
|
44304 |
|
|
|
44305 |
var highPrefix = [33, 16, 5, 32, 164, 27];
|
|
|
44306 |
var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252];
|
|
|
44307 |
var zeroFill = function (count) {
|
|
|
44308 |
var a = [];
|
|
|
44309 |
while (count--) {
|
|
|
44310 |
a.push(0);
|
|
|
44311 |
}
|
|
|
44312 |
return a;
|
|
|
44313 |
};
|
|
|
44314 |
var makeTable = function (metaTable) {
|
|
|
44315 |
return Object.keys(metaTable).reduce(function (obj, key) {
|
|
|
44316 |
obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) {
|
|
|
44317 |
return arr.concat(part);
|
|
|
44318 |
}, []));
|
|
|
44319 |
return obj;
|
|
|
44320 |
}, {});
|
|
|
44321 |
};
|
|
|
44322 |
var silence;
|
|
|
44323 |
var silence_1 = function () {
|
|
|
44324 |
if (!silence) {
|
|
|
44325 |
// Frames-of-silence to use for filling in missing AAC frames
|
|
|
44326 |
var coneOfSilence = {
|
|
|
44327 |
96000: [highPrefix, [227, 64], zeroFill(154), [56]],
|
|
|
44328 |
88200: [highPrefix, [231], zeroFill(170), [56]],
|
|
|
44329 |
64000: [highPrefix, [248, 192], zeroFill(240), [56]],
|
|
|
44330 |
48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]],
|
|
|
44331 |
44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]],
|
|
|
44332 |
32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]],
|
|
|
44333 |
24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]],
|
|
|
44334 |
16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]],
|
|
|
44335 |
12000: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 253, 128], zeroFill(259), [56]],
|
|
|
44336 |
11025: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 255, 192], zeroFill(268), [55, 175, 128], zeroFill(108), [112]],
|
|
|
44337 |
8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]]
|
|
|
44338 |
};
|
|
|
44339 |
silence = makeTable(coneOfSilence);
|
|
|
44340 |
}
|
|
|
44341 |
return silence;
|
|
|
44342 |
};
|
|
|
44343 |
/**
|
|
|
44344 |
* mux.js
|
|
|
44345 |
*
|
|
|
44346 |
* Copyright (c) Brightcove
|
|
|
44347 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44348 |
*/
|
|
|
44349 |
|
|
|
44350 |
var ONE_SECOND_IN_TS$4 = 90000,
|
|
|
44351 |
// 90kHz clock
|
|
|
44352 |
secondsToVideoTs,
|
|
|
44353 |
secondsToAudioTs,
|
|
|
44354 |
videoTsToSeconds,
|
|
|
44355 |
audioTsToSeconds,
|
|
|
44356 |
audioTsToVideoTs,
|
|
|
44357 |
videoTsToAudioTs,
|
|
|
44358 |
metadataTsToSeconds;
|
|
|
44359 |
secondsToVideoTs = function (seconds) {
|
|
|
44360 |
return seconds * ONE_SECOND_IN_TS$4;
|
|
|
44361 |
};
|
|
|
44362 |
secondsToAudioTs = function (seconds, sampleRate) {
|
|
|
44363 |
return seconds * sampleRate;
|
|
|
44364 |
};
|
|
|
44365 |
videoTsToSeconds = function (timestamp) {
|
|
|
44366 |
return timestamp / ONE_SECOND_IN_TS$4;
|
|
|
44367 |
};
|
|
|
44368 |
audioTsToSeconds = function (timestamp, sampleRate) {
|
|
|
44369 |
return timestamp / sampleRate;
|
|
|
44370 |
};
|
|
|
44371 |
audioTsToVideoTs = function (timestamp, sampleRate) {
|
|
|
44372 |
return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate));
|
|
|
44373 |
};
|
|
|
44374 |
videoTsToAudioTs = function (timestamp, sampleRate) {
|
|
|
44375 |
return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate);
|
|
|
44376 |
};
|
|
|
44377 |
/**
|
|
|
44378 |
* Adjust ID3 tag or caption timing information by the timeline pts values
|
|
|
44379 |
* (if keepOriginalTimestamps is false) and convert to seconds
|
|
|
44380 |
*/
|
|
|
44381 |
|
|
|
44382 |
metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) {
|
|
|
44383 |
return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts);
|
|
|
44384 |
};
|
|
|
44385 |
var clock$2 = {
|
|
|
44386 |
ONE_SECOND_IN_TS: ONE_SECOND_IN_TS$4,
|
|
|
44387 |
secondsToVideoTs: secondsToVideoTs,
|
|
|
44388 |
secondsToAudioTs: secondsToAudioTs,
|
|
|
44389 |
videoTsToSeconds: videoTsToSeconds,
|
|
|
44390 |
audioTsToSeconds: audioTsToSeconds,
|
|
|
44391 |
audioTsToVideoTs: audioTsToVideoTs,
|
|
|
44392 |
videoTsToAudioTs: videoTsToAudioTs,
|
|
|
44393 |
metadataTsToSeconds: metadataTsToSeconds
|
|
|
44394 |
};
|
|
|
44395 |
/**
|
|
|
44396 |
* mux.js
|
|
|
44397 |
*
|
|
|
44398 |
* Copyright (c) Brightcove
|
|
|
44399 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44400 |
*/
|
|
|
44401 |
|
|
|
44402 |
var coneOfSilence = silence_1;
|
|
|
44403 |
var clock$1 = clock$2;
|
|
|
44404 |
/**
|
|
|
44405 |
* Sum the `byteLength` properties of the data in each AAC frame
|
|
|
44406 |
*/
|
|
|
44407 |
|
|
|
44408 |
var sumFrameByteLengths = function (array) {
|
|
|
44409 |
var i,
|
|
|
44410 |
currentObj,
|
|
|
44411 |
sum = 0; // sum the byteLength's all each nal unit in the frame
|
|
|
44412 |
|
|
|
44413 |
for (i = 0; i < array.length; i++) {
|
|
|
44414 |
currentObj = array[i];
|
|
|
44415 |
sum += currentObj.data.byteLength;
|
|
|
44416 |
}
|
|
|
44417 |
return sum;
|
|
|
44418 |
}; // Possibly pad (prefix) the audio track with silence if appending this track
|
|
|
44419 |
// would lead to the introduction of a gap in the audio buffer
|
|
|
44420 |
|
|
|
44421 |
var prefixWithSilence = function (track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) {
|
|
|
44422 |
var baseMediaDecodeTimeTs,
|
|
|
44423 |
frameDuration = 0,
|
|
|
44424 |
audioGapDuration = 0,
|
|
|
44425 |
audioFillFrameCount = 0,
|
|
|
44426 |
audioFillDuration = 0,
|
|
|
44427 |
silentFrame,
|
|
|
44428 |
i,
|
|
|
44429 |
firstFrame;
|
|
|
44430 |
if (!frames.length) {
|
|
|
44431 |
return;
|
|
|
44432 |
}
|
|
|
44433 |
baseMediaDecodeTimeTs = clock$1.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills
|
|
|
44434 |
|
|
|
44435 |
frameDuration = Math.ceil(clock$1.ONE_SECOND_IN_TS / (track.samplerate / 1024));
|
|
|
44436 |
if (audioAppendStartTs && videoBaseMediaDecodeTime) {
|
|
|
44437 |
// insert the shortest possible amount (audio gap or audio to video gap)
|
|
|
44438 |
audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap
|
|
|
44439 |
|
|
|
44440 |
audioFillFrameCount = Math.floor(audioGapDuration / frameDuration);
|
|
|
44441 |
audioFillDuration = audioFillFrameCount * frameDuration;
|
|
|
44442 |
} // don't attempt to fill gaps smaller than a single frame or larger
|
|
|
44443 |
// than a half second
|
|
|
44444 |
|
|
|
44445 |
if (audioFillFrameCount < 1 || audioFillDuration > clock$1.ONE_SECOND_IN_TS / 2) {
|
|
|
44446 |
return;
|
|
|
44447 |
}
|
|
|
44448 |
silentFrame = coneOfSilence()[track.samplerate];
|
|
|
44449 |
if (!silentFrame) {
|
|
|
44450 |
// we don't have a silent frame pregenerated for the sample rate, so use a frame
|
|
|
44451 |
// from the content instead
|
|
|
44452 |
silentFrame = frames[0].data;
|
|
|
44453 |
}
|
|
|
44454 |
for (i = 0; i < audioFillFrameCount; i++) {
|
|
|
44455 |
firstFrame = frames[0];
|
|
|
44456 |
frames.splice(0, 0, {
|
|
|
44457 |
data: silentFrame,
|
|
|
44458 |
dts: firstFrame.dts - frameDuration,
|
|
|
44459 |
pts: firstFrame.pts - frameDuration
|
|
|
44460 |
});
|
|
|
44461 |
}
|
|
|
44462 |
track.baseMediaDecodeTime -= Math.floor(clock$1.videoTsToAudioTs(audioFillDuration, track.samplerate));
|
|
|
44463 |
return audioFillDuration;
|
|
|
44464 |
}; // If the audio segment extends before the earliest allowed dts
|
|
|
44465 |
// value, remove AAC frames until starts at or after the earliest
|
|
|
44466 |
// allowed DTS so that we don't end up with a negative baseMedia-
|
|
|
44467 |
// DecodeTime for the audio track
|
|
|
44468 |
|
|
|
44469 |
var trimAdtsFramesByEarliestDts = function (adtsFrames, track, earliestAllowedDts) {
|
|
|
44470 |
if (track.minSegmentDts >= earliestAllowedDts) {
|
|
|
44471 |
return adtsFrames;
|
|
|
44472 |
} // We will need to recalculate the earliest segment Dts
|
|
|
44473 |
|
|
|
44474 |
track.minSegmentDts = Infinity;
|
|
|
44475 |
return adtsFrames.filter(function (currentFrame) {
|
|
|
44476 |
// If this is an allowed frame, keep it and record it's Dts
|
|
|
44477 |
if (currentFrame.dts >= earliestAllowedDts) {
|
|
|
44478 |
track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts);
|
|
|
44479 |
track.minSegmentPts = track.minSegmentDts;
|
|
|
44480 |
return true;
|
|
|
44481 |
} // Otherwise, discard it
|
|
|
44482 |
|
|
|
44483 |
return false;
|
|
|
44484 |
});
|
|
|
44485 |
}; // generate the track's raw mdat data from an array of frames
|
|
|
44486 |
|
|
|
44487 |
var generateSampleTable = function (frames) {
|
|
|
44488 |
var i,
|
|
|
44489 |
currentFrame,
|
|
|
44490 |
samples = [];
|
|
|
44491 |
for (i = 0; i < frames.length; i++) {
|
|
|
44492 |
currentFrame = frames[i];
|
|
|
44493 |
samples.push({
|
|
|
44494 |
size: currentFrame.data.byteLength,
|
|
|
44495 |
duration: 1024 // For AAC audio, all samples contain 1024 samples
|
|
|
44496 |
});
|
|
|
44497 |
}
|
|
|
44498 |
|
|
|
44499 |
return samples;
|
|
|
44500 |
}; // generate the track's sample table from an array of frames
|
|
|
44501 |
|
|
|
44502 |
var concatenateFrameData = function (frames) {
|
|
|
44503 |
var i,
|
|
|
44504 |
currentFrame,
|
|
|
44505 |
dataOffset = 0,
|
|
|
44506 |
data = new Uint8Array(sumFrameByteLengths(frames));
|
|
|
44507 |
for (i = 0; i < frames.length; i++) {
|
|
|
44508 |
currentFrame = frames[i];
|
|
|
44509 |
data.set(currentFrame.data, dataOffset);
|
|
|
44510 |
dataOffset += currentFrame.data.byteLength;
|
|
|
44511 |
}
|
|
|
44512 |
return data;
|
|
|
44513 |
};
|
|
|
44514 |
var audioFrameUtils$1 = {
|
|
|
44515 |
prefixWithSilence: prefixWithSilence,
|
|
|
44516 |
trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts,
|
|
|
44517 |
generateSampleTable: generateSampleTable,
|
|
|
44518 |
concatenateFrameData: concatenateFrameData
|
|
|
44519 |
};
|
|
|
44520 |
/**
|
|
|
44521 |
* mux.js
|
|
|
44522 |
*
|
|
|
44523 |
* Copyright (c) Brightcove
|
|
|
44524 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44525 |
*/
|
|
|
44526 |
|
|
|
44527 |
var ONE_SECOND_IN_TS$3 = clock$2.ONE_SECOND_IN_TS;
|
|
|
44528 |
/**
|
|
|
44529 |
* Store information about the start and end of the track and the
|
|
|
44530 |
* duration for each frame/sample we process in order to calculate
|
|
|
44531 |
* the baseMediaDecodeTime
|
|
|
44532 |
*/
|
|
|
44533 |
|
|
|
44534 |
var collectDtsInfo = function (track, data) {
|
|
|
44535 |
if (typeof data.pts === 'number') {
|
|
|
44536 |
if (track.timelineStartInfo.pts === undefined) {
|
|
|
44537 |
track.timelineStartInfo.pts = data.pts;
|
|
|
44538 |
}
|
|
|
44539 |
if (track.minSegmentPts === undefined) {
|
|
|
44540 |
track.minSegmentPts = data.pts;
|
|
|
44541 |
} else {
|
|
|
44542 |
track.minSegmentPts = Math.min(track.minSegmentPts, data.pts);
|
|
|
44543 |
}
|
|
|
44544 |
if (track.maxSegmentPts === undefined) {
|
|
|
44545 |
track.maxSegmentPts = data.pts;
|
|
|
44546 |
} else {
|
|
|
44547 |
track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts);
|
|
|
44548 |
}
|
|
|
44549 |
}
|
|
|
44550 |
if (typeof data.dts === 'number') {
|
|
|
44551 |
if (track.timelineStartInfo.dts === undefined) {
|
|
|
44552 |
track.timelineStartInfo.dts = data.dts;
|
|
|
44553 |
}
|
|
|
44554 |
if (track.minSegmentDts === undefined) {
|
|
|
44555 |
track.minSegmentDts = data.dts;
|
|
|
44556 |
} else {
|
|
|
44557 |
track.minSegmentDts = Math.min(track.minSegmentDts, data.dts);
|
|
|
44558 |
}
|
|
|
44559 |
if (track.maxSegmentDts === undefined) {
|
|
|
44560 |
track.maxSegmentDts = data.dts;
|
|
|
44561 |
} else {
|
|
|
44562 |
track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts);
|
|
|
44563 |
}
|
|
|
44564 |
}
|
|
|
44565 |
};
|
|
|
44566 |
/**
|
|
|
44567 |
* Clear values used to calculate the baseMediaDecodeTime between
|
|
|
44568 |
* tracks
|
|
|
44569 |
*/
|
|
|
44570 |
|
|
|
44571 |
var clearDtsInfo = function (track) {
|
|
|
44572 |
delete track.minSegmentDts;
|
|
|
44573 |
delete track.maxSegmentDts;
|
|
|
44574 |
delete track.minSegmentPts;
|
|
|
44575 |
delete track.maxSegmentPts;
|
|
|
44576 |
};
|
|
|
44577 |
/**
|
|
|
44578 |
* Calculate the track's baseMediaDecodeTime based on the earliest
|
|
|
44579 |
* DTS the transmuxer has ever seen and the minimum DTS for the
|
|
|
44580 |
* current track
|
|
|
44581 |
* @param track {object} track metadata configuration
|
|
|
44582 |
* @param keepOriginalTimestamps {boolean} If true, keep the timestamps
|
|
|
44583 |
* in the source; false to adjust the first segment to start at 0.
|
|
|
44584 |
*/
|
|
|
44585 |
|
|
|
44586 |
var calculateTrackBaseMediaDecodeTime = function (track, keepOriginalTimestamps) {
|
|
|
44587 |
var baseMediaDecodeTime,
|
|
|
44588 |
scale,
|
|
|
44589 |
minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero.
|
|
|
44590 |
|
|
|
44591 |
if (!keepOriginalTimestamps) {
|
|
|
44592 |
minSegmentDts -= track.timelineStartInfo.dts;
|
|
|
44593 |
} // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where
|
|
|
44594 |
// we want the start of the first segment to be placed
|
|
|
44595 |
|
|
|
44596 |
baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first
|
|
|
44597 |
|
|
|
44598 |
baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative
|
|
|
44599 |
|
|
|
44600 |
baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime);
|
|
|
44601 |
if (track.type === 'audio') {
|
|
|
44602 |
// Audio has a different clock equal to the sampling_rate so we need to
|
|
|
44603 |
// scale the PTS values into the clock rate of the track
|
|
|
44604 |
scale = track.samplerate / ONE_SECOND_IN_TS$3;
|
|
|
44605 |
baseMediaDecodeTime *= scale;
|
|
|
44606 |
baseMediaDecodeTime = Math.floor(baseMediaDecodeTime);
|
|
|
44607 |
}
|
|
|
44608 |
return baseMediaDecodeTime;
|
|
|
44609 |
};
|
|
|
44610 |
var trackDecodeInfo$1 = {
|
|
|
44611 |
clearDtsInfo: clearDtsInfo,
|
|
|
44612 |
calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime,
|
|
|
44613 |
collectDtsInfo: collectDtsInfo
|
|
|
44614 |
};
|
|
|
44615 |
/**
|
|
|
44616 |
* mux.js
|
|
|
44617 |
*
|
|
|
44618 |
* Copyright (c) Brightcove
|
|
|
44619 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44620 |
*
|
|
|
44621 |
* Reads in-band caption information from a video elementary
|
|
|
44622 |
* stream. Captions must follow the CEA-708 standard for injection
|
|
|
44623 |
* into an MPEG-2 transport streams.
|
|
|
44624 |
* @see https://en.wikipedia.org/wiki/CEA-708
|
|
|
44625 |
* @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
|
|
|
44626 |
*/
|
|
|
44627 |
// payload type field to indicate how they are to be
|
|
|
44628 |
// interpreted. CEAS-708 caption content is always transmitted with
|
|
|
44629 |
// payload type 0x04.
|
|
|
44630 |
|
|
|
44631 |
var USER_DATA_REGISTERED_ITU_T_T35 = 4,
|
|
|
44632 |
RBSP_TRAILING_BITS = 128;
|
|
|
44633 |
/**
|
|
|
44634 |
* Parse a supplemental enhancement information (SEI) NAL unit.
|
|
|
44635 |
* Stops parsing once a message of type ITU T T35 has been found.
|
|
|
44636 |
*
|
|
|
44637 |
* @param bytes {Uint8Array} the bytes of a SEI NAL unit
|
|
|
44638 |
* @return {object} the parsed SEI payload
|
|
|
44639 |
* @see Rec. ITU-T H.264, 7.3.2.3.1
|
|
|
44640 |
*/
|
|
|
44641 |
|
|
|
44642 |
var parseSei = function (bytes) {
|
|
|
44643 |
var i = 0,
|
|
|
44644 |
result = {
|
|
|
44645 |
payloadType: -1,
|
|
|
44646 |
payloadSize: 0
|
|
|
44647 |
},
|
|
|
44648 |
payloadType = 0,
|
|
|
44649 |
payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message
|
|
|
44650 |
|
|
|
44651 |
while (i < bytes.byteLength) {
|
|
|
44652 |
// stop once we have hit the end of the sei_rbsp
|
|
|
44653 |
if (bytes[i] === RBSP_TRAILING_BITS) {
|
|
|
44654 |
break;
|
|
|
44655 |
} // Parse payload type
|
|
|
44656 |
|
|
|
44657 |
while (bytes[i] === 0xFF) {
|
|
|
44658 |
payloadType += 255;
|
|
|
44659 |
i++;
|
|
|
44660 |
}
|
|
|
44661 |
payloadType += bytes[i++]; // Parse payload size
|
|
|
44662 |
|
|
|
44663 |
while (bytes[i] === 0xFF) {
|
|
|
44664 |
payloadSize += 255;
|
|
|
44665 |
i++;
|
|
|
44666 |
}
|
|
|
44667 |
payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break
|
|
|
44668 |
// there can only ever be one caption message in a frame's sei
|
|
|
44669 |
|
|
|
44670 |
if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) {
|
|
|
44671 |
var userIdentifier = String.fromCharCode(bytes[i + 3], bytes[i + 4], bytes[i + 5], bytes[i + 6]);
|
|
|
44672 |
if (userIdentifier === 'GA94') {
|
|
|
44673 |
result.payloadType = payloadType;
|
|
|
44674 |
result.payloadSize = payloadSize;
|
|
|
44675 |
result.payload = bytes.subarray(i, i + payloadSize);
|
|
|
44676 |
break;
|
|
|
44677 |
} else {
|
|
|
44678 |
result.payload = void 0;
|
|
|
44679 |
}
|
|
|
44680 |
} // skip the payload and parse the next message
|
|
|
44681 |
|
|
|
44682 |
i += payloadSize;
|
|
|
44683 |
payloadType = 0;
|
|
|
44684 |
payloadSize = 0;
|
|
|
44685 |
}
|
|
|
44686 |
return result;
|
|
|
44687 |
}; // see ANSI/SCTE 128-1 (2013), section 8.1
|
|
|
44688 |
|
|
|
44689 |
var parseUserData = function (sei) {
|
|
|
44690 |
// itu_t_t35_contry_code must be 181 (United States) for
|
|
|
44691 |
// captions
|
|
|
44692 |
if (sei.payload[0] !== 181) {
|
|
|
44693 |
return null;
|
|
|
44694 |
} // itu_t_t35_provider_code should be 49 (ATSC) for captions
|
|
|
44695 |
|
|
|
44696 |
if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) {
|
|
|
44697 |
return null;
|
|
|
44698 |
} // the user_identifier should be "GA94" to indicate ATSC1 data
|
|
|
44699 |
|
|
|
44700 |
if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') {
|
|
|
44701 |
return null;
|
|
|
44702 |
} // finally, user_data_type_code should be 0x03 for caption data
|
|
|
44703 |
|
|
|
44704 |
if (sei.payload[7] !== 0x03) {
|
|
|
44705 |
return null;
|
|
|
44706 |
} // return the user_data_type_structure and strip the trailing
|
|
|
44707 |
// marker bits
|
|
|
44708 |
|
|
|
44709 |
return sei.payload.subarray(8, sei.payload.length - 1);
|
|
|
44710 |
}; // see CEA-708-D, section 4.4
|
|
|
44711 |
|
|
|
44712 |
var parseCaptionPackets = function (pts, userData) {
|
|
|
44713 |
var results = [],
|
|
|
44714 |
i,
|
|
|
44715 |
count,
|
|
|
44716 |
offset,
|
|
|
44717 |
data; // if this is just filler, return immediately
|
|
|
44718 |
|
|
|
44719 |
if (!(userData[0] & 0x40)) {
|
|
|
44720 |
return results;
|
|
|
44721 |
} // parse out the cc_data_1 and cc_data_2 fields
|
|
|
44722 |
|
|
|
44723 |
count = userData[0] & 0x1f;
|
|
|
44724 |
for (i = 0; i < count; i++) {
|
|
|
44725 |
offset = i * 3;
|
|
|
44726 |
data = {
|
|
|
44727 |
type: userData[offset + 2] & 0x03,
|
|
|
44728 |
pts: pts
|
|
|
44729 |
}; // capture cc data when cc_valid is 1
|
|
|
44730 |
|
|
|
44731 |
if (userData[offset + 2] & 0x04) {
|
|
|
44732 |
data.ccData = userData[offset + 3] << 8 | userData[offset + 4];
|
|
|
44733 |
results.push(data);
|
|
|
44734 |
}
|
|
|
44735 |
}
|
|
|
44736 |
return results;
|
|
|
44737 |
};
|
|
|
44738 |
var discardEmulationPreventionBytes$1 = function (data) {
|
|
|
44739 |
var length = data.byteLength,
|
|
|
44740 |
emulationPreventionBytesPositions = [],
|
|
|
44741 |
i = 1,
|
|
|
44742 |
newLength,
|
|
|
44743 |
newData; // Find all `Emulation Prevention Bytes`
|
|
|
44744 |
|
|
|
44745 |
while (i < length - 2) {
|
|
|
44746 |
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
|
|
|
44747 |
emulationPreventionBytesPositions.push(i + 2);
|
|
|
44748 |
i += 2;
|
|
|
44749 |
} else {
|
|
|
44750 |
i++;
|
|
|
44751 |
}
|
|
|
44752 |
} // If no Emulation Prevention Bytes were found just return the original
|
|
|
44753 |
// array
|
|
|
44754 |
|
|
|
44755 |
if (emulationPreventionBytesPositions.length === 0) {
|
|
|
44756 |
return data;
|
|
|
44757 |
} // Create a new array to hold the NAL unit data
|
|
|
44758 |
|
|
|
44759 |
newLength = length - emulationPreventionBytesPositions.length;
|
|
|
44760 |
newData = new Uint8Array(newLength);
|
|
|
44761 |
var sourceIndex = 0;
|
|
|
44762 |
for (i = 0; i < newLength; sourceIndex++, i++) {
|
|
|
44763 |
if (sourceIndex === emulationPreventionBytesPositions[0]) {
|
|
|
44764 |
// Skip this byte
|
|
|
44765 |
sourceIndex++; // Remove this position index
|
|
|
44766 |
|
|
|
44767 |
emulationPreventionBytesPositions.shift();
|
|
|
44768 |
}
|
|
|
44769 |
newData[i] = data[sourceIndex];
|
|
|
44770 |
}
|
|
|
44771 |
return newData;
|
|
|
44772 |
}; // exports
|
|
|
44773 |
|
|
|
44774 |
var captionPacketParser = {
|
|
|
44775 |
parseSei: parseSei,
|
|
|
44776 |
parseUserData: parseUserData,
|
|
|
44777 |
parseCaptionPackets: parseCaptionPackets,
|
|
|
44778 |
discardEmulationPreventionBytes: discardEmulationPreventionBytes$1,
|
|
|
44779 |
USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35
|
|
|
44780 |
};
|
|
|
44781 |
/**
|
|
|
44782 |
* mux.js
|
|
|
44783 |
*
|
|
|
44784 |
* Copyright (c) Brightcove
|
|
|
44785 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
44786 |
*
|
|
|
44787 |
* Reads in-band caption information from a video elementary
|
|
|
44788 |
* stream. Captions must follow the CEA-708 standard for injection
|
|
|
44789 |
* into an MPEG-2 transport streams.
|
|
|
44790 |
* @see https://en.wikipedia.org/wiki/CEA-708
|
|
|
44791 |
* @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
|
|
|
44792 |
*/
|
|
|
44793 |
// Link To Transport
|
|
|
44794 |
// -----------------
|
|
|
44795 |
|
|
|
44796 |
var Stream$7 = stream;
|
|
|
44797 |
var cea708Parser = captionPacketParser;
|
|
|
44798 |
var CaptionStream$2 = function (options) {
|
|
|
44799 |
options = options || {};
|
|
|
44800 |
CaptionStream$2.prototype.init.call(this); // parse708captions flag, default to true
|
|
|
44801 |
|
|
|
44802 |
this.parse708captions_ = typeof options.parse708captions === 'boolean' ? options.parse708captions : true;
|
|
|
44803 |
this.captionPackets_ = [];
|
|
|
44804 |
this.ccStreams_ = [new Cea608Stream(0, 0),
|
|
|
44805 |
// eslint-disable-line no-use-before-define
|
|
|
44806 |
new Cea608Stream(0, 1),
|
|
|
44807 |
// eslint-disable-line no-use-before-define
|
|
|
44808 |
new Cea608Stream(1, 0),
|
|
|
44809 |
// eslint-disable-line no-use-before-define
|
|
|
44810 |
new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define
|
|
|
44811 |
];
|
|
|
44812 |
|
|
|
44813 |
if (this.parse708captions_) {
|
|
|
44814 |
this.cc708Stream_ = new Cea708Stream({
|
|
|
44815 |
captionServices: options.captionServices
|
|
|
44816 |
}); // eslint-disable-line no-use-before-define
|
|
|
44817 |
}
|
|
|
44818 |
|
|
|
44819 |
this.reset(); // forward data and done events from CCs to this CaptionStream
|
|
|
44820 |
|
|
|
44821 |
this.ccStreams_.forEach(function (cc) {
|
|
|
44822 |
cc.on('data', this.trigger.bind(this, 'data'));
|
|
|
44823 |
cc.on('partialdone', this.trigger.bind(this, 'partialdone'));
|
|
|
44824 |
cc.on('done', this.trigger.bind(this, 'done'));
|
|
|
44825 |
}, this);
|
|
|
44826 |
if (this.parse708captions_) {
|
|
|
44827 |
this.cc708Stream_.on('data', this.trigger.bind(this, 'data'));
|
|
|
44828 |
this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone'));
|
|
|
44829 |
this.cc708Stream_.on('done', this.trigger.bind(this, 'done'));
|
|
|
44830 |
}
|
|
|
44831 |
};
|
|
|
44832 |
CaptionStream$2.prototype = new Stream$7();
|
|
|
44833 |
CaptionStream$2.prototype.push = function (event) {
|
|
|
44834 |
var sei, userData, newCaptionPackets; // only examine SEI NALs
|
|
|
44835 |
|
|
|
44836 |
if (event.nalUnitType !== 'sei_rbsp') {
|
|
|
44837 |
return;
|
|
|
44838 |
} // parse the sei
|
|
|
44839 |
|
|
|
44840 |
sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip
|
|
|
44841 |
|
|
|
44842 |
if (!sei.payload) {
|
|
|
44843 |
return;
|
|
|
44844 |
} // ignore everything but user_data_registered_itu_t_t35
|
|
|
44845 |
|
|
|
44846 |
if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) {
|
|
|
44847 |
return;
|
|
|
44848 |
} // parse out the user data payload
|
|
|
44849 |
|
|
|
44850 |
userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData
|
|
|
44851 |
|
|
|
44852 |
if (!userData) {
|
|
|
44853 |
return;
|
|
|
44854 |
} // Sometimes, the same segment # will be downloaded twice. To stop the
|
|
|
44855 |
// caption data from being processed twice, we track the latest dts we've
|
|
|
44856 |
// received and ignore everything with a dts before that. However, since
|
|
|
44857 |
// data for a specific dts can be split across packets on either side of
|
|
|
44858 |
// a segment boundary, we need to make sure we *don't* ignore the packets
|
|
|
44859 |
// from the *next* segment that have dts === this.latestDts_. By constantly
|
|
|
44860 |
// tracking the number of packets received with dts === this.latestDts_, we
|
|
|
44861 |
// know how many should be ignored once we start receiving duplicates.
|
|
|
44862 |
|
|
|
44863 |
if (event.dts < this.latestDts_) {
|
|
|
44864 |
// We've started getting older data, so set the flag.
|
|
|
44865 |
this.ignoreNextEqualDts_ = true;
|
|
|
44866 |
return;
|
|
|
44867 |
} else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) {
|
|
|
44868 |
this.numSameDts_--;
|
|
|
44869 |
if (!this.numSameDts_) {
|
|
|
44870 |
// We've received the last duplicate packet, time to start processing again
|
|
|
44871 |
this.ignoreNextEqualDts_ = false;
|
|
|
44872 |
}
|
|
|
44873 |
return;
|
|
|
44874 |
} // parse out CC data packets and save them for later
|
|
|
44875 |
|
|
|
44876 |
newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData);
|
|
|
44877 |
this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets);
|
|
|
44878 |
if (this.latestDts_ !== event.dts) {
|
|
|
44879 |
this.numSameDts_ = 0;
|
|
|
44880 |
}
|
|
|
44881 |
this.numSameDts_++;
|
|
|
44882 |
this.latestDts_ = event.dts;
|
|
|
44883 |
};
|
|
|
44884 |
CaptionStream$2.prototype.flushCCStreams = function (flushType) {
|
|
|
44885 |
this.ccStreams_.forEach(function (cc) {
|
|
|
44886 |
return flushType === 'flush' ? cc.flush() : cc.partialFlush();
|
|
|
44887 |
}, this);
|
|
|
44888 |
};
|
|
|
44889 |
CaptionStream$2.prototype.flushStream = function (flushType) {
|
|
|
44890 |
// make sure we actually parsed captions before proceeding
|
|
|
44891 |
if (!this.captionPackets_.length) {
|
|
|
44892 |
this.flushCCStreams(flushType);
|
|
|
44893 |
return;
|
|
|
44894 |
} // In Chrome, the Array#sort function is not stable so add a
|
|
|
44895 |
// presortIndex that we can use to ensure we get a stable-sort
|
|
|
44896 |
|
|
|
44897 |
this.captionPackets_.forEach(function (elem, idx) {
|
|
|
44898 |
elem.presortIndex = idx;
|
|
|
44899 |
}); // sort caption byte-pairs based on their PTS values
|
|
|
44900 |
|
|
|
44901 |
this.captionPackets_.sort(function (a, b) {
|
|
|
44902 |
if (a.pts === b.pts) {
|
|
|
44903 |
return a.presortIndex - b.presortIndex;
|
|
|
44904 |
}
|
|
|
44905 |
return a.pts - b.pts;
|
|
|
44906 |
});
|
|
|
44907 |
this.captionPackets_.forEach(function (packet) {
|
|
|
44908 |
if (packet.type < 2) {
|
|
|
44909 |
// Dispatch packet to the right Cea608Stream
|
|
|
44910 |
this.dispatchCea608Packet(packet);
|
|
|
44911 |
} else {
|
|
|
44912 |
// Dispatch packet to the Cea708Stream
|
|
|
44913 |
this.dispatchCea708Packet(packet);
|
|
|
44914 |
}
|
|
|
44915 |
}, this);
|
|
|
44916 |
this.captionPackets_.length = 0;
|
|
|
44917 |
this.flushCCStreams(flushType);
|
|
|
44918 |
};
|
|
|
44919 |
CaptionStream$2.prototype.flush = function () {
|
|
|
44920 |
return this.flushStream('flush');
|
|
|
44921 |
}; // Only called if handling partial data
|
|
|
44922 |
|
|
|
44923 |
CaptionStream$2.prototype.partialFlush = function () {
|
|
|
44924 |
return this.flushStream('partialFlush');
|
|
|
44925 |
};
|
|
|
44926 |
CaptionStream$2.prototype.reset = function () {
|
|
|
44927 |
this.latestDts_ = null;
|
|
|
44928 |
this.ignoreNextEqualDts_ = false;
|
|
|
44929 |
this.numSameDts_ = 0;
|
|
|
44930 |
this.activeCea608Channel_ = [null, null];
|
|
|
44931 |
this.ccStreams_.forEach(function (ccStream) {
|
|
|
44932 |
ccStream.reset();
|
|
|
44933 |
});
|
|
|
44934 |
}; // From the CEA-608 spec:
|
|
|
44935 |
|
|
|
44936 |
/*
|
|
|
44937 |
* When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed
|
|
|
44938 |
* by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is
|
|
|
44939 |
* used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair
|
|
|
44940 |
* and subsequent data should then be processed according to the FCC rules. It may be necessary for the
|
|
|
44941 |
* line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD)
|
|
|
44942 |
* to switch to captioning or Text.
|
|
|
44943 |
*/
|
|
|
44944 |
// With that in mind, we ignore any data between an XDS control code and a
|
|
|
44945 |
// subsequent closed-captioning control code.
|
|
|
44946 |
|
|
|
44947 |
CaptionStream$2.prototype.dispatchCea608Packet = function (packet) {
|
|
|
44948 |
// NOTE: packet.type is the CEA608 field
|
|
|
44949 |
if (this.setsTextOrXDSActive(packet)) {
|
|
|
44950 |
this.activeCea608Channel_[packet.type] = null;
|
|
|
44951 |
} else if (this.setsChannel1Active(packet)) {
|
|
|
44952 |
this.activeCea608Channel_[packet.type] = 0;
|
|
|
44953 |
} else if (this.setsChannel2Active(packet)) {
|
|
|
44954 |
this.activeCea608Channel_[packet.type] = 1;
|
|
|
44955 |
}
|
|
|
44956 |
if (this.activeCea608Channel_[packet.type] === null) {
|
|
|
44957 |
// If we haven't received anything to set the active channel, or the
|
|
|
44958 |
// packets are Text/XDS data, discard the data; we don't want jumbled
|
|
|
44959 |
// captions
|
|
|
44960 |
return;
|
|
|
44961 |
}
|
|
|
44962 |
this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet);
|
|
|
44963 |
};
|
|
|
44964 |
CaptionStream$2.prototype.setsChannel1Active = function (packet) {
|
|
|
44965 |
return (packet.ccData & 0x7800) === 0x1000;
|
|
|
44966 |
};
|
|
|
44967 |
CaptionStream$2.prototype.setsChannel2Active = function (packet) {
|
|
|
44968 |
return (packet.ccData & 0x7800) === 0x1800;
|
|
|
44969 |
};
|
|
|
44970 |
CaptionStream$2.prototype.setsTextOrXDSActive = function (packet) {
|
|
|
44971 |
return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a;
|
|
|
44972 |
};
|
|
|
44973 |
CaptionStream$2.prototype.dispatchCea708Packet = function (packet) {
|
|
|
44974 |
if (this.parse708captions_) {
|
|
|
44975 |
this.cc708Stream_.push(packet);
|
|
|
44976 |
}
|
|
|
44977 |
}; // ----------------------
|
|
|
44978 |
// Session to Application
|
|
|
44979 |
// ----------------------
|
|
|
44980 |
// This hash maps special and extended character codes to their
|
|
|
44981 |
// proper Unicode equivalent. The first one-byte key is just a
|
|
|
44982 |
// non-standard character code. The two-byte keys that follow are
|
|
|
44983 |
// the extended CEA708 character codes, along with the preceding
|
|
|
44984 |
// 0x10 extended character byte to distinguish these codes from
|
|
|
44985 |
// non-extended character codes. Every CEA708 character code that
|
|
|
44986 |
// is not in this object maps directly to a standard unicode
|
|
|
44987 |
// character code.
|
|
|
44988 |
// The transparent space and non-breaking transparent space are
|
|
|
44989 |
// technically not fully supported since there is no code to
|
|
|
44990 |
// make them transparent, so they have normal non-transparent
|
|
|
44991 |
// stand-ins.
|
|
|
44992 |
// The special closed caption (CC) character isn't a standard
|
|
|
44993 |
// unicode character, so a fairly similar unicode character was
|
|
|
44994 |
// chosen in it's place.
|
|
|
44995 |
|
|
|
44996 |
var CHARACTER_TRANSLATION_708 = {
|
|
|
44997 |
0x7f: 0x266a,
|
|
|
44998 |
// ♪
|
|
|
44999 |
0x1020: 0x20,
|
|
|
45000 |
// Transparent Space
|
|
|
45001 |
0x1021: 0xa0,
|
|
|
45002 |
// Nob-breaking Transparent Space
|
|
|
45003 |
0x1025: 0x2026,
|
|
|
45004 |
// …
|
|
|
45005 |
0x102a: 0x0160,
|
|
|
45006 |
// Š
|
|
|
45007 |
0x102c: 0x0152,
|
|
|
45008 |
// Å’
|
|
|
45009 |
0x1030: 0x2588,
|
|
|
45010 |
// █
|
|
|
45011 |
0x1031: 0x2018,
|
|
|
45012 |
// ‘
|
|
|
45013 |
0x1032: 0x2019,
|
|
|
45014 |
// ’
|
|
|
45015 |
0x1033: 0x201c,
|
|
|
45016 |
// “
|
|
|
45017 |
0x1034: 0x201d,
|
|
|
45018 |
// ”
|
|
|
45019 |
0x1035: 0x2022,
|
|
|
45020 |
// •
|
|
|
45021 |
0x1039: 0x2122,
|
|
|
45022 |
// â„¢
|
|
|
45023 |
0x103a: 0x0161,
|
|
|
45024 |
// š
|
|
|
45025 |
0x103c: 0x0153,
|
|
|
45026 |
// Å“
|
|
|
45027 |
0x103d: 0x2120,
|
|
|
45028 |
// ℠
|
|
|
45029 |
0x103f: 0x0178,
|
|
|
45030 |
// Ÿ
|
|
|
45031 |
0x1076: 0x215b,
|
|
|
45032 |
// ⅛
|
|
|
45033 |
0x1077: 0x215c,
|
|
|
45034 |
// ⅜
|
|
|
45035 |
0x1078: 0x215d,
|
|
|
45036 |
// ⅝
|
|
|
45037 |
0x1079: 0x215e,
|
|
|
45038 |
// ⅞
|
|
|
45039 |
0x107a: 0x23d0,
|
|
|
45040 |
// ⏐
|
|
|
45041 |
0x107b: 0x23a4,
|
|
|
45042 |
// ⎤
|
|
|
45043 |
0x107c: 0x23a3,
|
|
|
45044 |
// ⎣
|
|
|
45045 |
0x107d: 0x23af,
|
|
|
45046 |
// ⎯
|
|
|
45047 |
0x107e: 0x23a6,
|
|
|
45048 |
// ⎦
|
|
|
45049 |
0x107f: 0x23a1,
|
|
|
45050 |
// ⎡
|
|
|
45051 |
0x10a0: 0x3138 // ㄸ (CC char)
|
|
|
45052 |
};
|
|
|
45053 |
|
|
|
45054 |
var get708CharFromCode = function (code) {
|
|
|
45055 |
var newCode = CHARACTER_TRANSLATION_708[code] || code;
|
|
|
45056 |
if (code & 0x1000 && code === newCode) {
|
|
|
45057 |
// Invalid extended code
|
|
|
45058 |
return '';
|
|
|
45059 |
}
|
|
|
45060 |
return String.fromCharCode(newCode);
|
|
|
45061 |
};
|
|
|
45062 |
var within708TextBlock = function (b) {
|
|
|
45063 |
return 0x20 <= b && b <= 0x7f || 0xa0 <= b && b <= 0xff;
|
|
|
45064 |
};
|
|
|
45065 |
var Cea708Window = function (windowNum) {
|
|
|
45066 |
this.windowNum = windowNum;
|
|
|
45067 |
this.reset();
|
|
|
45068 |
};
|
|
|
45069 |
Cea708Window.prototype.reset = function () {
|
|
|
45070 |
this.clearText();
|
|
|
45071 |
this.pendingNewLine = false;
|
|
|
45072 |
this.winAttr = {};
|
|
|
45073 |
this.penAttr = {};
|
|
|
45074 |
this.penLoc = {};
|
|
|
45075 |
this.penColor = {}; // These default values are arbitrary,
|
|
|
45076 |
// defineWindow will usually override them
|
|
|
45077 |
|
|
|
45078 |
this.visible = 0;
|
|
|
45079 |
this.rowLock = 0;
|
|
|
45080 |
this.columnLock = 0;
|
|
|
45081 |
this.priority = 0;
|
|
|
45082 |
this.relativePositioning = 0;
|
|
|
45083 |
this.anchorVertical = 0;
|
|
|
45084 |
this.anchorHorizontal = 0;
|
|
|
45085 |
this.anchorPoint = 0;
|
|
|
45086 |
this.rowCount = 1;
|
|
|
45087 |
this.virtualRowCount = this.rowCount + 1;
|
|
|
45088 |
this.columnCount = 41;
|
|
|
45089 |
this.windowStyle = 0;
|
|
|
45090 |
this.penStyle = 0;
|
|
|
45091 |
};
|
|
|
45092 |
Cea708Window.prototype.getText = function () {
|
|
|
45093 |
return this.rows.join('\n');
|
|
|
45094 |
};
|
|
|
45095 |
Cea708Window.prototype.clearText = function () {
|
|
|
45096 |
this.rows = [''];
|
|
|
45097 |
this.rowIdx = 0;
|
|
|
45098 |
};
|
|
|
45099 |
Cea708Window.prototype.newLine = function (pts) {
|
|
|
45100 |
if (this.rows.length >= this.virtualRowCount && typeof this.beforeRowOverflow === 'function') {
|
|
|
45101 |
this.beforeRowOverflow(pts);
|
|
|
45102 |
}
|
|
|
45103 |
if (this.rows.length > 0) {
|
|
|
45104 |
this.rows.push('');
|
|
|
45105 |
this.rowIdx++;
|
|
|
45106 |
} // Show all virtual rows since there's no visible scrolling
|
|
|
45107 |
|
|
|
45108 |
while (this.rows.length > this.virtualRowCount) {
|
|
|
45109 |
this.rows.shift();
|
|
|
45110 |
this.rowIdx--;
|
|
|
45111 |
}
|
|
|
45112 |
};
|
|
|
45113 |
Cea708Window.prototype.isEmpty = function () {
|
|
|
45114 |
if (this.rows.length === 0) {
|
|
|
45115 |
return true;
|
|
|
45116 |
} else if (this.rows.length === 1) {
|
|
|
45117 |
return this.rows[0] === '';
|
|
|
45118 |
}
|
|
|
45119 |
return false;
|
|
|
45120 |
};
|
|
|
45121 |
Cea708Window.prototype.addText = function (text) {
|
|
|
45122 |
this.rows[this.rowIdx] += text;
|
|
|
45123 |
};
|
|
|
45124 |
Cea708Window.prototype.backspace = function () {
|
|
|
45125 |
if (!this.isEmpty()) {
|
|
|
45126 |
var row = this.rows[this.rowIdx];
|
|
|
45127 |
this.rows[this.rowIdx] = row.substr(0, row.length - 1);
|
|
|
45128 |
}
|
|
|
45129 |
};
|
|
|
45130 |
var Cea708Service = function (serviceNum, encoding, stream) {
|
|
|
45131 |
this.serviceNum = serviceNum;
|
|
|
45132 |
this.text = '';
|
|
|
45133 |
this.currentWindow = new Cea708Window(-1);
|
|
|
45134 |
this.windows = [];
|
|
|
45135 |
this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided
|
|
|
45136 |
|
|
|
45137 |
if (typeof encoding === 'string') {
|
|
|
45138 |
this.createTextDecoder(encoding);
|
|
|
45139 |
}
|
|
|
45140 |
};
|
|
|
45141 |
/**
|
|
|
45142 |
* Initialize service windows
|
|
|
45143 |
* Must be run before service use
|
|
|
45144 |
*
|
|
|
45145 |
* @param {Integer} pts PTS value
|
|
|
45146 |
* @param {Function} beforeRowOverflow Function to execute before row overflow of a window
|
|
|
45147 |
*/
|
|
|
45148 |
|
|
|
45149 |
Cea708Service.prototype.init = function (pts, beforeRowOverflow) {
|
|
|
45150 |
this.startPts = pts;
|
|
|
45151 |
for (var win = 0; win < 8; win++) {
|
|
|
45152 |
this.windows[win] = new Cea708Window(win);
|
|
|
45153 |
if (typeof beforeRowOverflow === 'function') {
|
|
|
45154 |
this.windows[win].beforeRowOverflow = beforeRowOverflow;
|
|
|
45155 |
}
|
|
|
45156 |
}
|
|
|
45157 |
};
|
|
|
45158 |
/**
|
|
|
45159 |
* Set current window of service to be affected by commands
|
|
|
45160 |
*
|
|
|
45161 |
* @param {Integer} windowNum Window number
|
|
|
45162 |
*/
|
|
|
45163 |
|
|
|
45164 |
Cea708Service.prototype.setCurrentWindow = function (windowNum) {
|
|
|
45165 |
this.currentWindow = this.windows[windowNum];
|
|
|
45166 |
};
|
|
|
45167 |
/**
|
|
|
45168 |
* Try to create a TextDecoder if it is natively supported
|
|
|
45169 |
*/
|
|
|
45170 |
|
|
|
45171 |
Cea708Service.prototype.createTextDecoder = function (encoding) {
|
|
|
45172 |
if (typeof TextDecoder === 'undefined') {
|
|
|
45173 |
this.stream.trigger('log', {
|
|
|
45174 |
level: 'warn',
|
|
|
45175 |
message: 'The `encoding` option is unsupported without TextDecoder support'
|
|
|
45176 |
});
|
|
|
45177 |
} else {
|
|
|
45178 |
try {
|
|
|
45179 |
this.textDecoder_ = new TextDecoder(encoding);
|
|
|
45180 |
} catch (error) {
|
|
|
45181 |
this.stream.trigger('log', {
|
|
|
45182 |
level: 'warn',
|
|
|
45183 |
message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error
|
|
|
45184 |
});
|
|
|
45185 |
}
|
|
|
45186 |
}
|
|
|
45187 |
};
|
|
|
45188 |
var Cea708Stream = function (options) {
|
|
|
45189 |
options = options || {};
|
|
|
45190 |
Cea708Stream.prototype.init.call(this);
|
|
|
45191 |
var self = this;
|
|
|
45192 |
var captionServices = options.captionServices || {};
|
|
|
45193 |
var captionServiceEncodings = {};
|
|
|
45194 |
var serviceProps; // Get service encodings from captionServices option block
|
|
|
45195 |
|
|
|
45196 |
Object.keys(captionServices).forEach(serviceName => {
|
|
|
45197 |
serviceProps = captionServices[serviceName];
|
|
|
45198 |
if (/^SERVICE/.test(serviceName)) {
|
|
|
45199 |
captionServiceEncodings[serviceName] = serviceProps.encoding;
|
|
|
45200 |
}
|
|
|
45201 |
});
|
|
|
45202 |
this.serviceEncodings = captionServiceEncodings;
|
|
|
45203 |
this.current708Packet = null;
|
|
|
45204 |
this.services = {};
|
|
|
45205 |
this.push = function (packet) {
|
|
|
45206 |
if (packet.type === 3) {
|
|
|
45207 |
// 708 packet start
|
|
|
45208 |
self.new708Packet();
|
|
|
45209 |
self.add708Bytes(packet);
|
|
|
45210 |
} else {
|
|
|
45211 |
if (self.current708Packet === null) {
|
|
|
45212 |
// This should only happen at the start of a file if there's no packet start.
|
|
|
45213 |
self.new708Packet();
|
|
|
45214 |
}
|
|
|
45215 |
self.add708Bytes(packet);
|
|
|
45216 |
}
|
|
|
45217 |
};
|
|
|
45218 |
};
|
|
|
45219 |
Cea708Stream.prototype = new Stream$7();
|
|
|
45220 |
/**
|
|
|
45221 |
* Push current 708 packet, create new 708 packet.
|
|
|
45222 |
*/
|
|
|
45223 |
|
|
|
45224 |
Cea708Stream.prototype.new708Packet = function () {
|
|
|
45225 |
if (this.current708Packet !== null) {
|
|
|
45226 |
this.push708Packet();
|
|
|
45227 |
}
|
|
|
45228 |
this.current708Packet = {
|
|
|
45229 |
data: [],
|
|
|
45230 |
ptsVals: []
|
|
|
45231 |
};
|
|
|
45232 |
};
|
|
|
45233 |
/**
|
|
|
45234 |
* Add pts and both bytes from packet into current 708 packet.
|
|
|
45235 |
*/
|
|
|
45236 |
|
|
|
45237 |
Cea708Stream.prototype.add708Bytes = function (packet) {
|
|
|
45238 |
var data = packet.ccData;
|
|
|
45239 |
var byte0 = data >>> 8;
|
|
|
45240 |
var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec
|
|
|
45241 |
// that service blocks will always line up with byte pairs.
|
|
|
45242 |
|
|
|
45243 |
this.current708Packet.ptsVals.push(packet.pts);
|
|
|
45244 |
this.current708Packet.data.push(byte0);
|
|
|
45245 |
this.current708Packet.data.push(byte1);
|
|
|
45246 |
};
|
|
|
45247 |
/**
|
|
|
45248 |
* Parse completed 708 packet into service blocks and push each service block.
|
|
|
45249 |
*/
|
|
|
45250 |
|
|
|
45251 |
Cea708Stream.prototype.push708Packet = function () {
|
|
|
45252 |
var packet708 = this.current708Packet;
|
|
|
45253 |
var packetData = packet708.data;
|
|
|
45254 |
var serviceNum = null;
|
|
|
45255 |
var blockSize = null;
|
|
|
45256 |
var i = 0;
|
|
|
45257 |
var b = packetData[i++];
|
|
|
45258 |
packet708.seq = b >> 6;
|
|
|
45259 |
packet708.sizeCode = b & 0x3f; // 0b00111111;
|
|
|
45260 |
|
|
|
45261 |
for (; i < packetData.length; i++) {
|
|
|
45262 |
b = packetData[i++];
|
|
|
45263 |
serviceNum = b >> 5;
|
|
|
45264 |
blockSize = b & 0x1f; // 0b00011111
|
|
|
45265 |
|
|
|
45266 |
if (serviceNum === 7 && blockSize > 0) {
|
|
|
45267 |
// Extended service num
|
|
|
45268 |
b = packetData[i++];
|
|
|
45269 |
serviceNum = b;
|
|
|
45270 |
}
|
|
|
45271 |
this.pushServiceBlock(serviceNum, i, blockSize);
|
|
|
45272 |
if (blockSize > 0) {
|
|
|
45273 |
i += blockSize - 1;
|
|
|
45274 |
}
|
|
|
45275 |
}
|
|
|
45276 |
};
|
|
|
45277 |
/**
|
|
|
45278 |
* Parse service block, execute commands, read text.
|
|
|
45279 |
*
|
|
|
45280 |
* Note: While many of these commands serve important purposes,
|
|
|
45281 |
* many others just parse out the parameters or attributes, but
|
|
|
45282 |
* nothing is done with them because this is not a full and complete
|
|
|
45283 |
* implementation of the entire 708 spec.
|
|
|
45284 |
*
|
|
|
45285 |
* @param {Integer} serviceNum Service number
|
|
|
45286 |
* @param {Integer} start Start index of the 708 packet data
|
|
|
45287 |
* @param {Integer} size Block size
|
|
|
45288 |
*/
|
|
|
45289 |
|
|
|
45290 |
Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) {
|
|
|
45291 |
var b;
|
|
|
45292 |
var i = start;
|
|
|
45293 |
var packetData = this.current708Packet.data;
|
|
|
45294 |
var service = this.services[serviceNum];
|
|
|
45295 |
if (!service) {
|
|
|
45296 |
service = this.initService(serviceNum, i);
|
|
|
45297 |
}
|
|
|
45298 |
for (; i < start + size && i < packetData.length; i++) {
|
|
|
45299 |
b = packetData[i];
|
|
|
45300 |
if (within708TextBlock(b)) {
|
|
|
45301 |
i = this.handleText(i, service);
|
|
|
45302 |
} else if (b === 0x18) {
|
|
|
45303 |
i = this.multiByteCharacter(i, service);
|
|
|
45304 |
} else if (b === 0x10) {
|
|
|
45305 |
i = this.extendedCommands(i, service);
|
|
|
45306 |
} else if (0x80 <= b && b <= 0x87) {
|
|
|
45307 |
i = this.setCurrentWindow(i, service);
|
|
|
45308 |
} else if (0x98 <= b && b <= 0x9f) {
|
|
|
45309 |
i = this.defineWindow(i, service);
|
|
|
45310 |
} else if (b === 0x88) {
|
|
|
45311 |
i = this.clearWindows(i, service);
|
|
|
45312 |
} else if (b === 0x8c) {
|
|
|
45313 |
i = this.deleteWindows(i, service);
|
|
|
45314 |
} else if (b === 0x89) {
|
|
|
45315 |
i = this.displayWindows(i, service);
|
|
|
45316 |
} else if (b === 0x8a) {
|
|
|
45317 |
i = this.hideWindows(i, service);
|
|
|
45318 |
} else if (b === 0x8b) {
|
|
|
45319 |
i = this.toggleWindows(i, service);
|
|
|
45320 |
} else if (b === 0x97) {
|
|
|
45321 |
i = this.setWindowAttributes(i, service);
|
|
|
45322 |
} else if (b === 0x90) {
|
|
|
45323 |
i = this.setPenAttributes(i, service);
|
|
|
45324 |
} else if (b === 0x91) {
|
|
|
45325 |
i = this.setPenColor(i, service);
|
|
|
45326 |
} else if (b === 0x92) {
|
|
|
45327 |
i = this.setPenLocation(i, service);
|
|
|
45328 |
} else if (b === 0x8f) {
|
|
|
45329 |
service = this.reset(i, service);
|
|
|
45330 |
} else if (b === 0x08) {
|
|
|
45331 |
// BS: Backspace
|
|
|
45332 |
service.currentWindow.backspace();
|
|
|
45333 |
} else if (b === 0x0c) {
|
|
|
45334 |
// FF: Form feed
|
|
|
45335 |
service.currentWindow.clearText();
|
|
|
45336 |
} else if (b === 0x0d) {
|
|
|
45337 |
// CR: Carriage return
|
|
|
45338 |
service.currentWindow.pendingNewLine = true;
|
|
|
45339 |
} else if (b === 0x0e) {
|
|
|
45340 |
// HCR: Horizontal carriage return
|
|
|
45341 |
service.currentWindow.clearText();
|
|
|
45342 |
} else if (b === 0x8d) {
|
|
|
45343 |
// DLY: Delay, nothing to do
|
|
|
45344 |
i++;
|
|
|
45345 |
} else ;
|
|
|
45346 |
}
|
|
|
45347 |
};
|
|
|
45348 |
/**
|
|
|
45349 |
* Execute an extended command
|
|
|
45350 |
*
|
|
|
45351 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45352 |
* @param {Service} service The service object to be affected
|
|
|
45353 |
* @return {Integer} New index after parsing
|
|
|
45354 |
*/
|
|
|
45355 |
|
|
|
45356 |
Cea708Stream.prototype.extendedCommands = function (i, service) {
|
|
|
45357 |
var packetData = this.current708Packet.data;
|
|
|
45358 |
var b = packetData[++i];
|
|
|
45359 |
if (within708TextBlock(b)) {
|
|
|
45360 |
i = this.handleText(i, service, {
|
|
|
45361 |
isExtended: true
|
|
|
45362 |
});
|
|
|
45363 |
}
|
|
|
45364 |
return i;
|
|
|
45365 |
};
|
|
|
45366 |
/**
|
|
|
45367 |
* Get PTS value of a given byte index
|
|
|
45368 |
*
|
|
|
45369 |
* @param {Integer} byteIndex Index of the byte
|
|
|
45370 |
* @return {Integer} PTS
|
|
|
45371 |
*/
|
|
|
45372 |
|
|
|
45373 |
Cea708Stream.prototype.getPts = function (byteIndex) {
|
|
|
45374 |
// There's 1 pts value per 2 bytes
|
|
|
45375 |
return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)];
|
|
|
45376 |
};
|
|
|
45377 |
/**
|
|
|
45378 |
* Initializes a service
|
|
|
45379 |
*
|
|
|
45380 |
* @param {Integer} serviceNum Service number
|
|
|
45381 |
* @return {Service} Initialized service object
|
|
|
45382 |
*/
|
|
|
45383 |
|
|
|
45384 |
Cea708Stream.prototype.initService = function (serviceNum, i) {
|
|
|
45385 |
var serviceName = 'SERVICE' + serviceNum;
|
|
|
45386 |
var self = this;
|
|
|
45387 |
var serviceName;
|
|
|
45388 |
var encoding;
|
|
|
45389 |
if (serviceName in this.serviceEncodings) {
|
|
|
45390 |
encoding = this.serviceEncodings[serviceName];
|
|
|
45391 |
}
|
|
|
45392 |
this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self);
|
|
|
45393 |
this.services[serviceNum].init(this.getPts(i), function (pts) {
|
|
|
45394 |
self.flushDisplayed(pts, self.services[serviceNum]);
|
|
|
45395 |
});
|
|
|
45396 |
return this.services[serviceNum];
|
|
|
45397 |
};
|
|
|
45398 |
/**
|
|
|
45399 |
* Execute text writing to current window
|
|
|
45400 |
*
|
|
|
45401 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45402 |
* @param {Service} service The service object to be affected
|
|
|
45403 |
* @return {Integer} New index after parsing
|
|
|
45404 |
*/
|
|
|
45405 |
|
|
|
45406 |
Cea708Stream.prototype.handleText = function (i, service, options) {
|
|
|
45407 |
var isExtended = options && options.isExtended;
|
|
|
45408 |
var isMultiByte = options && options.isMultiByte;
|
|
|
45409 |
var packetData = this.current708Packet.data;
|
|
|
45410 |
var extended = isExtended ? 0x1000 : 0x0000;
|
|
|
45411 |
var currentByte = packetData[i];
|
|
|
45412 |
var nextByte = packetData[i + 1];
|
|
|
45413 |
var win = service.currentWindow;
|
|
|
45414 |
var char;
|
|
|
45415 |
var charCodeArray; // Converts an array of bytes to a unicode hex string.
|
|
|
45416 |
|
|
|
45417 |
function toHexString(byteArray) {
|
|
|
45418 |
return byteArray.map(byte => {
|
|
|
45419 |
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
|
|
|
45420 |
}).join('');
|
|
|
45421 |
}
|
|
|
45422 |
if (isMultiByte) {
|
|
|
45423 |
charCodeArray = [currentByte, nextByte];
|
|
|
45424 |
i++;
|
|
|
45425 |
} else {
|
|
|
45426 |
charCodeArray = [currentByte];
|
|
|
45427 |
} // Use the TextDecoder if one was created for this service
|
|
|
45428 |
|
|
|
45429 |
if (service.textDecoder_ && !isExtended) {
|
|
|
45430 |
char = service.textDecoder_.decode(new Uint8Array(charCodeArray));
|
|
|
45431 |
} else {
|
|
|
45432 |
// We assume any multi-byte char without a decoder is unicode.
|
|
|
45433 |
if (isMultiByte) {
|
|
|
45434 |
const unicode = toHexString(charCodeArray); // Takes a unicode hex string and creates a single character.
|
|
|
45435 |
|
|
|
45436 |
char = String.fromCharCode(parseInt(unicode, 16));
|
|
|
45437 |
} else {
|
|
|
45438 |
char = get708CharFromCode(extended | currentByte);
|
|
|
45439 |
}
|
|
|
45440 |
}
|
|
|
45441 |
if (win.pendingNewLine && !win.isEmpty()) {
|
|
|
45442 |
win.newLine(this.getPts(i));
|
|
|
45443 |
}
|
|
|
45444 |
win.pendingNewLine = false;
|
|
|
45445 |
win.addText(char);
|
|
|
45446 |
return i;
|
|
|
45447 |
};
|
|
|
45448 |
/**
|
|
|
45449 |
* Handle decoding of multibyte character
|
|
|
45450 |
*
|
|
|
45451 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45452 |
* @param {Service} service The service object to be affected
|
|
|
45453 |
* @return {Integer} New index after parsing
|
|
|
45454 |
*/
|
|
|
45455 |
|
|
|
45456 |
Cea708Stream.prototype.multiByteCharacter = function (i, service) {
|
|
|
45457 |
var packetData = this.current708Packet.data;
|
|
|
45458 |
var firstByte = packetData[i + 1];
|
|
|
45459 |
var secondByte = packetData[i + 2];
|
|
|
45460 |
if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) {
|
|
|
45461 |
i = this.handleText(++i, service, {
|
|
|
45462 |
isMultiByte: true
|
|
|
45463 |
});
|
|
|
45464 |
}
|
|
|
45465 |
return i;
|
|
|
45466 |
};
|
|
|
45467 |
/**
|
|
|
45468 |
* Parse and execute the CW# command.
|
|
|
45469 |
*
|
|
|
45470 |
* Set the current window.
|
|
|
45471 |
*
|
|
|
45472 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45473 |
* @param {Service} service The service object to be affected
|
|
|
45474 |
* @return {Integer} New index after parsing
|
|
|
45475 |
*/
|
|
|
45476 |
|
|
|
45477 |
Cea708Stream.prototype.setCurrentWindow = function (i, service) {
|
|
|
45478 |
var packetData = this.current708Packet.data;
|
|
|
45479 |
var b = packetData[i];
|
|
|
45480 |
var windowNum = b & 0x07;
|
|
|
45481 |
service.setCurrentWindow(windowNum);
|
|
|
45482 |
return i;
|
|
|
45483 |
};
|
|
|
45484 |
/**
|
|
|
45485 |
* Parse and execute the DF# command.
|
|
|
45486 |
*
|
|
|
45487 |
* Define a window and set it as the current window.
|
|
|
45488 |
*
|
|
|
45489 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45490 |
* @param {Service} service The service object to be affected
|
|
|
45491 |
* @return {Integer} New index after parsing
|
|
|
45492 |
*/
|
|
|
45493 |
|
|
|
45494 |
Cea708Stream.prototype.defineWindow = function (i, service) {
|
|
|
45495 |
var packetData = this.current708Packet.data;
|
|
|
45496 |
var b = packetData[i];
|
|
|
45497 |
var windowNum = b & 0x07;
|
|
|
45498 |
service.setCurrentWindow(windowNum);
|
|
|
45499 |
var win = service.currentWindow;
|
|
|
45500 |
b = packetData[++i];
|
|
|
45501 |
win.visible = (b & 0x20) >> 5; // v
|
|
|
45502 |
|
|
|
45503 |
win.rowLock = (b & 0x10) >> 4; // rl
|
|
|
45504 |
|
|
|
45505 |
win.columnLock = (b & 0x08) >> 3; // cl
|
|
|
45506 |
|
|
|
45507 |
win.priority = b & 0x07; // p
|
|
|
45508 |
|
|
|
45509 |
b = packetData[++i];
|
|
|
45510 |
win.relativePositioning = (b & 0x80) >> 7; // rp
|
|
|
45511 |
|
|
|
45512 |
win.anchorVertical = b & 0x7f; // av
|
|
|
45513 |
|
|
|
45514 |
b = packetData[++i];
|
|
|
45515 |
win.anchorHorizontal = b; // ah
|
|
|
45516 |
|
|
|
45517 |
b = packetData[++i];
|
|
|
45518 |
win.anchorPoint = (b & 0xf0) >> 4; // ap
|
|
|
45519 |
|
|
|
45520 |
win.rowCount = b & 0x0f; // rc
|
|
|
45521 |
|
|
|
45522 |
b = packetData[++i];
|
|
|
45523 |
win.columnCount = b & 0x3f; // cc
|
|
|
45524 |
|
|
|
45525 |
b = packetData[++i];
|
|
|
45526 |
win.windowStyle = (b & 0x38) >> 3; // ws
|
|
|
45527 |
|
|
|
45528 |
win.penStyle = b & 0x07; // ps
|
|
|
45529 |
// The spec says there are (rowCount+1) "virtual rows"
|
|
|
45530 |
|
|
|
45531 |
win.virtualRowCount = win.rowCount + 1;
|
|
|
45532 |
return i;
|
|
|
45533 |
};
|
|
|
45534 |
/**
|
|
|
45535 |
* Parse and execute the SWA command.
|
|
|
45536 |
*
|
|
|
45537 |
* Set attributes of the current window.
|
|
|
45538 |
*
|
|
|
45539 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45540 |
* @param {Service} service The service object to be affected
|
|
|
45541 |
* @return {Integer} New index after parsing
|
|
|
45542 |
*/
|
|
|
45543 |
|
|
|
45544 |
Cea708Stream.prototype.setWindowAttributes = function (i, service) {
|
|
|
45545 |
var packetData = this.current708Packet.data;
|
|
|
45546 |
var b = packetData[i];
|
|
|
45547 |
var winAttr = service.currentWindow.winAttr;
|
|
|
45548 |
b = packetData[++i];
|
|
|
45549 |
winAttr.fillOpacity = (b & 0xc0) >> 6; // fo
|
|
|
45550 |
|
|
|
45551 |
winAttr.fillRed = (b & 0x30) >> 4; // fr
|
|
|
45552 |
|
|
|
45553 |
winAttr.fillGreen = (b & 0x0c) >> 2; // fg
|
|
|
45554 |
|
|
|
45555 |
winAttr.fillBlue = b & 0x03; // fb
|
|
|
45556 |
|
|
|
45557 |
b = packetData[++i];
|
|
|
45558 |
winAttr.borderType = (b & 0xc0) >> 6; // bt
|
|
|
45559 |
|
|
|
45560 |
winAttr.borderRed = (b & 0x30) >> 4; // br
|
|
|
45561 |
|
|
|
45562 |
winAttr.borderGreen = (b & 0x0c) >> 2; // bg
|
|
|
45563 |
|
|
|
45564 |
winAttr.borderBlue = b & 0x03; // bb
|
|
|
45565 |
|
|
|
45566 |
b = packetData[++i];
|
|
|
45567 |
winAttr.borderType += (b & 0x80) >> 5; // bt
|
|
|
45568 |
|
|
|
45569 |
winAttr.wordWrap = (b & 0x40) >> 6; // ww
|
|
|
45570 |
|
|
|
45571 |
winAttr.printDirection = (b & 0x30) >> 4; // pd
|
|
|
45572 |
|
|
|
45573 |
winAttr.scrollDirection = (b & 0x0c) >> 2; // sd
|
|
|
45574 |
|
|
|
45575 |
winAttr.justify = b & 0x03; // j
|
|
|
45576 |
|
|
|
45577 |
b = packetData[++i];
|
|
|
45578 |
winAttr.effectSpeed = (b & 0xf0) >> 4; // es
|
|
|
45579 |
|
|
|
45580 |
winAttr.effectDirection = (b & 0x0c) >> 2; // ed
|
|
|
45581 |
|
|
|
45582 |
winAttr.displayEffect = b & 0x03; // de
|
|
|
45583 |
|
|
|
45584 |
return i;
|
|
|
45585 |
};
|
|
|
45586 |
/**
|
|
|
45587 |
* Gather text from all displayed windows and push a caption to output.
|
|
|
45588 |
*
|
|
|
45589 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45590 |
* @param {Service} service The service object to be affected
|
|
|
45591 |
*/
|
|
|
45592 |
|
|
|
45593 |
Cea708Stream.prototype.flushDisplayed = function (pts, service) {
|
|
|
45594 |
var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily
|
|
|
45595 |
// display text in the correct order, but sample files so far have not shown any issue.
|
|
|
45596 |
|
|
|
45597 |
for (var winId = 0; winId < 8; winId++) {
|
|
|
45598 |
if (service.windows[winId].visible && !service.windows[winId].isEmpty()) {
|
|
|
45599 |
displayedText.push(service.windows[winId].getText());
|
|
|
45600 |
}
|
|
|
45601 |
}
|
|
|
45602 |
service.endPts = pts;
|
|
|
45603 |
service.text = displayedText.join('\n\n');
|
|
|
45604 |
this.pushCaption(service);
|
|
|
45605 |
service.startPts = pts;
|
|
|
45606 |
};
|
|
|
45607 |
/**
|
|
|
45608 |
* Push a caption to output if the caption contains text.
|
|
|
45609 |
*
|
|
|
45610 |
* @param {Service} service The service object to be affected
|
|
|
45611 |
*/
|
|
|
45612 |
|
|
|
45613 |
Cea708Stream.prototype.pushCaption = function (service) {
|
|
|
45614 |
if (service.text !== '') {
|
|
|
45615 |
this.trigger('data', {
|
|
|
45616 |
startPts: service.startPts,
|
|
|
45617 |
endPts: service.endPts,
|
|
|
45618 |
text: service.text,
|
|
|
45619 |
stream: 'cc708_' + service.serviceNum
|
|
|
45620 |
});
|
|
|
45621 |
service.text = '';
|
|
|
45622 |
service.startPts = service.endPts;
|
|
|
45623 |
}
|
|
|
45624 |
};
|
|
|
45625 |
/**
|
|
|
45626 |
* Parse and execute the DSW command.
|
|
|
45627 |
*
|
|
|
45628 |
* Set visible property of windows based on the parsed bitmask.
|
|
|
45629 |
*
|
|
|
45630 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45631 |
* @param {Service} service The service object to be affected
|
|
|
45632 |
* @return {Integer} New index after parsing
|
|
|
45633 |
*/
|
|
|
45634 |
|
|
|
45635 |
Cea708Stream.prototype.displayWindows = function (i, service) {
|
|
|
45636 |
var packetData = this.current708Packet.data;
|
|
|
45637 |
var b = packetData[++i];
|
|
|
45638 |
var pts = this.getPts(i);
|
|
|
45639 |
this.flushDisplayed(pts, service);
|
|
|
45640 |
for (var winId = 0; winId < 8; winId++) {
|
|
|
45641 |
if (b & 0x01 << winId) {
|
|
|
45642 |
service.windows[winId].visible = 1;
|
|
|
45643 |
}
|
|
|
45644 |
}
|
|
|
45645 |
return i;
|
|
|
45646 |
};
|
|
|
45647 |
/**
|
|
|
45648 |
* Parse and execute the HDW command.
|
|
|
45649 |
*
|
|
|
45650 |
* Set visible property of windows based on the parsed bitmask.
|
|
|
45651 |
*
|
|
|
45652 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45653 |
* @param {Service} service The service object to be affected
|
|
|
45654 |
* @return {Integer} New index after parsing
|
|
|
45655 |
*/
|
|
|
45656 |
|
|
|
45657 |
Cea708Stream.prototype.hideWindows = function (i, service) {
|
|
|
45658 |
var packetData = this.current708Packet.data;
|
|
|
45659 |
var b = packetData[++i];
|
|
|
45660 |
var pts = this.getPts(i);
|
|
|
45661 |
this.flushDisplayed(pts, service);
|
|
|
45662 |
for (var winId = 0; winId < 8; winId++) {
|
|
|
45663 |
if (b & 0x01 << winId) {
|
|
|
45664 |
service.windows[winId].visible = 0;
|
|
|
45665 |
}
|
|
|
45666 |
}
|
|
|
45667 |
return i;
|
|
|
45668 |
};
|
|
|
45669 |
/**
|
|
|
45670 |
* Parse and execute the TGW command.
|
|
|
45671 |
*
|
|
|
45672 |
* Set visible property of windows based on the parsed bitmask.
|
|
|
45673 |
*
|
|
|
45674 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45675 |
* @param {Service} service The service object to be affected
|
|
|
45676 |
* @return {Integer} New index after parsing
|
|
|
45677 |
*/
|
|
|
45678 |
|
|
|
45679 |
Cea708Stream.prototype.toggleWindows = function (i, service) {
|
|
|
45680 |
var packetData = this.current708Packet.data;
|
|
|
45681 |
var b = packetData[++i];
|
|
|
45682 |
var pts = this.getPts(i);
|
|
|
45683 |
this.flushDisplayed(pts, service);
|
|
|
45684 |
for (var winId = 0; winId < 8; winId++) {
|
|
|
45685 |
if (b & 0x01 << winId) {
|
|
|
45686 |
service.windows[winId].visible ^= 1;
|
|
|
45687 |
}
|
|
|
45688 |
}
|
|
|
45689 |
return i;
|
|
|
45690 |
};
|
|
|
45691 |
/**
|
|
|
45692 |
* Parse and execute the CLW command.
|
|
|
45693 |
*
|
|
|
45694 |
* Clear text of windows based on the parsed bitmask.
|
|
|
45695 |
*
|
|
|
45696 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45697 |
* @param {Service} service The service object to be affected
|
|
|
45698 |
* @return {Integer} New index after parsing
|
|
|
45699 |
*/
|
|
|
45700 |
|
|
|
45701 |
Cea708Stream.prototype.clearWindows = function (i, service) {
|
|
|
45702 |
var packetData = this.current708Packet.data;
|
|
|
45703 |
var b = packetData[++i];
|
|
|
45704 |
var pts = this.getPts(i);
|
|
|
45705 |
this.flushDisplayed(pts, service);
|
|
|
45706 |
for (var winId = 0; winId < 8; winId++) {
|
|
|
45707 |
if (b & 0x01 << winId) {
|
|
|
45708 |
service.windows[winId].clearText();
|
|
|
45709 |
}
|
|
|
45710 |
}
|
|
|
45711 |
return i;
|
|
|
45712 |
};
|
|
|
45713 |
/**
|
|
|
45714 |
* Parse and execute the DLW command.
|
|
|
45715 |
*
|
|
|
45716 |
* Re-initialize windows based on the parsed bitmask.
|
|
|
45717 |
*
|
|
|
45718 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45719 |
* @param {Service} service The service object to be affected
|
|
|
45720 |
* @return {Integer} New index after parsing
|
|
|
45721 |
*/
|
|
|
45722 |
|
|
|
45723 |
Cea708Stream.prototype.deleteWindows = function (i, service) {
|
|
|
45724 |
var packetData = this.current708Packet.data;
|
|
|
45725 |
var b = packetData[++i];
|
|
|
45726 |
var pts = this.getPts(i);
|
|
|
45727 |
this.flushDisplayed(pts, service);
|
|
|
45728 |
for (var winId = 0; winId < 8; winId++) {
|
|
|
45729 |
if (b & 0x01 << winId) {
|
|
|
45730 |
service.windows[winId].reset();
|
|
|
45731 |
}
|
|
|
45732 |
}
|
|
|
45733 |
return i;
|
|
|
45734 |
};
|
|
|
45735 |
/**
|
|
|
45736 |
* Parse and execute the SPA command.
|
|
|
45737 |
*
|
|
|
45738 |
* Set pen attributes of the current window.
|
|
|
45739 |
*
|
|
|
45740 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45741 |
* @param {Service} service The service object to be affected
|
|
|
45742 |
* @return {Integer} New index after parsing
|
|
|
45743 |
*/
|
|
|
45744 |
|
|
|
45745 |
Cea708Stream.prototype.setPenAttributes = function (i, service) {
|
|
|
45746 |
var packetData = this.current708Packet.data;
|
|
|
45747 |
var b = packetData[i];
|
|
|
45748 |
var penAttr = service.currentWindow.penAttr;
|
|
|
45749 |
b = packetData[++i];
|
|
|
45750 |
penAttr.textTag = (b & 0xf0) >> 4; // tt
|
|
|
45751 |
|
|
|
45752 |
penAttr.offset = (b & 0x0c) >> 2; // o
|
|
|
45753 |
|
|
|
45754 |
penAttr.penSize = b & 0x03; // s
|
|
|
45755 |
|
|
|
45756 |
b = packetData[++i];
|
|
|
45757 |
penAttr.italics = (b & 0x80) >> 7; // i
|
|
|
45758 |
|
|
|
45759 |
penAttr.underline = (b & 0x40) >> 6; // u
|
|
|
45760 |
|
|
|
45761 |
penAttr.edgeType = (b & 0x38) >> 3; // et
|
|
|
45762 |
|
|
|
45763 |
penAttr.fontStyle = b & 0x07; // fs
|
|
|
45764 |
|
|
|
45765 |
return i;
|
|
|
45766 |
};
|
|
|
45767 |
/**
|
|
|
45768 |
* Parse and execute the SPC command.
|
|
|
45769 |
*
|
|
|
45770 |
* Set pen color of the current window.
|
|
|
45771 |
*
|
|
|
45772 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45773 |
* @param {Service} service The service object to be affected
|
|
|
45774 |
* @return {Integer} New index after parsing
|
|
|
45775 |
*/
|
|
|
45776 |
|
|
|
45777 |
Cea708Stream.prototype.setPenColor = function (i, service) {
|
|
|
45778 |
var packetData = this.current708Packet.data;
|
|
|
45779 |
var b = packetData[i];
|
|
|
45780 |
var penColor = service.currentWindow.penColor;
|
|
|
45781 |
b = packetData[++i];
|
|
|
45782 |
penColor.fgOpacity = (b & 0xc0) >> 6; // fo
|
|
|
45783 |
|
|
|
45784 |
penColor.fgRed = (b & 0x30) >> 4; // fr
|
|
|
45785 |
|
|
|
45786 |
penColor.fgGreen = (b & 0x0c) >> 2; // fg
|
|
|
45787 |
|
|
|
45788 |
penColor.fgBlue = b & 0x03; // fb
|
|
|
45789 |
|
|
|
45790 |
b = packetData[++i];
|
|
|
45791 |
penColor.bgOpacity = (b & 0xc0) >> 6; // bo
|
|
|
45792 |
|
|
|
45793 |
penColor.bgRed = (b & 0x30) >> 4; // br
|
|
|
45794 |
|
|
|
45795 |
penColor.bgGreen = (b & 0x0c) >> 2; // bg
|
|
|
45796 |
|
|
|
45797 |
penColor.bgBlue = b & 0x03; // bb
|
|
|
45798 |
|
|
|
45799 |
b = packetData[++i];
|
|
|
45800 |
penColor.edgeRed = (b & 0x30) >> 4; // er
|
|
|
45801 |
|
|
|
45802 |
penColor.edgeGreen = (b & 0x0c) >> 2; // eg
|
|
|
45803 |
|
|
|
45804 |
penColor.edgeBlue = b & 0x03; // eb
|
|
|
45805 |
|
|
|
45806 |
return i;
|
|
|
45807 |
};
|
|
|
45808 |
/**
|
|
|
45809 |
* Parse and execute the SPL command.
|
|
|
45810 |
*
|
|
|
45811 |
* Set pen location of the current window.
|
|
|
45812 |
*
|
|
|
45813 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45814 |
* @param {Service} service The service object to be affected
|
|
|
45815 |
* @return {Integer} New index after parsing
|
|
|
45816 |
*/
|
|
|
45817 |
|
|
|
45818 |
Cea708Stream.prototype.setPenLocation = function (i, service) {
|
|
|
45819 |
var packetData = this.current708Packet.data;
|
|
|
45820 |
var b = packetData[i];
|
|
|
45821 |
var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak
|
|
|
45822 |
|
|
|
45823 |
service.currentWindow.pendingNewLine = true;
|
|
|
45824 |
b = packetData[++i];
|
|
|
45825 |
penLoc.row = b & 0x0f; // r
|
|
|
45826 |
|
|
|
45827 |
b = packetData[++i];
|
|
|
45828 |
penLoc.column = b & 0x3f; // c
|
|
|
45829 |
|
|
|
45830 |
return i;
|
|
|
45831 |
};
|
|
|
45832 |
/**
|
|
|
45833 |
* Execute the RST command.
|
|
|
45834 |
*
|
|
|
45835 |
* Reset service to a clean slate. Re-initialize.
|
|
|
45836 |
*
|
|
|
45837 |
* @param {Integer} i Current index in the 708 packet
|
|
|
45838 |
* @param {Service} service The service object to be affected
|
|
|
45839 |
* @return {Service} Re-initialized service
|
|
|
45840 |
*/
|
|
|
45841 |
|
|
|
45842 |
Cea708Stream.prototype.reset = function (i, service) {
|
|
|
45843 |
var pts = this.getPts(i);
|
|
|
45844 |
this.flushDisplayed(pts, service);
|
|
|
45845 |
return this.initService(service.serviceNum, i);
|
|
|
45846 |
}; // This hash maps non-ASCII, special, and extended character codes to their
|
|
|
45847 |
// proper Unicode equivalent. The first keys that are only a single byte
|
|
|
45848 |
// are the non-standard ASCII characters, which simply map the CEA608 byte
|
|
|
45849 |
// to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608
|
|
|
45850 |
// character codes, but have their MSB bitmasked with 0x03 so that a lookup
|
|
|
45851 |
// can be performed regardless of the field and data channel on which the
|
|
|
45852 |
// character code was received.
|
|
|
45853 |
|
|
|
45854 |
var CHARACTER_TRANSLATION = {
|
|
|
45855 |
0x2a: 0xe1,
|
|
|
45856 |
// á
|
|
|
45857 |
0x5c: 0xe9,
|
|
|
45858 |
// é
|
|
|
45859 |
0x5e: 0xed,
|
|
|
45860 |
// í
|
|
|
45861 |
0x5f: 0xf3,
|
|
|
45862 |
// ó
|
|
|
45863 |
0x60: 0xfa,
|
|
|
45864 |
// ú
|
|
|
45865 |
0x7b: 0xe7,
|
|
|
45866 |
// ç
|
|
|
45867 |
0x7c: 0xf7,
|
|
|
45868 |
// ÷
|
|
|
45869 |
0x7d: 0xd1,
|
|
|
45870 |
// Ñ
|
|
|
45871 |
0x7e: 0xf1,
|
|
|
45872 |
// ñ
|
|
|
45873 |
0x7f: 0x2588,
|
|
|
45874 |
// █
|
|
|
45875 |
0x0130: 0xae,
|
|
|
45876 |
// ®
|
|
|
45877 |
0x0131: 0xb0,
|
|
|
45878 |
// °
|
|
|
45879 |
0x0132: 0xbd,
|
|
|
45880 |
// ½
|
|
|
45881 |
0x0133: 0xbf,
|
|
|
45882 |
// ¿
|
|
|
45883 |
0x0134: 0x2122,
|
|
|
45884 |
// â„¢
|
|
|
45885 |
0x0135: 0xa2,
|
|
|
45886 |
// ¢
|
|
|
45887 |
0x0136: 0xa3,
|
|
|
45888 |
// £
|
|
|
45889 |
0x0137: 0x266a,
|
|
|
45890 |
// ♪
|
|
|
45891 |
0x0138: 0xe0,
|
|
|
45892 |
// à
|
|
|
45893 |
0x0139: 0xa0,
|
|
|
45894 |
//
|
|
|
45895 |
0x013a: 0xe8,
|
|
|
45896 |
// è
|
|
|
45897 |
0x013b: 0xe2,
|
|
|
45898 |
// â
|
|
|
45899 |
0x013c: 0xea,
|
|
|
45900 |
// ê
|
|
|
45901 |
0x013d: 0xee,
|
|
|
45902 |
// î
|
|
|
45903 |
0x013e: 0xf4,
|
|
|
45904 |
// ô
|
|
|
45905 |
0x013f: 0xfb,
|
|
|
45906 |
// û
|
|
|
45907 |
0x0220: 0xc1,
|
|
|
45908 |
// Á
|
|
|
45909 |
0x0221: 0xc9,
|
|
|
45910 |
// É
|
|
|
45911 |
0x0222: 0xd3,
|
|
|
45912 |
// Ó
|
|
|
45913 |
0x0223: 0xda,
|
|
|
45914 |
// Ú
|
|
|
45915 |
0x0224: 0xdc,
|
|
|
45916 |
// Ü
|
|
|
45917 |
0x0225: 0xfc,
|
|
|
45918 |
// ü
|
|
|
45919 |
0x0226: 0x2018,
|
|
|
45920 |
// ‘
|
|
|
45921 |
0x0227: 0xa1,
|
|
|
45922 |
// ¡
|
|
|
45923 |
0x0228: 0x2a,
|
|
|
45924 |
// *
|
|
|
45925 |
0x0229: 0x27,
|
|
|
45926 |
// '
|
|
|
45927 |
0x022a: 0x2014,
|
|
|
45928 |
// —
|
|
|
45929 |
0x022b: 0xa9,
|
|
|
45930 |
// ©
|
|
|
45931 |
0x022c: 0x2120,
|
|
|
45932 |
// ℠
|
|
|
45933 |
0x022d: 0x2022,
|
|
|
45934 |
// •
|
|
|
45935 |
0x022e: 0x201c,
|
|
|
45936 |
// “
|
|
|
45937 |
0x022f: 0x201d,
|
|
|
45938 |
// ”
|
|
|
45939 |
0x0230: 0xc0,
|
|
|
45940 |
// À
|
|
|
45941 |
0x0231: 0xc2,
|
|
|
45942 |
// Â
|
|
|
45943 |
0x0232: 0xc7,
|
|
|
45944 |
// Ç
|
|
|
45945 |
0x0233: 0xc8,
|
|
|
45946 |
// È
|
|
|
45947 |
0x0234: 0xca,
|
|
|
45948 |
// Ê
|
|
|
45949 |
0x0235: 0xcb,
|
|
|
45950 |
// Ë
|
|
|
45951 |
0x0236: 0xeb,
|
|
|
45952 |
// ë
|
|
|
45953 |
0x0237: 0xce,
|
|
|
45954 |
// Î
|
|
|
45955 |
0x0238: 0xcf,
|
|
|
45956 |
// Ï
|
|
|
45957 |
0x0239: 0xef,
|
|
|
45958 |
// ï
|
|
|
45959 |
0x023a: 0xd4,
|
|
|
45960 |
// Ô
|
|
|
45961 |
0x023b: 0xd9,
|
|
|
45962 |
// Ù
|
|
|
45963 |
0x023c: 0xf9,
|
|
|
45964 |
// ù
|
|
|
45965 |
0x023d: 0xdb,
|
|
|
45966 |
// Û
|
|
|
45967 |
0x023e: 0xab,
|
|
|
45968 |
// «
|
|
|
45969 |
0x023f: 0xbb,
|
|
|
45970 |
// »
|
|
|
45971 |
0x0320: 0xc3,
|
|
|
45972 |
// Ã
|
|
|
45973 |
0x0321: 0xe3,
|
|
|
45974 |
// ã
|
|
|
45975 |
0x0322: 0xcd,
|
|
|
45976 |
// Í
|
|
|
45977 |
0x0323: 0xcc,
|
|
|
45978 |
// Ì
|
|
|
45979 |
0x0324: 0xec,
|
|
|
45980 |
// ì
|
|
|
45981 |
0x0325: 0xd2,
|
|
|
45982 |
// Ò
|
|
|
45983 |
0x0326: 0xf2,
|
|
|
45984 |
// ò
|
|
|
45985 |
0x0327: 0xd5,
|
|
|
45986 |
// Õ
|
|
|
45987 |
0x0328: 0xf5,
|
|
|
45988 |
// õ
|
|
|
45989 |
0x0329: 0x7b,
|
|
|
45990 |
// {
|
|
|
45991 |
0x032a: 0x7d,
|
|
|
45992 |
// }
|
|
|
45993 |
0x032b: 0x5c,
|
|
|
45994 |
// \
|
|
|
45995 |
0x032c: 0x5e,
|
|
|
45996 |
// ^
|
|
|
45997 |
0x032d: 0x5f,
|
|
|
45998 |
// _
|
|
|
45999 |
0x032e: 0x7c,
|
|
|
46000 |
// |
|
|
|
46001 |
0x032f: 0x7e,
|
|
|
46002 |
// ~
|
|
|
46003 |
0x0330: 0xc4,
|
|
|
46004 |
// Ä
|
|
|
46005 |
0x0331: 0xe4,
|
|
|
46006 |
// ä
|
|
|
46007 |
0x0332: 0xd6,
|
|
|
46008 |
// Ö
|
|
|
46009 |
0x0333: 0xf6,
|
|
|
46010 |
// ö
|
|
|
46011 |
0x0334: 0xdf,
|
|
|
46012 |
// ß
|
|
|
46013 |
0x0335: 0xa5,
|
|
|
46014 |
// ¥
|
|
|
46015 |
0x0336: 0xa4,
|
|
|
46016 |
// ¤
|
|
|
46017 |
0x0337: 0x2502,
|
|
|
46018 |
// │
|
|
|
46019 |
0x0338: 0xc5,
|
|
|
46020 |
// Å
|
|
|
46021 |
0x0339: 0xe5,
|
|
|
46022 |
// å
|
|
|
46023 |
0x033a: 0xd8,
|
|
|
46024 |
// Ø
|
|
|
46025 |
0x033b: 0xf8,
|
|
|
46026 |
// ø
|
|
|
46027 |
0x033c: 0x250c,
|
|
|
46028 |
// ┌
|
|
|
46029 |
0x033d: 0x2510,
|
|
|
46030 |
// ┐
|
|
|
46031 |
0x033e: 0x2514,
|
|
|
46032 |
// â””
|
|
|
46033 |
0x033f: 0x2518 // ┘
|
|
|
46034 |
};
|
|
|
46035 |
|
|
|
46036 |
var getCharFromCode = function (code) {
|
|
|
46037 |
if (code === null) {
|
|
|
46038 |
return '';
|
|
|
46039 |
}
|
|
|
46040 |
code = CHARACTER_TRANSLATION[code] || code;
|
|
|
46041 |
return String.fromCharCode(code);
|
|
|
46042 |
}; // the index of the last row in a CEA-608 display buffer
|
|
|
46043 |
|
|
|
46044 |
var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of
|
|
|
46045 |
// getting it through bit logic.
|
|
|
46046 |
|
|
|
46047 |
var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; // CEA-608 captions are rendered onto a 34x15 matrix of character
|
|
|
46048 |
// cells. The "bottom" row is the last element in the outer array.
|
|
|
46049 |
// We keep track of positioning information as we go by storing the
|
|
|
46050 |
// number of indentations and the tab offset in this buffer.
|
|
|
46051 |
|
|
|
46052 |
var createDisplayBuffer = function () {
|
|
|
46053 |
var result = [],
|
|
|
46054 |
i = BOTTOM_ROW + 1;
|
|
|
46055 |
while (i--) {
|
|
|
46056 |
result.push({
|
|
|
46057 |
text: '',
|
|
|
46058 |
indent: 0,
|
|
|
46059 |
offset: 0
|
|
|
46060 |
});
|
|
|
46061 |
}
|
|
|
46062 |
return result;
|
|
|
46063 |
};
|
|
|
46064 |
var Cea608Stream = function (field, dataChannel) {
|
|
|
46065 |
Cea608Stream.prototype.init.call(this);
|
|
|
46066 |
this.field_ = field || 0;
|
|
|
46067 |
this.dataChannel_ = dataChannel || 0;
|
|
|
46068 |
this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1);
|
|
|
46069 |
this.setConstants();
|
|
|
46070 |
this.reset();
|
|
|
46071 |
this.push = function (packet) {
|
|
|
46072 |
var data, swap, char0, char1, text; // remove the parity bits
|
|
|
46073 |
|
|
|
46074 |
data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice
|
|
|
46075 |
|
|
|
46076 |
if (data === this.lastControlCode_) {
|
|
|
46077 |
this.lastControlCode_ = null;
|
|
|
46078 |
return;
|
|
|
46079 |
} // Store control codes
|
|
|
46080 |
|
|
|
46081 |
if ((data & 0xf000) === 0x1000) {
|
|
|
46082 |
this.lastControlCode_ = data;
|
|
|
46083 |
} else if (data !== this.PADDING_) {
|
|
|
46084 |
this.lastControlCode_ = null;
|
|
|
46085 |
}
|
|
|
46086 |
char0 = data >>> 8;
|
|
|
46087 |
char1 = data & 0xff;
|
|
|
46088 |
if (data === this.PADDING_) {
|
|
|
46089 |
return;
|
|
|
46090 |
} else if (data === this.RESUME_CAPTION_LOADING_) {
|
|
|
46091 |
this.mode_ = 'popOn';
|
|
|
46092 |
} else if (data === this.END_OF_CAPTION_) {
|
|
|
46093 |
// If an EOC is received while in paint-on mode, the displayed caption
|
|
|
46094 |
// text should be swapped to non-displayed memory as if it was a pop-on
|
|
|
46095 |
// caption. Because of that, we should explicitly switch back to pop-on
|
|
|
46096 |
// mode
|
|
|
46097 |
this.mode_ = 'popOn';
|
|
|
46098 |
this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now
|
|
|
46099 |
|
|
|
46100 |
this.flushDisplayed(packet.pts); // flip memory
|
|
|
46101 |
|
|
|
46102 |
swap = this.displayed_;
|
|
|
46103 |
this.displayed_ = this.nonDisplayed_;
|
|
|
46104 |
this.nonDisplayed_ = swap; // start measuring the time to display the caption
|
|
|
46105 |
|
|
|
46106 |
this.startPts_ = packet.pts;
|
|
|
46107 |
} else if (data === this.ROLL_UP_2_ROWS_) {
|
|
|
46108 |
this.rollUpRows_ = 2;
|
|
|
46109 |
this.setRollUp(packet.pts);
|
|
|
46110 |
} else if (data === this.ROLL_UP_3_ROWS_) {
|
|
|
46111 |
this.rollUpRows_ = 3;
|
|
|
46112 |
this.setRollUp(packet.pts);
|
|
|
46113 |
} else if (data === this.ROLL_UP_4_ROWS_) {
|
|
|
46114 |
this.rollUpRows_ = 4;
|
|
|
46115 |
this.setRollUp(packet.pts);
|
|
|
46116 |
} else if (data === this.CARRIAGE_RETURN_) {
|
|
|
46117 |
this.clearFormatting(packet.pts);
|
|
|
46118 |
this.flushDisplayed(packet.pts);
|
|
|
46119 |
this.shiftRowsUp_();
|
|
|
46120 |
this.startPts_ = packet.pts;
|
|
|
46121 |
} else if (data === this.BACKSPACE_) {
|
|
|
46122 |
if (this.mode_ === 'popOn') {
|
|
|
46123 |
this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1);
|
|
|
46124 |
} else {
|
|
|
46125 |
this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1);
|
|
|
46126 |
}
|
|
|
46127 |
} else if (data === this.ERASE_DISPLAYED_MEMORY_) {
|
|
|
46128 |
this.flushDisplayed(packet.pts);
|
|
|
46129 |
this.displayed_ = createDisplayBuffer();
|
|
|
46130 |
} else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
|
|
|
46131 |
this.nonDisplayed_ = createDisplayBuffer();
|
|
|
46132 |
} else if (data === this.RESUME_DIRECT_CAPTIONING_) {
|
|
|
46133 |
if (this.mode_ !== 'paintOn') {
|
|
|
46134 |
// NOTE: This should be removed when proper caption positioning is
|
|
|
46135 |
// implemented
|
|
|
46136 |
this.flushDisplayed(packet.pts);
|
|
|
46137 |
this.displayed_ = createDisplayBuffer();
|
|
|
46138 |
}
|
|
|
46139 |
this.mode_ = 'paintOn';
|
|
|
46140 |
this.startPts_ = packet.pts; // Append special characters to caption text
|
|
|
46141 |
} else if (this.isSpecialCharacter(char0, char1)) {
|
|
|
46142 |
// Bitmask char0 so that we can apply character transformations
|
|
|
46143 |
// regardless of field and data channel.
|
|
|
46144 |
// Then byte-shift to the left and OR with char1 so we can pass the
|
|
|
46145 |
// entire character code to `getCharFromCode`.
|
|
|
46146 |
char0 = (char0 & 0x03) << 8;
|
|
|
46147 |
text = getCharFromCode(char0 | char1);
|
|
|
46148 |
this[this.mode_](packet.pts, text);
|
|
|
46149 |
this.column_++; // Append extended characters to caption text
|
|
|
46150 |
} else if (this.isExtCharacter(char0, char1)) {
|
|
|
46151 |
// Extended characters always follow their "non-extended" equivalents.
|
|
|
46152 |
// IE if a "è" is desired, you'll always receive "eè"; non-compliant
|
|
|
46153 |
// decoders are supposed to drop the "è", while compliant decoders
|
|
|
46154 |
// backspace the "e" and insert "è".
|
|
|
46155 |
// Delete the previous character
|
|
|
46156 |
if (this.mode_ === 'popOn') {
|
|
|
46157 |
this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1);
|
|
|
46158 |
} else {
|
|
|
46159 |
this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1);
|
|
|
46160 |
} // Bitmask char0 so that we can apply character transformations
|
|
|
46161 |
// regardless of field and data channel.
|
|
|
46162 |
// Then byte-shift to the left and OR with char1 so we can pass the
|
|
|
46163 |
// entire character code to `getCharFromCode`.
|
|
|
46164 |
|
|
|
46165 |
char0 = (char0 & 0x03) << 8;
|
|
|
46166 |
text = getCharFromCode(char0 | char1);
|
|
|
46167 |
this[this.mode_](packet.pts, text);
|
|
|
46168 |
this.column_++; // Process mid-row codes
|
|
|
46169 |
} else if (this.isMidRowCode(char0, char1)) {
|
|
|
46170 |
// Attributes are not additive, so clear all formatting
|
|
|
46171 |
this.clearFormatting(packet.pts); // According to the standard, mid-row codes
|
|
|
46172 |
// should be replaced with spaces, so add one now
|
|
|
46173 |
|
|
|
46174 |
this[this.mode_](packet.pts, ' ');
|
|
|
46175 |
this.column_++;
|
|
|
46176 |
if ((char1 & 0xe) === 0xe) {
|
|
|
46177 |
this.addFormatting(packet.pts, ['i']);
|
|
|
46178 |
}
|
|
|
46179 |
if ((char1 & 0x1) === 0x1) {
|
|
|
46180 |
this.addFormatting(packet.pts, ['u']);
|
|
|
46181 |
} // Detect offset control codes and adjust cursor
|
|
|
46182 |
} else if (this.isOffsetControlCode(char0, char1)) {
|
|
|
46183 |
// Cursor position is set by indent PAC (see below) in 4-column
|
|
|
46184 |
// increments, with an additional offset code of 1-3 to reach any
|
|
|
46185 |
// of the 32 columns specified by CEA-608. So all we need to do
|
|
|
46186 |
// here is increment the column cursor by the given offset.
|
|
|
46187 |
const offset = char1 & 0x03; // For an offest value 1-3, set the offset for that caption
|
|
|
46188 |
// in the non-displayed array.
|
|
|
46189 |
|
|
|
46190 |
this.nonDisplayed_[this.row_].offset = offset;
|
|
|
46191 |
this.column_ += offset; // Detect PACs (Preamble Address Codes)
|
|
|
46192 |
} else if (this.isPAC(char0, char1)) {
|
|
|
46193 |
// There's no logic for PAC -> row mapping, so we have to just
|
|
|
46194 |
// find the row code in an array and use its index :(
|
|
|
46195 |
var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode
|
|
|
46196 |
|
|
|
46197 |
if (this.mode_ === 'rollUp') {
|
|
|
46198 |
// This implies that the base row is incorrectly set.
|
|
|
46199 |
// As per the recommendation in CEA-608(Base Row Implementation), defer to the number
|
|
|
46200 |
// of roll-up rows set.
|
|
|
46201 |
if (row - this.rollUpRows_ + 1 < 0) {
|
|
|
46202 |
row = this.rollUpRows_ - 1;
|
|
|
46203 |
}
|
|
|
46204 |
this.setRollUp(packet.pts, row);
|
|
|
46205 |
}
|
|
|
46206 |
if (row !== this.row_) {
|
|
|
46207 |
// formatting is only persistent for current row
|
|
|
46208 |
this.clearFormatting(packet.pts);
|
|
|
46209 |
this.row_ = row;
|
|
|
46210 |
} // All PACs can apply underline, so detect and apply
|
|
|
46211 |
// (All odd-numbered second bytes set underline)
|
|
|
46212 |
|
|
|
46213 |
if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) {
|
|
|
46214 |
this.addFormatting(packet.pts, ['u']);
|
|
|
46215 |
}
|
|
|
46216 |
if ((data & 0x10) === 0x10) {
|
|
|
46217 |
// We've got an indent level code. Each successive even number
|
|
|
46218 |
// increments the column cursor by 4, so we can get the desired
|
|
|
46219 |
// column position by bit-shifting to the right (to get n/2)
|
|
|
46220 |
// and multiplying by 4.
|
|
|
46221 |
const indentations = (data & 0xe) >> 1;
|
|
|
46222 |
this.column_ = indentations * 4; // add to the number of indentations for positioning
|
|
|
46223 |
|
|
|
46224 |
this.nonDisplayed_[this.row_].indent += indentations;
|
|
|
46225 |
}
|
|
|
46226 |
if (this.isColorPAC(char1)) {
|
|
|
46227 |
// it's a color code, though we only support white, which
|
|
|
46228 |
// can be either normal or italicized. white italics can be
|
|
|
46229 |
// either 0x4e or 0x6e depending on the row, so we just
|
|
|
46230 |
// bitwise-and with 0xe to see if italics should be turned on
|
|
|
46231 |
if ((char1 & 0xe) === 0xe) {
|
|
|
46232 |
this.addFormatting(packet.pts, ['i']);
|
|
|
46233 |
}
|
|
|
46234 |
} // We have a normal character in char0, and possibly one in char1
|
|
|
46235 |
} else if (this.isNormalChar(char0)) {
|
|
|
46236 |
if (char1 === 0x00) {
|
|
|
46237 |
char1 = null;
|
|
|
46238 |
}
|
|
|
46239 |
text = getCharFromCode(char0);
|
|
|
46240 |
text += getCharFromCode(char1);
|
|
|
46241 |
this[this.mode_](packet.pts, text);
|
|
|
46242 |
this.column_ += text.length;
|
|
|
46243 |
} // finish data processing
|
|
|
46244 |
};
|
|
|
46245 |
};
|
|
|
46246 |
|
|
|
46247 |
Cea608Stream.prototype = new Stream$7(); // Trigger a cue point that captures the current state of the
|
|
|
46248 |
// display buffer
|
|
|
46249 |
|
|
|
46250 |
Cea608Stream.prototype.flushDisplayed = function (pts) {
|
|
|
46251 |
const logWarning = index => {
|
|
|
46252 |
this.trigger('log', {
|
|
|
46253 |
level: 'warn',
|
|
|
46254 |
message: 'Skipping a malformed 608 caption at index ' + index + '.'
|
|
|
46255 |
});
|
|
|
46256 |
};
|
|
|
46257 |
const content = [];
|
|
|
46258 |
this.displayed_.forEach((row, i) => {
|
|
|
46259 |
if (row && row.text && row.text.length) {
|
|
|
46260 |
try {
|
|
|
46261 |
// remove spaces from the start and end of the string
|
|
|
46262 |
row.text = row.text.trim();
|
|
|
46263 |
} catch (e) {
|
|
|
46264 |
// Ordinarily, this shouldn't happen. However, caption
|
|
|
46265 |
// parsing errors should not throw exceptions and
|
|
|
46266 |
// break playback.
|
|
|
46267 |
logWarning(i);
|
|
|
46268 |
} // See the below link for more details on the following fields:
|
|
|
46269 |
// https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608
|
|
|
46270 |
|
|
|
46271 |
if (row.text.length) {
|
|
|
46272 |
content.push({
|
|
|
46273 |
// The text to be displayed in the caption from this specific row, with whitespace removed.
|
|
|
46274 |
text: row.text,
|
|
|
46275 |
// Value between 1 and 15 representing the PAC row used to calculate line height.
|
|
|
46276 |
line: i + 1,
|
|
|
46277 |
// A number representing the indent position by percentage (CEA-608 PAC indent code).
|
|
|
46278 |
// The value will be a number between 10 and 80. Offset is used to add an aditional
|
|
|
46279 |
// value to the position if necessary.
|
|
|
46280 |
position: 10 + Math.min(70, row.indent * 10) + row.offset * 2.5
|
|
|
46281 |
});
|
|
|
46282 |
}
|
|
|
46283 |
} else if (row === undefined || row === null) {
|
|
|
46284 |
logWarning(i);
|
|
|
46285 |
}
|
|
|
46286 |
});
|
|
|
46287 |
if (content.length) {
|
|
|
46288 |
this.trigger('data', {
|
|
|
46289 |
startPts: this.startPts_,
|
|
|
46290 |
endPts: pts,
|
|
|
46291 |
content,
|
|
|
46292 |
stream: this.name_
|
|
|
46293 |
});
|
|
|
46294 |
}
|
|
|
46295 |
};
|
|
|
46296 |
/**
|
|
|
46297 |
* Zero out the data, used for startup and on seek
|
|
|
46298 |
*/
|
|
|
46299 |
|
|
|
46300 |
Cea608Stream.prototype.reset = function () {
|
|
|
46301 |
this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will
|
|
|
46302 |
// actually display captions. If a caption is shifted to a row
|
|
|
46303 |
// with a lower index than this, it is cleared from the display
|
|
|
46304 |
// buffer
|
|
|
46305 |
|
|
|
46306 |
this.topRow_ = 0;
|
|
|
46307 |
this.startPts_ = 0;
|
|
|
46308 |
this.displayed_ = createDisplayBuffer();
|
|
|
46309 |
this.nonDisplayed_ = createDisplayBuffer();
|
|
|
46310 |
this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing
|
|
|
46311 |
|
|
|
46312 |
this.column_ = 0;
|
|
|
46313 |
this.row_ = BOTTOM_ROW;
|
|
|
46314 |
this.rollUpRows_ = 2; // This variable holds currently-applied formatting
|
|
|
46315 |
|
|
|
46316 |
this.formatting_ = [];
|
|
|
46317 |
};
|
|
|
46318 |
/**
|
|
|
46319 |
* Sets up control code and related constants for this instance
|
|
|
46320 |
*/
|
|
|
46321 |
|
|
|
46322 |
Cea608Stream.prototype.setConstants = function () {
|
|
|
46323 |
// The following attributes have these uses:
|
|
|
46324 |
// ext_ : char0 for mid-row codes, and the base for extended
|
|
|
46325 |
// chars (ext_+0, ext_+1, and ext_+2 are char0s for
|
|
|
46326 |
// extended codes)
|
|
|
46327 |
// control_: char0 for control codes, except byte-shifted to the
|
|
|
46328 |
// left so that we can do this.control_ | CONTROL_CODE
|
|
|
46329 |
// offset_: char0 for tab offset codes
|
|
|
46330 |
//
|
|
|
46331 |
// It's also worth noting that control codes, and _only_ control codes,
|
|
|
46332 |
// differ between field 1 and field2. Field 2 control codes are always
|
|
|
46333 |
// their field 1 value plus 1. That's why there's the "| field" on the
|
|
|
46334 |
// control value.
|
|
|
46335 |
if (this.dataChannel_ === 0) {
|
|
|
46336 |
this.BASE_ = 0x10;
|
|
|
46337 |
this.EXT_ = 0x11;
|
|
|
46338 |
this.CONTROL_ = (0x14 | this.field_) << 8;
|
|
|
46339 |
this.OFFSET_ = 0x17;
|
|
|
46340 |
} else if (this.dataChannel_ === 1) {
|
|
|
46341 |
this.BASE_ = 0x18;
|
|
|
46342 |
this.EXT_ = 0x19;
|
|
|
46343 |
this.CONTROL_ = (0x1c | this.field_) << 8;
|
|
|
46344 |
this.OFFSET_ = 0x1f;
|
|
|
46345 |
} // Constants for the LSByte command codes recognized by Cea608Stream. This
|
|
|
46346 |
// list is not exhaustive. For a more comprehensive listing and semantics see
|
|
|
46347 |
// http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
|
|
|
46348 |
// Padding
|
|
|
46349 |
|
|
|
46350 |
this.PADDING_ = 0x0000; // Pop-on Mode
|
|
|
46351 |
|
|
|
46352 |
this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
|
|
|
46353 |
this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode
|
|
|
46354 |
|
|
|
46355 |
this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
|
|
|
46356 |
this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
|
|
|
46357 |
this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
|
|
|
46358 |
this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode
|
|
|
46359 |
|
|
|
46360 |
this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure
|
|
|
46361 |
|
|
|
46362 |
this.BACKSPACE_ = this.CONTROL_ | 0x21;
|
|
|
46363 |
this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
|
|
|
46364 |
this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
|
|
|
46365 |
};
|
|
|
46366 |
/**
|
|
|
46367 |
* Detects if the 2-byte packet data is a special character
|
|
|
46368 |
*
|
|
|
46369 |
* Special characters have a second byte in the range 0x30 to 0x3f,
|
|
|
46370 |
* with the first byte being 0x11 (for data channel 1) or 0x19 (for
|
|
|
46371 |
* data channel 2).
|
|
|
46372 |
*
|
|
|
46373 |
* @param {Integer} char0 The first byte
|
|
|
46374 |
* @param {Integer} char1 The second byte
|
|
|
46375 |
* @return {Boolean} Whether the 2 bytes are an special character
|
|
|
46376 |
*/
|
|
|
46377 |
|
|
|
46378 |
Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) {
|
|
|
46379 |
return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f;
|
|
|
46380 |
};
|
|
|
46381 |
/**
|
|
|
46382 |
* Detects if the 2-byte packet data is an extended character
|
|
|
46383 |
*
|
|
|
46384 |
* Extended characters have a second byte in the range 0x20 to 0x3f,
|
|
|
46385 |
* with the first byte being 0x12 or 0x13 (for data channel 1) or
|
|
|
46386 |
* 0x1a or 0x1b (for data channel 2).
|
|
|
46387 |
*
|
|
|
46388 |
* @param {Integer} char0 The first byte
|
|
|
46389 |
* @param {Integer} char1 The second byte
|
|
|
46390 |
* @return {Boolean} Whether the 2 bytes are an extended character
|
|
|
46391 |
*/
|
|
|
46392 |
|
|
|
46393 |
Cea608Stream.prototype.isExtCharacter = function (char0, char1) {
|
|
|
46394 |
return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f;
|
|
|
46395 |
};
|
|
|
46396 |
/**
|
|
|
46397 |
* Detects if the 2-byte packet is a mid-row code
|
|
|
46398 |
*
|
|
|
46399 |
* Mid-row codes have a second byte in the range 0x20 to 0x2f, with
|
|
|
46400 |
* the first byte being 0x11 (for data channel 1) or 0x19 (for data
|
|
|
46401 |
* channel 2).
|
|
|
46402 |
*
|
|
|
46403 |
* @param {Integer} char0 The first byte
|
|
|
46404 |
* @param {Integer} char1 The second byte
|
|
|
46405 |
* @return {Boolean} Whether the 2 bytes are a mid-row code
|
|
|
46406 |
*/
|
|
|
46407 |
|
|
|
46408 |
Cea608Stream.prototype.isMidRowCode = function (char0, char1) {
|
|
|
46409 |
return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f;
|
|
|
46410 |
};
|
|
|
46411 |
/**
|
|
|
46412 |
* Detects if the 2-byte packet is an offset control code
|
|
|
46413 |
*
|
|
|
46414 |
* Offset control codes have a second byte in the range 0x21 to 0x23,
|
|
|
46415 |
* with the first byte being 0x17 (for data channel 1) or 0x1f (for
|
|
|
46416 |
* data channel 2).
|
|
|
46417 |
*
|
|
|
46418 |
* @param {Integer} char0 The first byte
|
|
|
46419 |
* @param {Integer} char1 The second byte
|
|
|
46420 |
* @return {Boolean} Whether the 2 bytes are an offset control code
|
|
|
46421 |
*/
|
|
|
46422 |
|
|
|
46423 |
Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) {
|
|
|
46424 |
return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23;
|
|
|
46425 |
};
|
|
|
46426 |
/**
|
|
|
46427 |
* Detects if the 2-byte packet is a Preamble Address Code
|
|
|
46428 |
*
|
|
|
46429 |
* PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
|
|
|
46430 |
* or 0x18 to 0x1f (for data channel 2), with the second byte in the
|
|
|
46431 |
* range 0x40 to 0x7f.
|
|
|
46432 |
*
|
|
|
46433 |
* @param {Integer} char0 The first byte
|
|
|
46434 |
* @param {Integer} char1 The second byte
|
|
|
46435 |
* @return {Boolean} Whether the 2 bytes are a PAC
|
|
|
46436 |
*/
|
|
|
46437 |
|
|
|
46438 |
Cea608Stream.prototype.isPAC = function (char0, char1) {
|
|
|
46439 |
return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f;
|
|
|
46440 |
};
|
|
|
46441 |
/**
|
|
|
46442 |
* Detects if a packet's second byte is in the range of a PAC color code
|
|
|
46443 |
*
|
|
|
46444 |
* PAC color codes have the second byte be in the range 0x40 to 0x4f, or
|
|
|
46445 |
* 0x60 to 0x6f.
|
|
|
46446 |
*
|
|
|
46447 |
* @param {Integer} char1 The second byte
|
|
|
46448 |
* @return {Boolean} Whether the byte is a color PAC
|
|
|
46449 |
*/
|
|
|
46450 |
|
|
|
46451 |
Cea608Stream.prototype.isColorPAC = function (char1) {
|
|
|
46452 |
return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f;
|
|
|
46453 |
};
|
|
|
46454 |
/**
|
|
|
46455 |
* Detects if a single byte is in the range of a normal character
|
|
|
46456 |
*
|
|
|
46457 |
* Normal text bytes are in the range 0x20 to 0x7f.
|
|
|
46458 |
*
|
|
|
46459 |
* @param {Integer} char The byte
|
|
|
46460 |
* @return {Boolean} Whether the byte is a normal character
|
|
|
46461 |
*/
|
|
|
46462 |
|
|
|
46463 |
Cea608Stream.prototype.isNormalChar = function (char) {
|
|
|
46464 |
return char >= 0x20 && char <= 0x7f;
|
|
|
46465 |
};
|
|
|
46466 |
/**
|
|
|
46467 |
* Configures roll-up
|
|
|
46468 |
*
|
|
|
46469 |
* @param {Integer} pts Current PTS
|
|
|
46470 |
* @param {Integer} newBaseRow Used by PACs to slide the current window to
|
|
|
46471 |
* a new position
|
|
|
46472 |
*/
|
|
|
46473 |
|
|
|
46474 |
Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) {
|
|
|
46475 |
// Reset the base row to the bottom row when switching modes
|
|
|
46476 |
if (this.mode_ !== 'rollUp') {
|
|
|
46477 |
this.row_ = BOTTOM_ROW;
|
|
|
46478 |
this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up
|
|
|
46479 |
|
|
|
46480 |
this.flushDisplayed(pts);
|
|
|
46481 |
this.nonDisplayed_ = createDisplayBuffer();
|
|
|
46482 |
this.displayed_ = createDisplayBuffer();
|
|
|
46483 |
}
|
|
|
46484 |
if (newBaseRow !== undefined && newBaseRow !== this.row_) {
|
|
|
46485 |
// move currently displayed captions (up or down) to the new base row
|
|
|
46486 |
for (var i = 0; i < this.rollUpRows_; i++) {
|
|
|
46487 |
this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i];
|
|
|
46488 |
this.displayed_[this.row_ - i] = {
|
|
|
46489 |
text: '',
|
|
|
46490 |
indent: 0,
|
|
|
46491 |
offset: 0
|
|
|
46492 |
};
|
|
|
46493 |
}
|
|
|
46494 |
}
|
|
|
46495 |
if (newBaseRow === undefined) {
|
|
|
46496 |
newBaseRow = this.row_;
|
|
|
46497 |
}
|
|
|
46498 |
this.topRow_ = newBaseRow - this.rollUpRows_ + 1;
|
|
|
46499 |
}; // Adds the opening HTML tag for the passed character to the caption text,
|
|
|
46500 |
// and keeps track of it for later closing
|
|
|
46501 |
|
|
|
46502 |
Cea608Stream.prototype.addFormatting = function (pts, format) {
|
|
|
46503 |
this.formatting_ = this.formatting_.concat(format);
|
|
|
46504 |
var text = format.reduce(function (text, format) {
|
|
|
46505 |
return text + '<' + format + '>';
|
|
|
46506 |
}, '');
|
|
|
46507 |
this[this.mode_](pts, text);
|
|
|
46508 |
}; // Adds HTML closing tags for current formatting to caption text and
|
|
|
46509 |
// clears remembered formatting
|
|
|
46510 |
|
|
|
46511 |
Cea608Stream.prototype.clearFormatting = function (pts) {
|
|
|
46512 |
if (!this.formatting_.length) {
|
|
|
46513 |
return;
|
|
|
46514 |
}
|
|
|
46515 |
var text = this.formatting_.reverse().reduce(function (text, format) {
|
|
|
46516 |
return text + '</' + format + '>';
|
|
|
46517 |
}, '');
|
|
|
46518 |
this.formatting_ = [];
|
|
|
46519 |
this[this.mode_](pts, text);
|
|
|
46520 |
}; // Mode Implementations
|
|
|
46521 |
|
|
|
46522 |
Cea608Stream.prototype.popOn = function (pts, text) {
|
|
|
46523 |
var baseRow = this.nonDisplayed_[this.row_].text; // buffer characters
|
|
|
46524 |
|
|
|
46525 |
baseRow += text;
|
|
|
46526 |
this.nonDisplayed_[this.row_].text = baseRow;
|
|
|
46527 |
};
|
|
|
46528 |
Cea608Stream.prototype.rollUp = function (pts, text) {
|
|
|
46529 |
var baseRow = this.displayed_[this.row_].text;
|
|
|
46530 |
baseRow += text;
|
|
|
46531 |
this.displayed_[this.row_].text = baseRow;
|
|
|
46532 |
};
|
|
|
46533 |
Cea608Stream.prototype.shiftRowsUp_ = function () {
|
|
|
46534 |
var i; // clear out inactive rows
|
|
|
46535 |
|
|
|
46536 |
for (i = 0; i < this.topRow_; i++) {
|
|
|
46537 |
this.displayed_[i] = {
|
|
|
46538 |
text: '',
|
|
|
46539 |
indent: 0,
|
|
|
46540 |
offset: 0
|
|
|
46541 |
};
|
|
|
46542 |
}
|
|
|
46543 |
for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) {
|
|
|
46544 |
this.displayed_[i] = {
|
|
|
46545 |
text: '',
|
|
|
46546 |
indent: 0,
|
|
|
46547 |
offset: 0
|
|
|
46548 |
};
|
|
|
46549 |
} // shift displayed rows up
|
|
|
46550 |
|
|
|
46551 |
for (i = this.topRow_; i < this.row_; i++) {
|
|
|
46552 |
this.displayed_[i] = this.displayed_[i + 1];
|
|
|
46553 |
} // clear out the bottom row
|
|
|
46554 |
|
|
|
46555 |
this.displayed_[this.row_] = {
|
|
|
46556 |
text: '',
|
|
|
46557 |
indent: 0,
|
|
|
46558 |
offset: 0
|
|
|
46559 |
};
|
|
|
46560 |
};
|
|
|
46561 |
Cea608Stream.prototype.paintOn = function (pts, text) {
|
|
|
46562 |
var baseRow = this.displayed_[this.row_].text;
|
|
|
46563 |
baseRow += text;
|
|
|
46564 |
this.displayed_[this.row_].text = baseRow;
|
|
|
46565 |
}; // exports
|
|
|
46566 |
|
|
|
46567 |
var captionStream = {
|
|
|
46568 |
CaptionStream: CaptionStream$2,
|
|
|
46569 |
Cea608Stream: Cea608Stream,
|
|
|
46570 |
Cea708Stream: Cea708Stream
|
|
|
46571 |
};
|
|
|
46572 |
/**
|
|
|
46573 |
* mux.js
|
|
|
46574 |
*
|
|
|
46575 |
* Copyright (c) Brightcove
|
|
|
46576 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
46577 |
*/
|
|
|
46578 |
|
|
|
46579 |
var streamTypes = {
|
|
|
46580 |
H264_STREAM_TYPE: 0x1B,
|
|
|
46581 |
ADTS_STREAM_TYPE: 0x0F,
|
|
|
46582 |
METADATA_STREAM_TYPE: 0x15
|
|
|
46583 |
};
|
|
|
46584 |
/**
|
|
|
46585 |
* mux.js
|
|
|
46586 |
*
|
|
|
46587 |
* Copyright (c) Brightcove
|
|
|
46588 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
46589 |
*
|
|
|
46590 |
* Accepts program elementary stream (PES) data events and corrects
|
|
|
46591 |
* decode and presentation time stamps to account for a rollover
|
|
|
46592 |
* of the 33 bit value.
|
|
|
46593 |
*/
|
|
|
46594 |
|
|
|
46595 |
var Stream$6 = stream;
|
|
|
46596 |
var MAX_TS = 8589934592;
|
|
|
46597 |
var RO_THRESH = 4294967296;
|
|
|
46598 |
var TYPE_SHARED = 'shared';
|
|
|
46599 |
var handleRollover$1 = function (value, reference) {
|
|
|
46600 |
var direction = 1;
|
|
|
46601 |
if (value > reference) {
|
|
|
46602 |
// If the current timestamp value is greater than our reference timestamp and we detect a
|
|
|
46603 |
// timestamp rollover, this means the roll over is happening in the opposite direction.
|
|
|
46604 |
// Example scenario: Enter a long stream/video just after a rollover occurred. The reference
|
|
|
46605 |
// point will be set to a small number, e.g. 1. The user then seeks backwards over the
|
|
|
46606 |
// rollover point. In loading this segment, the timestamp values will be very large,
|
|
|
46607 |
// e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust
|
|
|
46608 |
// the time stamp to be `value - 2^33`.
|
|
|
46609 |
direction = -1;
|
|
|
46610 |
} // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will
|
|
|
46611 |
// cause an incorrect adjustment.
|
|
|
46612 |
|
|
|
46613 |
while (Math.abs(reference - value) > RO_THRESH) {
|
|
|
46614 |
value += direction * MAX_TS;
|
|
|
46615 |
}
|
|
|
46616 |
return value;
|
|
|
46617 |
};
|
|
|
46618 |
var TimestampRolloverStream$1 = function (type) {
|
|
|
46619 |
var lastDTS, referenceDTS;
|
|
|
46620 |
TimestampRolloverStream$1.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed
|
|
|
46621 |
// video and audio. We could use `undefined` here, but having a string
|
|
|
46622 |
// makes debugging a little clearer.
|
|
|
46623 |
|
|
|
46624 |
this.type_ = type || TYPE_SHARED;
|
|
|
46625 |
this.push = function (data) {
|
|
|
46626 |
/**
|
|
|
46627 |
* Rollover stream expects data from elementary stream.
|
|
|
46628 |
* Elementary stream can push forward 2 types of data
|
|
|
46629 |
* - Parsed Video/Audio/Timed-metadata PES (packetized elementary stream) packets
|
|
|
46630 |
* - Tracks metadata from PMT (Program Map Table)
|
|
|
46631 |
* Rollover stream expects pts/dts info to be available, since it stores lastDTS
|
|
|
46632 |
* We should ignore non-PES packets since they may override lastDTS to undefined.
|
|
|
46633 |
* lastDTS is important to signal the next segments
|
|
|
46634 |
* about rollover from the previous segments.
|
|
|
46635 |
*/
|
|
|
46636 |
if (data.type === 'metadata') {
|
|
|
46637 |
this.trigger('data', data);
|
|
|
46638 |
return;
|
|
|
46639 |
} // Any "shared" rollover streams will accept _all_ data. Otherwise,
|
|
|
46640 |
// streams will only accept data that matches their type.
|
|
|
46641 |
|
|
|
46642 |
if (this.type_ !== TYPE_SHARED && data.type !== this.type_) {
|
|
|
46643 |
return;
|
|
|
46644 |
}
|
|
|
46645 |
if (referenceDTS === undefined) {
|
|
|
46646 |
referenceDTS = data.dts;
|
|
|
46647 |
}
|
|
|
46648 |
data.dts = handleRollover$1(data.dts, referenceDTS);
|
|
|
46649 |
data.pts = handleRollover$1(data.pts, referenceDTS);
|
|
|
46650 |
lastDTS = data.dts;
|
|
|
46651 |
this.trigger('data', data);
|
|
|
46652 |
};
|
|
|
46653 |
this.flush = function () {
|
|
|
46654 |
referenceDTS = lastDTS;
|
|
|
46655 |
this.trigger('done');
|
|
|
46656 |
};
|
|
|
46657 |
this.endTimeline = function () {
|
|
|
46658 |
this.flush();
|
|
|
46659 |
this.trigger('endedtimeline');
|
|
|
46660 |
};
|
|
|
46661 |
this.discontinuity = function () {
|
|
|
46662 |
referenceDTS = void 0;
|
|
|
46663 |
lastDTS = void 0;
|
|
|
46664 |
};
|
|
|
46665 |
this.reset = function () {
|
|
|
46666 |
this.discontinuity();
|
|
|
46667 |
this.trigger('reset');
|
|
|
46668 |
};
|
|
|
46669 |
};
|
|
|
46670 |
TimestampRolloverStream$1.prototype = new Stream$6();
|
|
|
46671 |
var timestampRolloverStream = {
|
|
|
46672 |
TimestampRolloverStream: TimestampRolloverStream$1,
|
|
|
46673 |
handleRollover: handleRollover$1
|
|
|
46674 |
}; // Once IE11 support is dropped, this function should be removed.
|
|
|
46675 |
|
|
|
46676 |
var typedArrayIndexOf$1 = (typedArray, element, fromIndex) => {
|
|
|
46677 |
if (!typedArray) {
|
|
|
46678 |
return -1;
|
|
|
46679 |
}
|
|
|
46680 |
var currentIndex = fromIndex;
|
|
|
46681 |
for (; currentIndex < typedArray.length; currentIndex++) {
|
|
|
46682 |
if (typedArray[currentIndex] === element) {
|
|
|
46683 |
return currentIndex;
|
|
|
46684 |
}
|
|
|
46685 |
}
|
|
|
46686 |
return -1;
|
|
|
46687 |
};
|
|
|
46688 |
var typedArray = {
|
|
|
46689 |
typedArrayIndexOf: typedArrayIndexOf$1
|
|
|
46690 |
};
|
|
|
46691 |
/**
|
|
|
46692 |
* mux.js
|
|
|
46693 |
*
|
|
|
46694 |
* Copyright (c) Brightcove
|
|
|
46695 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
46696 |
*
|
|
|
46697 |
* Tools for parsing ID3 frame data
|
|
|
46698 |
* @see http://id3.org/id3v2.3.0
|
|
|
46699 |
*/
|
|
|
46700 |
|
|
|
46701 |
var typedArrayIndexOf = typedArray.typedArrayIndexOf,
|
|
|
46702 |
// Frames that allow different types of text encoding contain a text
|
|
|
46703 |
// encoding description byte [ID3v2.4.0 section 4.]
|
|
|
46704 |
textEncodingDescriptionByte = {
|
|
|
46705 |
Iso88591: 0x00,
|
|
|
46706 |
// ISO-8859-1, terminated with \0.
|
|
|
46707 |
Utf16: 0x01,
|
|
|
46708 |
// UTF-16 encoded Unicode BOM, terminated with \0\0
|
|
|
46709 |
Utf16be: 0x02,
|
|
|
46710 |
// UTF-16BE encoded Unicode, without BOM, terminated with \0\0
|
|
|
46711 |
Utf8: 0x03 // UTF-8 encoded Unicode, terminated with \0
|
|
|
46712 |
},
|
|
|
46713 |
// return a percent-encoded representation of the specified byte range
|
|
|
46714 |
// @see http://en.wikipedia.org/wiki/Percent-encoding
|
|
|
46715 |
percentEncode$1 = function (bytes, start, end) {
|
|
|
46716 |
var i,
|
|
|
46717 |
result = '';
|
|
|
46718 |
for (i = start; i < end; i++) {
|
|
|
46719 |
result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
|
|
|
46720 |
}
|
|
|
46721 |
return result;
|
|
|
46722 |
},
|
|
|
46723 |
// return the string representation of the specified byte range,
|
|
|
46724 |
// interpreted as UTf-8.
|
|
|
46725 |
parseUtf8 = function (bytes, start, end) {
|
|
|
46726 |
return decodeURIComponent(percentEncode$1(bytes, start, end));
|
|
|
46727 |
},
|
|
|
46728 |
// return the string representation of the specified byte range,
|
|
|
46729 |
// interpreted as ISO-8859-1.
|
|
|
46730 |
parseIso88591$1 = function (bytes, start, end) {
|
|
|
46731 |
return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line
|
|
|
46732 |
},
|
|
|
46733 |
parseSyncSafeInteger$1 = function (data) {
|
|
|
46734 |
return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
|
|
|
46735 |
},
|
|
|
46736 |
frameParsers = {
|
|
|
46737 |
'APIC': function (frame) {
|
|
|
46738 |
var i = 1,
|
|
|
46739 |
mimeTypeEndIndex,
|
|
|
46740 |
descriptionEndIndex,
|
|
|
46741 |
LINK_MIME_TYPE = '-->';
|
|
|
46742 |
if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
|
|
|
46743 |
// ignore frames with unrecognized character encodings
|
|
|
46744 |
return;
|
|
|
46745 |
} // parsing fields [ID3v2.4.0 section 4.14.]
|
|
|
46746 |
|
|
|
46747 |
mimeTypeEndIndex = typedArrayIndexOf(frame.data, 0, i);
|
|
|
46748 |
if (mimeTypeEndIndex < 0) {
|
|
|
46749 |
// malformed frame
|
|
|
46750 |
return;
|
|
|
46751 |
} // parsing Mime type field (terminated with \0)
|
|
|
46752 |
|
|
|
46753 |
frame.mimeType = parseIso88591$1(frame.data, i, mimeTypeEndIndex);
|
|
|
46754 |
i = mimeTypeEndIndex + 1; // parsing 1-byte Picture Type field
|
|
|
46755 |
|
|
|
46756 |
frame.pictureType = frame.data[i];
|
|
|
46757 |
i++;
|
|
|
46758 |
descriptionEndIndex = typedArrayIndexOf(frame.data, 0, i);
|
|
|
46759 |
if (descriptionEndIndex < 0) {
|
|
|
46760 |
// malformed frame
|
|
|
46761 |
return;
|
|
|
46762 |
} // parsing Description field (terminated with \0)
|
|
|
46763 |
|
|
|
46764 |
frame.description = parseUtf8(frame.data, i, descriptionEndIndex);
|
|
|
46765 |
i = descriptionEndIndex + 1;
|
|
|
46766 |
if (frame.mimeType === LINK_MIME_TYPE) {
|
|
|
46767 |
// parsing Picture Data field as URL (always represented as ISO-8859-1 [ID3v2.4.0 section 4.])
|
|
|
46768 |
frame.url = parseIso88591$1(frame.data, i, frame.data.length);
|
|
|
46769 |
} else {
|
|
|
46770 |
// parsing Picture Data field as binary data
|
|
|
46771 |
frame.pictureData = frame.data.subarray(i, frame.data.length);
|
|
|
46772 |
}
|
|
|
46773 |
},
|
|
|
46774 |
'T*': function (frame) {
|
|
|
46775 |
if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
|
|
|
46776 |
// ignore frames with unrecognized character encodings
|
|
|
46777 |
return;
|
|
|
46778 |
} // parse text field, do not include null terminator in the frame value
|
|
|
46779 |
// frames that allow different types of encoding contain terminated text [ID3v2.4.0 section 4.]
|
|
|
46780 |
|
|
|
46781 |
frame.value = parseUtf8(frame.data, 1, frame.data.length).replace(/\0*$/, ''); // text information frames supports multiple strings, stored as a terminator separated list [ID3v2.4.0 section 4.2.]
|
|
|
46782 |
|
|
|
46783 |
frame.values = frame.value.split('\0');
|
|
|
46784 |
},
|
|
|
46785 |
'TXXX': function (frame) {
|
|
|
46786 |
var descriptionEndIndex;
|
|
|
46787 |
if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
|
|
|
46788 |
// ignore frames with unrecognized character encodings
|
|
|
46789 |
return;
|
|
|
46790 |
}
|
|
|
46791 |
descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1);
|
|
|
46792 |
if (descriptionEndIndex === -1) {
|
|
|
46793 |
return;
|
|
|
46794 |
} // parse the text fields
|
|
|
46795 |
|
|
|
46796 |
frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // do not include the null terminator in the tag value
|
|
|
46797 |
// frames that allow different types of encoding contain terminated text
|
|
|
46798 |
// [ID3v2.4.0 section 4.]
|
|
|
46799 |
|
|
|
46800 |
frame.value = parseUtf8(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0*$/, '');
|
|
|
46801 |
frame.data = frame.value;
|
|
|
46802 |
},
|
|
|
46803 |
'W*': function (frame) {
|
|
|
46804 |
// parse URL field; URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.]
|
|
|
46805 |
// if the value is followed by a string termination all the following information should be ignored [ID3v2.4.0 section 4.3]
|
|
|
46806 |
frame.url = parseIso88591$1(frame.data, 0, frame.data.length).replace(/\0.*$/, '');
|
|
|
46807 |
},
|
|
|
46808 |
'WXXX': function (frame) {
|
|
|
46809 |
var descriptionEndIndex;
|
|
|
46810 |
if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
|
|
|
46811 |
// ignore frames with unrecognized character encodings
|
|
|
46812 |
return;
|
|
|
46813 |
}
|
|
|
46814 |
descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1);
|
|
|
46815 |
if (descriptionEndIndex === -1) {
|
|
|
46816 |
return;
|
|
|
46817 |
} // parse the description and URL fields
|
|
|
46818 |
|
|
|
46819 |
frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.]
|
|
|
46820 |
// if the value is followed by a string termination all the following information
|
|
|
46821 |
// should be ignored [ID3v2.4.0 section 4.3]
|
|
|
46822 |
|
|
|
46823 |
frame.url = parseIso88591$1(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0.*$/, '');
|
|
|
46824 |
},
|
|
|
46825 |
'PRIV': function (frame) {
|
|
|
46826 |
var i;
|
|
|
46827 |
for (i = 0; i < frame.data.length; i++) {
|
|
|
46828 |
if (frame.data[i] === 0) {
|
|
|
46829 |
// parse the description and URL fields
|
|
|
46830 |
frame.owner = parseIso88591$1(frame.data, 0, i);
|
|
|
46831 |
break;
|
|
|
46832 |
}
|
|
|
46833 |
}
|
|
|
46834 |
frame.privateData = frame.data.subarray(i + 1);
|
|
|
46835 |
frame.data = frame.privateData;
|
|
|
46836 |
}
|
|
|
46837 |
};
|
|
|
46838 |
var parseId3Frames$1 = function (data) {
|
|
|
46839 |
var frameSize,
|
|
|
46840 |
frameHeader,
|
|
|
46841 |
frameStart = 10,
|
|
|
46842 |
tagSize = 0,
|
|
|
46843 |
frames = []; // If we don't have enough data for a header, 10 bytes,
|
|
|
46844 |
// or 'ID3' in the first 3 bytes this is not a valid ID3 tag.
|
|
|
46845 |
|
|
|
46846 |
if (data.length < 10 || data[0] !== 'I'.charCodeAt(0) || data[1] !== 'D'.charCodeAt(0) || data[2] !== '3'.charCodeAt(0)) {
|
|
|
46847 |
return;
|
|
|
46848 |
} // the frame size is transmitted as a 28-bit integer in the
|
|
|
46849 |
// last four bytes of the ID3 header.
|
|
|
46850 |
// The most significant bit of each byte is dropped and the
|
|
|
46851 |
// results concatenated to recover the actual value.
|
|
|
46852 |
|
|
|
46853 |
tagSize = parseSyncSafeInteger$1(data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more
|
|
|
46854 |
// convenient for our comparisons to include it
|
|
|
46855 |
|
|
|
46856 |
tagSize += 10; // check bit 6 of byte 5 for the extended header flag.
|
|
|
46857 |
|
|
|
46858 |
var hasExtendedHeader = data[5] & 0x40;
|
|
|
46859 |
if (hasExtendedHeader) {
|
|
|
46860 |
// advance the frame start past the extended header
|
|
|
46861 |
frameStart += 4; // header size field
|
|
|
46862 |
|
|
|
46863 |
frameStart += parseSyncSafeInteger$1(data.subarray(10, 14));
|
|
|
46864 |
tagSize -= parseSyncSafeInteger$1(data.subarray(16, 20)); // clip any padding off the end
|
|
|
46865 |
} // parse one or more ID3 frames
|
|
|
46866 |
// http://id3.org/id3v2.3.0#ID3v2_frame_overview
|
|
|
46867 |
|
|
|
46868 |
do {
|
|
|
46869 |
// determine the number of bytes in this frame
|
|
|
46870 |
frameSize = parseSyncSafeInteger$1(data.subarray(frameStart + 4, frameStart + 8));
|
|
|
46871 |
if (frameSize < 1) {
|
|
|
46872 |
break;
|
|
|
46873 |
}
|
|
|
46874 |
frameHeader = String.fromCharCode(data[frameStart], data[frameStart + 1], data[frameStart + 2], data[frameStart + 3]);
|
|
|
46875 |
var frame = {
|
|
|
46876 |
id: frameHeader,
|
|
|
46877 |
data: data.subarray(frameStart + 10, frameStart + frameSize + 10)
|
|
|
46878 |
};
|
|
|
46879 |
frame.key = frame.id; // parse frame values
|
|
|
46880 |
|
|
|
46881 |
if (frameParsers[frame.id]) {
|
|
|
46882 |
// use frame specific parser
|
|
|
46883 |
frameParsers[frame.id](frame);
|
|
|
46884 |
} else if (frame.id[0] === 'T') {
|
|
|
46885 |
// use text frame generic parser
|
|
|
46886 |
frameParsers['T*'](frame);
|
|
|
46887 |
} else if (frame.id[0] === 'W') {
|
|
|
46888 |
// use URL link frame generic parser
|
|
|
46889 |
frameParsers['W*'](frame);
|
|
|
46890 |
}
|
|
|
46891 |
frames.push(frame);
|
|
|
46892 |
frameStart += 10; // advance past the frame header
|
|
|
46893 |
|
|
|
46894 |
frameStart += frameSize; // advance past the frame body
|
|
|
46895 |
} while (frameStart < tagSize);
|
|
|
46896 |
return frames;
|
|
|
46897 |
};
|
|
|
46898 |
var parseId3 = {
|
|
|
46899 |
parseId3Frames: parseId3Frames$1,
|
|
|
46900 |
parseSyncSafeInteger: parseSyncSafeInteger$1,
|
|
|
46901 |
frameParsers: frameParsers
|
|
|
46902 |
};
|
|
|
46903 |
/**
|
|
|
46904 |
* mux.js
|
|
|
46905 |
*
|
|
|
46906 |
* Copyright (c) Brightcove
|
|
|
46907 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
46908 |
*
|
|
|
46909 |
* Accepts program elementary stream (PES) data events and parses out
|
|
|
46910 |
* ID3 metadata from them, if present.
|
|
|
46911 |
* @see http://id3.org/id3v2.3.0
|
|
|
46912 |
*/
|
|
|
46913 |
|
|
|
46914 |
var Stream$5 = stream,
|
|
|
46915 |
StreamTypes$3 = streamTypes,
|
|
|
46916 |
id3 = parseId3,
|
|
|
46917 |
MetadataStream;
|
|
|
46918 |
MetadataStream = function (options) {
|
|
|
46919 |
var settings = {
|
|
|
46920 |
// the bytes of the program-level descriptor field in MP2T
|
|
|
46921 |
// see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
|
|
|
46922 |
// program element descriptors"
|
|
|
46923 |
descriptor: options && options.descriptor
|
|
|
46924 |
},
|
|
|
46925 |
// the total size in bytes of the ID3 tag being parsed
|
|
|
46926 |
tagSize = 0,
|
|
|
46927 |
// tag data that is not complete enough to be parsed
|
|
|
46928 |
buffer = [],
|
|
|
46929 |
// the total number of bytes currently in the buffer
|
|
|
46930 |
bufferSize = 0,
|
|
|
46931 |
i;
|
|
|
46932 |
MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type
|
|
|
46933 |
// https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
|
|
|
46934 |
|
|
|
46935 |
this.dispatchType = StreamTypes$3.METADATA_STREAM_TYPE.toString(16);
|
|
|
46936 |
if (settings.descriptor) {
|
|
|
46937 |
for (i = 0; i < settings.descriptor.length; i++) {
|
|
|
46938 |
this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
|
|
|
46939 |
}
|
|
|
46940 |
}
|
|
|
46941 |
this.push = function (chunk) {
|
|
|
46942 |
var tag, frameStart, frameSize, frame, i, frameHeader;
|
|
|
46943 |
if (chunk.type !== 'timed-metadata') {
|
|
|
46944 |
return;
|
|
|
46945 |
} // if data_alignment_indicator is set in the PES header,
|
|
|
46946 |
// we must have the start of a new ID3 tag. Assume anything
|
|
|
46947 |
// remaining in the buffer was malformed and throw it out
|
|
|
46948 |
|
|
|
46949 |
if (chunk.dataAlignmentIndicator) {
|
|
|
46950 |
bufferSize = 0;
|
|
|
46951 |
buffer.length = 0;
|
|
|
46952 |
} // ignore events that don't look like ID3 data
|
|
|
46953 |
|
|
|
46954 |
if (buffer.length === 0 && (chunk.data.length < 10 || chunk.data[0] !== 'I'.charCodeAt(0) || chunk.data[1] !== 'D'.charCodeAt(0) || chunk.data[2] !== '3'.charCodeAt(0))) {
|
|
|
46955 |
this.trigger('log', {
|
|
|
46956 |
level: 'warn',
|
|
|
46957 |
message: 'Skipping unrecognized metadata packet'
|
|
|
46958 |
});
|
|
|
46959 |
return;
|
|
|
46960 |
} // add this chunk to the data we've collected so far
|
|
|
46961 |
|
|
|
46962 |
buffer.push(chunk);
|
|
|
46963 |
bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header
|
|
|
46964 |
|
|
|
46965 |
if (buffer.length === 1) {
|
|
|
46966 |
// the frame size is transmitted as a 28-bit integer in the
|
|
|
46967 |
// last four bytes of the ID3 header.
|
|
|
46968 |
// The most significant bit of each byte is dropped and the
|
|
|
46969 |
// results concatenated to recover the actual value.
|
|
|
46970 |
tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more
|
|
|
46971 |
// convenient for our comparisons to include it
|
|
|
46972 |
|
|
|
46973 |
tagSize += 10;
|
|
|
46974 |
} // if the entire frame has not arrived, wait for more data
|
|
|
46975 |
|
|
|
46976 |
if (bufferSize < tagSize) {
|
|
|
46977 |
return;
|
|
|
46978 |
} // collect the entire frame so it can be parsed
|
|
|
46979 |
|
|
|
46980 |
tag = {
|
|
|
46981 |
data: new Uint8Array(tagSize),
|
|
|
46982 |
frames: [],
|
|
|
46983 |
pts: buffer[0].pts,
|
|
|
46984 |
dts: buffer[0].dts
|
|
|
46985 |
};
|
|
|
46986 |
for (i = 0; i < tagSize;) {
|
|
|
46987 |
tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
|
|
|
46988 |
i += buffer[0].data.byteLength;
|
|
|
46989 |
bufferSize -= buffer[0].data.byteLength;
|
|
|
46990 |
buffer.shift();
|
|
|
46991 |
} // find the start of the first frame and the end of the tag
|
|
|
46992 |
|
|
|
46993 |
frameStart = 10;
|
|
|
46994 |
if (tag.data[5] & 0x40) {
|
|
|
46995 |
// advance the frame start past the extended header
|
|
|
46996 |
frameStart += 4; // header size field
|
|
|
46997 |
|
|
|
46998 |
frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end
|
|
|
46999 |
|
|
|
47000 |
tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20));
|
|
|
47001 |
} // parse one or more ID3 frames
|
|
|
47002 |
// http://id3.org/id3v2.3.0#ID3v2_frame_overview
|
|
|
47003 |
|
|
|
47004 |
do {
|
|
|
47005 |
// determine the number of bytes in this frame
|
|
|
47006 |
frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
|
|
|
47007 |
if (frameSize < 1) {
|
|
|
47008 |
this.trigger('log', {
|
|
|
47009 |
level: 'warn',
|
|
|
47010 |
message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.'
|
|
|
47011 |
}); // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames
|
|
|
47012 |
// to be sent along.
|
|
|
47013 |
|
|
|
47014 |
break;
|
|
|
47015 |
}
|
|
|
47016 |
frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]);
|
|
|
47017 |
frame = {
|
|
|
47018 |
id: frameHeader,
|
|
|
47019 |
data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
|
|
|
47020 |
};
|
|
|
47021 |
frame.key = frame.id; // parse frame values
|
|
|
47022 |
|
|
|
47023 |
if (id3.frameParsers[frame.id]) {
|
|
|
47024 |
// use frame specific parser
|
|
|
47025 |
id3.frameParsers[frame.id](frame);
|
|
|
47026 |
} else if (frame.id[0] === 'T') {
|
|
|
47027 |
// use text frame generic parser
|
|
|
47028 |
id3.frameParsers['T*'](frame);
|
|
|
47029 |
} else if (frame.id[0] === 'W') {
|
|
|
47030 |
// use URL link frame generic parser
|
|
|
47031 |
id3.frameParsers['W*'](frame);
|
|
|
47032 |
} // handle the special PRIV frame used to indicate the start
|
|
|
47033 |
// time for raw AAC data
|
|
|
47034 |
|
|
|
47035 |
if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
|
|
|
47036 |
var d = frame.data,
|
|
|
47037 |
size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
|
|
|
47038 |
size *= 4;
|
|
|
47039 |
size += d[7] & 0x03;
|
|
|
47040 |
frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based
|
|
|
47041 |
// on the value of this frame
|
|
|
47042 |
// we couldn't have known the appropriate pts and dts before
|
|
|
47043 |
// parsing this ID3 tag so set those values now
|
|
|
47044 |
|
|
|
47045 |
if (tag.pts === undefined && tag.dts === undefined) {
|
|
|
47046 |
tag.pts = frame.timeStamp;
|
|
|
47047 |
tag.dts = frame.timeStamp;
|
|
|
47048 |
}
|
|
|
47049 |
this.trigger('timestamp', frame);
|
|
|
47050 |
}
|
|
|
47051 |
tag.frames.push(frame);
|
|
|
47052 |
frameStart += 10; // advance past the frame header
|
|
|
47053 |
|
|
|
47054 |
frameStart += frameSize; // advance past the frame body
|
|
|
47055 |
} while (frameStart < tagSize);
|
|
|
47056 |
this.trigger('data', tag);
|
|
|
47057 |
};
|
|
|
47058 |
};
|
|
|
47059 |
MetadataStream.prototype = new Stream$5();
|
|
|
47060 |
var metadataStream = MetadataStream;
|
|
|
47061 |
/**
|
|
|
47062 |
* mux.js
|
|
|
47063 |
*
|
|
|
47064 |
* Copyright (c) Brightcove
|
|
|
47065 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
47066 |
*
|
|
|
47067 |
* A stream-based mp2t to mp4 converter. This utility can be used to
|
|
|
47068 |
* deliver mp4s to a SourceBuffer on platforms that support native
|
|
|
47069 |
* Media Source Extensions.
|
|
|
47070 |
*/
|
|
|
47071 |
|
|
|
47072 |
var Stream$4 = stream,
|
|
|
47073 |
CaptionStream$1 = captionStream,
|
|
|
47074 |
StreamTypes$2 = streamTypes,
|
|
|
47075 |
TimestampRolloverStream = timestampRolloverStream.TimestampRolloverStream; // object types
|
|
|
47076 |
|
|
|
47077 |
var TransportPacketStream, TransportParseStream, ElementaryStream; // constants
|
|
|
47078 |
|
|
|
47079 |
var MP2T_PACKET_LENGTH$1 = 188,
|
|
|
47080 |
// bytes
|
|
|
47081 |
SYNC_BYTE$1 = 0x47;
|
|
|
47082 |
/**
|
|
|
47083 |
* Splits an incoming stream of binary data into MPEG-2 Transport
|
|
|
47084 |
* Stream packets.
|
|
|
47085 |
*/
|
|
|
47086 |
|
|
|
47087 |
TransportPacketStream = function () {
|
|
|
47088 |
var buffer = new Uint8Array(MP2T_PACKET_LENGTH$1),
|
|
|
47089 |
bytesInBuffer = 0;
|
|
|
47090 |
TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream.
|
|
|
47091 |
|
|
|
47092 |
/**
|
|
|
47093 |
* Split a stream of data into M2TS packets
|
|
|
47094 |
**/
|
|
|
47095 |
|
|
|
47096 |
this.push = function (bytes) {
|
|
|
47097 |
var startIndex = 0,
|
|
|
47098 |
endIndex = MP2T_PACKET_LENGTH$1,
|
|
|
47099 |
everything; // If there are bytes remaining from the last segment, prepend them to the
|
|
|
47100 |
// bytes that were pushed in
|
|
|
47101 |
|
|
|
47102 |
if (bytesInBuffer) {
|
|
|
47103 |
everything = new Uint8Array(bytes.byteLength + bytesInBuffer);
|
|
|
47104 |
everything.set(buffer.subarray(0, bytesInBuffer));
|
|
|
47105 |
everything.set(bytes, bytesInBuffer);
|
|
|
47106 |
bytesInBuffer = 0;
|
|
|
47107 |
} else {
|
|
|
47108 |
everything = bytes;
|
|
|
47109 |
} // While we have enough data for a packet
|
|
|
47110 |
|
|
|
47111 |
while (endIndex < everything.byteLength) {
|
|
|
47112 |
// Look for a pair of start and end sync bytes in the data..
|
|
|
47113 |
if (everything[startIndex] === SYNC_BYTE$1 && everything[endIndex] === SYNC_BYTE$1) {
|
|
|
47114 |
// We found a packet so emit it and jump one whole packet forward in
|
|
|
47115 |
// the stream
|
|
|
47116 |
this.trigger('data', everything.subarray(startIndex, endIndex));
|
|
|
47117 |
startIndex += MP2T_PACKET_LENGTH$1;
|
|
|
47118 |
endIndex += MP2T_PACKET_LENGTH$1;
|
|
|
47119 |
continue;
|
|
|
47120 |
} // If we get here, we have somehow become de-synchronized and we need to step
|
|
|
47121 |
// forward one byte at a time until we find a pair of sync bytes that denote
|
|
|
47122 |
// a packet
|
|
|
47123 |
|
|
|
47124 |
startIndex++;
|
|
|
47125 |
endIndex++;
|
|
|
47126 |
} // If there was some data left over at the end of the segment that couldn't
|
|
|
47127 |
// possibly be a whole packet, keep it because it might be the start of a packet
|
|
|
47128 |
// that continues in the next segment
|
|
|
47129 |
|
|
|
47130 |
if (startIndex < everything.byteLength) {
|
|
|
47131 |
buffer.set(everything.subarray(startIndex), 0);
|
|
|
47132 |
bytesInBuffer = everything.byteLength - startIndex;
|
|
|
47133 |
}
|
|
|
47134 |
};
|
|
|
47135 |
/**
|
|
|
47136 |
* Passes identified M2TS packets to the TransportParseStream to be parsed
|
|
|
47137 |
**/
|
|
|
47138 |
|
|
|
47139 |
this.flush = function () {
|
|
|
47140 |
// If the buffer contains a whole packet when we are being flushed, emit it
|
|
|
47141 |
// and empty the buffer. Otherwise hold onto the data because it may be
|
|
|
47142 |
// important for decoding the next segment
|
|
|
47143 |
if (bytesInBuffer === MP2T_PACKET_LENGTH$1 && buffer[0] === SYNC_BYTE$1) {
|
|
|
47144 |
this.trigger('data', buffer);
|
|
|
47145 |
bytesInBuffer = 0;
|
|
|
47146 |
}
|
|
|
47147 |
this.trigger('done');
|
|
|
47148 |
};
|
|
|
47149 |
this.endTimeline = function () {
|
|
|
47150 |
this.flush();
|
|
|
47151 |
this.trigger('endedtimeline');
|
|
|
47152 |
};
|
|
|
47153 |
this.reset = function () {
|
|
|
47154 |
bytesInBuffer = 0;
|
|
|
47155 |
this.trigger('reset');
|
|
|
47156 |
};
|
|
|
47157 |
};
|
|
|
47158 |
TransportPacketStream.prototype = new Stream$4();
|
|
|
47159 |
/**
|
|
|
47160 |
* Accepts an MP2T TransportPacketStream and emits data events with parsed
|
|
|
47161 |
* forms of the individual transport stream packets.
|
|
|
47162 |
*/
|
|
|
47163 |
|
|
|
47164 |
TransportParseStream = function () {
|
|
|
47165 |
var parsePsi, parsePat, parsePmt, self;
|
|
|
47166 |
TransportParseStream.prototype.init.call(this);
|
|
|
47167 |
self = this;
|
|
|
47168 |
this.packetsWaitingForPmt = [];
|
|
|
47169 |
this.programMapTable = undefined;
|
|
|
47170 |
parsePsi = function (payload, psi) {
|
|
|
47171 |
var offset = 0; // PSI packets may be split into multiple sections and those
|
|
|
47172 |
// sections may be split into multiple packets. If a PSI
|
|
|
47173 |
// section starts in this packet, the payload_unit_start_indicator
|
|
|
47174 |
// will be true and the first byte of the payload will indicate
|
|
|
47175 |
// the offset from the current position to the start of the
|
|
|
47176 |
// section.
|
|
|
47177 |
|
|
|
47178 |
if (psi.payloadUnitStartIndicator) {
|
|
|
47179 |
offset += payload[offset] + 1;
|
|
|
47180 |
}
|
|
|
47181 |
if (psi.type === 'pat') {
|
|
|
47182 |
parsePat(payload.subarray(offset), psi);
|
|
|
47183 |
} else {
|
|
|
47184 |
parsePmt(payload.subarray(offset), psi);
|
|
|
47185 |
}
|
|
|
47186 |
};
|
|
|
47187 |
parsePat = function (payload, pat) {
|
|
|
47188 |
pat.section_number = payload[7]; // eslint-disable-line camelcase
|
|
|
47189 |
|
|
|
47190 |
pat.last_section_number = payload[8]; // eslint-disable-line camelcase
|
|
|
47191 |
// skip the PSI header and parse the first PMT entry
|
|
|
47192 |
|
|
|
47193 |
self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
|
|
|
47194 |
pat.pmtPid = self.pmtPid;
|
|
|
47195 |
};
|
|
|
47196 |
/**
|
|
|
47197 |
* Parse out the relevant fields of a Program Map Table (PMT).
|
|
|
47198 |
* @param payload {Uint8Array} the PMT-specific portion of an MP2T
|
|
|
47199 |
* packet. The first byte in this array should be the table_id
|
|
|
47200 |
* field.
|
|
|
47201 |
* @param pmt {object} the object that should be decorated with
|
|
|
47202 |
* fields parsed from the PMT.
|
|
|
47203 |
*/
|
|
|
47204 |
|
|
|
47205 |
parsePmt = function (payload, pmt) {
|
|
|
47206 |
var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually
|
|
|
47207 |
// take effect. We don't believe this should ever be the case
|
|
|
47208 |
// for HLS but we'll ignore "forward" PMT declarations if we see
|
|
|
47209 |
// them. Future PMT declarations have the current_next_indicator
|
|
|
47210 |
// set to zero.
|
|
|
47211 |
|
|
|
47212 |
if (!(payload[5] & 0x01)) {
|
|
|
47213 |
return;
|
|
|
47214 |
} // overwrite any existing program map table
|
|
|
47215 |
|
|
|
47216 |
self.programMapTable = {
|
|
|
47217 |
video: null,
|
|
|
47218 |
audio: null,
|
|
|
47219 |
'timed-metadata': {}
|
|
|
47220 |
}; // the mapping table ends at the end of the current section
|
|
|
47221 |
|
|
|
47222 |
sectionLength = (payload[1] & 0x0f) << 8 | payload[2];
|
|
|
47223 |
tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how
|
|
|
47224 |
// long the program info descriptors are
|
|
|
47225 |
|
|
|
47226 |
programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table
|
|
|
47227 |
|
|
|
47228 |
offset = 12 + programInfoLength;
|
|
|
47229 |
while (offset < tableEnd) {
|
|
|
47230 |
var streamType = payload[offset];
|
|
|
47231 |
var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types
|
|
|
47232 |
// TODO: should this be done for metadata too? for now maintain behavior of
|
|
|
47233 |
// multiple metadata streams
|
|
|
47234 |
|
|
|
47235 |
if (streamType === StreamTypes$2.H264_STREAM_TYPE && self.programMapTable.video === null) {
|
|
|
47236 |
self.programMapTable.video = pid;
|
|
|
47237 |
} else if (streamType === StreamTypes$2.ADTS_STREAM_TYPE && self.programMapTable.audio === null) {
|
|
|
47238 |
self.programMapTable.audio = pid;
|
|
|
47239 |
} else if (streamType === StreamTypes$2.METADATA_STREAM_TYPE) {
|
|
|
47240 |
// map pid to stream type for metadata streams
|
|
|
47241 |
self.programMapTable['timed-metadata'][pid] = streamType;
|
|
|
47242 |
} // move to the next table entry
|
|
|
47243 |
// skip past the elementary stream descriptors, if present
|
|
|
47244 |
|
|
|
47245 |
offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
|
|
|
47246 |
} // record the map on the packet as well
|
|
|
47247 |
|
|
|
47248 |
pmt.programMapTable = self.programMapTable;
|
|
|
47249 |
};
|
|
|
47250 |
/**
|
|
|
47251 |
* Deliver a new MP2T packet to the next stream in the pipeline.
|
|
|
47252 |
*/
|
|
|
47253 |
|
|
|
47254 |
this.push = function (packet) {
|
|
|
47255 |
var result = {},
|
|
|
47256 |
offset = 4;
|
|
|
47257 |
result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1]
|
|
|
47258 |
|
|
|
47259 |
result.pid = packet[1] & 0x1f;
|
|
|
47260 |
result.pid <<= 8;
|
|
|
47261 |
result.pid |= packet[2]; // if an adaption field is present, its length is specified by the
|
|
|
47262 |
// fifth byte of the TS packet header. The adaptation field is
|
|
|
47263 |
// used to add stuffing to PES packets that don't fill a complete
|
|
|
47264 |
// TS packet, and to specify some forms of timing and control data
|
|
|
47265 |
// that we do not currently use.
|
|
|
47266 |
|
|
|
47267 |
if ((packet[3] & 0x30) >>> 4 > 0x01) {
|
|
|
47268 |
offset += packet[offset] + 1;
|
|
|
47269 |
} // parse the rest of the packet based on the type
|
|
|
47270 |
|
|
|
47271 |
if (result.pid === 0) {
|
|
|
47272 |
result.type = 'pat';
|
|
|
47273 |
parsePsi(packet.subarray(offset), result);
|
|
|
47274 |
this.trigger('data', result);
|
|
|
47275 |
} else if (result.pid === this.pmtPid) {
|
|
|
47276 |
result.type = 'pmt';
|
|
|
47277 |
parsePsi(packet.subarray(offset), result);
|
|
|
47278 |
this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now
|
|
|
47279 |
|
|
|
47280 |
while (this.packetsWaitingForPmt.length) {
|
|
|
47281 |
this.processPes_.apply(this, this.packetsWaitingForPmt.shift());
|
|
|
47282 |
}
|
|
|
47283 |
} else if (this.programMapTable === undefined) {
|
|
|
47284 |
// When we have not seen a PMT yet, defer further processing of
|
|
|
47285 |
// PES packets until one has been parsed
|
|
|
47286 |
this.packetsWaitingForPmt.push([packet, offset, result]);
|
|
|
47287 |
} else {
|
|
|
47288 |
this.processPes_(packet, offset, result);
|
|
|
47289 |
}
|
|
|
47290 |
};
|
|
|
47291 |
this.processPes_ = function (packet, offset, result) {
|
|
|
47292 |
// set the appropriate stream type
|
|
|
47293 |
if (result.pid === this.programMapTable.video) {
|
|
|
47294 |
result.streamType = StreamTypes$2.H264_STREAM_TYPE;
|
|
|
47295 |
} else if (result.pid === this.programMapTable.audio) {
|
|
|
47296 |
result.streamType = StreamTypes$2.ADTS_STREAM_TYPE;
|
|
|
47297 |
} else {
|
|
|
47298 |
// if not video or audio, it is timed-metadata or unknown
|
|
|
47299 |
// if unknown, streamType will be undefined
|
|
|
47300 |
result.streamType = this.programMapTable['timed-metadata'][result.pid];
|
|
|
47301 |
}
|
|
|
47302 |
result.type = 'pes';
|
|
|
47303 |
result.data = packet.subarray(offset);
|
|
|
47304 |
this.trigger('data', result);
|
|
|
47305 |
};
|
|
|
47306 |
};
|
|
|
47307 |
TransportParseStream.prototype = new Stream$4();
|
|
|
47308 |
TransportParseStream.STREAM_TYPES = {
|
|
|
47309 |
h264: 0x1b,
|
|
|
47310 |
adts: 0x0f
|
|
|
47311 |
};
|
|
|
47312 |
/**
|
|
|
47313 |
* Reconsistutes program elementary stream (PES) packets from parsed
|
|
|
47314 |
* transport stream packets. That is, if you pipe an
|
|
|
47315 |
* mp2t.TransportParseStream into a mp2t.ElementaryStream, the output
|
|
|
47316 |
* events will be events which capture the bytes for individual PES
|
|
|
47317 |
* packets plus relevant metadata that has been extracted from the
|
|
|
47318 |
* container.
|
|
|
47319 |
*/
|
|
|
47320 |
|
|
|
47321 |
ElementaryStream = function () {
|
|
|
47322 |
var self = this,
|
|
|
47323 |
segmentHadPmt = false,
|
|
|
47324 |
// PES packet fragments
|
|
|
47325 |
video = {
|
|
|
47326 |
data: [],
|
|
|
47327 |
size: 0
|
|
|
47328 |
},
|
|
|
47329 |
audio = {
|
|
|
47330 |
data: [],
|
|
|
47331 |
size: 0
|
|
|
47332 |
},
|
|
|
47333 |
timedMetadata = {
|
|
|
47334 |
data: [],
|
|
|
47335 |
size: 0
|
|
|
47336 |
},
|
|
|
47337 |
programMapTable,
|
|
|
47338 |
parsePes = function (payload, pes) {
|
|
|
47339 |
var ptsDtsFlags;
|
|
|
47340 |
const startPrefix = payload[0] << 16 | payload[1] << 8 | payload[2]; // default to an empty array
|
|
|
47341 |
|
|
|
47342 |
pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets
|
|
|
47343 |
// that are frame data that is continuing from the previous fragment. This
|
|
|
47344 |
// is to check that the pes data is the start of a new pes payload
|
|
|
47345 |
|
|
|
47346 |
if (startPrefix !== 1) {
|
|
|
47347 |
return;
|
|
|
47348 |
} // get the packet length, this will be 0 for video
|
|
|
47349 |
|
|
|
47350 |
pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe
|
|
|
47351 |
|
|
|
47352 |
pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value
|
|
|
47353 |
// and a DTS value. Determine what combination of values is
|
|
|
47354 |
// available to work with.
|
|
|
47355 |
|
|
|
47356 |
ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript
|
|
|
47357 |
// performs all bitwise operations on 32-bit integers but javascript
|
|
|
47358 |
// supports a much greater range (52-bits) of integer using standard
|
|
|
47359 |
// mathematical operations.
|
|
|
47360 |
// We construct a 31-bit value using bitwise operators over the 31
|
|
|
47361 |
// most significant bits and then multiply by 4 (equal to a left-shift
|
|
|
47362 |
// of 2) before we add the final 2 least significant bits of the
|
|
|
47363 |
// timestamp (equal to an OR.)
|
|
|
47364 |
|
|
|
47365 |
if (ptsDtsFlags & 0xC0) {
|
|
|
47366 |
// the PTS and DTS are not written out directly. For information
|
|
|
47367 |
// on how they are encoded, see
|
|
|
47368 |
// http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
|
|
|
47369 |
pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3;
|
|
|
47370 |
pes.pts *= 4; // Left shift by 2
|
|
|
47371 |
|
|
|
47372 |
pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs
|
|
|
47373 |
|
|
|
47374 |
pes.dts = pes.pts;
|
|
|
47375 |
if (ptsDtsFlags & 0x40) {
|
|
|
47376 |
pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3;
|
|
|
47377 |
pes.dts *= 4; // Left shift by 2
|
|
|
47378 |
|
|
|
47379 |
pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs
|
|
|
47380 |
}
|
|
|
47381 |
} // the data section starts immediately after the PES header.
|
|
|
47382 |
// pes_header_data_length specifies the number of header bytes
|
|
|
47383 |
// that follow the last byte of the field.
|
|
|
47384 |
|
|
|
47385 |
pes.data = payload.subarray(9 + payload[8]);
|
|
|
47386 |
},
|
|
|
47387 |
/**
|
|
|
47388 |
* Pass completely parsed PES packets to the next stream in the pipeline
|
|
|
47389 |
**/
|
|
|
47390 |
flushStream = function (stream, type, forceFlush) {
|
|
|
47391 |
var packetData = new Uint8Array(stream.size),
|
|
|
47392 |
event = {
|
|
|
47393 |
type: type
|
|
|
47394 |
},
|
|
|
47395 |
i = 0,
|
|
|
47396 |
offset = 0,
|
|
|
47397 |
packetFlushable = false,
|
|
|
47398 |
fragment; // do nothing if there is not enough buffered data for a complete
|
|
|
47399 |
// PES header
|
|
|
47400 |
|
|
|
47401 |
if (!stream.data.length || stream.size < 9) {
|
|
|
47402 |
return;
|
|
|
47403 |
}
|
|
|
47404 |
event.trackId = stream.data[0].pid; // reassemble the packet
|
|
|
47405 |
|
|
|
47406 |
for (i = 0; i < stream.data.length; i++) {
|
|
|
47407 |
fragment = stream.data[i];
|
|
|
47408 |
packetData.set(fragment.data, offset);
|
|
|
47409 |
offset += fragment.data.byteLength;
|
|
|
47410 |
} // parse assembled packet's PES header
|
|
|
47411 |
|
|
|
47412 |
parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length
|
|
|
47413 |
// check that there is enough stream data to fill the packet
|
|
|
47414 |
|
|
|
47415 |
packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right
|
|
|
47416 |
|
|
|
47417 |
if (forceFlush || packetFlushable) {
|
|
|
47418 |
stream.size = 0;
|
|
|
47419 |
stream.data.length = 0;
|
|
|
47420 |
} // only emit packets that are complete. this is to avoid assembling
|
|
|
47421 |
// incomplete PES packets due to poor segmentation
|
|
|
47422 |
|
|
|
47423 |
if (packetFlushable) {
|
|
|
47424 |
self.trigger('data', event);
|
|
|
47425 |
}
|
|
|
47426 |
};
|
|
|
47427 |
ElementaryStream.prototype.init.call(this);
|
|
|
47428 |
/**
|
|
|
47429 |
* Identifies M2TS packet types and parses PES packets using metadata
|
|
|
47430 |
* parsed from the PMT
|
|
|
47431 |
**/
|
|
|
47432 |
|
|
|
47433 |
this.push = function (data) {
|
|
|
47434 |
({
|
|
|
47435 |
pat: function () {// we have to wait for the PMT to arrive as well before we
|
|
|
47436 |
// have any meaningful metadata
|
|
|
47437 |
},
|
|
|
47438 |
pes: function () {
|
|
|
47439 |
var stream, streamType;
|
|
|
47440 |
switch (data.streamType) {
|
|
|
47441 |
case StreamTypes$2.H264_STREAM_TYPE:
|
|
|
47442 |
stream = video;
|
|
|
47443 |
streamType = 'video';
|
|
|
47444 |
break;
|
|
|
47445 |
case StreamTypes$2.ADTS_STREAM_TYPE:
|
|
|
47446 |
stream = audio;
|
|
|
47447 |
streamType = 'audio';
|
|
|
47448 |
break;
|
|
|
47449 |
case StreamTypes$2.METADATA_STREAM_TYPE:
|
|
|
47450 |
stream = timedMetadata;
|
|
|
47451 |
streamType = 'timed-metadata';
|
|
|
47452 |
break;
|
|
|
47453 |
default:
|
|
|
47454 |
// ignore unknown stream types
|
|
|
47455 |
return;
|
|
|
47456 |
} // if a new packet is starting, we can flush the completed
|
|
|
47457 |
// packet
|
|
|
47458 |
|
|
|
47459 |
if (data.payloadUnitStartIndicator) {
|
|
|
47460 |
flushStream(stream, streamType, true);
|
|
|
47461 |
} // buffer this fragment until we are sure we've received the
|
|
|
47462 |
// complete payload
|
|
|
47463 |
|
|
|
47464 |
stream.data.push(data);
|
|
|
47465 |
stream.size += data.data.byteLength;
|
|
|
47466 |
},
|
|
|
47467 |
pmt: function () {
|
|
|
47468 |
var event = {
|
|
|
47469 |
type: 'metadata',
|
|
|
47470 |
tracks: []
|
|
|
47471 |
};
|
|
|
47472 |
programMapTable = data.programMapTable; // translate audio and video streams to tracks
|
|
|
47473 |
|
|
|
47474 |
if (programMapTable.video !== null) {
|
|
|
47475 |
event.tracks.push({
|
|
|
47476 |
timelineStartInfo: {
|
|
|
47477 |
baseMediaDecodeTime: 0
|
|
|
47478 |
},
|
|
|
47479 |
id: +programMapTable.video,
|
|
|
47480 |
codec: 'avc',
|
|
|
47481 |
type: 'video'
|
|
|
47482 |
});
|
|
|
47483 |
}
|
|
|
47484 |
if (programMapTable.audio !== null) {
|
|
|
47485 |
event.tracks.push({
|
|
|
47486 |
timelineStartInfo: {
|
|
|
47487 |
baseMediaDecodeTime: 0
|
|
|
47488 |
},
|
|
|
47489 |
id: +programMapTable.audio,
|
|
|
47490 |
codec: 'adts',
|
|
|
47491 |
type: 'audio'
|
|
|
47492 |
});
|
|
|
47493 |
}
|
|
|
47494 |
segmentHadPmt = true;
|
|
|
47495 |
self.trigger('data', event);
|
|
|
47496 |
}
|
|
|
47497 |
})[data.type]();
|
|
|
47498 |
};
|
|
|
47499 |
this.reset = function () {
|
|
|
47500 |
video.size = 0;
|
|
|
47501 |
video.data.length = 0;
|
|
|
47502 |
audio.size = 0;
|
|
|
47503 |
audio.data.length = 0;
|
|
|
47504 |
this.trigger('reset');
|
|
|
47505 |
};
|
|
|
47506 |
/**
|
|
|
47507 |
* Flush any remaining input. Video PES packets may be of variable
|
|
|
47508 |
* length. Normally, the start of a new video packet can trigger the
|
|
|
47509 |
* finalization of the previous packet. That is not possible if no
|
|
|
47510 |
* more video is forthcoming, however. In that case, some other
|
|
|
47511 |
* mechanism (like the end of the file) has to be employed. When it is
|
|
|
47512 |
* clear that no additional data is forthcoming, calling this method
|
|
|
47513 |
* will flush the buffered packets.
|
|
|
47514 |
*/
|
|
|
47515 |
|
|
|
47516 |
this.flushStreams_ = function () {
|
|
|
47517 |
// !!THIS ORDER IS IMPORTANT!!
|
|
|
47518 |
// video first then audio
|
|
|
47519 |
flushStream(video, 'video');
|
|
|
47520 |
flushStream(audio, 'audio');
|
|
|
47521 |
flushStream(timedMetadata, 'timed-metadata');
|
|
|
47522 |
};
|
|
|
47523 |
this.flush = function () {
|
|
|
47524 |
// if on flush we haven't had a pmt emitted
|
|
|
47525 |
// and we have a pmt to emit. emit the pmt
|
|
|
47526 |
// so that we trigger a trackinfo downstream.
|
|
|
47527 |
if (!segmentHadPmt && programMapTable) {
|
|
|
47528 |
var pmt = {
|
|
|
47529 |
type: 'metadata',
|
|
|
47530 |
tracks: []
|
|
|
47531 |
}; // translate audio and video streams to tracks
|
|
|
47532 |
|
|
|
47533 |
if (programMapTable.video !== null) {
|
|
|
47534 |
pmt.tracks.push({
|
|
|
47535 |
timelineStartInfo: {
|
|
|
47536 |
baseMediaDecodeTime: 0
|
|
|
47537 |
},
|
|
|
47538 |
id: +programMapTable.video,
|
|
|
47539 |
codec: 'avc',
|
|
|
47540 |
type: 'video'
|
|
|
47541 |
});
|
|
|
47542 |
}
|
|
|
47543 |
if (programMapTable.audio !== null) {
|
|
|
47544 |
pmt.tracks.push({
|
|
|
47545 |
timelineStartInfo: {
|
|
|
47546 |
baseMediaDecodeTime: 0
|
|
|
47547 |
},
|
|
|
47548 |
id: +programMapTable.audio,
|
|
|
47549 |
codec: 'adts',
|
|
|
47550 |
type: 'audio'
|
|
|
47551 |
});
|
|
|
47552 |
}
|
|
|
47553 |
self.trigger('data', pmt);
|
|
|
47554 |
}
|
|
|
47555 |
segmentHadPmt = false;
|
|
|
47556 |
this.flushStreams_();
|
|
|
47557 |
this.trigger('done');
|
|
|
47558 |
};
|
|
|
47559 |
};
|
|
|
47560 |
ElementaryStream.prototype = new Stream$4();
|
|
|
47561 |
var m2ts$1 = {
|
|
|
47562 |
PAT_PID: 0x0000,
|
|
|
47563 |
MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH$1,
|
|
|
47564 |
TransportPacketStream: TransportPacketStream,
|
|
|
47565 |
TransportParseStream: TransportParseStream,
|
|
|
47566 |
ElementaryStream: ElementaryStream,
|
|
|
47567 |
TimestampRolloverStream: TimestampRolloverStream,
|
|
|
47568 |
CaptionStream: CaptionStream$1.CaptionStream,
|
|
|
47569 |
Cea608Stream: CaptionStream$1.Cea608Stream,
|
|
|
47570 |
Cea708Stream: CaptionStream$1.Cea708Stream,
|
|
|
47571 |
MetadataStream: metadataStream
|
|
|
47572 |
};
|
|
|
47573 |
for (var type in StreamTypes$2) {
|
|
|
47574 |
if (StreamTypes$2.hasOwnProperty(type)) {
|
|
|
47575 |
m2ts$1[type] = StreamTypes$2[type];
|
|
|
47576 |
}
|
|
|
47577 |
}
|
|
|
47578 |
var m2ts_1 = m2ts$1;
|
|
|
47579 |
/**
|
|
|
47580 |
* mux.js
|
|
|
47581 |
*
|
|
|
47582 |
* Copyright (c) Brightcove
|
|
|
47583 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
47584 |
*/
|
|
|
47585 |
|
|
|
47586 |
var Stream$3 = stream;
|
|
|
47587 |
var ONE_SECOND_IN_TS$2 = clock$2.ONE_SECOND_IN_TS;
|
|
|
47588 |
var AdtsStream$1;
|
|
|
47589 |
var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
|
|
47590 |
/*
|
|
|
47591 |
* Accepts a ElementaryStream and emits data events with parsed
|
|
|
47592 |
* AAC Audio Frames of the individual packets. Input audio in ADTS
|
|
|
47593 |
* format is unpacked and re-emitted as AAC frames.
|
|
|
47594 |
*
|
|
|
47595 |
* @see http://wiki.multimedia.cx/index.php?title=ADTS
|
|
|
47596 |
* @see http://wiki.multimedia.cx/?title=Understanding_AAC
|
|
|
47597 |
*/
|
|
|
47598 |
|
|
|
47599 |
AdtsStream$1 = function (handlePartialSegments) {
|
|
|
47600 |
var buffer,
|
|
|
47601 |
frameNum = 0;
|
|
|
47602 |
AdtsStream$1.prototype.init.call(this);
|
|
|
47603 |
this.skipWarn_ = function (start, end) {
|
|
|
47604 |
this.trigger('log', {
|
|
|
47605 |
level: 'warn',
|
|
|
47606 |
message: `adts skiping bytes ${start} to ${end} in frame ${frameNum} outside syncword`
|
|
|
47607 |
});
|
|
|
47608 |
};
|
|
|
47609 |
this.push = function (packet) {
|
|
|
47610 |
var i = 0,
|
|
|
47611 |
frameLength,
|
|
|
47612 |
protectionSkipBytes,
|
|
|
47613 |
oldBuffer,
|
|
|
47614 |
sampleCount,
|
|
|
47615 |
adtsFrameDuration;
|
|
|
47616 |
if (!handlePartialSegments) {
|
|
|
47617 |
frameNum = 0;
|
|
|
47618 |
}
|
|
|
47619 |
if (packet.type !== 'audio') {
|
|
|
47620 |
// ignore non-audio data
|
|
|
47621 |
return;
|
|
|
47622 |
} // Prepend any data in the buffer to the input data so that we can parse
|
|
|
47623 |
// aac frames the cross a PES packet boundary
|
|
|
47624 |
|
|
|
47625 |
if (buffer && buffer.length) {
|
|
|
47626 |
oldBuffer = buffer;
|
|
|
47627 |
buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength);
|
|
|
47628 |
buffer.set(oldBuffer);
|
|
|
47629 |
buffer.set(packet.data, oldBuffer.byteLength);
|
|
|
47630 |
} else {
|
|
|
47631 |
buffer = packet.data;
|
|
|
47632 |
} // unpack any ADTS frames which have been fully received
|
|
|
47633 |
// for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS
|
|
|
47634 |
|
|
|
47635 |
var skip; // We use i + 7 here because we want to be able to parse the entire header.
|
|
|
47636 |
// If we don't have enough bytes to do that, then we definitely won't have a full frame.
|
|
|
47637 |
|
|
|
47638 |
while (i + 7 < buffer.length) {
|
|
|
47639 |
// Look for the start of an ADTS header..
|
|
|
47640 |
if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) {
|
|
|
47641 |
if (typeof skip !== 'number') {
|
|
|
47642 |
skip = i;
|
|
|
47643 |
} // If a valid header was not found, jump one forward and attempt to
|
|
|
47644 |
// find a valid ADTS header starting at the next byte
|
|
|
47645 |
|
|
|
47646 |
i++;
|
|
|
47647 |
continue;
|
|
|
47648 |
}
|
|
|
47649 |
if (typeof skip === 'number') {
|
|
|
47650 |
this.skipWarn_(skip, i);
|
|
|
47651 |
skip = null;
|
|
|
47652 |
} // The protection skip bit tells us if we have 2 bytes of CRC data at the
|
|
|
47653 |
// end of the ADTS header
|
|
|
47654 |
|
|
|
47655 |
protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the
|
|
|
47656 |
// end of the sync sequence
|
|
|
47657 |
// NOTE: frame length includes the size of the header
|
|
|
47658 |
|
|
|
47659 |
frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5;
|
|
|
47660 |
sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024;
|
|
|
47661 |
adtsFrameDuration = sampleCount * ONE_SECOND_IN_TS$2 / ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2]; // If we don't have enough data to actually finish this ADTS frame,
|
|
|
47662 |
// then we have to wait for more data
|
|
|
47663 |
|
|
|
47664 |
if (buffer.byteLength - i < frameLength) {
|
|
|
47665 |
break;
|
|
|
47666 |
} // Otherwise, deliver the complete AAC frame
|
|
|
47667 |
|
|
|
47668 |
this.trigger('data', {
|
|
|
47669 |
pts: packet.pts + frameNum * adtsFrameDuration,
|
|
|
47670 |
dts: packet.dts + frameNum * adtsFrameDuration,
|
|
|
47671 |
sampleCount: sampleCount,
|
|
|
47672 |
audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1,
|
|
|
47673 |
channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6,
|
|
|
47674 |
samplerate: ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2],
|
|
|
47675 |
samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2,
|
|
|
47676 |
// assume ISO/IEC 14496-12 AudioSampleEntry default of 16
|
|
|
47677 |
samplesize: 16,
|
|
|
47678 |
// data is the frame without it's header
|
|
|
47679 |
data: buffer.subarray(i + 7 + protectionSkipBytes, i + frameLength)
|
|
|
47680 |
});
|
|
|
47681 |
frameNum++;
|
|
|
47682 |
i += frameLength;
|
|
|
47683 |
}
|
|
|
47684 |
if (typeof skip === 'number') {
|
|
|
47685 |
this.skipWarn_(skip, i);
|
|
|
47686 |
skip = null;
|
|
|
47687 |
} // remove processed bytes from the buffer.
|
|
|
47688 |
|
|
|
47689 |
buffer = buffer.subarray(i);
|
|
|
47690 |
};
|
|
|
47691 |
this.flush = function () {
|
|
|
47692 |
frameNum = 0;
|
|
|
47693 |
this.trigger('done');
|
|
|
47694 |
};
|
|
|
47695 |
this.reset = function () {
|
|
|
47696 |
buffer = void 0;
|
|
|
47697 |
this.trigger('reset');
|
|
|
47698 |
};
|
|
|
47699 |
this.endTimeline = function () {
|
|
|
47700 |
buffer = void 0;
|
|
|
47701 |
this.trigger('endedtimeline');
|
|
|
47702 |
};
|
|
|
47703 |
};
|
|
|
47704 |
AdtsStream$1.prototype = new Stream$3();
|
|
|
47705 |
var adts = AdtsStream$1;
|
|
|
47706 |
/**
|
|
|
47707 |
* mux.js
|
|
|
47708 |
*
|
|
|
47709 |
* Copyright (c) Brightcove
|
|
|
47710 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
47711 |
*/
|
|
|
47712 |
|
|
|
47713 |
var ExpGolomb$1;
|
|
|
47714 |
/**
|
|
|
47715 |
* Parser for exponential Golomb codes, a variable-bitwidth number encoding
|
|
|
47716 |
* scheme used by h264.
|
|
|
47717 |
*/
|
|
|
47718 |
|
|
|
47719 |
ExpGolomb$1 = function (workingData) {
|
|
|
47720 |
var
|
|
|
47721 |
// the number of bytes left to examine in workingData
|
|
|
47722 |
workingBytesAvailable = workingData.byteLength,
|
|
|
47723 |
// the current word being examined
|
|
|
47724 |
workingWord = 0,
|
|
|
47725 |
// :uint
|
|
|
47726 |
// the number of bits left to examine in the current word
|
|
|
47727 |
workingBitsAvailable = 0; // :uint;
|
|
|
47728 |
// ():uint
|
|
|
47729 |
|
|
|
47730 |
this.length = function () {
|
|
|
47731 |
return 8 * workingBytesAvailable;
|
|
|
47732 |
}; // ():uint
|
|
|
47733 |
|
|
|
47734 |
this.bitsAvailable = function () {
|
|
|
47735 |
return 8 * workingBytesAvailable + workingBitsAvailable;
|
|
|
47736 |
}; // ():void
|
|
|
47737 |
|
|
|
47738 |
this.loadWord = function () {
|
|
|
47739 |
var position = workingData.byteLength - workingBytesAvailable,
|
|
|
47740 |
workingBytes = new Uint8Array(4),
|
|
|
47741 |
availableBytes = Math.min(4, workingBytesAvailable);
|
|
|
47742 |
if (availableBytes === 0) {
|
|
|
47743 |
throw new Error('no bytes available');
|
|
|
47744 |
}
|
|
|
47745 |
workingBytes.set(workingData.subarray(position, position + availableBytes));
|
|
|
47746 |
workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed
|
|
|
47747 |
|
|
|
47748 |
workingBitsAvailable = availableBytes * 8;
|
|
|
47749 |
workingBytesAvailable -= availableBytes;
|
|
|
47750 |
}; // (count:int):void
|
|
|
47751 |
|
|
|
47752 |
this.skipBits = function (count) {
|
|
|
47753 |
var skipBytes; // :int
|
|
|
47754 |
|
|
|
47755 |
if (workingBitsAvailable > count) {
|
|
|
47756 |
workingWord <<= count;
|
|
|
47757 |
workingBitsAvailable -= count;
|
|
|
47758 |
} else {
|
|
|
47759 |
count -= workingBitsAvailable;
|
|
|
47760 |
skipBytes = Math.floor(count / 8);
|
|
|
47761 |
count -= skipBytes * 8;
|
|
|
47762 |
workingBytesAvailable -= skipBytes;
|
|
|
47763 |
this.loadWord();
|
|
|
47764 |
workingWord <<= count;
|
|
|
47765 |
workingBitsAvailable -= count;
|
|
|
47766 |
}
|
|
|
47767 |
}; // (size:int):uint
|
|
|
47768 |
|
|
|
47769 |
this.readBits = function (size) {
|
|
|
47770 |
var bits = Math.min(workingBitsAvailable, size),
|
|
|
47771 |
// :uint
|
|
|
47772 |
valu = workingWord >>> 32 - bits; // :uint
|
|
|
47773 |
// if size > 31, handle error
|
|
|
47774 |
|
|
|
47775 |
workingBitsAvailable -= bits;
|
|
|
47776 |
if (workingBitsAvailable > 0) {
|
|
|
47777 |
workingWord <<= bits;
|
|
|
47778 |
} else if (workingBytesAvailable > 0) {
|
|
|
47779 |
this.loadWord();
|
|
|
47780 |
}
|
|
|
47781 |
bits = size - bits;
|
|
|
47782 |
if (bits > 0) {
|
|
|
47783 |
return valu << bits | this.readBits(bits);
|
|
|
47784 |
}
|
|
|
47785 |
return valu;
|
|
|
47786 |
}; // ():uint
|
|
|
47787 |
|
|
|
47788 |
this.skipLeadingZeros = function () {
|
|
|
47789 |
var leadingZeroCount; // :uint
|
|
|
47790 |
|
|
|
47791 |
for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) {
|
|
|
47792 |
if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) {
|
|
|
47793 |
// the first bit of working word is 1
|
|
|
47794 |
workingWord <<= leadingZeroCount;
|
|
|
47795 |
workingBitsAvailable -= leadingZeroCount;
|
|
|
47796 |
return leadingZeroCount;
|
|
|
47797 |
}
|
|
|
47798 |
} // we exhausted workingWord and still have not found a 1
|
|
|
47799 |
|
|
|
47800 |
this.loadWord();
|
|
|
47801 |
return leadingZeroCount + this.skipLeadingZeros();
|
|
|
47802 |
}; // ():void
|
|
|
47803 |
|
|
|
47804 |
this.skipUnsignedExpGolomb = function () {
|
|
|
47805 |
this.skipBits(1 + this.skipLeadingZeros());
|
|
|
47806 |
}; // ():void
|
|
|
47807 |
|
|
|
47808 |
this.skipExpGolomb = function () {
|
|
|
47809 |
this.skipBits(1 + this.skipLeadingZeros());
|
|
|
47810 |
}; // ():uint
|
|
|
47811 |
|
|
|
47812 |
this.readUnsignedExpGolomb = function () {
|
|
|
47813 |
var clz = this.skipLeadingZeros(); // :uint
|
|
|
47814 |
|
|
|
47815 |
return this.readBits(clz + 1) - 1;
|
|
|
47816 |
}; // ():int
|
|
|
47817 |
|
|
|
47818 |
this.readExpGolomb = function () {
|
|
|
47819 |
var valu = this.readUnsignedExpGolomb(); // :int
|
|
|
47820 |
|
|
|
47821 |
if (0x01 & valu) {
|
|
|
47822 |
// the number is odd if the low order bit is set
|
|
|
47823 |
return 1 + valu >>> 1; // add 1 to make it even, and divide by 2
|
|
|
47824 |
}
|
|
|
47825 |
|
|
|
47826 |
return -1 * (valu >>> 1); // divide by two then make it negative
|
|
|
47827 |
}; // Some convenience functions
|
|
|
47828 |
// :Boolean
|
|
|
47829 |
|
|
|
47830 |
this.readBoolean = function () {
|
|
|
47831 |
return this.readBits(1) === 1;
|
|
|
47832 |
}; // ():int
|
|
|
47833 |
|
|
|
47834 |
this.readUnsignedByte = function () {
|
|
|
47835 |
return this.readBits(8);
|
|
|
47836 |
};
|
|
|
47837 |
this.loadWord();
|
|
|
47838 |
};
|
|
|
47839 |
var expGolomb = ExpGolomb$1;
|
|
|
47840 |
/**
|
|
|
47841 |
* mux.js
|
|
|
47842 |
*
|
|
|
47843 |
* Copyright (c) Brightcove
|
|
|
47844 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
47845 |
*/
|
|
|
47846 |
|
|
|
47847 |
var Stream$2 = stream;
|
|
|
47848 |
var ExpGolomb = expGolomb;
|
|
|
47849 |
var H264Stream$1, NalByteStream;
|
|
|
47850 |
var PROFILES_WITH_OPTIONAL_SPS_DATA;
|
|
|
47851 |
/**
|
|
|
47852 |
* Accepts a NAL unit byte stream and unpacks the embedded NAL units.
|
|
|
47853 |
*/
|
|
|
47854 |
|
|
|
47855 |
NalByteStream = function () {
|
|
|
47856 |
var syncPoint = 0,
|
|
|
47857 |
i,
|
|
|
47858 |
buffer;
|
|
|
47859 |
NalByteStream.prototype.init.call(this);
|
|
|
47860 |
/*
|
|
|
47861 |
* Scans a byte stream and triggers a data event with the NAL units found.
|
|
|
47862 |
* @param {Object} data Event received from H264Stream
|
|
|
47863 |
* @param {Uint8Array} data.data The h264 byte stream to be scanned
|
|
|
47864 |
*
|
|
|
47865 |
* @see H264Stream.push
|
|
|
47866 |
*/
|
|
|
47867 |
|
|
|
47868 |
this.push = function (data) {
|
|
|
47869 |
var swapBuffer;
|
|
|
47870 |
if (!buffer) {
|
|
|
47871 |
buffer = data.data;
|
|
|
47872 |
} else {
|
|
|
47873 |
swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength);
|
|
|
47874 |
swapBuffer.set(buffer);
|
|
|
47875 |
swapBuffer.set(data.data, buffer.byteLength);
|
|
|
47876 |
buffer = swapBuffer;
|
|
|
47877 |
}
|
|
|
47878 |
var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B
|
|
|
47879 |
// scan for NAL unit boundaries
|
|
|
47880 |
// a match looks like this:
|
|
|
47881 |
// 0 0 1 .. NAL .. 0 0 1
|
|
|
47882 |
// ^ sync point ^ i
|
|
|
47883 |
// or this:
|
|
|
47884 |
// 0 0 1 .. NAL .. 0 0 0
|
|
|
47885 |
// ^ sync point ^ i
|
|
|
47886 |
// advance the sync point to a NAL start, if necessary
|
|
|
47887 |
|
|
|
47888 |
for (; syncPoint < len - 3; syncPoint++) {
|
|
|
47889 |
if (buffer[syncPoint + 2] === 1) {
|
|
|
47890 |
// the sync point is properly aligned
|
|
|
47891 |
i = syncPoint + 5;
|
|
|
47892 |
break;
|
|
|
47893 |
}
|
|
|
47894 |
}
|
|
|
47895 |
while (i < len) {
|
|
|
47896 |
// look at the current byte to determine if we've hit the end of
|
|
|
47897 |
// a NAL unit boundary
|
|
|
47898 |
switch (buffer[i]) {
|
|
|
47899 |
case 0:
|
|
|
47900 |
// skip past non-sync sequences
|
|
|
47901 |
if (buffer[i - 1] !== 0) {
|
|
|
47902 |
i += 2;
|
|
|
47903 |
break;
|
|
|
47904 |
} else if (buffer[i - 2] !== 0) {
|
|
|
47905 |
i++;
|
|
|
47906 |
break;
|
|
|
47907 |
} // deliver the NAL unit if it isn't empty
|
|
|
47908 |
|
|
|
47909 |
if (syncPoint + 3 !== i - 2) {
|
|
|
47910 |
this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
|
|
|
47911 |
} // drop trailing zeroes
|
|
|
47912 |
|
|
|
47913 |
do {
|
|
|
47914 |
i++;
|
|
|
47915 |
} while (buffer[i] !== 1 && i < len);
|
|
|
47916 |
syncPoint = i - 2;
|
|
|
47917 |
i += 3;
|
|
|
47918 |
break;
|
|
|
47919 |
case 1:
|
|
|
47920 |
// skip past non-sync sequences
|
|
|
47921 |
if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) {
|
|
|
47922 |
i += 3;
|
|
|
47923 |
break;
|
|
|
47924 |
} // deliver the NAL unit
|
|
|
47925 |
|
|
|
47926 |
this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
|
|
|
47927 |
syncPoint = i - 2;
|
|
|
47928 |
i += 3;
|
|
|
47929 |
break;
|
|
|
47930 |
default:
|
|
|
47931 |
// the current byte isn't a one or zero, so it cannot be part
|
|
|
47932 |
// of a sync sequence
|
|
|
47933 |
i += 3;
|
|
|
47934 |
break;
|
|
|
47935 |
}
|
|
|
47936 |
} // filter out the NAL units that were delivered
|
|
|
47937 |
|
|
|
47938 |
buffer = buffer.subarray(syncPoint);
|
|
|
47939 |
i -= syncPoint;
|
|
|
47940 |
syncPoint = 0;
|
|
|
47941 |
};
|
|
|
47942 |
this.reset = function () {
|
|
|
47943 |
buffer = null;
|
|
|
47944 |
syncPoint = 0;
|
|
|
47945 |
this.trigger('reset');
|
|
|
47946 |
};
|
|
|
47947 |
this.flush = function () {
|
|
|
47948 |
// deliver the last buffered NAL unit
|
|
|
47949 |
if (buffer && buffer.byteLength > 3) {
|
|
|
47950 |
this.trigger('data', buffer.subarray(syncPoint + 3));
|
|
|
47951 |
} // reset the stream state
|
|
|
47952 |
|
|
|
47953 |
buffer = null;
|
|
|
47954 |
syncPoint = 0;
|
|
|
47955 |
this.trigger('done');
|
|
|
47956 |
};
|
|
|
47957 |
this.endTimeline = function () {
|
|
|
47958 |
this.flush();
|
|
|
47959 |
this.trigger('endedtimeline');
|
|
|
47960 |
};
|
|
|
47961 |
};
|
|
|
47962 |
NalByteStream.prototype = new Stream$2(); // values of profile_idc that indicate additional fields are included in the SPS
|
|
|
47963 |
// see Recommendation ITU-T H.264 (4/2013),
|
|
|
47964 |
// 7.3.2.1.1 Sequence parameter set data syntax
|
|
|
47965 |
|
|
|
47966 |
PROFILES_WITH_OPTIONAL_SPS_DATA = {
|
|
|
47967 |
100: true,
|
|
|
47968 |
110: true,
|
|
|
47969 |
122: true,
|
|
|
47970 |
244: true,
|
|
|
47971 |
44: true,
|
|
|
47972 |
83: true,
|
|
|
47973 |
86: true,
|
|
|
47974 |
118: true,
|
|
|
47975 |
128: true,
|
|
|
47976 |
// TODO: the three profiles below don't
|
|
|
47977 |
// appear to have sps data in the specificiation anymore?
|
|
|
47978 |
138: true,
|
|
|
47979 |
139: true,
|
|
|
47980 |
134: true
|
|
|
47981 |
};
|
|
|
47982 |
/**
|
|
|
47983 |
* Accepts input from a ElementaryStream and produces H.264 NAL unit data
|
|
|
47984 |
* events.
|
|
|
47985 |
*/
|
|
|
47986 |
|
|
|
47987 |
H264Stream$1 = function () {
|
|
|
47988 |
var nalByteStream = new NalByteStream(),
|
|
|
47989 |
self,
|
|
|
47990 |
trackId,
|
|
|
47991 |
currentPts,
|
|
|
47992 |
currentDts,
|
|
|
47993 |
discardEmulationPreventionBytes,
|
|
|
47994 |
readSequenceParameterSet,
|
|
|
47995 |
skipScalingList;
|
|
|
47996 |
H264Stream$1.prototype.init.call(this);
|
|
|
47997 |
self = this;
|
|
|
47998 |
/*
|
|
|
47999 |
* Pushes a packet from a stream onto the NalByteStream
|
|
|
48000 |
*
|
|
|
48001 |
* @param {Object} packet - A packet received from a stream
|
|
|
48002 |
* @param {Uint8Array} packet.data - The raw bytes of the packet
|
|
|
48003 |
* @param {Number} packet.dts - Decode timestamp of the packet
|
|
|
48004 |
* @param {Number} packet.pts - Presentation timestamp of the packet
|
|
|
48005 |
* @param {Number} packet.trackId - The id of the h264 track this packet came from
|
|
|
48006 |
* @param {('video'|'audio')} packet.type - The type of packet
|
|
|
48007 |
*
|
|
|
48008 |
*/
|
|
|
48009 |
|
|
|
48010 |
this.push = function (packet) {
|
|
|
48011 |
if (packet.type !== 'video') {
|
|
|
48012 |
return;
|
|
|
48013 |
}
|
|
|
48014 |
trackId = packet.trackId;
|
|
|
48015 |
currentPts = packet.pts;
|
|
|
48016 |
currentDts = packet.dts;
|
|
|
48017 |
nalByteStream.push(packet);
|
|
|
48018 |
};
|
|
|
48019 |
/*
|
|
|
48020 |
* Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps
|
|
|
48021 |
* for the NALUs to the next stream component.
|
|
|
48022 |
* Also, preprocess caption and sequence parameter NALUs.
|
|
|
48023 |
*
|
|
|
48024 |
* @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push`
|
|
|
48025 |
* @see NalByteStream.push
|
|
|
48026 |
*/
|
|
|
48027 |
|
|
|
48028 |
nalByteStream.on('data', function (data) {
|
|
|
48029 |
var event = {
|
|
|
48030 |
trackId: trackId,
|
|
|
48031 |
pts: currentPts,
|
|
|
48032 |
dts: currentDts,
|
|
|
48033 |
data: data,
|
|
|
48034 |
nalUnitTypeCode: data[0] & 0x1f
|
|
|
48035 |
};
|
|
|
48036 |
switch (event.nalUnitTypeCode) {
|
|
|
48037 |
case 0x05:
|
|
|
48038 |
event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr';
|
|
|
48039 |
break;
|
|
|
48040 |
case 0x06:
|
|
|
48041 |
event.nalUnitType = 'sei_rbsp';
|
|
|
48042 |
event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
|
|
|
48043 |
break;
|
|
|
48044 |
case 0x07:
|
|
|
48045 |
event.nalUnitType = 'seq_parameter_set_rbsp';
|
|
|
48046 |
event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
|
|
|
48047 |
event.config = readSequenceParameterSet(event.escapedRBSP);
|
|
|
48048 |
break;
|
|
|
48049 |
case 0x08:
|
|
|
48050 |
event.nalUnitType = 'pic_parameter_set_rbsp';
|
|
|
48051 |
break;
|
|
|
48052 |
case 0x09:
|
|
|
48053 |
event.nalUnitType = 'access_unit_delimiter_rbsp';
|
|
|
48054 |
break;
|
|
|
48055 |
} // This triggers data on the H264Stream
|
|
|
48056 |
|
|
|
48057 |
self.trigger('data', event);
|
|
|
48058 |
});
|
|
|
48059 |
nalByteStream.on('done', function () {
|
|
|
48060 |
self.trigger('done');
|
|
|
48061 |
});
|
|
|
48062 |
nalByteStream.on('partialdone', function () {
|
|
|
48063 |
self.trigger('partialdone');
|
|
|
48064 |
});
|
|
|
48065 |
nalByteStream.on('reset', function () {
|
|
|
48066 |
self.trigger('reset');
|
|
|
48067 |
});
|
|
|
48068 |
nalByteStream.on('endedtimeline', function () {
|
|
|
48069 |
self.trigger('endedtimeline');
|
|
|
48070 |
});
|
|
|
48071 |
this.flush = function () {
|
|
|
48072 |
nalByteStream.flush();
|
|
|
48073 |
};
|
|
|
48074 |
this.partialFlush = function () {
|
|
|
48075 |
nalByteStream.partialFlush();
|
|
|
48076 |
};
|
|
|
48077 |
this.reset = function () {
|
|
|
48078 |
nalByteStream.reset();
|
|
|
48079 |
};
|
|
|
48080 |
this.endTimeline = function () {
|
|
|
48081 |
nalByteStream.endTimeline();
|
|
|
48082 |
};
|
|
|
48083 |
/**
|
|
|
48084 |
* Advance the ExpGolomb decoder past a scaling list. The scaling
|
|
|
48085 |
* list is optionally transmitted as part of a sequence parameter
|
|
|
48086 |
* set and is not relevant to transmuxing.
|
|
|
48087 |
* @param count {number} the number of entries in this scaling list
|
|
|
48088 |
* @param expGolombDecoder {object} an ExpGolomb pointed to the
|
|
|
48089 |
* start of a scaling list
|
|
|
48090 |
* @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
|
|
|
48091 |
*/
|
|
|
48092 |
|
|
|
48093 |
skipScalingList = function (count, expGolombDecoder) {
|
|
|
48094 |
var lastScale = 8,
|
|
|
48095 |
nextScale = 8,
|
|
|
48096 |
j,
|
|
|
48097 |
deltaScale;
|
|
|
48098 |
for (j = 0; j < count; j++) {
|
|
|
48099 |
if (nextScale !== 0) {
|
|
|
48100 |
deltaScale = expGolombDecoder.readExpGolomb();
|
|
|
48101 |
nextScale = (lastScale + deltaScale + 256) % 256;
|
|
|
48102 |
}
|
|
|
48103 |
lastScale = nextScale === 0 ? lastScale : nextScale;
|
|
|
48104 |
}
|
|
|
48105 |
};
|
|
|
48106 |
/**
|
|
|
48107 |
* Expunge any "Emulation Prevention" bytes from a "Raw Byte
|
|
|
48108 |
* Sequence Payload"
|
|
|
48109 |
* @param data {Uint8Array} the bytes of a RBSP from a NAL
|
|
|
48110 |
* unit
|
|
|
48111 |
* @return {Uint8Array} the RBSP without any Emulation
|
|
|
48112 |
* Prevention Bytes
|
|
|
48113 |
*/
|
|
|
48114 |
|
|
|
48115 |
discardEmulationPreventionBytes = function (data) {
|
|
|
48116 |
var length = data.byteLength,
|
|
|
48117 |
emulationPreventionBytesPositions = [],
|
|
|
48118 |
i = 1,
|
|
|
48119 |
newLength,
|
|
|
48120 |
newData; // Find all `Emulation Prevention Bytes`
|
|
|
48121 |
|
|
|
48122 |
while (i < length - 2) {
|
|
|
48123 |
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
|
|
|
48124 |
emulationPreventionBytesPositions.push(i + 2);
|
|
|
48125 |
i += 2;
|
|
|
48126 |
} else {
|
|
|
48127 |
i++;
|
|
|
48128 |
}
|
|
|
48129 |
} // If no Emulation Prevention Bytes were found just return the original
|
|
|
48130 |
// array
|
|
|
48131 |
|
|
|
48132 |
if (emulationPreventionBytesPositions.length === 0) {
|
|
|
48133 |
return data;
|
|
|
48134 |
} // Create a new array to hold the NAL unit data
|
|
|
48135 |
|
|
|
48136 |
newLength = length - emulationPreventionBytesPositions.length;
|
|
|
48137 |
newData = new Uint8Array(newLength);
|
|
|
48138 |
var sourceIndex = 0;
|
|
|
48139 |
for (i = 0; i < newLength; sourceIndex++, i++) {
|
|
|
48140 |
if (sourceIndex === emulationPreventionBytesPositions[0]) {
|
|
|
48141 |
// Skip this byte
|
|
|
48142 |
sourceIndex++; // Remove this position index
|
|
|
48143 |
|
|
|
48144 |
emulationPreventionBytesPositions.shift();
|
|
|
48145 |
}
|
|
|
48146 |
newData[i] = data[sourceIndex];
|
|
|
48147 |
}
|
|
|
48148 |
return newData;
|
|
|
48149 |
};
|
|
|
48150 |
/**
|
|
|
48151 |
* Read a sequence parameter set and return some interesting video
|
|
|
48152 |
* properties. A sequence parameter set is the H264 metadata that
|
|
|
48153 |
* describes the properties of upcoming video frames.
|
|
|
48154 |
* @param data {Uint8Array} the bytes of a sequence parameter set
|
|
|
48155 |
* @return {object} an object with configuration parsed from the
|
|
|
48156 |
* sequence parameter set, including the dimensions of the
|
|
|
48157 |
* associated video frames.
|
|
|
48158 |
*/
|
|
|
48159 |
|
|
|
48160 |
readSequenceParameterSet = function (data) {
|
|
|
48161 |
var frameCropLeftOffset = 0,
|
|
|
48162 |
frameCropRightOffset = 0,
|
|
|
48163 |
frameCropTopOffset = 0,
|
|
|
48164 |
frameCropBottomOffset = 0,
|
|
|
48165 |
expGolombDecoder,
|
|
|
48166 |
profileIdc,
|
|
|
48167 |
levelIdc,
|
|
|
48168 |
profileCompatibility,
|
|
|
48169 |
chromaFormatIdc,
|
|
|
48170 |
picOrderCntType,
|
|
|
48171 |
numRefFramesInPicOrderCntCycle,
|
|
|
48172 |
picWidthInMbsMinus1,
|
|
|
48173 |
picHeightInMapUnitsMinus1,
|
|
|
48174 |
frameMbsOnlyFlag,
|
|
|
48175 |
scalingListCount,
|
|
|
48176 |
sarRatio = [1, 1],
|
|
|
48177 |
aspectRatioIdc,
|
|
|
48178 |
i;
|
|
|
48179 |
expGolombDecoder = new ExpGolomb(data);
|
|
|
48180 |
profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc
|
|
|
48181 |
|
|
|
48182 |
profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag
|
|
|
48183 |
|
|
|
48184 |
levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8)
|
|
|
48185 |
|
|
|
48186 |
expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id
|
|
|
48187 |
// some profiles have more optional data we don't need
|
|
|
48188 |
|
|
|
48189 |
if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) {
|
|
|
48190 |
chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48191 |
if (chromaFormatIdc === 3) {
|
|
|
48192 |
expGolombDecoder.skipBits(1); // separate_colour_plane_flag
|
|
|
48193 |
}
|
|
|
48194 |
|
|
|
48195 |
expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8
|
|
|
48196 |
|
|
|
48197 |
expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8
|
|
|
48198 |
|
|
|
48199 |
expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag
|
|
|
48200 |
|
|
|
48201 |
if (expGolombDecoder.readBoolean()) {
|
|
|
48202 |
// seq_scaling_matrix_present_flag
|
|
|
48203 |
scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
|
|
|
48204 |
for (i = 0; i < scalingListCount; i++) {
|
|
|
48205 |
if (expGolombDecoder.readBoolean()) {
|
|
|
48206 |
// seq_scaling_list_present_flag[ i ]
|
|
|
48207 |
if (i < 6) {
|
|
|
48208 |
skipScalingList(16, expGolombDecoder);
|
|
|
48209 |
} else {
|
|
|
48210 |
skipScalingList(64, expGolombDecoder);
|
|
|
48211 |
}
|
|
|
48212 |
}
|
|
|
48213 |
}
|
|
|
48214 |
}
|
|
|
48215 |
}
|
|
|
48216 |
expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4
|
|
|
48217 |
|
|
|
48218 |
picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48219 |
if (picOrderCntType === 0) {
|
|
|
48220 |
expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4
|
|
|
48221 |
} else if (picOrderCntType === 1) {
|
|
|
48222 |
expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag
|
|
|
48223 |
|
|
|
48224 |
expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic
|
|
|
48225 |
|
|
|
48226 |
expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field
|
|
|
48227 |
|
|
|
48228 |
numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48229 |
for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
|
|
|
48230 |
expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ]
|
|
|
48231 |
}
|
|
|
48232 |
}
|
|
|
48233 |
|
|
|
48234 |
expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames
|
|
|
48235 |
|
|
|
48236 |
expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag
|
|
|
48237 |
|
|
|
48238 |
picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48239 |
picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48240 |
frameMbsOnlyFlag = expGolombDecoder.readBits(1);
|
|
|
48241 |
if (frameMbsOnlyFlag === 0) {
|
|
|
48242 |
expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag
|
|
|
48243 |
}
|
|
|
48244 |
|
|
|
48245 |
expGolombDecoder.skipBits(1); // direct_8x8_inference_flag
|
|
|
48246 |
|
|
|
48247 |
if (expGolombDecoder.readBoolean()) {
|
|
|
48248 |
// frame_cropping_flag
|
|
|
48249 |
frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48250 |
frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48251 |
frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48252 |
frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
|
|
|
48253 |
}
|
|
|
48254 |
if (expGolombDecoder.readBoolean()) {
|
|
|
48255 |
// vui_parameters_present_flag
|
|
|
48256 |
if (expGolombDecoder.readBoolean()) {
|
|
|
48257 |
// aspect_ratio_info_present_flag
|
|
|
48258 |
aspectRatioIdc = expGolombDecoder.readUnsignedByte();
|
|
|
48259 |
switch (aspectRatioIdc) {
|
|
|
48260 |
case 1:
|
|
|
48261 |
sarRatio = [1, 1];
|
|
|
48262 |
break;
|
|
|
48263 |
case 2:
|
|
|
48264 |
sarRatio = [12, 11];
|
|
|
48265 |
break;
|
|
|
48266 |
case 3:
|
|
|
48267 |
sarRatio = [10, 11];
|
|
|
48268 |
break;
|
|
|
48269 |
case 4:
|
|
|
48270 |
sarRatio = [16, 11];
|
|
|
48271 |
break;
|
|
|
48272 |
case 5:
|
|
|
48273 |
sarRatio = [40, 33];
|
|
|
48274 |
break;
|
|
|
48275 |
case 6:
|
|
|
48276 |
sarRatio = [24, 11];
|
|
|
48277 |
break;
|
|
|
48278 |
case 7:
|
|
|
48279 |
sarRatio = [20, 11];
|
|
|
48280 |
break;
|
|
|
48281 |
case 8:
|
|
|
48282 |
sarRatio = [32, 11];
|
|
|
48283 |
break;
|
|
|
48284 |
case 9:
|
|
|
48285 |
sarRatio = [80, 33];
|
|
|
48286 |
break;
|
|
|
48287 |
case 10:
|
|
|
48288 |
sarRatio = [18, 11];
|
|
|
48289 |
break;
|
|
|
48290 |
case 11:
|
|
|
48291 |
sarRatio = [15, 11];
|
|
|
48292 |
break;
|
|
|
48293 |
case 12:
|
|
|
48294 |
sarRatio = [64, 33];
|
|
|
48295 |
break;
|
|
|
48296 |
case 13:
|
|
|
48297 |
sarRatio = [160, 99];
|
|
|
48298 |
break;
|
|
|
48299 |
case 14:
|
|
|
48300 |
sarRatio = [4, 3];
|
|
|
48301 |
break;
|
|
|
48302 |
case 15:
|
|
|
48303 |
sarRatio = [3, 2];
|
|
|
48304 |
break;
|
|
|
48305 |
case 16:
|
|
|
48306 |
sarRatio = [2, 1];
|
|
|
48307 |
break;
|
|
|
48308 |
case 255:
|
|
|
48309 |
{
|
|
|
48310 |
sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()];
|
|
|
48311 |
break;
|
|
|
48312 |
}
|
|
|
48313 |
}
|
|
|
48314 |
if (sarRatio) {
|
|
|
48315 |
sarRatio[0] / sarRatio[1];
|
|
|
48316 |
}
|
|
|
48317 |
}
|
|
|
48318 |
}
|
|
|
48319 |
return {
|
|
|
48320 |
profileIdc: profileIdc,
|
|
|
48321 |
levelIdc: levelIdc,
|
|
|
48322 |
profileCompatibility: profileCompatibility,
|
|
|
48323 |
width: (picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2,
|
|
|
48324 |
height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2,
|
|
|
48325 |
// sar is sample aspect ratio
|
|
|
48326 |
sarRatio: sarRatio
|
|
|
48327 |
};
|
|
|
48328 |
};
|
|
|
48329 |
};
|
|
|
48330 |
H264Stream$1.prototype = new Stream$2();
|
|
|
48331 |
var h264 = {
|
|
|
48332 |
H264Stream: H264Stream$1,
|
|
|
48333 |
NalByteStream: NalByteStream
|
|
|
48334 |
};
|
|
|
48335 |
/**
|
|
|
48336 |
* mux.js
|
|
|
48337 |
*
|
|
|
48338 |
* Copyright (c) Brightcove
|
|
|
48339 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
48340 |
*
|
|
|
48341 |
* Utilities to detect basic properties and metadata about Aac data.
|
|
|
48342 |
*/
|
|
|
48343 |
|
|
|
48344 |
var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
|
|
48345 |
var parseId3TagSize = function (header, byteIndex) {
|
|
|
48346 |
var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9],
|
|
|
48347 |
flags = header[byteIndex + 5],
|
|
|
48348 |
footerPresent = (flags & 16) >> 4; // if we get a negative returnSize clamp it to 0
|
|
|
48349 |
|
|
|
48350 |
returnSize = returnSize >= 0 ? returnSize : 0;
|
|
|
48351 |
if (footerPresent) {
|
|
|
48352 |
return returnSize + 20;
|
|
|
48353 |
}
|
|
|
48354 |
return returnSize + 10;
|
|
|
48355 |
};
|
|
|
48356 |
var getId3Offset = function (data, offset) {
|
|
|
48357 |
if (data.length - offset < 10 || data[offset] !== 'I'.charCodeAt(0) || data[offset + 1] !== 'D'.charCodeAt(0) || data[offset + 2] !== '3'.charCodeAt(0)) {
|
|
|
48358 |
return offset;
|
|
|
48359 |
}
|
|
|
48360 |
offset += parseId3TagSize(data, offset);
|
|
|
48361 |
return getId3Offset(data, offset);
|
|
|
48362 |
}; // TODO: use vhs-utils
|
|
|
48363 |
|
|
|
48364 |
var isLikelyAacData$1 = function (data) {
|
|
|
48365 |
var offset = getId3Offset(data, 0);
|
|
|
48366 |
return data.length >= offset + 2 && (data[offset] & 0xFF) === 0xFF && (data[offset + 1] & 0xF0) === 0xF0 &&
|
|
|
48367 |
// verify that the 2 layer bits are 0, aka this
|
|
|
48368 |
// is not mp3 data but aac data.
|
|
|
48369 |
(data[offset + 1] & 0x16) === 0x10;
|
|
|
48370 |
};
|
|
|
48371 |
var parseSyncSafeInteger = function (data) {
|
|
|
48372 |
return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
|
|
|
48373 |
}; // return a percent-encoded representation of the specified byte range
|
|
|
48374 |
// @see http://en.wikipedia.org/wiki/Percent-encoding
|
|
|
48375 |
|
|
|
48376 |
var percentEncode = function (bytes, start, end) {
|
|
|
48377 |
var i,
|
|
|
48378 |
result = '';
|
|
|
48379 |
for (i = start; i < end; i++) {
|
|
|
48380 |
result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
|
|
|
48381 |
}
|
|
|
48382 |
return result;
|
|
|
48383 |
}; // return the string representation of the specified byte range,
|
|
|
48384 |
// interpreted as ISO-8859-1.
|
|
|
48385 |
|
|
|
48386 |
var parseIso88591 = function (bytes, start, end) {
|
|
|
48387 |
return unescape(percentEncode(bytes, start, end)); // jshint ignore:line
|
|
|
48388 |
};
|
|
|
48389 |
|
|
|
48390 |
var parseAdtsSize = function (header, byteIndex) {
|
|
|
48391 |
var lowThree = (header[byteIndex + 5] & 0xE0) >> 5,
|
|
|
48392 |
middle = header[byteIndex + 4] << 3,
|
|
|
48393 |
highTwo = header[byteIndex + 3] & 0x3 << 11;
|
|
|
48394 |
return highTwo | middle | lowThree;
|
|
|
48395 |
};
|
|
|
48396 |
var parseType$4 = function (header, byteIndex) {
|
|
|
48397 |
if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) {
|
|
|
48398 |
return 'timed-metadata';
|
|
|
48399 |
} else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) {
|
|
|
48400 |
return 'audio';
|
|
|
48401 |
}
|
|
|
48402 |
return null;
|
|
|
48403 |
};
|
|
|
48404 |
var parseSampleRate = function (packet) {
|
|
|
48405 |
var i = 0;
|
|
|
48406 |
while (i + 5 < packet.length) {
|
|
|
48407 |
if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) {
|
|
|
48408 |
// If a valid header was not found, jump one forward and attempt to
|
|
|
48409 |
// find a valid ADTS header starting at the next byte
|
|
|
48410 |
i++;
|
|
|
48411 |
continue;
|
|
|
48412 |
}
|
|
|
48413 |
return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2];
|
|
|
48414 |
}
|
|
|
48415 |
return null;
|
|
|
48416 |
};
|
|
|
48417 |
var parseAacTimestamp = function (packet) {
|
|
|
48418 |
var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag
|
|
|
48419 |
|
|
|
48420 |
frameStart = 10;
|
|
|
48421 |
if (packet[5] & 0x40) {
|
|
|
48422 |
// advance the frame start past the extended header
|
|
|
48423 |
frameStart += 4; // header size field
|
|
|
48424 |
|
|
|
48425 |
frameStart += parseSyncSafeInteger(packet.subarray(10, 14));
|
|
|
48426 |
} // parse one or more ID3 frames
|
|
|
48427 |
// http://id3.org/id3v2.3.0#ID3v2_frame_overview
|
|
|
48428 |
|
|
|
48429 |
do {
|
|
|
48430 |
// determine the number of bytes in this frame
|
|
|
48431 |
frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8));
|
|
|
48432 |
if (frameSize < 1) {
|
|
|
48433 |
return null;
|
|
|
48434 |
}
|
|
|
48435 |
frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]);
|
|
|
48436 |
if (frameHeader === 'PRIV') {
|
|
|
48437 |
frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10);
|
|
|
48438 |
for (var i = 0; i < frame.byteLength; i++) {
|
|
|
48439 |
if (frame[i] === 0) {
|
|
|
48440 |
var owner = parseIso88591(frame, 0, i);
|
|
|
48441 |
if (owner === 'com.apple.streaming.transportStreamTimestamp') {
|
|
|
48442 |
var d = frame.subarray(i + 1);
|
|
|
48443 |
var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
|
|
|
48444 |
size *= 4;
|
|
|
48445 |
size += d[7] & 0x03;
|
|
|
48446 |
return size;
|
|
|
48447 |
}
|
|
|
48448 |
break;
|
|
|
48449 |
}
|
|
|
48450 |
}
|
|
|
48451 |
}
|
|
|
48452 |
frameStart += 10; // advance past the frame header
|
|
|
48453 |
|
|
|
48454 |
frameStart += frameSize; // advance past the frame body
|
|
|
48455 |
} while (frameStart < packet.byteLength);
|
|
|
48456 |
return null;
|
|
|
48457 |
};
|
|
|
48458 |
var utils = {
|
|
|
48459 |
isLikelyAacData: isLikelyAacData$1,
|
|
|
48460 |
parseId3TagSize: parseId3TagSize,
|
|
|
48461 |
parseAdtsSize: parseAdtsSize,
|
|
|
48462 |
parseType: parseType$4,
|
|
|
48463 |
parseSampleRate: parseSampleRate,
|
|
|
48464 |
parseAacTimestamp: parseAacTimestamp
|
|
|
48465 |
};
|
|
|
48466 |
/**
|
|
|
48467 |
* mux.js
|
|
|
48468 |
*
|
|
|
48469 |
* Copyright (c) Brightcove
|
|
|
48470 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
48471 |
*
|
|
|
48472 |
* A stream-based aac to mp4 converter. This utility can be used to
|
|
|
48473 |
* deliver mp4s to a SourceBuffer on platforms that support native
|
|
|
48474 |
* Media Source Extensions.
|
|
|
48475 |
*/
|
|
|
48476 |
|
|
|
48477 |
var Stream$1 = stream;
|
|
|
48478 |
var aacUtils = utils; // Constants
|
|
|
48479 |
|
|
|
48480 |
var AacStream$1;
|
|
|
48481 |
/**
|
|
|
48482 |
* Splits an incoming stream of binary data into ADTS and ID3 Frames.
|
|
|
48483 |
*/
|
|
|
48484 |
|
|
|
48485 |
AacStream$1 = function () {
|
|
|
48486 |
var everything = new Uint8Array(),
|
|
|
48487 |
timeStamp = 0;
|
|
|
48488 |
AacStream$1.prototype.init.call(this);
|
|
|
48489 |
this.setTimestamp = function (timestamp) {
|
|
|
48490 |
timeStamp = timestamp;
|
|
|
48491 |
};
|
|
|
48492 |
this.push = function (bytes) {
|
|
|
48493 |
var frameSize = 0,
|
|
|
48494 |
byteIndex = 0,
|
|
|
48495 |
bytesLeft,
|
|
|
48496 |
chunk,
|
|
|
48497 |
packet,
|
|
|
48498 |
tempLength; // If there are bytes remaining from the last segment, prepend them to the
|
|
|
48499 |
// bytes that were pushed in
|
|
|
48500 |
|
|
|
48501 |
if (everything.length) {
|
|
|
48502 |
tempLength = everything.length;
|
|
|
48503 |
everything = new Uint8Array(bytes.byteLength + tempLength);
|
|
|
48504 |
everything.set(everything.subarray(0, tempLength));
|
|
|
48505 |
everything.set(bytes, tempLength);
|
|
|
48506 |
} else {
|
|
|
48507 |
everything = bytes;
|
|
|
48508 |
}
|
|
|
48509 |
while (everything.length - byteIndex >= 3) {
|
|
|
48510 |
if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) {
|
|
|
48511 |
// Exit early because we don't have enough to parse
|
|
|
48512 |
// the ID3 tag header
|
|
|
48513 |
if (everything.length - byteIndex < 10) {
|
|
|
48514 |
break;
|
|
|
48515 |
} // check framesize
|
|
|
48516 |
|
|
|
48517 |
frameSize = aacUtils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer
|
|
|
48518 |
// to emit a full packet
|
|
|
48519 |
// Add to byteIndex to support multiple ID3 tags in sequence
|
|
|
48520 |
|
|
|
48521 |
if (byteIndex + frameSize > everything.length) {
|
|
|
48522 |
break;
|
|
|
48523 |
}
|
|
|
48524 |
chunk = {
|
|
|
48525 |
type: 'timed-metadata',
|
|
|
48526 |
data: everything.subarray(byteIndex, byteIndex + frameSize)
|
|
|
48527 |
};
|
|
|
48528 |
this.trigger('data', chunk);
|
|
|
48529 |
byteIndex += frameSize;
|
|
|
48530 |
continue;
|
|
|
48531 |
} else if ((everything[byteIndex] & 0xff) === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) {
|
|
|
48532 |
// Exit early because we don't have enough to parse
|
|
|
48533 |
// the ADTS frame header
|
|
|
48534 |
if (everything.length - byteIndex < 7) {
|
|
|
48535 |
break;
|
|
|
48536 |
}
|
|
|
48537 |
frameSize = aacUtils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer
|
|
|
48538 |
// to emit a full packet
|
|
|
48539 |
|
|
|
48540 |
if (byteIndex + frameSize > everything.length) {
|
|
|
48541 |
break;
|
|
|
48542 |
}
|
|
|
48543 |
packet = {
|
|
|
48544 |
type: 'audio',
|
|
|
48545 |
data: everything.subarray(byteIndex, byteIndex + frameSize),
|
|
|
48546 |
pts: timeStamp,
|
|
|
48547 |
dts: timeStamp
|
|
|
48548 |
};
|
|
|
48549 |
this.trigger('data', packet);
|
|
|
48550 |
byteIndex += frameSize;
|
|
|
48551 |
continue;
|
|
|
48552 |
}
|
|
|
48553 |
byteIndex++;
|
|
|
48554 |
}
|
|
|
48555 |
bytesLeft = everything.length - byteIndex;
|
|
|
48556 |
if (bytesLeft > 0) {
|
|
|
48557 |
everything = everything.subarray(byteIndex);
|
|
|
48558 |
} else {
|
|
|
48559 |
everything = new Uint8Array();
|
|
|
48560 |
}
|
|
|
48561 |
};
|
|
|
48562 |
this.reset = function () {
|
|
|
48563 |
everything = new Uint8Array();
|
|
|
48564 |
this.trigger('reset');
|
|
|
48565 |
};
|
|
|
48566 |
this.endTimeline = function () {
|
|
|
48567 |
everything = new Uint8Array();
|
|
|
48568 |
this.trigger('endedtimeline');
|
|
|
48569 |
};
|
|
|
48570 |
};
|
|
|
48571 |
AacStream$1.prototype = new Stream$1();
|
|
|
48572 |
var aac = AacStream$1;
|
|
|
48573 |
var AUDIO_PROPERTIES$1 = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize'];
|
|
|
48574 |
var audioProperties = AUDIO_PROPERTIES$1;
|
|
|
48575 |
var VIDEO_PROPERTIES$1 = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility', 'sarRatio'];
|
|
|
48576 |
var videoProperties = VIDEO_PROPERTIES$1;
|
|
|
48577 |
/**
|
|
|
48578 |
* mux.js
|
|
|
48579 |
*
|
|
|
48580 |
* Copyright (c) Brightcove
|
|
|
48581 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
48582 |
*
|
|
|
48583 |
* A stream-based mp2t to mp4 converter. This utility can be used to
|
|
|
48584 |
* deliver mp4s to a SourceBuffer on platforms that support native
|
|
|
48585 |
* Media Source Extensions.
|
|
|
48586 |
*/
|
|
|
48587 |
|
|
|
48588 |
var Stream = stream;
|
|
|
48589 |
var mp4 = mp4Generator;
|
|
|
48590 |
var frameUtils = frameUtils$1;
|
|
|
48591 |
var audioFrameUtils = audioFrameUtils$1;
|
|
|
48592 |
var trackDecodeInfo = trackDecodeInfo$1;
|
|
|
48593 |
var m2ts = m2ts_1;
|
|
|
48594 |
var clock = clock$2;
|
|
|
48595 |
var AdtsStream = adts;
|
|
|
48596 |
var H264Stream = h264.H264Stream;
|
|
|
48597 |
var AacStream = aac;
|
|
|
48598 |
var isLikelyAacData = utils.isLikelyAacData;
|
|
|
48599 |
var ONE_SECOND_IN_TS$1 = clock$2.ONE_SECOND_IN_TS;
|
|
|
48600 |
var AUDIO_PROPERTIES = audioProperties;
|
|
|
48601 |
var VIDEO_PROPERTIES = videoProperties; // object types
|
|
|
48602 |
|
|
|
48603 |
var VideoSegmentStream, AudioSegmentStream, Transmuxer, CoalesceStream;
|
|
|
48604 |
var retriggerForStream = function (key, event) {
|
|
|
48605 |
event.stream = key;
|
|
|
48606 |
this.trigger('log', event);
|
|
|
48607 |
};
|
|
|
48608 |
var addPipelineLogRetriggers = function (transmuxer, pipeline) {
|
|
|
48609 |
var keys = Object.keys(pipeline);
|
|
|
48610 |
for (var i = 0; i < keys.length; i++) {
|
|
|
48611 |
var key = keys[i]; // skip non-stream keys and headOfPipeline
|
|
|
48612 |
// which is just a duplicate
|
|
|
48613 |
|
|
|
48614 |
if (key === 'headOfPipeline' || !pipeline[key].on) {
|
|
|
48615 |
continue;
|
|
|
48616 |
}
|
|
|
48617 |
pipeline[key].on('log', retriggerForStream.bind(transmuxer, key));
|
|
|
48618 |
}
|
|
|
48619 |
};
|
|
|
48620 |
/**
|
|
|
48621 |
* Compare two arrays (even typed) for same-ness
|
|
|
48622 |
*/
|
|
|
48623 |
|
|
|
48624 |
var arrayEquals = function (a, b) {
|
|
|
48625 |
var i;
|
|
|
48626 |
if (a.length !== b.length) {
|
|
|
48627 |
return false;
|
|
|
48628 |
} // compare the value of each element in the array
|
|
|
48629 |
|
|
|
48630 |
for (i = 0; i < a.length; i++) {
|
|
|
48631 |
if (a[i] !== b[i]) {
|
|
|
48632 |
return false;
|
|
|
48633 |
}
|
|
|
48634 |
}
|
|
|
48635 |
return true;
|
|
|
48636 |
};
|
|
|
48637 |
var generateSegmentTimingInfo = function (baseMediaDecodeTime, startDts, startPts, endDts, endPts, prependedContentDuration) {
|
|
|
48638 |
var ptsOffsetFromDts = startPts - startDts,
|
|
|
48639 |
decodeDuration = endDts - startDts,
|
|
|
48640 |
presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment,
|
|
|
48641 |
// however, the player time values will reflect a start from the baseMediaDecodeTime.
|
|
|
48642 |
// In order to provide relevant values for the player times, base timing info on the
|
|
|
48643 |
// baseMediaDecodeTime and the DTS and PTS durations of the segment.
|
|
|
48644 |
|
|
|
48645 |
return {
|
|
|
48646 |
start: {
|
|
|
48647 |
dts: baseMediaDecodeTime,
|
|
|
48648 |
pts: baseMediaDecodeTime + ptsOffsetFromDts
|
|
|
48649 |
},
|
|
|
48650 |
end: {
|
|
|
48651 |
dts: baseMediaDecodeTime + decodeDuration,
|
|
|
48652 |
pts: baseMediaDecodeTime + presentationDuration
|
|
|
48653 |
},
|
|
|
48654 |
prependedContentDuration: prependedContentDuration,
|
|
|
48655 |
baseMediaDecodeTime: baseMediaDecodeTime
|
|
|
48656 |
};
|
|
|
48657 |
};
|
|
|
48658 |
/**
|
|
|
48659 |
* Constructs a single-track, ISO BMFF media segment from AAC data
|
|
|
48660 |
* events. The output of this stream can be fed to a SourceBuffer
|
|
|
48661 |
* configured with a suitable initialization segment.
|
|
|
48662 |
* @param track {object} track metadata configuration
|
|
|
48663 |
* @param options {object} transmuxer options object
|
|
|
48664 |
* @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
|
|
|
48665 |
* in the source; false to adjust the first segment to start at 0.
|
|
|
48666 |
*/
|
|
|
48667 |
|
|
|
48668 |
AudioSegmentStream = function (track, options) {
|
|
|
48669 |
var adtsFrames = [],
|
|
|
48670 |
sequenceNumber,
|
|
|
48671 |
earliestAllowedDts = 0,
|
|
|
48672 |
audioAppendStartTs = 0,
|
|
|
48673 |
videoBaseMediaDecodeTime = Infinity;
|
|
|
48674 |
options = options || {};
|
|
|
48675 |
sequenceNumber = options.firstSequenceNumber || 0;
|
|
|
48676 |
AudioSegmentStream.prototype.init.call(this);
|
|
|
48677 |
this.push = function (data) {
|
|
|
48678 |
trackDecodeInfo.collectDtsInfo(track, data);
|
|
|
48679 |
if (track) {
|
|
|
48680 |
AUDIO_PROPERTIES.forEach(function (prop) {
|
|
|
48681 |
track[prop] = data[prop];
|
|
|
48682 |
});
|
|
|
48683 |
} // buffer audio data until end() is called
|
|
|
48684 |
|
|
|
48685 |
adtsFrames.push(data);
|
|
|
48686 |
};
|
|
|
48687 |
this.setEarliestDts = function (earliestDts) {
|
|
|
48688 |
earliestAllowedDts = earliestDts;
|
|
|
48689 |
};
|
|
|
48690 |
this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) {
|
|
|
48691 |
videoBaseMediaDecodeTime = baseMediaDecodeTime;
|
|
|
48692 |
};
|
|
|
48693 |
this.setAudioAppendStart = function (timestamp) {
|
|
|
48694 |
audioAppendStartTs = timestamp;
|
|
|
48695 |
};
|
|
|
48696 |
this.flush = function () {
|
|
|
48697 |
var frames, moof, mdat, boxes, frameDuration, segmentDuration, videoClockCyclesOfSilencePrefixed; // return early if no audio data has been observed
|
|
|
48698 |
|
|
|
48699 |
if (adtsFrames.length === 0) {
|
|
|
48700 |
this.trigger('done', 'AudioSegmentStream');
|
|
|
48701 |
return;
|
|
|
48702 |
}
|
|
|
48703 |
frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts);
|
|
|
48704 |
track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); // amount of audio filled but the value is in video clock rather than audio clock
|
|
|
48705 |
|
|
|
48706 |
videoClockCyclesOfSilencePrefixed = audioFrameUtils.prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime); // we have to build the index from byte locations to
|
|
|
48707 |
// samples (that is, adts frames) in the audio data
|
|
|
48708 |
|
|
|
48709 |
track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat
|
|
|
48710 |
|
|
|
48711 |
mdat = mp4.mdat(audioFrameUtils.concatenateFrameData(frames));
|
|
|
48712 |
adtsFrames = [];
|
|
|
48713 |
moof = mp4.moof(sequenceNumber, [track]);
|
|
|
48714 |
boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time
|
|
|
48715 |
|
|
|
48716 |
sequenceNumber++;
|
|
|
48717 |
boxes.set(moof);
|
|
|
48718 |
boxes.set(mdat, moof.byteLength);
|
|
|
48719 |
trackDecodeInfo.clearDtsInfo(track);
|
|
|
48720 |
frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 * 1024 / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with
|
|
|
48721 |
// tests) on adding the timingInfo event. However, it seems unlikely that there's a
|
|
|
48722 |
// valid use-case where an init segment/data should be triggered without associated
|
|
|
48723 |
// frames. Leaving for now, but should be looked into.
|
|
|
48724 |
|
|
|
48725 |
if (frames.length) {
|
|
|
48726 |
segmentDuration = frames.length * frameDuration;
|
|
|
48727 |
this.trigger('segmentTimingInfo', generateSegmentTimingInfo(
|
|
|
48728 |
// The audio track's baseMediaDecodeTime is in audio clock cycles, but the
|
|
|
48729 |
// frame info is in video clock cycles. Convert to match expectation of
|
|
|
48730 |
// listeners (that all timestamps will be based on video clock cycles).
|
|
|
48731 |
clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate),
|
|
|
48732 |
// frame times are already in video clock, as is segment duration
|
|
|
48733 |
frames[0].dts, frames[0].pts, frames[0].dts + segmentDuration, frames[0].pts + segmentDuration, videoClockCyclesOfSilencePrefixed || 0));
|
|
|
48734 |
this.trigger('timingInfo', {
|
|
|
48735 |
start: frames[0].pts,
|
|
|
48736 |
end: frames[0].pts + segmentDuration
|
|
|
48737 |
});
|
|
|
48738 |
}
|
|
|
48739 |
this.trigger('data', {
|
|
|
48740 |
track: track,
|
|
|
48741 |
boxes: boxes
|
|
|
48742 |
});
|
|
|
48743 |
this.trigger('done', 'AudioSegmentStream');
|
|
|
48744 |
};
|
|
|
48745 |
this.reset = function () {
|
|
|
48746 |
trackDecodeInfo.clearDtsInfo(track);
|
|
|
48747 |
adtsFrames = [];
|
|
|
48748 |
this.trigger('reset');
|
|
|
48749 |
};
|
|
|
48750 |
};
|
|
|
48751 |
AudioSegmentStream.prototype = new Stream();
|
|
|
48752 |
/**
|
|
|
48753 |
* Constructs a single-track, ISO BMFF media segment from H264 data
|
|
|
48754 |
* events. The output of this stream can be fed to a SourceBuffer
|
|
|
48755 |
* configured with a suitable initialization segment.
|
|
|
48756 |
* @param track {object} track metadata configuration
|
|
|
48757 |
* @param options {object} transmuxer options object
|
|
|
48758 |
* @param options.alignGopsAtEnd {boolean} If true, start from the end of the
|
|
|
48759 |
* gopsToAlignWith list when attempting to align gop pts
|
|
|
48760 |
* @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
|
|
|
48761 |
* in the source; false to adjust the first segment to start at 0.
|
|
|
48762 |
*/
|
|
|
48763 |
|
|
|
48764 |
VideoSegmentStream = function (track, options) {
|
|
|
48765 |
var sequenceNumber,
|
|
|
48766 |
nalUnits = [],
|
|
|
48767 |
gopsToAlignWith = [],
|
|
|
48768 |
config,
|
|
|
48769 |
pps;
|
|
|
48770 |
options = options || {};
|
|
|
48771 |
sequenceNumber = options.firstSequenceNumber || 0;
|
|
|
48772 |
VideoSegmentStream.prototype.init.call(this);
|
|
|
48773 |
delete track.minPTS;
|
|
|
48774 |
this.gopCache_ = [];
|
|
|
48775 |
/**
|
|
|
48776 |
* Constructs a ISO BMFF segment given H264 nalUnits
|
|
|
48777 |
* @param {Object} nalUnit A data event representing a nalUnit
|
|
|
48778 |
* @param {String} nalUnit.nalUnitType
|
|
|
48779 |
* @param {Object} nalUnit.config Properties for a mp4 track
|
|
|
48780 |
* @param {Uint8Array} nalUnit.data The nalUnit bytes
|
|
|
48781 |
* @see lib/codecs/h264.js
|
|
|
48782 |
**/
|
|
|
48783 |
|
|
|
48784 |
this.push = function (nalUnit) {
|
|
|
48785 |
trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config
|
|
|
48786 |
|
|
|
48787 |
if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) {
|
|
|
48788 |
config = nalUnit.config;
|
|
|
48789 |
track.sps = [nalUnit.data];
|
|
|
48790 |
VIDEO_PROPERTIES.forEach(function (prop) {
|
|
|
48791 |
track[prop] = config[prop];
|
|
|
48792 |
}, this);
|
|
|
48793 |
}
|
|
|
48794 |
if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) {
|
|
|
48795 |
pps = nalUnit.data;
|
|
|
48796 |
track.pps = [nalUnit.data];
|
|
|
48797 |
} // buffer video until flush() is called
|
|
|
48798 |
|
|
|
48799 |
nalUnits.push(nalUnit);
|
|
|
48800 |
};
|
|
|
48801 |
/**
|
|
|
48802 |
* Pass constructed ISO BMFF track and boxes on to the
|
|
|
48803 |
* next stream in the pipeline
|
|
|
48804 |
**/
|
|
|
48805 |
|
|
|
48806 |
this.flush = function () {
|
|
|
48807 |
var frames,
|
|
|
48808 |
gopForFusion,
|
|
|
48809 |
gops,
|
|
|
48810 |
moof,
|
|
|
48811 |
mdat,
|
|
|
48812 |
boxes,
|
|
|
48813 |
prependedContentDuration = 0,
|
|
|
48814 |
firstGop,
|
|
|
48815 |
lastGop; // Throw away nalUnits at the start of the byte stream until
|
|
|
48816 |
// we find the first AUD
|
|
|
48817 |
|
|
|
48818 |
while (nalUnits.length) {
|
|
|
48819 |
if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') {
|
|
|
48820 |
break;
|
|
|
48821 |
}
|
|
|
48822 |
nalUnits.shift();
|
|
|
48823 |
} // Return early if no video data has been observed
|
|
|
48824 |
|
|
|
48825 |
if (nalUnits.length === 0) {
|
|
|
48826 |
this.resetStream_();
|
|
|
48827 |
this.trigger('done', 'VideoSegmentStream');
|
|
|
48828 |
return;
|
|
|
48829 |
} // Organize the raw nal-units into arrays that represent
|
|
|
48830 |
// higher-level constructs such as frames and gops
|
|
|
48831 |
// (group-of-pictures)
|
|
|
48832 |
|
|
|
48833 |
frames = frameUtils.groupNalsIntoFrames(nalUnits);
|
|
|
48834 |
gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have
|
|
|
48835 |
// a problem since MSE (on Chrome) requires a leading keyframe.
|
|
|
48836 |
//
|
|
|
48837 |
// We have two approaches to repairing this situation:
|
|
|
48838 |
// 1) GOP-FUSION:
|
|
|
48839 |
// This is where we keep track of the GOPS (group-of-pictures)
|
|
|
48840 |
// from previous fragments and attempt to find one that we can
|
|
|
48841 |
// prepend to the current fragment in order to create a valid
|
|
|
48842 |
// fragment.
|
|
|
48843 |
// 2) KEYFRAME-PULLING:
|
|
|
48844 |
// Here we search for the first keyframe in the fragment and
|
|
|
48845 |
// throw away all the frames between the start of the fragment
|
|
|
48846 |
// and that keyframe. We then extend the duration and pull the
|
|
|
48847 |
// PTS of the keyframe forward so that it covers the time range
|
|
|
48848 |
// of the frames that were disposed of.
|
|
|
48849 |
//
|
|
|
48850 |
// #1 is far prefereable over #2 which can cause "stuttering" but
|
|
|
48851 |
// requires more things to be just right.
|
|
|
48852 |
|
|
|
48853 |
if (!gops[0][0].keyFrame) {
|
|
|
48854 |
// Search for a gop for fusion from our gopCache
|
|
|
48855 |
gopForFusion = this.getGopForFusion_(nalUnits[0], track);
|
|
|
48856 |
if (gopForFusion) {
|
|
|
48857 |
// in order to provide more accurate timing information about the segment, save
|
|
|
48858 |
// the number of seconds prepended to the original segment due to GOP fusion
|
|
|
48859 |
prependedContentDuration = gopForFusion.duration;
|
|
|
48860 |
gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the
|
|
|
48861 |
// new gop at the beginning
|
|
|
48862 |
|
|
|
48863 |
gops.byteLength += gopForFusion.byteLength;
|
|
|
48864 |
gops.nalCount += gopForFusion.nalCount;
|
|
|
48865 |
gops.pts = gopForFusion.pts;
|
|
|
48866 |
gops.dts = gopForFusion.dts;
|
|
|
48867 |
gops.duration += gopForFusion.duration;
|
|
|
48868 |
} else {
|
|
|
48869 |
// If we didn't find a candidate gop fall back to keyframe-pulling
|
|
|
48870 |
gops = frameUtils.extendFirstKeyFrame(gops);
|
|
|
48871 |
}
|
|
|
48872 |
} // Trim gops to align with gopsToAlignWith
|
|
|
48873 |
|
|
|
48874 |
if (gopsToAlignWith.length) {
|
|
|
48875 |
var alignedGops;
|
|
|
48876 |
if (options.alignGopsAtEnd) {
|
|
|
48877 |
alignedGops = this.alignGopsAtEnd_(gops);
|
|
|
48878 |
} else {
|
|
|
48879 |
alignedGops = this.alignGopsAtStart_(gops);
|
|
|
48880 |
}
|
|
|
48881 |
if (!alignedGops) {
|
|
|
48882 |
// save all the nals in the last GOP into the gop cache
|
|
|
48883 |
this.gopCache_.unshift({
|
|
|
48884 |
gop: gops.pop(),
|
|
|
48885 |
pps: track.pps,
|
|
|
48886 |
sps: track.sps
|
|
|
48887 |
}); // Keep a maximum of 6 GOPs in the cache
|
|
|
48888 |
|
|
|
48889 |
this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits
|
|
|
48890 |
|
|
|
48891 |
nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith
|
|
|
48892 |
|
|
|
48893 |
this.resetStream_();
|
|
|
48894 |
this.trigger('done', 'VideoSegmentStream');
|
|
|
48895 |
return;
|
|
|
48896 |
} // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct
|
|
|
48897 |
// when recalculated before sending off to CoalesceStream
|
|
|
48898 |
|
|
|
48899 |
trackDecodeInfo.clearDtsInfo(track);
|
|
|
48900 |
gops = alignedGops;
|
|
|
48901 |
}
|
|
|
48902 |
trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to
|
|
|
48903 |
// samples (that is, frames) in the video data
|
|
|
48904 |
|
|
|
48905 |
track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat
|
|
|
48906 |
|
|
|
48907 |
mdat = mp4.mdat(frameUtils.concatenateNalData(gops));
|
|
|
48908 |
track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps);
|
|
|
48909 |
this.trigger('processedGopsInfo', gops.map(function (gop) {
|
|
|
48910 |
return {
|
|
|
48911 |
pts: gop.pts,
|
|
|
48912 |
dts: gop.dts,
|
|
|
48913 |
byteLength: gop.byteLength
|
|
|
48914 |
};
|
|
|
48915 |
}));
|
|
|
48916 |
firstGop = gops[0];
|
|
|
48917 |
lastGop = gops[gops.length - 1];
|
|
|
48918 |
this.trigger('segmentTimingInfo', generateSegmentTimingInfo(track.baseMediaDecodeTime, firstGop.dts, firstGop.pts, lastGop.dts + lastGop.duration, lastGop.pts + lastGop.duration, prependedContentDuration));
|
|
|
48919 |
this.trigger('timingInfo', {
|
|
|
48920 |
start: gops[0].pts,
|
|
|
48921 |
end: gops[gops.length - 1].pts + gops[gops.length - 1].duration
|
|
|
48922 |
}); // save all the nals in the last GOP into the gop cache
|
|
|
48923 |
|
|
|
48924 |
this.gopCache_.unshift({
|
|
|
48925 |
gop: gops.pop(),
|
|
|
48926 |
pps: track.pps,
|
|
|
48927 |
sps: track.sps
|
|
|
48928 |
}); // Keep a maximum of 6 GOPs in the cache
|
|
|
48929 |
|
|
|
48930 |
this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits
|
|
|
48931 |
|
|
|
48932 |
nalUnits = [];
|
|
|
48933 |
this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime);
|
|
|
48934 |
this.trigger('timelineStartInfo', track.timelineStartInfo);
|
|
|
48935 |
moof = mp4.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of
|
|
|
48936 |
// throwing away hundreds of media segment fragments
|
|
|
48937 |
|
|
|
48938 |
boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time
|
|
|
48939 |
|
|
|
48940 |
sequenceNumber++;
|
|
|
48941 |
boxes.set(moof);
|
|
|
48942 |
boxes.set(mdat, moof.byteLength);
|
|
|
48943 |
this.trigger('data', {
|
|
|
48944 |
track: track,
|
|
|
48945 |
boxes: boxes
|
|
|
48946 |
});
|
|
|
48947 |
this.resetStream_(); // Continue with the flush process now
|
|
|
48948 |
|
|
|
48949 |
this.trigger('done', 'VideoSegmentStream');
|
|
|
48950 |
};
|
|
|
48951 |
this.reset = function () {
|
|
|
48952 |
this.resetStream_();
|
|
|
48953 |
nalUnits = [];
|
|
|
48954 |
this.gopCache_.length = 0;
|
|
|
48955 |
gopsToAlignWith.length = 0;
|
|
|
48956 |
this.trigger('reset');
|
|
|
48957 |
};
|
|
|
48958 |
this.resetStream_ = function () {
|
|
|
48959 |
trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments
|
|
|
48960 |
// for instance, when we are rendition switching
|
|
|
48961 |
|
|
|
48962 |
config = undefined;
|
|
|
48963 |
pps = undefined;
|
|
|
48964 |
}; // Search for a candidate Gop for gop-fusion from the gop cache and
|
|
|
48965 |
// return it or return null if no good candidate was found
|
|
|
48966 |
|
|
|
48967 |
this.getGopForFusion_ = function (nalUnit) {
|
|
|
48968 |
var halfSecond = 45000,
|
|
|
48969 |
// Half-a-second in a 90khz clock
|
|
|
48970 |
allowableOverlap = 10000,
|
|
|
48971 |
// About 3 frames @ 30fps
|
|
|
48972 |
nearestDistance = Infinity,
|
|
|
48973 |
dtsDistance,
|
|
|
48974 |
nearestGopObj,
|
|
|
48975 |
currentGop,
|
|
|
48976 |
currentGopObj,
|
|
|
48977 |
i; // Search for the GOP nearest to the beginning of this nal unit
|
|
|
48978 |
|
|
|
48979 |
for (i = 0; i < this.gopCache_.length; i++) {
|
|
|
48980 |
currentGopObj = this.gopCache_[i];
|
|
|
48981 |
currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS
|
|
|
48982 |
|
|
|
48983 |
if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) {
|
|
|
48984 |
continue;
|
|
|
48985 |
} // Reject Gops that would require a negative baseMediaDecodeTime
|
|
|
48986 |
|
|
|
48987 |
if (currentGop.dts < track.timelineStartInfo.dts) {
|
|
|
48988 |
continue;
|
|
|
48989 |
} // The distance between the end of the gop and the start of the nalUnit
|
|
|
48990 |
|
|
|
48991 |
dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within
|
|
|
48992 |
// a half-second of the nal unit
|
|
|
48993 |
|
|
|
48994 |
if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) {
|
|
|
48995 |
// Always use the closest GOP we found if there is more than
|
|
|
48996 |
// one candidate
|
|
|
48997 |
if (!nearestGopObj || nearestDistance > dtsDistance) {
|
|
|
48998 |
nearestGopObj = currentGopObj;
|
|
|
48999 |
nearestDistance = dtsDistance;
|
|
|
49000 |
}
|
|
|
49001 |
}
|
|
|
49002 |
}
|
|
|
49003 |
if (nearestGopObj) {
|
|
|
49004 |
return nearestGopObj.gop;
|
|
|
49005 |
}
|
|
|
49006 |
return null;
|
|
|
49007 |
}; // trim gop list to the first gop found that has a matching pts with a gop in the list
|
|
|
49008 |
// of gopsToAlignWith starting from the START of the list
|
|
|
49009 |
|
|
|
49010 |
this.alignGopsAtStart_ = function (gops) {
|
|
|
49011 |
var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops;
|
|
|
49012 |
byteLength = gops.byteLength;
|
|
|
49013 |
nalCount = gops.nalCount;
|
|
|
49014 |
duration = gops.duration;
|
|
|
49015 |
alignIndex = gopIndex = 0;
|
|
|
49016 |
while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) {
|
|
|
49017 |
align = gopsToAlignWith[alignIndex];
|
|
|
49018 |
gop = gops[gopIndex];
|
|
|
49019 |
if (align.pts === gop.pts) {
|
|
|
49020 |
break;
|
|
|
49021 |
}
|
|
|
49022 |
if (gop.pts > align.pts) {
|
|
|
49023 |
// this current gop starts after the current gop we want to align on, so increment
|
|
|
49024 |
// align index
|
|
|
49025 |
alignIndex++;
|
|
|
49026 |
continue;
|
|
|
49027 |
} // current gop starts before the current gop we want to align on. so increment gop
|
|
|
49028 |
// index
|
|
|
49029 |
|
|
|
49030 |
gopIndex++;
|
|
|
49031 |
byteLength -= gop.byteLength;
|
|
|
49032 |
nalCount -= gop.nalCount;
|
|
|
49033 |
duration -= gop.duration;
|
|
|
49034 |
}
|
|
|
49035 |
if (gopIndex === 0) {
|
|
|
49036 |
// no gops to trim
|
|
|
49037 |
return gops;
|
|
|
49038 |
}
|
|
|
49039 |
if (gopIndex === gops.length) {
|
|
|
49040 |
// all gops trimmed, skip appending all gops
|
|
|
49041 |
return null;
|
|
|
49042 |
}
|
|
|
49043 |
alignedGops = gops.slice(gopIndex);
|
|
|
49044 |
alignedGops.byteLength = byteLength;
|
|
|
49045 |
alignedGops.duration = duration;
|
|
|
49046 |
alignedGops.nalCount = nalCount;
|
|
|
49047 |
alignedGops.pts = alignedGops[0].pts;
|
|
|
49048 |
alignedGops.dts = alignedGops[0].dts;
|
|
|
49049 |
return alignedGops;
|
|
|
49050 |
}; // trim gop list to the first gop found that has a matching pts with a gop in the list
|
|
|
49051 |
// of gopsToAlignWith starting from the END of the list
|
|
|
49052 |
|
|
|
49053 |
this.alignGopsAtEnd_ = function (gops) {
|
|
|
49054 |
var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound;
|
|
|
49055 |
alignIndex = gopsToAlignWith.length - 1;
|
|
|
49056 |
gopIndex = gops.length - 1;
|
|
|
49057 |
alignEndIndex = null;
|
|
|
49058 |
matchFound = false;
|
|
|
49059 |
while (alignIndex >= 0 && gopIndex >= 0) {
|
|
|
49060 |
align = gopsToAlignWith[alignIndex];
|
|
|
49061 |
gop = gops[gopIndex];
|
|
|
49062 |
if (align.pts === gop.pts) {
|
|
|
49063 |
matchFound = true;
|
|
|
49064 |
break;
|
|
|
49065 |
}
|
|
|
49066 |
if (align.pts > gop.pts) {
|
|
|
49067 |
alignIndex--;
|
|
|
49068 |
continue;
|
|
|
49069 |
}
|
|
|
49070 |
if (alignIndex === gopsToAlignWith.length - 1) {
|
|
|
49071 |
// gop.pts is greater than the last alignment candidate. If no match is found
|
|
|
49072 |
// by the end of this loop, we still want to append gops that come after this
|
|
|
49073 |
// point
|
|
|
49074 |
alignEndIndex = gopIndex;
|
|
|
49075 |
}
|
|
|
49076 |
gopIndex--;
|
|
|
49077 |
}
|
|
|
49078 |
if (!matchFound && alignEndIndex === null) {
|
|
|
49079 |
return null;
|
|
|
49080 |
}
|
|
|
49081 |
var trimIndex;
|
|
|
49082 |
if (matchFound) {
|
|
|
49083 |
trimIndex = gopIndex;
|
|
|
49084 |
} else {
|
|
|
49085 |
trimIndex = alignEndIndex;
|
|
|
49086 |
}
|
|
|
49087 |
if (trimIndex === 0) {
|
|
|
49088 |
return gops;
|
|
|
49089 |
}
|
|
|
49090 |
var alignedGops = gops.slice(trimIndex);
|
|
|
49091 |
var metadata = alignedGops.reduce(function (total, gop) {
|
|
|
49092 |
total.byteLength += gop.byteLength;
|
|
|
49093 |
total.duration += gop.duration;
|
|
|
49094 |
total.nalCount += gop.nalCount;
|
|
|
49095 |
return total;
|
|
|
49096 |
}, {
|
|
|
49097 |
byteLength: 0,
|
|
|
49098 |
duration: 0,
|
|
|
49099 |
nalCount: 0
|
|
|
49100 |
});
|
|
|
49101 |
alignedGops.byteLength = metadata.byteLength;
|
|
|
49102 |
alignedGops.duration = metadata.duration;
|
|
|
49103 |
alignedGops.nalCount = metadata.nalCount;
|
|
|
49104 |
alignedGops.pts = alignedGops[0].pts;
|
|
|
49105 |
alignedGops.dts = alignedGops[0].dts;
|
|
|
49106 |
return alignedGops;
|
|
|
49107 |
};
|
|
|
49108 |
this.alignGopsWith = function (newGopsToAlignWith) {
|
|
|
49109 |
gopsToAlignWith = newGopsToAlignWith;
|
|
|
49110 |
};
|
|
|
49111 |
};
|
|
|
49112 |
VideoSegmentStream.prototype = new Stream();
|
|
|
49113 |
/**
|
|
|
49114 |
* A Stream that can combine multiple streams (ie. audio & video)
|
|
|
49115 |
* into a single output segment for MSE. Also supports audio-only
|
|
|
49116 |
* and video-only streams.
|
|
|
49117 |
* @param options {object} transmuxer options object
|
|
|
49118 |
* @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
|
|
|
49119 |
* in the source; false to adjust the first segment to start at media timeline start.
|
|
|
49120 |
*/
|
|
|
49121 |
|
|
|
49122 |
CoalesceStream = function (options, metadataStream) {
|
|
|
49123 |
// Number of Tracks per output segment
|
|
|
49124 |
// If greater than 1, we combine multiple
|
|
|
49125 |
// tracks into a single segment
|
|
|
49126 |
this.numberOfTracks = 0;
|
|
|
49127 |
this.metadataStream = metadataStream;
|
|
|
49128 |
options = options || {};
|
|
|
49129 |
if (typeof options.remux !== 'undefined') {
|
|
|
49130 |
this.remuxTracks = !!options.remux;
|
|
|
49131 |
} else {
|
|
|
49132 |
this.remuxTracks = true;
|
|
|
49133 |
}
|
|
|
49134 |
if (typeof options.keepOriginalTimestamps === 'boolean') {
|
|
|
49135 |
this.keepOriginalTimestamps = options.keepOriginalTimestamps;
|
|
|
49136 |
} else {
|
|
|
49137 |
this.keepOriginalTimestamps = false;
|
|
|
49138 |
}
|
|
|
49139 |
this.pendingTracks = [];
|
|
|
49140 |
this.videoTrack = null;
|
|
|
49141 |
this.pendingBoxes = [];
|
|
|
49142 |
this.pendingCaptions = [];
|
|
|
49143 |
this.pendingMetadata = [];
|
|
|
49144 |
this.pendingBytes = 0;
|
|
|
49145 |
this.emittedTracks = 0;
|
|
|
49146 |
CoalesceStream.prototype.init.call(this); // Take output from multiple
|
|
|
49147 |
|
|
|
49148 |
this.push = function (output) {
|
|
|
49149 |
// buffer incoming captions until the associated video segment
|
|
|
49150 |
// finishes
|
|
|
49151 |
if (output.content || output.text) {
|
|
|
49152 |
return this.pendingCaptions.push(output);
|
|
|
49153 |
} // buffer incoming id3 tags until the final flush
|
|
|
49154 |
|
|
|
49155 |
if (output.frames) {
|
|
|
49156 |
return this.pendingMetadata.push(output);
|
|
|
49157 |
} // Add this track to the list of pending tracks and store
|
|
|
49158 |
// important information required for the construction of
|
|
|
49159 |
// the final segment
|
|
|
49160 |
|
|
|
49161 |
this.pendingTracks.push(output.track);
|
|
|
49162 |
this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome?
|
|
|
49163 |
// We unshift audio and push video because
|
|
|
49164 |
// as of Chrome 75 when switching from
|
|
|
49165 |
// one init segment to another if the video
|
|
|
49166 |
// mdat does not appear after the audio mdat
|
|
|
49167 |
// only audio will play for the duration of our transmux.
|
|
|
49168 |
|
|
|
49169 |
if (output.track.type === 'video') {
|
|
|
49170 |
this.videoTrack = output.track;
|
|
|
49171 |
this.pendingBoxes.push(output.boxes);
|
|
|
49172 |
}
|
|
|
49173 |
if (output.track.type === 'audio') {
|
|
|
49174 |
this.audioTrack = output.track;
|
|
|
49175 |
this.pendingBoxes.unshift(output.boxes);
|
|
|
49176 |
}
|
|
|
49177 |
};
|
|
|
49178 |
};
|
|
|
49179 |
CoalesceStream.prototype = new Stream();
|
|
|
49180 |
CoalesceStream.prototype.flush = function (flushSource) {
|
|
|
49181 |
var offset = 0,
|
|
|
49182 |
event = {
|
|
|
49183 |
captions: [],
|
|
|
49184 |
captionStreams: {},
|
|
|
49185 |
metadata: [],
|
|
|
49186 |
info: {}
|
|
|
49187 |
},
|
|
|
49188 |
caption,
|
|
|
49189 |
id3,
|
|
|
49190 |
initSegment,
|
|
|
49191 |
timelineStartPts = 0,
|
|
|
49192 |
i;
|
|
|
49193 |
if (this.pendingTracks.length < this.numberOfTracks) {
|
|
|
49194 |
if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') {
|
|
|
49195 |
// Return because we haven't received a flush from a data-generating
|
|
|
49196 |
// portion of the segment (meaning that we have only recieved meta-data
|
|
|
49197 |
// or captions.)
|
|
|
49198 |
return;
|
|
|
49199 |
} else if (this.remuxTracks) {
|
|
|
49200 |
// Return until we have enough tracks from the pipeline to remux (if we
|
|
|
49201 |
// are remuxing audio and video into a single MP4)
|
|
|
49202 |
return;
|
|
|
49203 |
} else if (this.pendingTracks.length === 0) {
|
|
|
49204 |
// In the case where we receive a flush without any data having been
|
|
|
49205 |
// received we consider it an emitted track for the purposes of coalescing
|
|
|
49206 |
// `done` events.
|
|
|
49207 |
// We do this for the case where there is an audio and video track in the
|
|
|
49208 |
// segment but no audio data. (seen in several playlists with alternate
|
|
|
49209 |
// audio tracks and no audio present in the main TS segments.)
|
|
|
49210 |
this.emittedTracks++;
|
|
|
49211 |
if (this.emittedTracks >= this.numberOfTracks) {
|
|
|
49212 |
this.trigger('done');
|
|
|
49213 |
this.emittedTracks = 0;
|
|
|
49214 |
}
|
|
|
49215 |
return;
|
|
|
49216 |
}
|
|
|
49217 |
}
|
|
|
49218 |
if (this.videoTrack) {
|
|
|
49219 |
timelineStartPts = this.videoTrack.timelineStartInfo.pts;
|
|
|
49220 |
VIDEO_PROPERTIES.forEach(function (prop) {
|
|
|
49221 |
event.info[prop] = this.videoTrack[prop];
|
|
|
49222 |
}, this);
|
|
|
49223 |
} else if (this.audioTrack) {
|
|
|
49224 |
timelineStartPts = this.audioTrack.timelineStartInfo.pts;
|
|
|
49225 |
AUDIO_PROPERTIES.forEach(function (prop) {
|
|
|
49226 |
event.info[prop] = this.audioTrack[prop];
|
|
|
49227 |
}, this);
|
|
|
49228 |
}
|
|
|
49229 |
if (this.videoTrack || this.audioTrack) {
|
|
|
49230 |
if (this.pendingTracks.length === 1) {
|
|
|
49231 |
event.type = this.pendingTracks[0].type;
|
|
|
49232 |
} else {
|
|
|
49233 |
event.type = 'combined';
|
|
|
49234 |
}
|
|
|
49235 |
this.emittedTracks += this.pendingTracks.length;
|
|
|
49236 |
initSegment = mp4.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment
|
|
|
49237 |
|
|
|
49238 |
event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov
|
|
|
49239 |
// and track definitions
|
|
|
49240 |
|
|
|
49241 |
event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats
|
|
|
49242 |
|
|
|
49243 |
event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together
|
|
|
49244 |
|
|
|
49245 |
for (i = 0; i < this.pendingBoxes.length; i++) {
|
|
|
49246 |
event.data.set(this.pendingBoxes[i], offset);
|
|
|
49247 |
offset += this.pendingBoxes[i].byteLength;
|
|
|
49248 |
} // Translate caption PTS times into second offsets to match the
|
|
|
49249 |
// video timeline for the segment, and add track info
|
|
|
49250 |
|
|
|
49251 |
for (i = 0; i < this.pendingCaptions.length; i++) {
|
|
|
49252 |
caption = this.pendingCaptions[i];
|
|
|
49253 |
caption.startTime = clock.metadataTsToSeconds(caption.startPts, timelineStartPts, this.keepOriginalTimestamps);
|
|
|
49254 |
caption.endTime = clock.metadataTsToSeconds(caption.endPts, timelineStartPts, this.keepOriginalTimestamps);
|
|
|
49255 |
event.captionStreams[caption.stream] = true;
|
|
|
49256 |
event.captions.push(caption);
|
|
|
49257 |
} // Translate ID3 frame PTS times into second offsets to match the
|
|
|
49258 |
// video timeline for the segment
|
|
|
49259 |
|
|
|
49260 |
for (i = 0; i < this.pendingMetadata.length; i++) {
|
|
|
49261 |
id3 = this.pendingMetadata[i];
|
|
|
49262 |
id3.cueTime = clock.metadataTsToSeconds(id3.pts, timelineStartPts, this.keepOriginalTimestamps);
|
|
|
49263 |
event.metadata.push(id3);
|
|
|
49264 |
} // We add this to every single emitted segment even though we only need
|
|
|
49265 |
// it for the first
|
|
|
49266 |
|
|
|
49267 |
event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state
|
|
|
49268 |
|
|
|
49269 |
this.pendingTracks.length = 0;
|
|
|
49270 |
this.videoTrack = null;
|
|
|
49271 |
this.pendingBoxes.length = 0;
|
|
|
49272 |
this.pendingCaptions.length = 0;
|
|
|
49273 |
this.pendingBytes = 0;
|
|
|
49274 |
this.pendingMetadata.length = 0; // Emit the built segment
|
|
|
49275 |
// We include captions and ID3 tags for backwards compatibility,
|
|
|
49276 |
// ideally we should send only video and audio in the data event
|
|
|
49277 |
|
|
|
49278 |
this.trigger('data', event); // Emit each caption to the outside world
|
|
|
49279 |
// Ideally, this would happen immediately on parsing captions,
|
|
|
49280 |
// but we need to ensure that video data is sent back first
|
|
|
49281 |
// so that caption timing can be adjusted to match video timing
|
|
|
49282 |
|
|
|
49283 |
for (i = 0; i < event.captions.length; i++) {
|
|
|
49284 |
caption = event.captions[i];
|
|
|
49285 |
this.trigger('caption', caption);
|
|
|
49286 |
} // Emit each id3 tag to the outside world
|
|
|
49287 |
// Ideally, this would happen immediately on parsing the tag,
|
|
|
49288 |
// but we need to ensure that video data is sent back first
|
|
|
49289 |
// so that ID3 frame timing can be adjusted to match video timing
|
|
|
49290 |
|
|
|
49291 |
for (i = 0; i < event.metadata.length; i++) {
|
|
|
49292 |
id3 = event.metadata[i];
|
|
|
49293 |
this.trigger('id3Frame', id3);
|
|
|
49294 |
}
|
|
|
49295 |
} // Only emit `done` if all tracks have been flushed and emitted
|
|
|
49296 |
|
|
|
49297 |
if (this.emittedTracks >= this.numberOfTracks) {
|
|
|
49298 |
this.trigger('done');
|
|
|
49299 |
this.emittedTracks = 0;
|
|
|
49300 |
}
|
|
|
49301 |
};
|
|
|
49302 |
CoalesceStream.prototype.setRemux = function (val) {
|
|
|
49303 |
this.remuxTracks = val;
|
|
|
49304 |
};
|
|
|
49305 |
/**
|
|
|
49306 |
* A Stream that expects MP2T binary data as input and produces
|
|
|
49307 |
* corresponding media segments, suitable for use with Media Source
|
|
|
49308 |
* Extension (MSE) implementations that support the ISO BMFF byte
|
|
|
49309 |
* stream format, like Chrome.
|
|
|
49310 |
*/
|
|
|
49311 |
|
|
|
49312 |
Transmuxer = function (options) {
|
|
|
49313 |
var self = this,
|
|
|
49314 |
hasFlushed = true,
|
|
|
49315 |
videoTrack,
|
|
|
49316 |
audioTrack;
|
|
|
49317 |
Transmuxer.prototype.init.call(this);
|
|
|
49318 |
options = options || {};
|
|
|
49319 |
this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0;
|
|
|
49320 |
this.transmuxPipeline_ = {};
|
|
|
49321 |
this.setupAacPipeline = function () {
|
|
|
49322 |
var pipeline = {};
|
|
|
49323 |
this.transmuxPipeline_ = pipeline;
|
|
|
49324 |
pipeline.type = 'aac';
|
|
|
49325 |
pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline
|
|
|
49326 |
|
|
|
49327 |
pipeline.aacStream = new AacStream();
|
|
|
49328 |
pipeline.audioTimestampRolloverStream = new m2ts.TimestampRolloverStream('audio');
|
|
|
49329 |
pipeline.timedMetadataTimestampRolloverStream = new m2ts.TimestampRolloverStream('timed-metadata');
|
|
|
49330 |
pipeline.adtsStream = new AdtsStream();
|
|
|
49331 |
pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream);
|
|
|
49332 |
pipeline.headOfPipeline = pipeline.aacStream;
|
|
|
49333 |
pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream);
|
|
|
49334 |
pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream);
|
|
|
49335 |
pipeline.metadataStream.on('timestamp', function (frame) {
|
|
|
49336 |
pipeline.aacStream.setTimestamp(frame.timeStamp);
|
|
|
49337 |
});
|
|
|
49338 |
pipeline.aacStream.on('data', function (data) {
|
|
|
49339 |
if (data.type !== 'timed-metadata' && data.type !== 'audio' || pipeline.audioSegmentStream) {
|
|
|
49340 |
return;
|
|
|
49341 |
}
|
|
|
49342 |
audioTrack = audioTrack || {
|
|
|
49343 |
timelineStartInfo: {
|
|
|
49344 |
baseMediaDecodeTime: self.baseMediaDecodeTime
|
|
|
49345 |
},
|
|
|
49346 |
codec: 'adts',
|
|
|
49347 |
type: 'audio'
|
|
|
49348 |
}; // hook up the audio segment stream to the first track with aac data
|
|
|
49349 |
|
|
|
49350 |
pipeline.coalesceStream.numberOfTracks++;
|
|
|
49351 |
pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options);
|
|
|
49352 |
pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream'));
|
|
|
49353 |
pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline
|
|
|
49354 |
|
|
|
49355 |
pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); // emit pmt info
|
|
|
49356 |
|
|
|
49357 |
self.trigger('trackinfo', {
|
|
|
49358 |
hasAudio: !!audioTrack,
|
|
|
49359 |
hasVideo: !!videoTrack
|
|
|
49360 |
});
|
|
|
49361 |
}); // Re-emit any data coming from the coalesce stream to the outside world
|
|
|
49362 |
|
|
|
49363 |
pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline
|
|
|
49364 |
|
|
|
49365 |
pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
|
|
|
49366 |
addPipelineLogRetriggers(this, pipeline);
|
|
|
49367 |
};
|
|
|
49368 |
this.setupTsPipeline = function () {
|
|
|
49369 |
var pipeline = {};
|
|
|
49370 |
this.transmuxPipeline_ = pipeline;
|
|
|
49371 |
pipeline.type = 'ts';
|
|
|
49372 |
pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline
|
|
|
49373 |
|
|
|
49374 |
pipeline.packetStream = new m2ts.TransportPacketStream();
|
|
|
49375 |
pipeline.parseStream = new m2ts.TransportParseStream();
|
|
|
49376 |
pipeline.elementaryStream = new m2ts.ElementaryStream();
|
|
|
49377 |
pipeline.timestampRolloverStream = new m2ts.TimestampRolloverStream();
|
|
|
49378 |
pipeline.adtsStream = new AdtsStream();
|
|
|
49379 |
pipeline.h264Stream = new H264Stream();
|
|
|
49380 |
pipeline.captionStream = new m2ts.CaptionStream(options);
|
|
|
49381 |
pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream);
|
|
|
49382 |
pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams
|
|
|
49383 |
|
|
|
49384 |
pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream).pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!!
|
|
|
49385 |
// demux the streams
|
|
|
49386 |
|
|
|
49387 |
pipeline.timestampRolloverStream.pipe(pipeline.h264Stream);
|
|
|
49388 |
pipeline.timestampRolloverStream.pipe(pipeline.adtsStream);
|
|
|
49389 |
pipeline.timestampRolloverStream.pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream
|
|
|
49390 |
|
|
|
49391 |
pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream);
|
|
|
49392 |
pipeline.elementaryStream.on('data', function (data) {
|
|
|
49393 |
var i;
|
|
|
49394 |
if (data.type === 'metadata') {
|
|
|
49395 |
i = data.tracks.length; // scan the tracks listed in the metadata
|
|
|
49396 |
|
|
|
49397 |
while (i--) {
|
|
|
49398 |
if (!videoTrack && data.tracks[i].type === 'video') {
|
|
|
49399 |
videoTrack = data.tracks[i];
|
|
|
49400 |
videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
|
|
|
49401 |
} else if (!audioTrack && data.tracks[i].type === 'audio') {
|
|
|
49402 |
audioTrack = data.tracks[i];
|
|
|
49403 |
audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
|
|
|
49404 |
}
|
|
|
49405 |
} // hook up the video segment stream to the first track with h264 data
|
|
|
49406 |
|
|
|
49407 |
if (videoTrack && !pipeline.videoSegmentStream) {
|
|
|
49408 |
pipeline.coalesceStream.numberOfTracks++;
|
|
|
49409 |
pipeline.videoSegmentStream = new VideoSegmentStream(videoTrack, options);
|
|
|
49410 |
pipeline.videoSegmentStream.on('log', self.getLogTrigger_('videoSegmentStream'));
|
|
|
49411 |
pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) {
|
|
|
49412 |
// When video emits timelineStartInfo data after a flush, we forward that
|
|
|
49413 |
// info to the AudioSegmentStream, if it exists, because video timeline
|
|
|
49414 |
// data takes precedence. Do not do this if keepOriginalTimestamps is set,
|
|
|
49415 |
// because this is a particularly subtle form of timestamp alteration.
|
|
|
49416 |
if (audioTrack && !options.keepOriginalTimestamps) {
|
|
|
49417 |
audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the
|
|
|
49418 |
// very earliest DTS we have seen in video because Chrome will
|
|
|
49419 |
// interpret any video track with a baseMediaDecodeTime that is
|
|
|
49420 |
// non-zero as a gap.
|
|
|
49421 |
|
|
|
49422 |
pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts - self.baseMediaDecodeTime);
|
|
|
49423 |
}
|
|
|
49424 |
});
|
|
|
49425 |
pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo'));
|
|
|
49426 |
pipeline.videoSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'videoSegmentTimingInfo'));
|
|
|
49427 |
pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) {
|
|
|
49428 |
if (audioTrack) {
|
|
|
49429 |
pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime);
|
|
|
49430 |
}
|
|
|
49431 |
});
|
|
|
49432 |
pipeline.videoSegmentStream.on('timingInfo', self.trigger.bind(self, 'videoTimingInfo')); // Set up the final part of the video pipeline
|
|
|
49433 |
|
|
|
49434 |
pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream);
|
|
|
49435 |
}
|
|
|
49436 |
if (audioTrack && !pipeline.audioSegmentStream) {
|
|
|
49437 |
// hook up the audio segment stream to the first track with aac data
|
|
|
49438 |
pipeline.coalesceStream.numberOfTracks++;
|
|
|
49439 |
pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options);
|
|
|
49440 |
pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream'));
|
|
|
49441 |
pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo'));
|
|
|
49442 |
pipeline.audioSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'audioSegmentTimingInfo')); // Set up the final part of the audio pipeline
|
|
|
49443 |
|
|
|
49444 |
pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream);
|
|
|
49445 |
} // emit pmt info
|
|
|
49446 |
|
|
|
49447 |
self.trigger('trackinfo', {
|
|
|
49448 |
hasAudio: !!audioTrack,
|
|
|
49449 |
hasVideo: !!videoTrack
|
|
|
49450 |
});
|
|
|
49451 |
}
|
|
|
49452 |
}); // Re-emit any data coming from the coalesce stream to the outside world
|
|
|
49453 |
|
|
|
49454 |
pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data'));
|
|
|
49455 |
pipeline.coalesceStream.on('id3Frame', function (id3Frame) {
|
|
|
49456 |
id3Frame.dispatchType = pipeline.metadataStream.dispatchType;
|
|
|
49457 |
self.trigger('id3Frame', id3Frame);
|
|
|
49458 |
});
|
|
|
49459 |
pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline
|
|
|
49460 |
|
|
|
49461 |
pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
|
|
|
49462 |
addPipelineLogRetriggers(this, pipeline);
|
|
|
49463 |
}; // hook up the segment streams once track metadata is delivered
|
|
|
49464 |
|
|
|
49465 |
this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) {
|
|
|
49466 |
var pipeline = this.transmuxPipeline_;
|
|
|
49467 |
if (!options.keepOriginalTimestamps) {
|
|
|
49468 |
this.baseMediaDecodeTime = baseMediaDecodeTime;
|
|
|
49469 |
}
|
|
|
49470 |
if (audioTrack) {
|
|
|
49471 |
audioTrack.timelineStartInfo.dts = undefined;
|
|
|
49472 |
audioTrack.timelineStartInfo.pts = undefined;
|
|
|
49473 |
trackDecodeInfo.clearDtsInfo(audioTrack);
|
|
|
49474 |
if (pipeline.audioTimestampRolloverStream) {
|
|
|
49475 |
pipeline.audioTimestampRolloverStream.discontinuity();
|
|
|
49476 |
}
|
|
|
49477 |
}
|
|
|
49478 |
if (videoTrack) {
|
|
|
49479 |
if (pipeline.videoSegmentStream) {
|
|
|
49480 |
pipeline.videoSegmentStream.gopCache_ = [];
|
|
|
49481 |
}
|
|
|
49482 |
videoTrack.timelineStartInfo.dts = undefined;
|
|
|
49483 |
videoTrack.timelineStartInfo.pts = undefined;
|
|
|
49484 |
trackDecodeInfo.clearDtsInfo(videoTrack);
|
|
|
49485 |
pipeline.captionStream.reset();
|
|
|
49486 |
}
|
|
|
49487 |
if (pipeline.timestampRolloverStream) {
|
|
|
49488 |
pipeline.timestampRolloverStream.discontinuity();
|
|
|
49489 |
}
|
|
|
49490 |
};
|
|
|
49491 |
this.setAudioAppendStart = function (timestamp) {
|
|
|
49492 |
if (audioTrack) {
|
|
|
49493 |
this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp);
|
|
|
49494 |
}
|
|
|
49495 |
};
|
|
|
49496 |
this.setRemux = function (val) {
|
|
|
49497 |
var pipeline = this.transmuxPipeline_;
|
|
|
49498 |
options.remux = val;
|
|
|
49499 |
if (pipeline && pipeline.coalesceStream) {
|
|
|
49500 |
pipeline.coalesceStream.setRemux(val);
|
|
|
49501 |
}
|
|
|
49502 |
};
|
|
|
49503 |
this.alignGopsWith = function (gopsToAlignWith) {
|
|
|
49504 |
if (videoTrack && this.transmuxPipeline_.videoSegmentStream) {
|
|
|
49505 |
this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith);
|
|
|
49506 |
}
|
|
|
49507 |
};
|
|
|
49508 |
this.getLogTrigger_ = function (key) {
|
|
|
49509 |
var self = this;
|
|
|
49510 |
return function (event) {
|
|
|
49511 |
event.stream = key;
|
|
|
49512 |
self.trigger('log', event);
|
|
|
49513 |
};
|
|
|
49514 |
}; // feed incoming data to the front of the parsing pipeline
|
|
|
49515 |
|
|
|
49516 |
this.push = function (data) {
|
|
|
49517 |
if (hasFlushed) {
|
|
|
49518 |
var isAac = isLikelyAacData(data);
|
|
|
49519 |
if (isAac && this.transmuxPipeline_.type !== 'aac') {
|
|
|
49520 |
this.setupAacPipeline();
|
|
|
49521 |
} else if (!isAac && this.transmuxPipeline_.type !== 'ts') {
|
|
|
49522 |
this.setupTsPipeline();
|
|
|
49523 |
}
|
|
|
49524 |
hasFlushed = false;
|
|
|
49525 |
}
|
|
|
49526 |
this.transmuxPipeline_.headOfPipeline.push(data);
|
|
|
49527 |
}; // flush any buffered data
|
|
|
49528 |
|
|
|
49529 |
this.flush = function () {
|
|
|
49530 |
hasFlushed = true; // Start at the top of the pipeline and flush all pending work
|
|
|
49531 |
|
|
|
49532 |
this.transmuxPipeline_.headOfPipeline.flush();
|
|
|
49533 |
};
|
|
|
49534 |
this.endTimeline = function () {
|
|
|
49535 |
this.transmuxPipeline_.headOfPipeline.endTimeline();
|
|
|
49536 |
};
|
|
|
49537 |
this.reset = function () {
|
|
|
49538 |
if (this.transmuxPipeline_.headOfPipeline) {
|
|
|
49539 |
this.transmuxPipeline_.headOfPipeline.reset();
|
|
|
49540 |
}
|
|
|
49541 |
}; // Caption data has to be reset when seeking outside buffered range
|
|
|
49542 |
|
|
|
49543 |
this.resetCaptions = function () {
|
|
|
49544 |
if (this.transmuxPipeline_.captionStream) {
|
|
|
49545 |
this.transmuxPipeline_.captionStream.reset();
|
|
|
49546 |
}
|
|
|
49547 |
};
|
|
|
49548 |
};
|
|
|
49549 |
Transmuxer.prototype = new Stream();
|
|
|
49550 |
var transmuxer = {
|
|
|
49551 |
Transmuxer: Transmuxer,
|
|
|
49552 |
VideoSegmentStream: VideoSegmentStream,
|
|
|
49553 |
AudioSegmentStream: AudioSegmentStream,
|
|
|
49554 |
AUDIO_PROPERTIES: AUDIO_PROPERTIES,
|
|
|
49555 |
VIDEO_PROPERTIES: VIDEO_PROPERTIES,
|
|
|
49556 |
// exported for testing
|
|
|
49557 |
generateSegmentTimingInfo: generateSegmentTimingInfo
|
|
|
49558 |
};
|
|
|
49559 |
/**
|
|
|
49560 |
* mux.js
|
|
|
49561 |
*
|
|
|
49562 |
* Copyright (c) Brightcove
|
|
|
49563 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
49564 |
*/
|
|
|
49565 |
|
|
|
49566 |
var toUnsigned$3 = function (value) {
|
|
|
49567 |
return value >>> 0;
|
|
|
49568 |
};
|
|
|
49569 |
var toHexString$1 = function (value) {
|
|
|
49570 |
return ('00' + value.toString(16)).slice(-2);
|
|
|
49571 |
};
|
|
|
49572 |
var bin = {
|
|
|
49573 |
toUnsigned: toUnsigned$3,
|
|
|
49574 |
toHexString: toHexString$1
|
|
|
49575 |
};
|
|
|
49576 |
var parseType$3 = function (buffer) {
|
|
|
49577 |
var result = '';
|
|
|
49578 |
result += String.fromCharCode(buffer[0]);
|
|
|
49579 |
result += String.fromCharCode(buffer[1]);
|
|
|
49580 |
result += String.fromCharCode(buffer[2]);
|
|
|
49581 |
result += String.fromCharCode(buffer[3]);
|
|
|
49582 |
return result;
|
|
|
49583 |
};
|
|
|
49584 |
var parseType_1 = parseType$3;
|
|
|
49585 |
var toUnsigned$2 = bin.toUnsigned;
|
|
|
49586 |
var parseType$2 = parseType_1;
|
|
|
49587 |
var findBox$2 = function (data, path) {
|
|
|
49588 |
var results = [],
|
|
|
49589 |
i,
|
|
|
49590 |
size,
|
|
|
49591 |
type,
|
|
|
49592 |
end,
|
|
|
49593 |
subresults;
|
|
|
49594 |
if (!path.length) {
|
|
|
49595 |
// short-circuit the search for empty paths
|
|
|
49596 |
return null;
|
|
|
49597 |
}
|
|
|
49598 |
for (i = 0; i < data.byteLength;) {
|
|
|
49599 |
size = toUnsigned$2(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]);
|
|
|
49600 |
type = parseType$2(data.subarray(i + 4, i + 8));
|
|
|
49601 |
end = size > 1 ? i + size : data.byteLength;
|
|
|
49602 |
if (type === path[0]) {
|
|
|
49603 |
if (path.length === 1) {
|
|
|
49604 |
// this is the end of the path and we've found the box we were
|
|
|
49605 |
// looking for
|
|
|
49606 |
results.push(data.subarray(i + 8, end));
|
|
|
49607 |
} else {
|
|
|
49608 |
// recursively search for the next box along the path
|
|
|
49609 |
subresults = findBox$2(data.subarray(i + 8, end), path.slice(1));
|
|
|
49610 |
if (subresults.length) {
|
|
|
49611 |
results = results.concat(subresults);
|
|
|
49612 |
}
|
|
|
49613 |
}
|
|
|
49614 |
}
|
|
|
49615 |
i = end;
|
|
|
49616 |
} // we've finished searching all of data
|
|
|
49617 |
|
|
|
49618 |
return results;
|
|
|
49619 |
};
|
|
|
49620 |
var findBox_1 = findBox$2;
|
|
|
49621 |
var toUnsigned$1 = bin.toUnsigned;
|
|
|
49622 |
var getUint64$2 = numbers.getUint64;
|
|
|
49623 |
var tfdt = function (data) {
|
|
|
49624 |
var result = {
|
|
|
49625 |
version: data[0],
|
|
|
49626 |
flags: new Uint8Array(data.subarray(1, 4))
|
|
|
49627 |
};
|
|
|
49628 |
if (result.version === 1) {
|
|
|
49629 |
result.baseMediaDecodeTime = getUint64$2(data.subarray(4));
|
|
|
49630 |
} else {
|
|
|
49631 |
result.baseMediaDecodeTime = toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]);
|
|
|
49632 |
}
|
|
|
49633 |
return result;
|
|
|
49634 |
};
|
|
|
49635 |
var parseTfdt$2 = tfdt;
|
|
|
49636 |
var parseSampleFlags$1 = function (flags) {
|
|
|
49637 |
return {
|
|
|
49638 |
isLeading: (flags[0] & 0x0c) >>> 2,
|
|
|
49639 |
dependsOn: flags[0] & 0x03,
|
|
|
49640 |
isDependedOn: (flags[1] & 0xc0) >>> 6,
|
|
|
49641 |
hasRedundancy: (flags[1] & 0x30) >>> 4,
|
|
|
49642 |
paddingValue: (flags[1] & 0x0e) >>> 1,
|
|
|
49643 |
isNonSyncSample: flags[1] & 0x01,
|
|
|
49644 |
degradationPriority: flags[2] << 8 | flags[3]
|
|
|
49645 |
};
|
|
|
49646 |
};
|
|
|
49647 |
var parseSampleFlags_1 = parseSampleFlags$1;
|
|
|
49648 |
var parseSampleFlags = parseSampleFlags_1;
|
|
|
49649 |
var trun = function (data) {
|
|
|
49650 |
var result = {
|
|
|
49651 |
version: data[0],
|
|
|
49652 |
flags: new Uint8Array(data.subarray(1, 4)),
|
|
|
49653 |
samples: []
|
|
|
49654 |
},
|
|
|
49655 |
view = new DataView(data.buffer, data.byteOffset, data.byteLength),
|
|
|
49656 |
// Flag interpretation
|
|
|
49657 |
dataOffsetPresent = result.flags[2] & 0x01,
|
|
|
49658 |
// compare with 2nd byte of 0x1
|
|
|
49659 |
firstSampleFlagsPresent = result.flags[2] & 0x04,
|
|
|
49660 |
// compare with 2nd byte of 0x4
|
|
|
49661 |
sampleDurationPresent = result.flags[1] & 0x01,
|
|
|
49662 |
// compare with 2nd byte of 0x100
|
|
|
49663 |
sampleSizePresent = result.flags[1] & 0x02,
|
|
|
49664 |
// compare with 2nd byte of 0x200
|
|
|
49665 |
sampleFlagsPresent = result.flags[1] & 0x04,
|
|
|
49666 |
// compare with 2nd byte of 0x400
|
|
|
49667 |
sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08,
|
|
|
49668 |
// compare with 2nd byte of 0x800
|
|
|
49669 |
sampleCount = view.getUint32(4),
|
|
|
49670 |
offset = 8,
|
|
|
49671 |
sample;
|
|
|
49672 |
if (dataOffsetPresent) {
|
|
|
49673 |
// 32 bit signed integer
|
|
|
49674 |
result.dataOffset = view.getInt32(offset);
|
|
|
49675 |
offset += 4;
|
|
|
49676 |
} // Overrides the flags for the first sample only. The order of
|
|
|
49677 |
// optional values will be: duration, size, compositionTimeOffset
|
|
|
49678 |
|
|
|
49679 |
if (firstSampleFlagsPresent && sampleCount) {
|
|
|
49680 |
sample = {
|
|
|
49681 |
flags: parseSampleFlags(data.subarray(offset, offset + 4))
|
|
|
49682 |
};
|
|
|
49683 |
offset += 4;
|
|
|
49684 |
if (sampleDurationPresent) {
|
|
|
49685 |
sample.duration = view.getUint32(offset);
|
|
|
49686 |
offset += 4;
|
|
|
49687 |
}
|
|
|
49688 |
if (sampleSizePresent) {
|
|
|
49689 |
sample.size = view.getUint32(offset);
|
|
|
49690 |
offset += 4;
|
|
|
49691 |
}
|
|
|
49692 |
if (sampleCompositionTimeOffsetPresent) {
|
|
|
49693 |
if (result.version === 1) {
|
|
|
49694 |
sample.compositionTimeOffset = view.getInt32(offset);
|
|
|
49695 |
} else {
|
|
|
49696 |
sample.compositionTimeOffset = view.getUint32(offset);
|
|
|
49697 |
}
|
|
|
49698 |
offset += 4;
|
|
|
49699 |
}
|
|
|
49700 |
result.samples.push(sample);
|
|
|
49701 |
sampleCount--;
|
|
|
49702 |
}
|
|
|
49703 |
while (sampleCount--) {
|
|
|
49704 |
sample = {};
|
|
|
49705 |
if (sampleDurationPresent) {
|
|
|
49706 |
sample.duration = view.getUint32(offset);
|
|
|
49707 |
offset += 4;
|
|
|
49708 |
}
|
|
|
49709 |
if (sampleSizePresent) {
|
|
|
49710 |
sample.size = view.getUint32(offset);
|
|
|
49711 |
offset += 4;
|
|
|
49712 |
}
|
|
|
49713 |
if (sampleFlagsPresent) {
|
|
|
49714 |
sample.flags = parseSampleFlags(data.subarray(offset, offset + 4));
|
|
|
49715 |
offset += 4;
|
|
|
49716 |
}
|
|
|
49717 |
if (sampleCompositionTimeOffsetPresent) {
|
|
|
49718 |
if (result.version === 1) {
|
|
|
49719 |
sample.compositionTimeOffset = view.getInt32(offset);
|
|
|
49720 |
} else {
|
|
|
49721 |
sample.compositionTimeOffset = view.getUint32(offset);
|
|
|
49722 |
}
|
|
|
49723 |
offset += 4;
|
|
|
49724 |
}
|
|
|
49725 |
result.samples.push(sample);
|
|
|
49726 |
}
|
|
|
49727 |
return result;
|
|
|
49728 |
};
|
|
|
49729 |
var parseTrun$2 = trun;
|
|
|
49730 |
var tfhd = function (data) {
|
|
|
49731 |
var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
|
|
|
49732 |
result = {
|
|
|
49733 |
version: data[0],
|
|
|
49734 |
flags: new Uint8Array(data.subarray(1, 4)),
|
|
|
49735 |
trackId: view.getUint32(4)
|
|
|
49736 |
},
|
|
|
49737 |
baseDataOffsetPresent = result.flags[2] & 0x01,
|
|
|
49738 |
sampleDescriptionIndexPresent = result.flags[2] & 0x02,
|
|
|
49739 |
defaultSampleDurationPresent = result.flags[2] & 0x08,
|
|
|
49740 |
defaultSampleSizePresent = result.flags[2] & 0x10,
|
|
|
49741 |
defaultSampleFlagsPresent = result.flags[2] & 0x20,
|
|
|
49742 |
durationIsEmpty = result.flags[0] & 0x010000,
|
|
|
49743 |
defaultBaseIsMoof = result.flags[0] & 0x020000,
|
|
|
49744 |
i;
|
|
|
49745 |
i = 8;
|
|
|
49746 |
if (baseDataOffsetPresent) {
|
|
|
49747 |
i += 4; // truncate top 4 bytes
|
|
|
49748 |
// FIXME: should we read the full 64 bits?
|
|
|
49749 |
|
|
|
49750 |
result.baseDataOffset = view.getUint32(12);
|
|
|
49751 |
i += 4;
|
|
|
49752 |
}
|
|
|
49753 |
if (sampleDescriptionIndexPresent) {
|
|
|
49754 |
result.sampleDescriptionIndex = view.getUint32(i);
|
|
|
49755 |
i += 4;
|
|
|
49756 |
}
|
|
|
49757 |
if (defaultSampleDurationPresent) {
|
|
|
49758 |
result.defaultSampleDuration = view.getUint32(i);
|
|
|
49759 |
i += 4;
|
|
|
49760 |
}
|
|
|
49761 |
if (defaultSampleSizePresent) {
|
|
|
49762 |
result.defaultSampleSize = view.getUint32(i);
|
|
|
49763 |
i += 4;
|
|
|
49764 |
}
|
|
|
49765 |
if (defaultSampleFlagsPresent) {
|
|
|
49766 |
result.defaultSampleFlags = view.getUint32(i);
|
|
|
49767 |
}
|
|
|
49768 |
if (durationIsEmpty) {
|
|
|
49769 |
result.durationIsEmpty = true;
|
|
|
49770 |
}
|
|
|
49771 |
if (!baseDataOffsetPresent && defaultBaseIsMoof) {
|
|
|
49772 |
result.baseDataOffsetIsMoof = true;
|
|
|
49773 |
}
|
|
|
49774 |
return result;
|
|
|
49775 |
};
|
|
|
49776 |
var parseTfhd$2 = tfhd;
|
|
|
49777 |
var win;
|
|
|
49778 |
if (typeof window !== "undefined") {
|
|
|
49779 |
win = window;
|
|
|
49780 |
} else if (typeof commonjsGlobal !== "undefined") {
|
|
|
49781 |
win = commonjsGlobal;
|
|
|
49782 |
} else if (typeof self !== "undefined") {
|
|
|
49783 |
win = self;
|
|
|
49784 |
} else {
|
|
|
49785 |
win = {};
|
|
|
49786 |
}
|
|
|
49787 |
var window_1 = win;
|
|
|
49788 |
/**
|
|
|
49789 |
* mux.js
|
|
|
49790 |
*
|
|
|
49791 |
* Copyright (c) Brightcove
|
|
|
49792 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
49793 |
*
|
|
|
49794 |
* Reads in-band CEA-708 captions out of FMP4 segments.
|
|
|
49795 |
* @see https://en.wikipedia.org/wiki/CEA-708
|
|
|
49796 |
*/
|
|
|
49797 |
|
|
|
49798 |
var discardEmulationPreventionBytes = captionPacketParser.discardEmulationPreventionBytes;
|
|
|
49799 |
var CaptionStream = captionStream.CaptionStream;
|
|
|
49800 |
var findBox$1 = findBox_1;
|
|
|
49801 |
var parseTfdt$1 = parseTfdt$2;
|
|
|
49802 |
var parseTrun$1 = parseTrun$2;
|
|
|
49803 |
var parseTfhd$1 = parseTfhd$2;
|
|
|
49804 |
var window$2 = window_1;
|
|
|
49805 |
/**
|
|
|
49806 |
* Maps an offset in the mdat to a sample based on the the size of the samples.
|
|
|
49807 |
* Assumes that `parseSamples` has been called first.
|
|
|
49808 |
*
|
|
|
49809 |
* @param {Number} offset - The offset into the mdat
|
|
|
49810 |
* @param {Object[]} samples - An array of samples, parsed using `parseSamples`
|
|
|
49811 |
* @return {?Object} The matching sample, or null if no match was found.
|
|
|
49812 |
*
|
|
|
49813 |
* @see ISO-BMFF-12/2015, Section 8.8.8
|
|
|
49814 |
**/
|
|
|
49815 |
|
|
|
49816 |
var mapToSample = function (offset, samples) {
|
|
|
49817 |
var approximateOffset = offset;
|
|
|
49818 |
for (var i = 0; i < samples.length; i++) {
|
|
|
49819 |
var sample = samples[i];
|
|
|
49820 |
if (approximateOffset < sample.size) {
|
|
|
49821 |
return sample;
|
|
|
49822 |
}
|
|
|
49823 |
approximateOffset -= sample.size;
|
|
|
49824 |
}
|
|
|
49825 |
return null;
|
|
|
49826 |
};
|
|
|
49827 |
/**
|
|
|
49828 |
* Finds SEI nal units contained in a Media Data Box.
|
|
|
49829 |
* Assumes that `parseSamples` has been called first.
|
|
|
49830 |
*
|
|
|
49831 |
* @param {Uint8Array} avcStream - The bytes of the mdat
|
|
|
49832 |
* @param {Object[]} samples - The samples parsed out by `parseSamples`
|
|
|
49833 |
* @param {Number} trackId - The trackId of this video track
|
|
|
49834 |
* @return {Object[]} seiNals - the parsed SEI NALUs found.
|
|
|
49835 |
* The contents of the seiNal should match what is expected by
|
|
|
49836 |
* CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts)
|
|
|
49837 |
*
|
|
|
49838 |
* @see ISO-BMFF-12/2015, Section 8.1.1
|
|
|
49839 |
* @see Rec. ITU-T H.264, 7.3.2.3.1
|
|
|
49840 |
**/
|
|
|
49841 |
|
|
|
49842 |
var findSeiNals = function (avcStream, samples, trackId) {
|
|
|
49843 |
var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
|
|
|
49844 |
result = {
|
|
|
49845 |
logs: [],
|
|
|
49846 |
seiNals: []
|
|
|
49847 |
},
|
|
|
49848 |
seiNal,
|
|
|
49849 |
i,
|
|
|
49850 |
length,
|
|
|
49851 |
lastMatchedSample;
|
|
|
49852 |
for (i = 0; i + 4 < avcStream.length; i += length) {
|
|
|
49853 |
length = avcView.getUint32(i);
|
|
|
49854 |
i += 4; // Bail if this doesn't appear to be an H264 stream
|
|
|
49855 |
|
|
|
49856 |
if (length <= 0) {
|
|
|
49857 |
continue;
|
|
|
49858 |
}
|
|
|
49859 |
switch (avcStream[i] & 0x1F) {
|
|
|
49860 |
case 0x06:
|
|
|
49861 |
var data = avcStream.subarray(i + 1, i + 1 + length);
|
|
|
49862 |
var matchingSample = mapToSample(i, samples);
|
|
|
49863 |
seiNal = {
|
|
|
49864 |
nalUnitType: 'sei_rbsp',
|
|
|
49865 |
size: length,
|
|
|
49866 |
data: data,
|
|
|
49867 |
escapedRBSP: discardEmulationPreventionBytes(data),
|
|
|
49868 |
trackId: trackId
|
|
|
49869 |
};
|
|
|
49870 |
if (matchingSample) {
|
|
|
49871 |
seiNal.pts = matchingSample.pts;
|
|
|
49872 |
seiNal.dts = matchingSample.dts;
|
|
|
49873 |
lastMatchedSample = matchingSample;
|
|
|
49874 |
} else if (lastMatchedSample) {
|
|
|
49875 |
// If a matching sample cannot be found, use the last
|
|
|
49876 |
// sample's values as they should be as close as possible
|
|
|
49877 |
seiNal.pts = lastMatchedSample.pts;
|
|
|
49878 |
seiNal.dts = lastMatchedSample.dts;
|
|
|
49879 |
} else {
|
|
|
49880 |
result.logs.push({
|
|
|
49881 |
level: 'warn',
|
|
|
49882 |
message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.'
|
|
|
49883 |
});
|
|
|
49884 |
break;
|
|
|
49885 |
}
|
|
|
49886 |
result.seiNals.push(seiNal);
|
|
|
49887 |
break;
|
|
|
49888 |
}
|
|
|
49889 |
}
|
|
|
49890 |
return result;
|
|
|
49891 |
};
|
|
|
49892 |
/**
|
|
|
49893 |
* Parses sample information out of Track Run Boxes and calculates
|
|
|
49894 |
* the absolute presentation and decode timestamps of each sample.
|
|
|
49895 |
*
|
|
|
49896 |
* @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed
|
|
|
49897 |
* @param {Number|BigInt} baseMediaDecodeTime - base media decode time from tfdt
|
|
|
49898 |
@see ISO-BMFF-12/2015, Section 8.8.12
|
|
|
49899 |
* @param {Object} tfhd - The parsed Track Fragment Header
|
|
|
49900 |
* @see inspect.parseTfhd
|
|
|
49901 |
* @return {Object[]} the parsed samples
|
|
|
49902 |
*
|
|
|
49903 |
* @see ISO-BMFF-12/2015, Section 8.8.8
|
|
|
49904 |
**/
|
|
|
49905 |
|
|
|
49906 |
var parseSamples = function (truns, baseMediaDecodeTime, tfhd) {
|
|
|
49907 |
var currentDts = baseMediaDecodeTime;
|
|
|
49908 |
var defaultSampleDuration = tfhd.defaultSampleDuration || 0;
|
|
|
49909 |
var defaultSampleSize = tfhd.defaultSampleSize || 0;
|
|
|
49910 |
var trackId = tfhd.trackId;
|
|
|
49911 |
var allSamples = [];
|
|
|
49912 |
truns.forEach(function (trun) {
|
|
|
49913 |
// Note: We currently do not parse the sample table as well
|
|
|
49914 |
// as the trun. It's possible some sources will require this.
|
|
|
49915 |
// moov > trak > mdia > minf > stbl
|
|
|
49916 |
var trackRun = parseTrun$1(trun);
|
|
|
49917 |
var samples = trackRun.samples;
|
|
|
49918 |
samples.forEach(function (sample) {
|
|
|
49919 |
if (sample.duration === undefined) {
|
|
|
49920 |
sample.duration = defaultSampleDuration;
|
|
|
49921 |
}
|
|
|
49922 |
if (sample.size === undefined) {
|
|
|
49923 |
sample.size = defaultSampleSize;
|
|
|
49924 |
}
|
|
|
49925 |
sample.trackId = trackId;
|
|
|
49926 |
sample.dts = currentDts;
|
|
|
49927 |
if (sample.compositionTimeOffset === undefined) {
|
|
|
49928 |
sample.compositionTimeOffset = 0;
|
|
|
49929 |
}
|
|
|
49930 |
if (typeof currentDts === 'bigint') {
|
|
|
49931 |
sample.pts = currentDts + window$2.BigInt(sample.compositionTimeOffset);
|
|
|
49932 |
currentDts += window$2.BigInt(sample.duration);
|
|
|
49933 |
} else {
|
|
|
49934 |
sample.pts = currentDts + sample.compositionTimeOffset;
|
|
|
49935 |
currentDts += sample.duration;
|
|
|
49936 |
}
|
|
|
49937 |
});
|
|
|
49938 |
allSamples = allSamples.concat(samples);
|
|
|
49939 |
});
|
|
|
49940 |
return allSamples;
|
|
|
49941 |
};
|
|
|
49942 |
/**
|
|
|
49943 |
* Parses out caption nals from an FMP4 segment's video tracks.
|
|
|
49944 |
*
|
|
|
49945 |
* @param {Uint8Array} segment - The bytes of a single segment
|
|
|
49946 |
* @param {Number} videoTrackId - The trackId of a video track in the segment
|
|
|
49947 |
* @return {Object.<Number, Object[]>} A mapping of video trackId to
|
|
|
49948 |
* a list of seiNals found in that track
|
|
|
49949 |
**/
|
|
|
49950 |
|
|
|
49951 |
var parseCaptionNals = function (segment, videoTrackId) {
|
|
|
49952 |
// To get the samples
|
|
|
49953 |
var trafs = findBox$1(segment, ['moof', 'traf']); // To get SEI NAL units
|
|
|
49954 |
|
|
|
49955 |
var mdats = findBox$1(segment, ['mdat']);
|
|
|
49956 |
var captionNals = {};
|
|
|
49957 |
var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs
|
|
|
49958 |
|
|
|
49959 |
mdats.forEach(function (mdat, index) {
|
|
|
49960 |
var matchingTraf = trafs[index];
|
|
|
49961 |
mdatTrafPairs.push({
|
|
|
49962 |
mdat: mdat,
|
|
|
49963 |
traf: matchingTraf
|
|
|
49964 |
});
|
|
|
49965 |
});
|
|
|
49966 |
mdatTrafPairs.forEach(function (pair) {
|
|
|
49967 |
var mdat = pair.mdat;
|
|
|
49968 |
var traf = pair.traf;
|
|
|
49969 |
var tfhd = findBox$1(traf, ['tfhd']); // Exactly 1 tfhd per traf
|
|
|
49970 |
|
|
|
49971 |
var headerInfo = parseTfhd$1(tfhd[0]);
|
|
|
49972 |
var trackId = headerInfo.trackId;
|
|
|
49973 |
var tfdt = findBox$1(traf, ['tfdt']); // Either 0 or 1 tfdt per traf
|
|
|
49974 |
|
|
|
49975 |
var baseMediaDecodeTime = tfdt.length > 0 ? parseTfdt$1(tfdt[0]).baseMediaDecodeTime : 0;
|
|
|
49976 |
var truns = findBox$1(traf, ['trun']);
|
|
|
49977 |
var samples;
|
|
|
49978 |
var result; // Only parse video data for the chosen video track
|
|
|
49979 |
|
|
|
49980 |
if (videoTrackId === trackId && truns.length > 0) {
|
|
|
49981 |
samples = parseSamples(truns, baseMediaDecodeTime, headerInfo);
|
|
|
49982 |
result = findSeiNals(mdat, samples, trackId);
|
|
|
49983 |
if (!captionNals[trackId]) {
|
|
|
49984 |
captionNals[trackId] = {
|
|
|
49985 |
seiNals: [],
|
|
|
49986 |
logs: []
|
|
|
49987 |
};
|
|
|
49988 |
}
|
|
|
49989 |
captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals);
|
|
|
49990 |
captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs);
|
|
|
49991 |
}
|
|
|
49992 |
});
|
|
|
49993 |
return captionNals;
|
|
|
49994 |
};
|
|
|
49995 |
/**
|
|
|
49996 |
* Parses out inband captions from an MP4 container and returns
|
|
|
49997 |
* caption objects that can be used by WebVTT and the TextTrack API.
|
|
|
49998 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue
|
|
|
49999 |
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack
|
|
|
50000 |
* Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first
|
|
|
50001 |
*
|
|
|
50002 |
* @param {Uint8Array} segment - The fmp4 segment containing embedded captions
|
|
|
50003 |
* @param {Number} trackId - The id of the video track to parse
|
|
|
50004 |
* @param {Number} timescale - The timescale for the video track from the init segment
|
|
|
50005 |
*
|
|
|
50006 |
* @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks
|
|
|
50007 |
* @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds
|
|
|
50008 |
* @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds
|
|
|
50009 |
* @return {Object[]} parsedCaptions[].content - A list of individual caption segments
|
|
|
50010 |
* @return {String} parsedCaptions[].content.text - The visible content of the caption segment
|
|
|
50011 |
* @return {Number} parsedCaptions[].content.line - The line height from 1-15 for positioning of the caption segment
|
|
|
50012 |
* @return {Number} parsedCaptions[].content.position - The column indent percentage for cue positioning from 10-80
|
|
|
50013 |
**/
|
|
|
50014 |
|
|
|
50015 |
var parseEmbeddedCaptions = function (segment, trackId, timescale) {
|
|
|
50016 |
var captionNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
|
|
|
50017 |
|
|
|
50018 |
if (trackId === null) {
|
|
|
50019 |
return null;
|
|
|
50020 |
}
|
|
|
50021 |
captionNals = parseCaptionNals(segment, trackId);
|
|
|
50022 |
var trackNals = captionNals[trackId] || {};
|
|
|
50023 |
return {
|
|
|
50024 |
seiNals: trackNals.seiNals,
|
|
|
50025 |
logs: trackNals.logs,
|
|
|
50026 |
timescale: timescale
|
|
|
50027 |
};
|
|
|
50028 |
};
|
|
|
50029 |
/**
|
|
|
50030 |
* Converts SEI NALUs into captions that can be used by video.js
|
|
|
50031 |
**/
|
|
|
50032 |
|
|
|
50033 |
var CaptionParser = function () {
|
|
|
50034 |
var isInitialized = false;
|
|
|
50035 |
var captionStream; // Stores segments seen before trackId and timescale are set
|
|
|
50036 |
|
|
|
50037 |
var segmentCache; // Stores video track ID of the track being parsed
|
|
|
50038 |
|
|
|
50039 |
var trackId; // Stores the timescale of the track being parsed
|
|
|
50040 |
|
|
|
50041 |
var timescale; // Stores captions parsed so far
|
|
|
50042 |
|
|
|
50043 |
var parsedCaptions; // Stores whether we are receiving partial data or not
|
|
|
50044 |
|
|
|
50045 |
var parsingPartial;
|
|
|
50046 |
/**
|
|
|
50047 |
* A method to indicate whether a CaptionParser has been initalized
|
|
|
50048 |
* @returns {Boolean}
|
|
|
50049 |
**/
|
|
|
50050 |
|
|
|
50051 |
this.isInitialized = function () {
|
|
|
50052 |
return isInitialized;
|
|
|
50053 |
};
|
|
|
50054 |
/**
|
|
|
50055 |
* Initializes the underlying CaptionStream, SEI NAL parsing
|
|
|
50056 |
* and management, and caption collection
|
|
|
50057 |
**/
|
|
|
50058 |
|
|
|
50059 |
this.init = function (options) {
|
|
|
50060 |
captionStream = new CaptionStream();
|
|
|
50061 |
isInitialized = true;
|
|
|
50062 |
parsingPartial = options ? options.isPartial : false; // Collect dispatched captions
|
|
|
50063 |
|
|
|
50064 |
captionStream.on('data', function (event) {
|
|
|
50065 |
// Convert to seconds in the source's timescale
|
|
|
50066 |
event.startTime = event.startPts / timescale;
|
|
|
50067 |
event.endTime = event.endPts / timescale;
|
|
|
50068 |
parsedCaptions.captions.push(event);
|
|
|
50069 |
parsedCaptions.captionStreams[event.stream] = true;
|
|
|
50070 |
});
|
|
|
50071 |
captionStream.on('log', function (log) {
|
|
|
50072 |
parsedCaptions.logs.push(log);
|
|
|
50073 |
});
|
|
|
50074 |
};
|
|
|
50075 |
/**
|
|
|
50076 |
* Determines if a new video track will be selected
|
|
|
50077 |
* or if the timescale changed
|
|
|
50078 |
* @return {Boolean}
|
|
|
50079 |
**/
|
|
|
50080 |
|
|
|
50081 |
this.isNewInit = function (videoTrackIds, timescales) {
|
|
|
50082 |
if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) {
|
|
|
50083 |
return false;
|
|
|
50084 |
}
|
|
|
50085 |
return trackId !== videoTrackIds[0] || timescale !== timescales[trackId];
|
|
|
50086 |
};
|
|
|
50087 |
/**
|
|
|
50088 |
* Parses out SEI captions and interacts with underlying
|
|
|
50089 |
* CaptionStream to return dispatched captions
|
|
|
50090 |
*
|
|
|
50091 |
* @param {Uint8Array} segment - The fmp4 segment containing embedded captions
|
|
|
50092 |
* @param {Number[]} videoTrackIds - A list of video tracks found in the init segment
|
|
|
50093 |
* @param {Object.<Number, Number>} timescales - The timescales found in the init segment
|
|
|
50094 |
* @see parseEmbeddedCaptions
|
|
|
50095 |
* @see m2ts/caption-stream.js
|
|
|
50096 |
**/
|
|
|
50097 |
|
|
|
50098 |
this.parse = function (segment, videoTrackIds, timescales) {
|
|
|
50099 |
var parsedData;
|
|
|
50100 |
if (!this.isInitialized()) {
|
|
|
50101 |
return null; // This is not likely to be a video segment
|
|
|
50102 |
} else if (!videoTrackIds || !timescales) {
|
|
|
50103 |
return null;
|
|
|
50104 |
} else if (this.isNewInit(videoTrackIds, timescales)) {
|
|
|
50105 |
// Use the first video track only as there is no
|
|
|
50106 |
// mechanism to switch to other video tracks
|
|
|
50107 |
trackId = videoTrackIds[0];
|
|
|
50108 |
timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment
|
|
|
50109 |
// data until we have one.
|
|
|
50110 |
// the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
|
|
|
50111 |
} else if (trackId === null || !timescale) {
|
|
|
50112 |
segmentCache.push(segment);
|
|
|
50113 |
return null;
|
|
|
50114 |
} // Now that a timescale and trackId is set, parse cached segments
|
|
|
50115 |
|
|
|
50116 |
while (segmentCache.length > 0) {
|
|
|
50117 |
var cachedSegment = segmentCache.shift();
|
|
|
50118 |
this.parse(cachedSegment, videoTrackIds, timescales);
|
|
|
50119 |
}
|
|
|
50120 |
parsedData = parseEmbeddedCaptions(segment, trackId, timescale);
|
|
|
50121 |
if (parsedData && parsedData.logs) {
|
|
|
50122 |
parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs);
|
|
|
50123 |
}
|
|
|
50124 |
if (parsedData === null || !parsedData.seiNals) {
|
|
|
50125 |
if (parsedCaptions.logs.length) {
|
|
|
50126 |
return {
|
|
|
50127 |
logs: parsedCaptions.logs,
|
|
|
50128 |
captions: [],
|
|
|
50129 |
captionStreams: []
|
|
|
50130 |
};
|
|
|
50131 |
}
|
|
|
50132 |
return null;
|
|
|
50133 |
}
|
|
|
50134 |
this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched
|
|
|
50135 |
|
|
|
50136 |
this.flushStream();
|
|
|
50137 |
return parsedCaptions;
|
|
|
50138 |
};
|
|
|
50139 |
/**
|
|
|
50140 |
* Pushes SEI NALUs onto CaptionStream
|
|
|
50141 |
* @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals`
|
|
|
50142 |
* Assumes that `parseCaptionNals` has been called first
|
|
|
50143 |
* @see m2ts/caption-stream.js
|
|
|
50144 |
**/
|
|
|
50145 |
|
|
|
50146 |
this.pushNals = function (nals) {
|
|
|
50147 |
if (!this.isInitialized() || !nals || nals.length === 0) {
|
|
|
50148 |
return null;
|
|
|
50149 |
}
|
|
|
50150 |
nals.forEach(function (nal) {
|
|
|
50151 |
captionStream.push(nal);
|
|
|
50152 |
});
|
|
|
50153 |
};
|
|
|
50154 |
/**
|
|
|
50155 |
* Flushes underlying CaptionStream to dispatch processed, displayable captions
|
|
|
50156 |
* @see m2ts/caption-stream.js
|
|
|
50157 |
**/
|
|
|
50158 |
|
|
|
50159 |
this.flushStream = function () {
|
|
|
50160 |
if (!this.isInitialized()) {
|
|
|
50161 |
return null;
|
|
|
50162 |
}
|
|
|
50163 |
if (!parsingPartial) {
|
|
|
50164 |
captionStream.flush();
|
|
|
50165 |
} else {
|
|
|
50166 |
captionStream.partialFlush();
|
|
|
50167 |
}
|
|
|
50168 |
};
|
|
|
50169 |
/**
|
|
|
50170 |
* Reset caption buckets for new data
|
|
|
50171 |
**/
|
|
|
50172 |
|
|
|
50173 |
this.clearParsedCaptions = function () {
|
|
|
50174 |
parsedCaptions.captions = [];
|
|
|
50175 |
parsedCaptions.captionStreams = {};
|
|
|
50176 |
parsedCaptions.logs = [];
|
|
|
50177 |
};
|
|
|
50178 |
/**
|
|
|
50179 |
* Resets underlying CaptionStream
|
|
|
50180 |
* @see m2ts/caption-stream.js
|
|
|
50181 |
**/
|
|
|
50182 |
|
|
|
50183 |
this.resetCaptionStream = function () {
|
|
|
50184 |
if (!this.isInitialized()) {
|
|
|
50185 |
return null;
|
|
|
50186 |
}
|
|
|
50187 |
captionStream.reset();
|
|
|
50188 |
};
|
|
|
50189 |
/**
|
|
|
50190 |
* Convenience method to clear all captions flushed from the
|
|
|
50191 |
* CaptionStream and still being parsed
|
|
|
50192 |
* @see m2ts/caption-stream.js
|
|
|
50193 |
**/
|
|
|
50194 |
|
|
|
50195 |
this.clearAllCaptions = function () {
|
|
|
50196 |
this.clearParsedCaptions();
|
|
|
50197 |
this.resetCaptionStream();
|
|
|
50198 |
};
|
|
|
50199 |
/**
|
|
|
50200 |
* Reset caption parser
|
|
|
50201 |
**/
|
|
|
50202 |
|
|
|
50203 |
this.reset = function () {
|
|
|
50204 |
segmentCache = [];
|
|
|
50205 |
trackId = null;
|
|
|
50206 |
timescale = null;
|
|
|
50207 |
if (!parsedCaptions) {
|
|
|
50208 |
parsedCaptions = {
|
|
|
50209 |
captions: [],
|
|
|
50210 |
// CC1, CC2, CC3, CC4
|
|
|
50211 |
captionStreams: {},
|
|
|
50212 |
logs: []
|
|
|
50213 |
};
|
|
|
50214 |
} else {
|
|
|
50215 |
this.clearParsedCaptions();
|
|
|
50216 |
}
|
|
|
50217 |
this.resetCaptionStream();
|
|
|
50218 |
};
|
|
|
50219 |
this.reset();
|
|
|
50220 |
};
|
|
|
50221 |
var captionParser = CaptionParser;
|
|
|
50222 |
/**
|
|
|
50223 |
* Returns the first string in the data array ending with a null char '\0'
|
|
|
50224 |
* @param {UInt8} data
|
|
|
50225 |
* @returns the string with the null char
|
|
|
50226 |
*/
|
|
|
50227 |
|
|
|
50228 |
var uint8ToCString$1 = function (data) {
|
|
|
50229 |
var index = 0;
|
|
|
50230 |
var curChar = String.fromCharCode(data[index]);
|
|
|
50231 |
var retString = '';
|
|
|
50232 |
while (curChar !== '\0') {
|
|
|
50233 |
retString += curChar;
|
|
|
50234 |
index++;
|
|
|
50235 |
curChar = String.fromCharCode(data[index]);
|
|
|
50236 |
} // Add nullChar
|
|
|
50237 |
|
|
|
50238 |
retString += curChar;
|
|
|
50239 |
return retString;
|
|
|
50240 |
};
|
|
|
50241 |
var string = {
|
|
|
50242 |
uint8ToCString: uint8ToCString$1
|
|
|
50243 |
};
|
|
|
50244 |
var uint8ToCString = string.uint8ToCString;
|
|
|
50245 |
var getUint64$1 = numbers.getUint64;
|
|
|
50246 |
/**
|
|
|
50247 |
* Based on: ISO/IEC 23009 Section: 5.10.3.3
|
|
|
50248 |
* References:
|
|
|
50249 |
* https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format
|
|
|
50250 |
* https://aomediacodec.github.io/id3-emsg/
|
|
|
50251 |
*
|
|
|
50252 |
* Takes emsg box data as a uint8 array and returns a emsg box object
|
|
|
50253 |
* @param {UInt8Array} boxData data from emsg box
|
|
|
50254 |
* @returns A parsed emsg box object
|
|
|
50255 |
*/
|
|
|
50256 |
|
|
|
50257 |
var parseEmsgBox = function (boxData) {
|
|
|
50258 |
// version + flags
|
|
|
50259 |
var offset = 4;
|
|
|
50260 |
var version = boxData[0];
|
|
|
50261 |
var scheme_id_uri, value, timescale, presentation_time, presentation_time_delta, event_duration, id, message_data;
|
|
|
50262 |
if (version === 0) {
|
|
|
50263 |
scheme_id_uri = uint8ToCString(boxData.subarray(offset));
|
|
|
50264 |
offset += scheme_id_uri.length;
|
|
|
50265 |
value = uint8ToCString(boxData.subarray(offset));
|
|
|
50266 |
offset += value.length;
|
|
|
50267 |
var dv = new DataView(boxData.buffer);
|
|
|
50268 |
timescale = dv.getUint32(offset);
|
|
|
50269 |
offset += 4;
|
|
|
50270 |
presentation_time_delta = dv.getUint32(offset);
|
|
|
50271 |
offset += 4;
|
|
|
50272 |
event_duration = dv.getUint32(offset);
|
|
|
50273 |
offset += 4;
|
|
|
50274 |
id = dv.getUint32(offset);
|
|
|
50275 |
offset += 4;
|
|
|
50276 |
} else if (version === 1) {
|
|
|
50277 |
var dv = new DataView(boxData.buffer);
|
|
|
50278 |
timescale = dv.getUint32(offset);
|
|
|
50279 |
offset += 4;
|
|
|
50280 |
presentation_time = getUint64$1(boxData.subarray(offset));
|
|
|
50281 |
offset += 8;
|
|
|
50282 |
event_duration = dv.getUint32(offset);
|
|
|
50283 |
offset += 4;
|
|
|
50284 |
id = dv.getUint32(offset);
|
|
|
50285 |
offset += 4;
|
|
|
50286 |
scheme_id_uri = uint8ToCString(boxData.subarray(offset));
|
|
|
50287 |
offset += scheme_id_uri.length;
|
|
|
50288 |
value = uint8ToCString(boxData.subarray(offset));
|
|
|
50289 |
offset += value.length;
|
|
|
50290 |
}
|
|
|
50291 |
message_data = new Uint8Array(boxData.subarray(offset, boxData.byteLength));
|
|
|
50292 |
var emsgBox = {
|
|
|
50293 |
scheme_id_uri,
|
|
|
50294 |
value,
|
|
|
50295 |
// if timescale is undefined or 0 set to 1
|
|
|
50296 |
timescale: timescale ? timescale : 1,
|
|
|
50297 |
presentation_time,
|
|
|
50298 |
presentation_time_delta,
|
|
|
50299 |
event_duration,
|
|
|
50300 |
id,
|
|
|
50301 |
message_data
|
|
|
50302 |
};
|
|
|
50303 |
return isValidEmsgBox(version, emsgBox) ? emsgBox : undefined;
|
|
|
50304 |
};
|
|
|
50305 |
/**
|
|
|
50306 |
* Scales a presentation time or time delta with an offset with a provided timescale
|
|
|
50307 |
* @param {number} presentationTime
|
|
|
50308 |
* @param {number} timescale
|
|
|
50309 |
* @param {number} timeDelta
|
|
|
50310 |
* @param {number} offset
|
|
|
50311 |
* @returns the scaled time as a number
|
|
|
50312 |
*/
|
|
|
50313 |
|
|
|
50314 |
var scaleTime = function (presentationTime, timescale, timeDelta, offset) {
|
|
|
50315 |
return presentationTime || presentationTime === 0 ? presentationTime / timescale : offset + timeDelta / timescale;
|
|
|
50316 |
};
|
|
|
50317 |
/**
|
|
|
50318 |
* Checks the emsg box data for validity based on the version
|
|
|
50319 |
* @param {number} version of the emsg box to validate
|
|
|
50320 |
* @param {Object} emsg the emsg data to validate
|
|
|
50321 |
* @returns if the box is valid as a boolean
|
|
|
50322 |
*/
|
|
|
50323 |
|
|
|
50324 |
var isValidEmsgBox = function (version, emsg) {
|
|
|
50325 |
var hasScheme = emsg.scheme_id_uri !== '\0';
|
|
|
50326 |
var isValidV0Box = version === 0 && isDefined(emsg.presentation_time_delta) && hasScheme;
|
|
|
50327 |
var isValidV1Box = version === 1 && isDefined(emsg.presentation_time) && hasScheme; // Only valid versions of emsg are 0 and 1
|
|
|
50328 |
|
|
|
50329 |
return !(version > 1) && isValidV0Box || isValidV1Box;
|
|
|
50330 |
}; // Utility function to check if an object is defined
|
|
|
50331 |
|
|
|
50332 |
var isDefined = function (data) {
|
|
|
50333 |
return data !== undefined || data !== null;
|
|
|
50334 |
};
|
|
|
50335 |
var emsg$1 = {
|
|
|
50336 |
parseEmsgBox: parseEmsgBox,
|
|
|
50337 |
scaleTime: scaleTime
|
|
|
50338 |
};
|
|
|
50339 |
/**
|
|
|
50340 |
* mux.js
|
|
|
50341 |
*
|
|
|
50342 |
* Copyright (c) Brightcove
|
|
|
50343 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
50344 |
*
|
|
|
50345 |
* Utilities to detect basic properties and metadata about MP4s.
|
|
|
50346 |
*/
|
|
|
50347 |
|
|
|
50348 |
var toUnsigned = bin.toUnsigned;
|
|
|
50349 |
var toHexString = bin.toHexString;
|
|
|
50350 |
var findBox = findBox_1;
|
|
|
50351 |
var parseType$1 = parseType_1;
|
|
|
50352 |
var emsg = emsg$1;
|
|
|
50353 |
var parseTfhd = parseTfhd$2;
|
|
|
50354 |
var parseTrun = parseTrun$2;
|
|
|
50355 |
var parseTfdt = parseTfdt$2;
|
|
|
50356 |
var getUint64 = numbers.getUint64;
|
|
|
50357 |
var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks, getTimescaleFromMediaHeader, getEmsgID3;
|
|
|
50358 |
var window$1 = window_1;
|
|
|
50359 |
var parseId3Frames = parseId3.parseId3Frames;
|
|
|
50360 |
/**
|
|
|
50361 |
* Parses an MP4 initialization segment and extracts the timescale
|
|
|
50362 |
* values for any declared tracks. Timescale values indicate the
|
|
|
50363 |
* number of clock ticks per second to assume for time-based values
|
|
|
50364 |
* elsewhere in the MP4.
|
|
|
50365 |
*
|
|
|
50366 |
* To determine the start time of an MP4, you need two pieces of
|
|
|
50367 |
* information: the timescale unit and the earliest base media decode
|
|
|
50368 |
* time. Multiple timescales can be specified within an MP4 but the
|
|
|
50369 |
* base media decode time is always expressed in the timescale from
|
|
|
50370 |
* the media header box for the track:
|
|
|
50371 |
* ```
|
|
|
50372 |
* moov > trak > mdia > mdhd.timescale
|
|
|
50373 |
* ```
|
|
|
50374 |
* @param init {Uint8Array} the bytes of the init segment
|
|
|
50375 |
* @return {object} a hash of track ids to timescale values or null if
|
|
|
50376 |
* the init segment is malformed.
|
|
|
50377 |
*/
|
|
|
50378 |
|
|
|
50379 |
timescale = function (init) {
|
|
|
50380 |
var result = {},
|
|
|
50381 |
traks = findBox(init, ['moov', 'trak']); // mdhd timescale
|
|
|
50382 |
|
|
|
50383 |
return traks.reduce(function (result, trak) {
|
|
|
50384 |
var tkhd, version, index, id, mdhd;
|
|
|
50385 |
tkhd = findBox(trak, ['tkhd'])[0];
|
|
|
50386 |
if (!tkhd) {
|
|
|
50387 |
return null;
|
|
|
50388 |
}
|
|
|
50389 |
version = tkhd[0];
|
|
|
50390 |
index = version === 0 ? 12 : 20;
|
|
|
50391 |
id = toUnsigned(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]);
|
|
|
50392 |
mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
|
|
50393 |
if (!mdhd) {
|
|
|
50394 |
return null;
|
|
|
50395 |
}
|
|
|
50396 |
version = mdhd[0];
|
|
|
50397 |
index = version === 0 ? 12 : 20;
|
|
|
50398 |
result[id] = toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]);
|
|
|
50399 |
return result;
|
|
|
50400 |
}, result);
|
|
|
50401 |
};
|
|
|
50402 |
/**
|
|
|
50403 |
* Determine the base media decode start time, in seconds, for an MP4
|
|
|
50404 |
* fragment. If multiple fragments are specified, the earliest time is
|
|
|
50405 |
* returned.
|
|
|
50406 |
*
|
|
|
50407 |
* The base media decode time can be parsed from track fragment
|
|
|
50408 |
* metadata:
|
|
|
50409 |
* ```
|
|
|
50410 |
* moof > traf > tfdt.baseMediaDecodeTime
|
|
|
50411 |
* ```
|
|
|
50412 |
* It requires the timescale value from the mdhd to interpret.
|
|
|
50413 |
*
|
|
|
50414 |
* @param timescale {object} a hash of track ids to timescale values.
|
|
|
50415 |
* @return {number} the earliest base media decode start time for the
|
|
|
50416 |
* fragment, in seconds
|
|
|
50417 |
*/
|
|
|
50418 |
|
|
|
50419 |
startTime = function (timescale, fragment) {
|
|
|
50420 |
var trafs; // we need info from two childrend of each track fragment box
|
|
|
50421 |
|
|
|
50422 |
trafs = findBox(fragment, ['moof', 'traf']); // determine the start times for each track
|
|
|
50423 |
|
|
|
50424 |
var lowestTime = trafs.reduce(function (acc, traf) {
|
|
|
50425 |
var tfhd = findBox(traf, ['tfhd'])[0]; // get the track id from the tfhd
|
|
|
50426 |
|
|
|
50427 |
var id = toUnsigned(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); // assume a 90kHz clock if no timescale was specified
|
|
|
50428 |
|
|
|
50429 |
var scale = timescale[id] || 90e3; // get the base media decode time from the tfdt
|
|
|
50430 |
|
|
|
50431 |
var tfdt = findBox(traf, ['tfdt'])[0];
|
|
|
50432 |
var dv = new DataView(tfdt.buffer, tfdt.byteOffset, tfdt.byteLength);
|
|
|
50433 |
var baseTime; // version 1 is 64 bit
|
|
|
50434 |
|
|
|
50435 |
if (tfdt[0] === 1) {
|
|
|
50436 |
baseTime = getUint64(tfdt.subarray(4, 12));
|
|
|
50437 |
} else {
|
|
|
50438 |
baseTime = dv.getUint32(4);
|
|
|
50439 |
} // convert base time to seconds if it is a valid number.
|
|
|
50440 |
|
|
|
50441 |
let seconds;
|
|
|
50442 |
if (typeof baseTime === 'bigint') {
|
|
|
50443 |
seconds = baseTime / window$1.BigInt(scale);
|
|
|
50444 |
} else if (typeof baseTime === 'number' && !isNaN(baseTime)) {
|
|
|
50445 |
seconds = baseTime / scale;
|
|
|
50446 |
}
|
|
|
50447 |
if (seconds < Number.MAX_SAFE_INTEGER) {
|
|
|
50448 |
seconds = Number(seconds);
|
|
|
50449 |
}
|
|
|
50450 |
if (seconds < acc) {
|
|
|
50451 |
acc = seconds;
|
|
|
50452 |
}
|
|
|
50453 |
return acc;
|
|
|
50454 |
}, Infinity);
|
|
|
50455 |
return typeof lowestTime === 'bigint' || isFinite(lowestTime) ? lowestTime : 0;
|
|
|
50456 |
};
|
|
|
50457 |
/**
|
|
|
50458 |
* Determine the composition start, in seconds, for an MP4
|
|
|
50459 |
* fragment.
|
|
|
50460 |
*
|
|
|
50461 |
* The composition start time of a fragment can be calculated using the base
|
|
|
50462 |
* media decode time, composition time offset, and timescale, as follows:
|
|
|
50463 |
*
|
|
|
50464 |
* compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale
|
|
|
50465 |
*
|
|
|
50466 |
* All of the aforementioned information is contained within a media fragment's
|
|
|
50467 |
* `traf` box, except for timescale info, which comes from the initialization
|
|
|
50468 |
* segment, so a track id (also contained within a `traf`) is also necessary to
|
|
|
50469 |
* associate it with a timescale
|
|
|
50470 |
*
|
|
|
50471 |
*
|
|
|
50472 |
* @param timescales {object} - a hash of track ids to timescale values.
|
|
|
50473 |
* @param fragment {Unit8Array} - the bytes of a media segment
|
|
|
50474 |
* @return {number} the composition start time for the fragment, in seconds
|
|
|
50475 |
**/
|
|
|
50476 |
|
|
|
50477 |
compositionStartTime = function (timescales, fragment) {
|
|
|
50478 |
var trafBoxes = findBox(fragment, ['moof', 'traf']);
|
|
|
50479 |
var baseMediaDecodeTime = 0;
|
|
|
50480 |
var compositionTimeOffset = 0;
|
|
|
50481 |
var trackId;
|
|
|
50482 |
if (trafBoxes && trafBoxes.length) {
|
|
|
50483 |
// The spec states that track run samples contained within a `traf` box are contiguous, but
|
|
|
50484 |
// it does not explicitly state whether the `traf` boxes themselves are contiguous.
|
|
|
50485 |
// We will assume that they are, so we only need the first to calculate start time.
|
|
|
50486 |
var tfhd = findBox(trafBoxes[0], ['tfhd'])[0];
|
|
|
50487 |
var trun = findBox(trafBoxes[0], ['trun'])[0];
|
|
|
50488 |
var tfdt = findBox(trafBoxes[0], ['tfdt'])[0];
|
|
|
50489 |
if (tfhd) {
|
|
|
50490 |
var parsedTfhd = parseTfhd(tfhd);
|
|
|
50491 |
trackId = parsedTfhd.trackId;
|
|
|
50492 |
}
|
|
|
50493 |
if (tfdt) {
|
|
|
50494 |
var parsedTfdt = parseTfdt(tfdt);
|
|
|
50495 |
baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime;
|
|
|
50496 |
}
|
|
|
50497 |
if (trun) {
|
|
|
50498 |
var parsedTrun = parseTrun(trun);
|
|
|
50499 |
if (parsedTrun.samples && parsedTrun.samples.length) {
|
|
|
50500 |
compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0;
|
|
|
50501 |
}
|
|
|
50502 |
}
|
|
|
50503 |
} // Get timescale for this specific track. Assume a 90kHz clock if no timescale was
|
|
|
50504 |
// specified.
|
|
|
50505 |
|
|
|
50506 |
var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds
|
|
|
50507 |
|
|
|
50508 |
if (typeof baseMediaDecodeTime === 'bigint') {
|
|
|
50509 |
compositionTimeOffset = window$1.BigInt(compositionTimeOffset);
|
|
|
50510 |
timescale = window$1.BigInt(timescale);
|
|
|
50511 |
}
|
|
|
50512 |
var result = (baseMediaDecodeTime + compositionTimeOffset) / timescale;
|
|
|
50513 |
if (typeof result === 'bigint' && result < Number.MAX_SAFE_INTEGER) {
|
|
|
50514 |
result = Number(result);
|
|
|
50515 |
}
|
|
|
50516 |
return result;
|
|
|
50517 |
};
|
|
|
50518 |
/**
|
|
|
50519 |
* Find the trackIds of the video tracks in this source.
|
|
|
50520 |
* Found by parsing the Handler Reference and Track Header Boxes:
|
|
|
50521 |
* moov > trak > mdia > hdlr
|
|
|
50522 |
* moov > trak > tkhd
|
|
|
50523 |
*
|
|
|
50524 |
* @param {Uint8Array} init - The bytes of the init segment for this source
|
|
|
50525 |
* @return {Number[]} A list of trackIds
|
|
|
50526 |
*
|
|
|
50527 |
* @see ISO-BMFF-12/2015, Section 8.4.3
|
|
|
50528 |
**/
|
|
|
50529 |
|
|
|
50530 |
getVideoTrackIds = function (init) {
|
|
|
50531 |
var traks = findBox(init, ['moov', 'trak']);
|
|
|
50532 |
var videoTrackIds = [];
|
|
|
50533 |
traks.forEach(function (trak) {
|
|
|
50534 |
var hdlrs = findBox(trak, ['mdia', 'hdlr']);
|
|
|
50535 |
var tkhds = findBox(trak, ['tkhd']);
|
|
|
50536 |
hdlrs.forEach(function (hdlr, index) {
|
|
|
50537 |
var handlerType = parseType$1(hdlr.subarray(8, 12));
|
|
|
50538 |
var tkhd = tkhds[index];
|
|
|
50539 |
var view;
|
|
|
50540 |
var version;
|
|
|
50541 |
var trackId;
|
|
|
50542 |
if (handlerType === 'vide') {
|
|
|
50543 |
view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
|
|
|
50544 |
version = view.getUint8(0);
|
|
|
50545 |
trackId = version === 0 ? view.getUint32(12) : view.getUint32(20);
|
|
|
50546 |
videoTrackIds.push(trackId);
|
|
|
50547 |
}
|
|
|
50548 |
});
|
|
|
50549 |
});
|
|
|
50550 |
return videoTrackIds;
|
|
|
50551 |
};
|
|
|
50552 |
getTimescaleFromMediaHeader = function (mdhd) {
|
|
|
50553 |
// mdhd is a FullBox, meaning it will have its own version as the first byte
|
|
|
50554 |
var version = mdhd[0];
|
|
|
50555 |
var index = version === 0 ? 12 : 20;
|
|
|
50556 |
return toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]);
|
|
|
50557 |
};
|
|
|
50558 |
/**
|
|
|
50559 |
* Get all the video, audio, and hint tracks from a non fragmented
|
|
|
50560 |
* mp4 segment
|
|
|
50561 |
*/
|
|
|
50562 |
|
|
|
50563 |
getTracks = function (init) {
|
|
|
50564 |
var traks = findBox(init, ['moov', 'trak']);
|
|
|
50565 |
var tracks = [];
|
|
|
50566 |
traks.forEach(function (trak) {
|
|
|
50567 |
var track = {};
|
|
|
50568 |
var tkhd = findBox(trak, ['tkhd'])[0];
|
|
|
50569 |
var view, tkhdVersion; // id
|
|
|
50570 |
|
|
|
50571 |
if (tkhd) {
|
|
|
50572 |
view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
|
|
|
50573 |
tkhdVersion = view.getUint8(0);
|
|
|
50574 |
track.id = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20);
|
|
|
50575 |
}
|
|
|
50576 |
var hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; // type
|
|
|
50577 |
|
|
|
50578 |
if (hdlr) {
|
|
|
50579 |
var type = parseType$1(hdlr.subarray(8, 12));
|
|
|
50580 |
if (type === 'vide') {
|
|
|
50581 |
track.type = 'video';
|
|
|
50582 |
} else if (type === 'soun') {
|
|
|
50583 |
track.type = 'audio';
|
|
|
50584 |
} else {
|
|
|
50585 |
track.type = type;
|
|
|
50586 |
}
|
|
|
50587 |
} // codec
|
|
|
50588 |
|
|
|
50589 |
var stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
|
|
|
50590 |
if (stsd) {
|
|
|
50591 |
var sampleDescriptions = stsd.subarray(8); // gives the codec type string
|
|
|
50592 |
|
|
|
50593 |
track.codec = parseType$1(sampleDescriptions.subarray(4, 8));
|
|
|
50594 |
var codecBox = findBox(sampleDescriptions, [track.codec])[0];
|
|
|
50595 |
var codecConfig, codecConfigType;
|
|
|
50596 |
if (codecBox) {
|
|
|
50597 |
// https://tools.ietf.org/html/rfc6381#section-3.3
|
|
|
50598 |
if (/^[asm]vc[1-9]$/i.test(track.codec)) {
|
|
|
50599 |
// we don't need anything but the "config" parameter of the
|
|
|
50600 |
// avc1 codecBox
|
|
|
50601 |
codecConfig = codecBox.subarray(78);
|
|
|
50602 |
codecConfigType = parseType$1(codecConfig.subarray(4, 8));
|
|
|
50603 |
if (codecConfigType === 'avcC' && codecConfig.length > 11) {
|
|
|
50604 |
track.codec += '.'; // left padded with zeroes for single digit hex
|
|
|
50605 |
// profile idc
|
|
|
50606 |
|
|
|
50607 |
track.codec += toHexString(codecConfig[9]); // the byte containing the constraint_set flags
|
|
|
50608 |
|
|
|
50609 |
track.codec += toHexString(codecConfig[10]); // level idc
|
|
|
50610 |
|
|
|
50611 |
track.codec += toHexString(codecConfig[11]);
|
|
|
50612 |
} else {
|
|
|
50613 |
// TODO: show a warning that we couldn't parse the codec
|
|
|
50614 |
// and are using the default
|
|
|
50615 |
track.codec = 'avc1.4d400d';
|
|
|
50616 |
}
|
|
|
50617 |
} else if (/^mp4[a,v]$/i.test(track.codec)) {
|
|
|
50618 |
// we do not need anything but the streamDescriptor of the mp4a codecBox
|
|
|
50619 |
codecConfig = codecBox.subarray(28);
|
|
|
50620 |
codecConfigType = parseType$1(codecConfig.subarray(4, 8));
|
|
|
50621 |
if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) {
|
|
|
50622 |
track.codec += '.' + toHexString(codecConfig[19]); // this value is only a single digit
|
|
|
50623 |
|
|
|
50624 |
track.codec += '.' + toHexString(codecConfig[20] >>> 2 & 0x3f).replace(/^0/, '');
|
|
|
50625 |
} else {
|
|
|
50626 |
// TODO: show a warning that we couldn't parse the codec
|
|
|
50627 |
// and are using the default
|
|
|
50628 |
track.codec = 'mp4a.40.2';
|
|
|
50629 |
}
|
|
|
50630 |
} else {
|
|
|
50631 |
// flac, opus, etc
|
|
|
50632 |
track.codec = track.codec.toLowerCase();
|
|
|
50633 |
}
|
|
|
50634 |
}
|
|
|
50635 |
}
|
|
|
50636 |
var mdhd = findBox(trak, ['mdia', 'mdhd'])[0];
|
|
|
50637 |
if (mdhd) {
|
|
|
50638 |
track.timescale = getTimescaleFromMediaHeader(mdhd);
|
|
|
50639 |
}
|
|
|
50640 |
tracks.push(track);
|
|
|
50641 |
});
|
|
|
50642 |
return tracks;
|
|
|
50643 |
};
|
|
|
50644 |
/**
|
|
|
50645 |
* Returns an array of emsg ID3 data from the provided segmentData.
|
|
|
50646 |
* An offset can also be provided as the Latest Arrival Time to calculate
|
|
|
50647 |
* the Event Start Time of v0 EMSG boxes.
|
|
|
50648 |
* See: https://dashif-documents.azurewebsites.net/Events/master/event.html#Inband-event-timing
|
|
|
50649 |
*
|
|
|
50650 |
* @param {Uint8Array} segmentData the segment byte array.
|
|
|
50651 |
* @param {number} offset the segment start time or Latest Arrival Time,
|
|
|
50652 |
* @return {Object[]} an array of ID3 parsed from EMSG boxes
|
|
|
50653 |
*/
|
|
|
50654 |
|
|
|
50655 |
getEmsgID3 = function (segmentData, offset = 0) {
|
|
|
50656 |
var emsgBoxes = findBox(segmentData, ['emsg']);
|
|
|
50657 |
return emsgBoxes.map(data => {
|
|
|
50658 |
var parsedBox = emsg.parseEmsgBox(new Uint8Array(data));
|
|
|
50659 |
var parsedId3Frames = parseId3Frames(parsedBox.message_data);
|
|
|
50660 |
return {
|
|
|
50661 |
cueTime: emsg.scaleTime(parsedBox.presentation_time, parsedBox.timescale, parsedBox.presentation_time_delta, offset),
|
|
|
50662 |
duration: emsg.scaleTime(parsedBox.event_duration, parsedBox.timescale),
|
|
|
50663 |
frames: parsedId3Frames
|
|
|
50664 |
};
|
|
|
50665 |
});
|
|
|
50666 |
};
|
|
|
50667 |
var probe$2 = {
|
|
|
50668 |
// export mp4 inspector's findBox and parseType for backwards compatibility
|
|
|
50669 |
findBox: findBox,
|
|
|
50670 |
parseType: parseType$1,
|
|
|
50671 |
timescale: timescale,
|
|
|
50672 |
startTime: startTime,
|
|
|
50673 |
compositionStartTime: compositionStartTime,
|
|
|
50674 |
videoTrackIds: getVideoTrackIds,
|
|
|
50675 |
tracks: getTracks,
|
|
|
50676 |
getTimescaleFromMediaHeader: getTimescaleFromMediaHeader,
|
|
|
50677 |
getEmsgID3: getEmsgID3
|
|
|
50678 |
};
|
|
|
50679 |
/**
|
|
|
50680 |
* mux.js
|
|
|
50681 |
*
|
|
|
50682 |
* Copyright (c) Brightcove
|
|
|
50683 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
50684 |
*
|
|
|
50685 |
* Utilities to detect basic properties and metadata about TS Segments.
|
|
|
50686 |
*/
|
|
|
50687 |
|
|
|
50688 |
var StreamTypes$1 = streamTypes;
|
|
|
50689 |
var parsePid = function (packet) {
|
|
|
50690 |
var pid = packet[1] & 0x1f;
|
|
|
50691 |
pid <<= 8;
|
|
|
50692 |
pid |= packet[2];
|
|
|
50693 |
return pid;
|
|
|
50694 |
};
|
|
|
50695 |
var parsePayloadUnitStartIndicator = function (packet) {
|
|
|
50696 |
return !!(packet[1] & 0x40);
|
|
|
50697 |
};
|
|
|
50698 |
var parseAdaptionField = function (packet) {
|
|
|
50699 |
var offset = 0; // if an adaption field is present, its length is specified by the
|
|
|
50700 |
// fifth byte of the TS packet header. The adaptation field is
|
|
|
50701 |
// used to add stuffing to PES packets that don't fill a complete
|
|
|
50702 |
// TS packet, and to specify some forms of timing and control data
|
|
|
50703 |
// that we do not currently use.
|
|
|
50704 |
|
|
|
50705 |
if ((packet[3] & 0x30) >>> 4 > 0x01) {
|
|
|
50706 |
offset += packet[4] + 1;
|
|
|
50707 |
}
|
|
|
50708 |
return offset;
|
|
|
50709 |
};
|
|
|
50710 |
var parseType = function (packet, pmtPid) {
|
|
|
50711 |
var pid = parsePid(packet);
|
|
|
50712 |
if (pid === 0) {
|
|
|
50713 |
return 'pat';
|
|
|
50714 |
} else if (pid === pmtPid) {
|
|
|
50715 |
return 'pmt';
|
|
|
50716 |
} else if (pmtPid) {
|
|
|
50717 |
return 'pes';
|
|
|
50718 |
}
|
|
|
50719 |
return null;
|
|
|
50720 |
};
|
|
|
50721 |
var parsePat = function (packet) {
|
|
|
50722 |
var pusi = parsePayloadUnitStartIndicator(packet);
|
|
|
50723 |
var offset = 4 + parseAdaptionField(packet);
|
|
|
50724 |
if (pusi) {
|
|
|
50725 |
offset += packet[offset] + 1;
|
|
|
50726 |
}
|
|
|
50727 |
return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11];
|
|
|
50728 |
};
|
|
|
50729 |
var parsePmt = function (packet) {
|
|
|
50730 |
var programMapTable = {};
|
|
|
50731 |
var pusi = parsePayloadUnitStartIndicator(packet);
|
|
|
50732 |
var payloadOffset = 4 + parseAdaptionField(packet);
|
|
|
50733 |
if (pusi) {
|
|
|
50734 |
payloadOffset += packet[payloadOffset] + 1;
|
|
|
50735 |
} // PMTs can be sent ahead of the time when they should actually
|
|
|
50736 |
// take effect. We don't believe this should ever be the case
|
|
|
50737 |
// for HLS but we'll ignore "forward" PMT declarations if we see
|
|
|
50738 |
// them. Future PMT declarations have the current_next_indicator
|
|
|
50739 |
// set to zero.
|
|
|
50740 |
|
|
|
50741 |
if (!(packet[payloadOffset + 5] & 0x01)) {
|
|
|
50742 |
return;
|
|
|
50743 |
}
|
|
|
50744 |
var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section
|
|
|
50745 |
|
|
|
50746 |
sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2];
|
|
|
50747 |
tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how
|
|
|
50748 |
// long the program info descriptors are
|
|
|
50749 |
|
|
|
50750 |
programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table
|
|
|
50751 |
|
|
|
50752 |
var offset = 12 + programInfoLength;
|
|
|
50753 |
while (offset < tableEnd) {
|
|
|
50754 |
var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type
|
|
|
50755 |
|
|
|
50756 |
programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; // move to the next table entry
|
|
|
50757 |
// skip past the elementary stream descriptors, if present
|
|
|
50758 |
|
|
|
50759 |
offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5;
|
|
|
50760 |
}
|
|
|
50761 |
return programMapTable;
|
|
|
50762 |
};
|
|
|
50763 |
var parsePesType = function (packet, programMapTable) {
|
|
|
50764 |
var pid = parsePid(packet);
|
|
|
50765 |
var type = programMapTable[pid];
|
|
|
50766 |
switch (type) {
|
|
|
50767 |
case StreamTypes$1.H264_STREAM_TYPE:
|
|
|
50768 |
return 'video';
|
|
|
50769 |
case StreamTypes$1.ADTS_STREAM_TYPE:
|
|
|
50770 |
return 'audio';
|
|
|
50771 |
case StreamTypes$1.METADATA_STREAM_TYPE:
|
|
|
50772 |
return 'timed-metadata';
|
|
|
50773 |
default:
|
|
|
50774 |
return null;
|
|
|
50775 |
}
|
|
|
50776 |
};
|
|
|
50777 |
var parsePesTime = function (packet) {
|
|
|
50778 |
var pusi = parsePayloadUnitStartIndicator(packet);
|
|
|
50779 |
if (!pusi) {
|
|
|
50780 |
return null;
|
|
|
50781 |
}
|
|
|
50782 |
var offset = 4 + parseAdaptionField(packet);
|
|
|
50783 |
if (offset >= packet.byteLength) {
|
|
|
50784 |
// From the H 222.0 MPEG-TS spec
|
|
|
50785 |
// "For transport stream packets carrying PES packets, stuffing is needed when there
|
|
|
50786 |
// is insufficient PES packet data to completely fill the transport stream packet
|
|
|
50787 |
// payload bytes. Stuffing is accomplished by defining an adaptation field longer than
|
|
|
50788 |
// the sum of the lengths of the data elements in it, so that the payload bytes
|
|
|
50789 |
// remaining after the adaptation field exactly accommodates the available PES packet
|
|
|
50790 |
// data."
|
|
|
50791 |
//
|
|
|
50792 |
// If the offset is >= the length of the packet, then the packet contains no data
|
|
|
50793 |
// and instead is just adaption field stuffing bytes
|
|
|
50794 |
return null;
|
|
|
50795 |
}
|
|
|
50796 |
var pes = null;
|
|
|
50797 |
var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value
|
|
|
50798 |
// and a DTS value. Determine what combination of values is
|
|
|
50799 |
// available to work with.
|
|
|
50800 |
|
|
|
50801 |
ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number. Javascript
|
|
|
50802 |
// performs all bitwise operations on 32-bit integers but javascript
|
|
|
50803 |
// supports a much greater range (52-bits) of integer using standard
|
|
|
50804 |
// mathematical operations.
|
|
|
50805 |
// We construct a 31-bit value using bitwise operators over the 31
|
|
|
50806 |
// most significant bits and then multiply by 4 (equal to a left-shift
|
|
|
50807 |
// of 2) before we add the final 2 least significant bits of the
|
|
|
50808 |
// timestamp (equal to an OR.)
|
|
|
50809 |
|
|
|
50810 |
if (ptsDtsFlags & 0xC0) {
|
|
|
50811 |
pes = {}; // the PTS and DTS are not written out directly. For information
|
|
|
50812 |
// on how they are encoded, see
|
|
|
50813 |
// http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
|
|
|
50814 |
|
|
|
50815 |
pes.pts = (packet[offset + 9] & 0x0E) << 27 | (packet[offset + 10] & 0xFF) << 20 | (packet[offset + 11] & 0xFE) << 12 | (packet[offset + 12] & 0xFF) << 5 | (packet[offset + 13] & 0xFE) >>> 3;
|
|
|
50816 |
pes.pts *= 4; // Left shift by 2
|
|
|
50817 |
|
|
|
50818 |
pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs
|
|
|
50819 |
|
|
|
50820 |
pes.dts = pes.pts;
|
|
|
50821 |
if (ptsDtsFlags & 0x40) {
|
|
|
50822 |
pes.dts = (packet[offset + 14] & 0x0E) << 27 | (packet[offset + 15] & 0xFF) << 20 | (packet[offset + 16] & 0xFE) << 12 | (packet[offset + 17] & 0xFF) << 5 | (packet[offset + 18] & 0xFE) >>> 3;
|
|
|
50823 |
pes.dts *= 4; // Left shift by 2
|
|
|
50824 |
|
|
|
50825 |
pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs
|
|
|
50826 |
}
|
|
|
50827 |
}
|
|
|
50828 |
|
|
|
50829 |
return pes;
|
|
|
50830 |
};
|
|
|
50831 |
var parseNalUnitType = function (type) {
|
|
|
50832 |
switch (type) {
|
|
|
50833 |
case 0x05:
|
|
|
50834 |
return 'slice_layer_without_partitioning_rbsp_idr';
|
|
|
50835 |
case 0x06:
|
|
|
50836 |
return 'sei_rbsp';
|
|
|
50837 |
case 0x07:
|
|
|
50838 |
return 'seq_parameter_set_rbsp';
|
|
|
50839 |
case 0x08:
|
|
|
50840 |
return 'pic_parameter_set_rbsp';
|
|
|
50841 |
case 0x09:
|
|
|
50842 |
return 'access_unit_delimiter_rbsp';
|
|
|
50843 |
default:
|
|
|
50844 |
return null;
|
|
|
50845 |
}
|
|
|
50846 |
};
|
|
|
50847 |
var videoPacketContainsKeyFrame = function (packet) {
|
|
|
50848 |
var offset = 4 + parseAdaptionField(packet);
|
|
|
50849 |
var frameBuffer = packet.subarray(offset);
|
|
|
50850 |
var frameI = 0;
|
|
|
50851 |
var frameSyncPoint = 0;
|
|
|
50852 |
var foundKeyFrame = false;
|
|
|
50853 |
var nalType; // advance the sync point to a NAL start, if necessary
|
|
|
50854 |
|
|
|
50855 |
for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) {
|
|
|
50856 |
if (frameBuffer[frameSyncPoint + 2] === 1) {
|
|
|
50857 |
// the sync point is properly aligned
|
|
|
50858 |
frameI = frameSyncPoint + 5;
|
|
|
50859 |
break;
|
|
|
50860 |
}
|
|
|
50861 |
}
|
|
|
50862 |
while (frameI < frameBuffer.byteLength) {
|
|
|
50863 |
// look at the current byte to determine if we've hit the end of
|
|
|
50864 |
// a NAL unit boundary
|
|
|
50865 |
switch (frameBuffer[frameI]) {
|
|
|
50866 |
case 0:
|
|
|
50867 |
// skip past non-sync sequences
|
|
|
50868 |
if (frameBuffer[frameI - 1] !== 0) {
|
|
|
50869 |
frameI += 2;
|
|
|
50870 |
break;
|
|
|
50871 |
} else if (frameBuffer[frameI - 2] !== 0) {
|
|
|
50872 |
frameI++;
|
|
|
50873 |
break;
|
|
|
50874 |
}
|
|
|
50875 |
if (frameSyncPoint + 3 !== frameI - 2) {
|
|
|
50876 |
nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
|
|
|
50877 |
if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
|
|
|
50878 |
foundKeyFrame = true;
|
|
|
50879 |
}
|
|
|
50880 |
} // drop trailing zeroes
|
|
|
50881 |
|
|
|
50882 |
do {
|
|
|
50883 |
frameI++;
|
|
|
50884 |
} while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length);
|
|
|
50885 |
frameSyncPoint = frameI - 2;
|
|
|
50886 |
frameI += 3;
|
|
|
50887 |
break;
|
|
|
50888 |
case 1:
|
|
|
50889 |
// skip past non-sync sequences
|
|
|
50890 |
if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) {
|
|
|
50891 |
frameI += 3;
|
|
|
50892 |
break;
|
|
|
50893 |
}
|
|
|
50894 |
nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
|
|
|
50895 |
if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
|
|
|
50896 |
foundKeyFrame = true;
|
|
|
50897 |
}
|
|
|
50898 |
frameSyncPoint = frameI - 2;
|
|
|
50899 |
frameI += 3;
|
|
|
50900 |
break;
|
|
|
50901 |
default:
|
|
|
50902 |
// the current byte isn't a one or zero, so it cannot be part
|
|
|
50903 |
// of a sync sequence
|
|
|
50904 |
frameI += 3;
|
|
|
50905 |
break;
|
|
|
50906 |
}
|
|
|
50907 |
}
|
|
|
50908 |
frameBuffer = frameBuffer.subarray(frameSyncPoint);
|
|
|
50909 |
frameI -= frameSyncPoint;
|
|
|
50910 |
frameSyncPoint = 0; // parse the final nal
|
|
|
50911 |
|
|
|
50912 |
if (frameBuffer && frameBuffer.byteLength > 3) {
|
|
|
50913 |
nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
|
|
|
50914 |
if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
|
|
|
50915 |
foundKeyFrame = true;
|
|
|
50916 |
}
|
|
|
50917 |
}
|
|
|
50918 |
return foundKeyFrame;
|
|
|
50919 |
};
|
|
|
50920 |
var probe$1 = {
|
|
|
50921 |
parseType: parseType,
|
|
|
50922 |
parsePat: parsePat,
|
|
|
50923 |
parsePmt: parsePmt,
|
|
|
50924 |
parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator,
|
|
|
50925 |
parsePesType: parsePesType,
|
|
|
50926 |
parsePesTime: parsePesTime,
|
|
|
50927 |
videoPacketContainsKeyFrame: videoPacketContainsKeyFrame
|
|
|
50928 |
};
|
|
|
50929 |
/**
|
|
|
50930 |
* mux.js
|
|
|
50931 |
*
|
|
|
50932 |
* Copyright (c) Brightcove
|
|
|
50933 |
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
|
|
|
50934 |
*
|
|
|
50935 |
* Parse mpeg2 transport stream packets to extract basic timing information
|
|
|
50936 |
*/
|
|
|
50937 |
|
|
|
50938 |
var StreamTypes = streamTypes;
|
|
|
50939 |
var handleRollover = timestampRolloverStream.handleRollover;
|
|
|
50940 |
var probe = {};
|
|
|
50941 |
probe.ts = probe$1;
|
|
|
50942 |
probe.aac = utils;
|
|
|
50943 |
var ONE_SECOND_IN_TS = clock$2.ONE_SECOND_IN_TS;
|
|
|
50944 |
var MP2T_PACKET_LENGTH = 188,
|
|
|
50945 |
// bytes
|
|
|
50946 |
SYNC_BYTE = 0x47;
|
|
|
50947 |
/**
|
|
|
50948 |
* walks through segment data looking for pat and pmt packets to parse out
|
|
|
50949 |
* program map table information
|
|
|
50950 |
*/
|
|
|
50951 |
|
|
|
50952 |
var parsePsi_ = function (bytes, pmt) {
|
|
|
50953 |
var startIndex = 0,
|
|
|
50954 |
endIndex = MP2T_PACKET_LENGTH,
|
|
|
50955 |
packet,
|
|
|
50956 |
type;
|
|
|
50957 |
while (endIndex < bytes.byteLength) {
|
|
|
50958 |
// Look for a pair of start and end sync bytes in the data..
|
|
|
50959 |
if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) {
|
|
|
50960 |
// We found a packet
|
|
|
50961 |
packet = bytes.subarray(startIndex, endIndex);
|
|
|
50962 |
type = probe.ts.parseType(packet, pmt.pid);
|
|
|
50963 |
switch (type) {
|
|
|
50964 |
case 'pat':
|
|
|
50965 |
pmt.pid = probe.ts.parsePat(packet);
|
|
|
50966 |
break;
|
|
|
50967 |
case 'pmt':
|
|
|
50968 |
var table = probe.ts.parsePmt(packet);
|
|
|
50969 |
pmt.table = pmt.table || {};
|
|
|
50970 |
Object.keys(table).forEach(function (key) {
|
|
|
50971 |
pmt.table[key] = table[key];
|
|
|
50972 |
});
|
|
|
50973 |
break;
|
|
|
50974 |
}
|
|
|
50975 |
startIndex += MP2T_PACKET_LENGTH;
|
|
|
50976 |
endIndex += MP2T_PACKET_LENGTH;
|
|
|
50977 |
continue;
|
|
|
50978 |
} // If we get here, we have somehow become de-synchronized and we need to step
|
|
|
50979 |
// forward one byte at a time until we find a pair of sync bytes that denote
|
|
|
50980 |
// a packet
|
|
|
50981 |
|
|
|
50982 |
startIndex++;
|
|
|
50983 |
endIndex++;
|
|
|
50984 |
}
|
|
|
50985 |
};
|
|
|
50986 |
/**
|
|
|
50987 |
* walks through the segment data from the start and end to get timing information
|
|
|
50988 |
* for the first and last audio pes packets
|
|
|
50989 |
*/
|
|
|
50990 |
|
|
|
50991 |
var parseAudioPes_ = function (bytes, pmt, result) {
|
|
|
50992 |
var startIndex = 0,
|
|
|
50993 |
endIndex = MP2T_PACKET_LENGTH,
|
|
|
50994 |
packet,
|
|
|
50995 |
type,
|
|
|
50996 |
pesType,
|
|
|
50997 |
pusi,
|
|
|
50998 |
parsed;
|
|
|
50999 |
var endLoop = false; // Start walking from start of segment to get first audio packet
|
|
|
51000 |
|
|
|
51001 |
while (endIndex <= bytes.byteLength) {
|
|
|
51002 |
// Look for a pair of start and end sync bytes in the data..
|
|
|
51003 |
if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) {
|
|
|
51004 |
// We found a packet
|
|
|
51005 |
packet = bytes.subarray(startIndex, endIndex);
|
|
|
51006 |
type = probe.ts.parseType(packet, pmt.pid);
|
|
|
51007 |
switch (type) {
|
|
|
51008 |
case 'pes':
|
|
|
51009 |
pesType = probe.ts.parsePesType(packet, pmt.table);
|
|
|
51010 |
pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
|
|
|
51011 |
if (pesType === 'audio' && pusi) {
|
|
|
51012 |
parsed = probe.ts.parsePesTime(packet);
|
|
|
51013 |
if (parsed) {
|
|
|
51014 |
parsed.type = 'audio';
|
|
|
51015 |
result.audio.push(parsed);
|
|
|
51016 |
endLoop = true;
|
|
|
51017 |
}
|
|
|
51018 |
}
|
|
|
51019 |
break;
|
|
|
51020 |
}
|
|
|
51021 |
if (endLoop) {
|
|
|
51022 |
break;
|
|
|
51023 |
}
|
|
|
51024 |
startIndex += MP2T_PACKET_LENGTH;
|
|
|
51025 |
endIndex += MP2T_PACKET_LENGTH;
|
|
|
51026 |
continue;
|
|
|
51027 |
} // If we get here, we have somehow become de-synchronized and we need to step
|
|
|
51028 |
// forward one byte at a time until we find a pair of sync bytes that denote
|
|
|
51029 |
// a packet
|
|
|
51030 |
|
|
|
51031 |
startIndex++;
|
|
|
51032 |
endIndex++;
|
|
|
51033 |
} // Start walking from end of segment to get last audio packet
|
|
|
51034 |
|
|
|
51035 |
endIndex = bytes.byteLength;
|
|
|
51036 |
startIndex = endIndex - MP2T_PACKET_LENGTH;
|
|
|
51037 |
endLoop = false;
|
|
|
51038 |
while (startIndex >= 0) {
|
|
|
51039 |
// Look for a pair of start and end sync bytes in the data..
|
|
|
51040 |
if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) {
|
|
|
51041 |
// We found a packet
|
|
|
51042 |
packet = bytes.subarray(startIndex, endIndex);
|
|
|
51043 |
type = probe.ts.parseType(packet, pmt.pid);
|
|
|
51044 |
switch (type) {
|
|
|
51045 |
case 'pes':
|
|
|
51046 |
pesType = probe.ts.parsePesType(packet, pmt.table);
|
|
|
51047 |
pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
|
|
|
51048 |
if (pesType === 'audio' && pusi) {
|
|
|
51049 |
parsed = probe.ts.parsePesTime(packet);
|
|
|
51050 |
if (parsed) {
|
|
|
51051 |
parsed.type = 'audio';
|
|
|
51052 |
result.audio.push(parsed);
|
|
|
51053 |
endLoop = true;
|
|
|
51054 |
}
|
|
|
51055 |
}
|
|
|
51056 |
break;
|
|
|
51057 |
}
|
|
|
51058 |
if (endLoop) {
|
|
|
51059 |
break;
|
|
|
51060 |
}
|
|
|
51061 |
startIndex -= MP2T_PACKET_LENGTH;
|
|
|
51062 |
endIndex -= MP2T_PACKET_LENGTH;
|
|
|
51063 |
continue;
|
|
|
51064 |
} // If we get here, we have somehow become de-synchronized and we need to step
|
|
|
51065 |
// forward one byte at a time until we find a pair of sync bytes that denote
|
|
|
51066 |
// a packet
|
|
|
51067 |
|
|
|
51068 |
startIndex--;
|
|
|
51069 |
endIndex--;
|
|
|
51070 |
}
|
|
|
51071 |
};
|
|
|
51072 |
/**
|
|
|
51073 |
* walks through the segment data from the start and end to get timing information
|
|
|
51074 |
* for the first and last video pes packets as well as timing information for the first
|
|
|
51075 |
* key frame.
|
|
|
51076 |
*/
|
|
|
51077 |
|
|
|
51078 |
var parseVideoPes_ = function (bytes, pmt, result) {
|
|
|
51079 |
var startIndex = 0,
|
|
|
51080 |
endIndex = MP2T_PACKET_LENGTH,
|
|
|
51081 |
packet,
|
|
|
51082 |
type,
|
|
|
51083 |
pesType,
|
|
|
51084 |
pusi,
|
|
|
51085 |
parsed,
|
|
|
51086 |
frame,
|
|
|
51087 |
i,
|
|
|
51088 |
pes;
|
|
|
51089 |
var endLoop = false;
|
|
|
51090 |
var currentFrame = {
|
|
|
51091 |
data: [],
|
|
|
51092 |
size: 0
|
|
|
51093 |
}; // Start walking from start of segment to get first video packet
|
|
|
51094 |
|
|
|
51095 |
while (endIndex < bytes.byteLength) {
|
|
|
51096 |
// Look for a pair of start and end sync bytes in the data..
|
|
|
51097 |
if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) {
|
|
|
51098 |
// We found a packet
|
|
|
51099 |
packet = bytes.subarray(startIndex, endIndex);
|
|
|
51100 |
type = probe.ts.parseType(packet, pmt.pid);
|
|
|
51101 |
switch (type) {
|
|
|
51102 |
case 'pes':
|
|
|
51103 |
pesType = probe.ts.parsePesType(packet, pmt.table);
|
|
|
51104 |
pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
|
|
|
51105 |
if (pesType === 'video') {
|
|
|
51106 |
if (pusi && !endLoop) {
|
|
|
51107 |
parsed = probe.ts.parsePesTime(packet);
|
|
|
51108 |
if (parsed) {
|
|
|
51109 |
parsed.type = 'video';
|
|
|
51110 |
result.video.push(parsed);
|
|
|
51111 |
endLoop = true;
|
|
|
51112 |
}
|
|
|
51113 |
}
|
|
|
51114 |
if (!result.firstKeyFrame) {
|
|
|
51115 |
if (pusi) {
|
|
|
51116 |
if (currentFrame.size !== 0) {
|
|
|
51117 |
frame = new Uint8Array(currentFrame.size);
|
|
|
51118 |
i = 0;
|
|
|
51119 |
while (currentFrame.data.length) {
|
|
|
51120 |
pes = currentFrame.data.shift();
|
|
|
51121 |
frame.set(pes, i);
|
|
|
51122 |
i += pes.byteLength;
|
|
|
51123 |
}
|
|
|
51124 |
if (probe.ts.videoPacketContainsKeyFrame(frame)) {
|
|
|
51125 |
var firstKeyFrame = probe.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting
|
|
|
51126 |
// the keyframe seems to work fine with HLS playback
|
|
|
51127 |
// and definitely preferable to a crash with TypeError...
|
|
|
51128 |
|
|
|
51129 |
if (firstKeyFrame) {
|
|
|
51130 |
result.firstKeyFrame = firstKeyFrame;
|
|
|
51131 |
result.firstKeyFrame.type = 'video';
|
|
|
51132 |
} else {
|
|
|
51133 |
// eslint-disable-next-line
|
|
|
51134 |
console.warn('Failed to extract PTS/DTS from PES at first keyframe. ' + 'This could be an unusual TS segment, or else mux.js did not ' + 'parse your TS segment correctly. If you know your TS ' + 'segments do contain PTS/DTS on keyframes please file a bug ' + 'report! You can try ffprobe to double check for yourself.');
|
|
|
51135 |
}
|
|
|
51136 |
}
|
|
|
51137 |
currentFrame.size = 0;
|
|
|
51138 |
}
|
|
|
51139 |
}
|
|
|
51140 |
currentFrame.data.push(packet);
|
|
|
51141 |
currentFrame.size += packet.byteLength;
|
|
|
51142 |
}
|
|
|
51143 |
}
|
|
|
51144 |
break;
|
|
|
51145 |
}
|
|
|
51146 |
if (endLoop && result.firstKeyFrame) {
|
|
|
51147 |
break;
|
|
|
51148 |
}
|
|
|
51149 |
startIndex += MP2T_PACKET_LENGTH;
|
|
|
51150 |
endIndex += MP2T_PACKET_LENGTH;
|
|
|
51151 |
continue;
|
|
|
51152 |
} // If we get here, we have somehow become de-synchronized and we need to step
|
|
|
51153 |
// forward one byte at a time until we find a pair of sync bytes that denote
|
|
|
51154 |
// a packet
|
|
|
51155 |
|
|
|
51156 |
startIndex++;
|
|
|
51157 |
endIndex++;
|
|
|
51158 |
} // Start walking from end of segment to get last video packet
|
|
|
51159 |
|
|
|
51160 |
endIndex = bytes.byteLength;
|
|
|
51161 |
startIndex = endIndex - MP2T_PACKET_LENGTH;
|
|
|
51162 |
endLoop = false;
|
|
|
51163 |
while (startIndex >= 0) {
|
|
|
51164 |
// Look for a pair of start and end sync bytes in the data..
|
|
|
51165 |
if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) {
|
|
|
51166 |
// We found a packet
|
|
|
51167 |
packet = bytes.subarray(startIndex, endIndex);
|
|
|
51168 |
type = probe.ts.parseType(packet, pmt.pid);
|
|
|
51169 |
switch (type) {
|
|
|
51170 |
case 'pes':
|
|
|
51171 |
pesType = probe.ts.parsePesType(packet, pmt.table);
|
|
|
51172 |
pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
|
|
|
51173 |
if (pesType === 'video' && pusi) {
|
|
|
51174 |
parsed = probe.ts.parsePesTime(packet);
|
|
|
51175 |
if (parsed) {
|
|
|
51176 |
parsed.type = 'video';
|
|
|
51177 |
result.video.push(parsed);
|
|
|
51178 |
endLoop = true;
|
|
|
51179 |
}
|
|
|
51180 |
}
|
|
|
51181 |
break;
|
|
|
51182 |
}
|
|
|
51183 |
if (endLoop) {
|
|
|
51184 |
break;
|
|
|
51185 |
}
|
|
|
51186 |
startIndex -= MP2T_PACKET_LENGTH;
|
|
|
51187 |
endIndex -= MP2T_PACKET_LENGTH;
|
|
|
51188 |
continue;
|
|
|
51189 |
} // If we get here, we have somehow become de-synchronized and we need to step
|
|
|
51190 |
// forward one byte at a time until we find a pair of sync bytes that denote
|
|
|
51191 |
// a packet
|
|
|
51192 |
|
|
|
51193 |
startIndex--;
|
|
|
51194 |
endIndex--;
|
|
|
51195 |
}
|
|
|
51196 |
};
|
|
|
51197 |
/**
|
|
|
51198 |
* Adjusts the timestamp information for the segment to account for
|
|
|
51199 |
* rollover and convert to seconds based on pes packet timescale (90khz clock)
|
|
|
51200 |
*/
|
|
|
51201 |
|
|
|
51202 |
var adjustTimestamp_ = function (segmentInfo, baseTimestamp) {
|
|
|
51203 |
if (segmentInfo.audio && segmentInfo.audio.length) {
|
|
|
51204 |
var audioBaseTimestamp = baseTimestamp;
|
|
|
51205 |
if (typeof audioBaseTimestamp === 'undefined' || isNaN(audioBaseTimestamp)) {
|
|
|
51206 |
audioBaseTimestamp = segmentInfo.audio[0].dts;
|
|
|
51207 |
}
|
|
|
51208 |
segmentInfo.audio.forEach(function (info) {
|
|
|
51209 |
info.dts = handleRollover(info.dts, audioBaseTimestamp);
|
|
|
51210 |
info.pts = handleRollover(info.pts, audioBaseTimestamp); // time in seconds
|
|
|
51211 |
|
|
|
51212 |
info.dtsTime = info.dts / ONE_SECOND_IN_TS;
|
|
|
51213 |
info.ptsTime = info.pts / ONE_SECOND_IN_TS;
|
|
|
51214 |
});
|
|
|
51215 |
}
|
|
|
51216 |
if (segmentInfo.video && segmentInfo.video.length) {
|
|
|
51217 |
var videoBaseTimestamp = baseTimestamp;
|
|
|
51218 |
if (typeof videoBaseTimestamp === 'undefined' || isNaN(videoBaseTimestamp)) {
|
|
|
51219 |
videoBaseTimestamp = segmentInfo.video[0].dts;
|
|
|
51220 |
}
|
|
|
51221 |
segmentInfo.video.forEach(function (info) {
|
|
|
51222 |
info.dts = handleRollover(info.dts, videoBaseTimestamp);
|
|
|
51223 |
info.pts = handleRollover(info.pts, videoBaseTimestamp); // time in seconds
|
|
|
51224 |
|
|
|
51225 |
info.dtsTime = info.dts / ONE_SECOND_IN_TS;
|
|
|
51226 |
info.ptsTime = info.pts / ONE_SECOND_IN_TS;
|
|
|
51227 |
});
|
|
|
51228 |
if (segmentInfo.firstKeyFrame) {
|
|
|
51229 |
var frame = segmentInfo.firstKeyFrame;
|
|
|
51230 |
frame.dts = handleRollover(frame.dts, videoBaseTimestamp);
|
|
|
51231 |
frame.pts = handleRollover(frame.pts, videoBaseTimestamp); // time in seconds
|
|
|
51232 |
|
|
|
51233 |
frame.dtsTime = frame.dts / ONE_SECOND_IN_TS;
|
|
|
51234 |
frame.ptsTime = frame.pts / ONE_SECOND_IN_TS;
|
|
|
51235 |
}
|
|
|
51236 |
}
|
|
|
51237 |
};
|
|
|
51238 |
/**
|
|
|
51239 |
* inspects the aac data stream for start and end time information
|
|
|
51240 |
*/
|
|
|
51241 |
|
|
|
51242 |
var inspectAac_ = function (bytes) {
|
|
|
51243 |
var endLoop = false,
|
|
|
51244 |
audioCount = 0,
|
|
|
51245 |
sampleRate = null,
|
|
|
51246 |
timestamp = null,
|
|
|
51247 |
frameSize = 0,
|
|
|
51248 |
byteIndex = 0,
|
|
|
51249 |
packet;
|
|
|
51250 |
while (bytes.length - byteIndex >= 3) {
|
|
|
51251 |
var type = probe.aac.parseType(bytes, byteIndex);
|
|
|
51252 |
switch (type) {
|
|
|
51253 |
case 'timed-metadata':
|
|
|
51254 |
// Exit early because we don't have enough to parse
|
|
|
51255 |
// the ID3 tag header
|
|
|
51256 |
if (bytes.length - byteIndex < 10) {
|
|
|
51257 |
endLoop = true;
|
|
|
51258 |
break;
|
|
|
51259 |
}
|
|
|
51260 |
frameSize = probe.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer
|
|
|
51261 |
// to emit a full packet
|
|
|
51262 |
|
|
|
51263 |
if (frameSize > bytes.length) {
|
|
|
51264 |
endLoop = true;
|
|
|
51265 |
break;
|
|
|
51266 |
}
|
|
|
51267 |
if (timestamp === null) {
|
|
|
51268 |
packet = bytes.subarray(byteIndex, byteIndex + frameSize);
|
|
|
51269 |
timestamp = probe.aac.parseAacTimestamp(packet);
|
|
|
51270 |
}
|
|
|
51271 |
byteIndex += frameSize;
|
|
|
51272 |
break;
|
|
|
51273 |
case 'audio':
|
|
|
51274 |
// Exit early because we don't have enough to parse
|
|
|
51275 |
// the ADTS frame header
|
|
|
51276 |
if (bytes.length - byteIndex < 7) {
|
|
|
51277 |
endLoop = true;
|
|
|
51278 |
break;
|
|
|
51279 |
}
|
|
|
51280 |
frameSize = probe.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer
|
|
|
51281 |
// to emit a full packet
|
|
|
51282 |
|
|
|
51283 |
if (frameSize > bytes.length) {
|
|
|
51284 |
endLoop = true;
|
|
|
51285 |
break;
|
|
|
51286 |
}
|
|
|
51287 |
if (sampleRate === null) {
|
|
|
51288 |
packet = bytes.subarray(byteIndex, byteIndex + frameSize);
|
|
|
51289 |
sampleRate = probe.aac.parseSampleRate(packet);
|
|
|
51290 |
}
|
|
|
51291 |
audioCount++;
|
|
|
51292 |
byteIndex += frameSize;
|
|
|
51293 |
break;
|
|
|
51294 |
default:
|
|
|
51295 |
byteIndex++;
|
|
|
51296 |
break;
|
|
|
51297 |
}
|
|
|
51298 |
if (endLoop) {
|
|
|
51299 |
return null;
|
|
|
51300 |
}
|
|
|
51301 |
}
|
|
|
51302 |
if (sampleRate === null || timestamp === null) {
|
|
|
51303 |
return null;
|
|
|
51304 |
}
|
|
|
51305 |
var audioTimescale = ONE_SECOND_IN_TS / sampleRate;
|
|
|
51306 |
var result = {
|
|
|
51307 |
audio: [{
|
|
|
51308 |
type: 'audio',
|
|
|
51309 |
dts: timestamp,
|
|
|
51310 |
pts: timestamp
|
|
|
51311 |
}, {
|
|
|
51312 |
type: 'audio',
|
|
|
51313 |
dts: timestamp + audioCount * 1024 * audioTimescale,
|
|
|
51314 |
pts: timestamp + audioCount * 1024 * audioTimescale
|
|
|
51315 |
}]
|
|
|
51316 |
};
|
|
|
51317 |
return result;
|
|
|
51318 |
};
|
|
|
51319 |
/**
|
|
|
51320 |
* inspects the transport stream segment data for start and end time information
|
|
|
51321 |
* of the audio and video tracks (when present) as well as the first key frame's
|
|
|
51322 |
* start time.
|
|
|
51323 |
*/
|
|
|
51324 |
|
|
|
51325 |
var inspectTs_ = function (bytes) {
|
|
|
51326 |
var pmt = {
|
|
|
51327 |
pid: null,
|
|
|
51328 |
table: null
|
|
|
51329 |
};
|
|
|
51330 |
var result = {};
|
|
|
51331 |
parsePsi_(bytes, pmt);
|
|
|
51332 |
for (var pid in pmt.table) {
|
|
|
51333 |
if (pmt.table.hasOwnProperty(pid)) {
|
|
|
51334 |
var type = pmt.table[pid];
|
|
|
51335 |
switch (type) {
|
|
|
51336 |
case StreamTypes.H264_STREAM_TYPE:
|
|
|
51337 |
result.video = [];
|
|
|
51338 |
parseVideoPes_(bytes, pmt, result);
|
|
|
51339 |
if (result.video.length === 0) {
|
|
|
51340 |
delete result.video;
|
|
|
51341 |
}
|
|
|
51342 |
break;
|
|
|
51343 |
case StreamTypes.ADTS_STREAM_TYPE:
|
|
|
51344 |
result.audio = [];
|
|
|
51345 |
parseAudioPes_(bytes, pmt, result);
|
|
|
51346 |
if (result.audio.length === 0) {
|
|
|
51347 |
delete result.audio;
|
|
|
51348 |
}
|
|
|
51349 |
break;
|
|
|
51350 |
}
|
|
|
51351 |
}
|
|
|
51352 |
}
|
|
|
51353 |
return result;
|
|
|
51354 |
};
|
|
|
51355 |
/**
|
|
|
51356 |
* Inspects segment byte data and returns an object with start and end timing information
|
|
|
51357 |
*
|
|
|
51358 |
* @param {Uint8Array} bytes The segment byte data
|
|
|
51359 |
* @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame
|
|
|
51360 |
* timestamps for rollover. This value must be in 90khz clock.
|
|
|
51361 |
* @return {Object} Object containing start and end frame timing info of segment.
|
|
|
51362 |
*/
|
|
|
51363 |
|
|
|
51364 |
var inspect = function (bytes, baseTimestamp) {
|
|
|
51365 |
var isAacData = probe.aac.isLikelyAacData(bytes);
|
|
|
51366 |
var result;
|
|
|
51367 |
if (isAacData) {
|
|
|
51368 |
result = inspectAac_(bytes);
|
|
|
51369 |
} else {
|
|
|
51370 |
result = inspectTs_(bytes);
|
|
|
51371 |
}
|
|
|
51372 |
if (!result || !result.audio && !result.video) {
|
|
|
51373 |
return null;
|
|
|
51374 |
}
|
|
|
51375 |
adjustTimestamp_(result, baseTimestamp);
|
|
|
51376 |
return result;
|
|
|
51377 |
};
|
|
|
51378 |
var tsInspector = {
|
|
|
51379 |
inspect: inspect,
|
|
|
51380 |
parseAudioPes_: parseAudioPes_
|
|
|
51381 |
};
|
|
|
51382 |
/* global self */
|
|
|
51383 |
|
|
|
51384 |
/**
|
|
|
51385 |
* Re-emits transmuxer events by converting them into messages to the
|
|
|
51386 |
* world outside the worker.
|
|
|
51387 |
*
|
|
|
51388 |
* @param {Object} transmuxer the transmuxer to wire events on
|
|
|
51389 |
* @private
|
|
|
51390 |
*/
|
|
|
51391 |
|
|
|
51392 |
const wireTransmuxerEvents = function (self, transmuxer) {
|
|
|
51393 |
transmuxer.on('data', function (segment) {
|
|
|
51394 |
// transfer ownership of the underlying ArrayBuffer
|
|
|
51395 |
// instead of doing a copy to save memory
|
|
|
51396 |
// ArrayBuffers are transferable but generic TypedArrays are not
|
|
|
51397 |
// @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects)
|
|
|
51398 |
const initArray = segment.initSegment;
|
|
|
51399 |
segment.initSegment = {
|
|
|
51400 |
data: initArray.buffer,
|
|
|
51401 |
byteOffset: initArray.byteOffset,
|
|
|
51402 |
byteLength: initArray.byteLength
|
|
|
51403 |
};
|
|
|
51404 |
const typedArray = segment.data;
|
|
|
51405 |
segment.data = typedArray.buffer;
|
|
|
51406 |
self.postMessage({
|
|
|
51407 |
action: 'data',
|
|
|
51408 |
segment,
|
|
|
51409 |
byteOffset: typedArray.byteOffset,
|
|
|
51410 |
byteLength: typedArray.byteLength
|
|
|
51411 |
}, [segment.data]);
|
|
|
51412 |
});
|
|
|
51413 |
transmuxer.on('done', function (data) {
|
|
|
51414 |
self.postMessage({
|
|
|
51415 |
action: 'done'
|
|
|
51416 |
});
|
|
|
51417 |
});
|
|
|
51418 |
transmuxer.on('gopInfo', function (gopInfo) {
|
|
|
51419 |
self.postMessage({
|
|
|
51420 |
action: 'gopInfo',
|
|
|
51421 |
gopInfo
|
|
|
51422 |
});
|
|
|
51423 |
});
|
|
|
51424 |
transmuxer.on('videoSegmentTimingInfo', function (timingInfo) {
|
|
|
51425 |
const videoSegmentTimingInfo = {
|
|
|
51426 |
start: {
|
|
|
51427 |
decode: clock$2.videoTsToSeconds(timingInfo.start.dts),
|
|
|
51428 |
presentation: clock$2.videoTsToSeconds(timingInfo.start.pts)
|
|
|
51429 |
},
|
|
|
51430 |
end: {
|
|
|
51431 |
decode: clock$2.videoTsToSeconds(timingInfo.end.dts),
|
|
|
51432 |
presentation: clock$2.videoTsToSeconds(timingInfo.end.pts)
|
|
|
51433 |
},
|
|
|
51434 |
baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime)
|
|
|
51435 |
};
|
|
|
51436 |
if (timingInfo.prependedContentDuration) {
|
|
|
51437 |
videoSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration);
|
|
|
51438 |
}
|
|
|
51439 |
self.postMessage({
|
|
|
51440 |
action: 'videoSegmentTimingInfo',
|
|
|
51441 |
videoSegmentTimingInfo
|
|
|
51442 |
});
|
|
|
51443 |
});
|
|
|
51444 |
transmuxer.on('audioSegmentTimingInfo', function (timingInfo) {
|
|
|
51445 |
// Note that all times for [audio/video]SegmentTimingInfo events are in video clock
|
|
|
51446 |
const audioSegmentTimingInfo = {
|
|
|
51447 |
start: {
|
|
|
51448 |
decode: clock$2.videoTsToSeconds(timingInfo.start.dts),
|
|
|
51449 |
presentation: clock$2.videoTsToSeconds(timingInfo.start.pts)
|
|
|
51450 |
},
|
|
|
51451 |
end: {
|
|
|
51452 |
decode: clock$2.videoTsToSeconds(timingInfo.end.dts),
|
|
|
51453 |
presentation: clock$2.videoTsToSeconds(timingInfo.end.pts)
|
|
|
51454 |
},
|
|
|
51455 |
baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime)
|
|
|
51456 |
};
|
|
|
51457 |
if (timingInfo.prependedContentDuration) {
|
|
|
51458 |
audioSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration);
|
|
|
51459 |
}
|
|
|
51460 |
self.postMessage({
|
|
|
51461 |
action: 'audioSegmentTimingInfo',
|
|
|
51462 |
audioSegmentTimingInfo
|
|
|
51463 |
});
|
|
|
51464 |
});
|
|
|
51465 |
transmuxer.on('id3Frame', function (id3Frame) {
|
|
|
51466 |
self.postMessage({
|
|
|
51467 |
action: 'id3Frame',
|
|
|
51468 |
id3Frame
|
|
|
51469 |
});
|
|
|
51470 |
});
|
|
|
51471 |
transmuxer.on('caption', function (caption) {
|
|
|
51472 |
self.postMessage({
|
|
|
51473 |
action: 'caption',
|
|
|
51474 |
caption
|
|
|
51475 |
});
|
|
|
51476 |
});
|
|
|
51477 |
transmuxer.on('trackinfo', function (trackInfo) {
|
|
|
51478 |
self.postMessage({
|
|
|
51479 |
action: 'trackinfo',
|
|
|
51480 |
trackInfo
|
|
|
51481 |
});
|
|
|
51482 |
});
|
|
|
51483 |
transmuxer.on('audioTimingInfo', function (audioTimingInfo) {
|
|
|
51484 |
// convert to video TS since we prioritize video time over audio
|
|
|
51485 |
self.postMessage({
|
|
|
51486 |
action: 'audioTimingInfo',
|
|
|
51487 |
audioTimingInfo: {
|
|
|
51488 |
start: clock$2.videoTsToSeconds(audioTimingInfo.start),
|
|
|
51489 |
end: clock$2.videoTsToSeconds(audioTimingInfo.end)
|
|
|
51490 |
}
|
|
|
51491 |
});
|
|
|
51492 |
});
|
|
|
51493 |
transmuxer.on('videoTimingInfo', function (videoTimingInfo) {
|
|
|
51494 |
self.postMessage({
|
|
|
51495 |
action: 'videoTimingInfo',
|
|
|
51496 |
videoTimingInfo: {
|
|
|
51497 |
start: clock$2.videoTsToSeconds(videoTimingInfo.start),
|
|
|
51498 |
end: clock$2.videoTsToSeconds(videoTimingInfo.end)
|
|
|
51499 |
}
|
|
|
51500 |
});
|
|
|
51501 |
});
|
|
|
51502 |
transmuxer.on('log', function (log) {
|
|
|
51503 |
self.postMessage({
|
|
|
51504 |
action: 'log',
|
|
|
51505 |
log
|
|
|
51506 |
});
|
|
|
51507 |
});
|
|
|
51508 |
};
|
|
|
51509 |
/**
|
|
|
51510 |
* All incoming messages route through this hash. If no function exists
|
|
|
51511 |
* to handle an incoming message, then we ignore the message.
|
|
|
51512 |
*
|
|
|
51513 |
* @class MessageHandlers
|
|
|
51514 |
* @param {Object} options the options to initialize with
|
|
|
51515 |
*/
|
|
|
51516 |
|
|
|
51517 |
class MessageHandlers {
|
|
|
51518 |
constructor(self, options) {
|
|
|
51519 |
this.options = options || {};
|
|
|
51520 |
this.self = self;
|
|
|
51521 |
this.init();
|
|
|
51522 |
}
|
|
|
51523 |
/**
|
|
|
51524 |
* initialize our web worker and wire all the events.
|
|
|
51525 |
*/
|
|
|
51526 |
|
|
|
51527 |
init() {
|
|
|
51528 |
if (this.transmuxer) {
|
|
|
51529 |
this.transmuxer.dispose();
|
|
|
51530 |
}
|
|
|
51531 |
this.transmuxer = new transmuxer.Transmuxer(this.options);
|
|
|
51532 |
wireTransmuxerEvents(this.self, this.transmuxer);
|
|
|
51533 |
}
|
|
|
51534 |
pushMp4Captions(data) {
|
|
|
51535 |
if (!this.captionParser) {
|
|
|
51536 |
this.captionParser = new captionParser();
|
|
|
51537 |
this.captionParser.init();
|
|
|
51538 |
}
|
|
|
51539 |
const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
|
|
|
51540 |
const parsed = this.captionParser.parse(segment, data.trackIds, data.timescales);
|
|
|
51541 |
this.self.postMessage({
|
|
|
51542 |
action: 'mp4Captions',
|
|
|
51543 |
captions: parsed && parsed.captions || [],
|
|
|
51544 |
logs: parsed && parsed.logs || [],
|
|
|
51545 |
data: segment.buffer
|
|
|
51546 |
}, [segment.buffer]);
|
|
|
51547 |
}
|
|
|
51548 |
probeMp4StartTime({
|
|
|
51549 |
timescales,
|
|
|
51550 |
data
|
|
|
51551 |
}) {
|
|
|
51552 |
const startTime = probe$2.startTime(timescales, data);
|
|
|
51553 |
this.self.postMessage({
|
|
|
51554 |
action: 'probeMp4StartTime',
|
|
|
51555 |
startTime,
|
|
|
51556 |
data
|
|
|
51557 |
}, [data.buffer]);
|
|
|
51558 |
}
|
|
|
51559 |
probeMp4Tracks({
|
|
|
51560 |
data
|
|
|
51561 |
}) {
|
|
|
51562 |
const tracks = probe$2.tracks(data);
|
|
|
51563 |
this.self.postMessage({
|
|
|
51564 |
action: 'probeMp4Tracks',
|
|
|
51565 |
tracks,
|
|
|
51566 |
data
|
|
|
51567 |
}, [data.buffer]);
|
|
|
51568 |
}
|
|
|
51569 |
/**
|
|
|
51570 |
* Probes an mp4 segment for EMSG boxes containing ID3 data.
|
|
|
51571 |
* https://aomediacodec.github.io/id3-emsg/
|
|
|
51572 |
*
|
|
|
51573 |
* @param {Uint8Array} data segment data
|
|
|
51574 |
* @param {number} offset segment start time
|
|
|
51575 |
* @return {Object[]} an array of ID3 frames
|
|
|
51576 |
*/
|
|
|
51577 |
|
|
|
51578 |
probeEmsgID3({
|
|
|
51579 |
data,
|
|
|
51580 |
offset
|
|
|
51581 |
}) {
|
|
|
51582 |
const id3Frames = probe$2.getEmsgID3(data, offset);
|
|
|
51583 |
this.self.postMessage({
|
|
|
51584 |
action: 'probeEmsgID3',
|
|
|
51585 |
id3Frames,
|
|
|
51586 |
emsgData: data
|
|
|
51587 |
}, [data.buffer]);
|
|
|
51588 |
}
|
|
|
51589 |
/**
|
|
|
51590 |
* Probe an mpeg2-ts segment to determine the start time of the segment in it's
|
|
|
51591 |
* internal "media time," as well as whether it contains video and/or audio.
|
|
|
51592 |
*
|
|
|
51593 |
* @private
|
|
|
51594 |
* @param {Uint8Array} bytes - segment bytes
|
|
|
51595 |
* @param {number} baseStartTime
|
|
|
51596 |
* Relative reference timestamp used when adjusting frame timestamps for rollover.
|
|
|
51597 |
* This value should be in seconds, as it's converted to a 90khz clock within the
|
|
|
51598 |
* function body.
|
|
|
51599 |
* @return {Object} The start time of the current segment in "media time" as well as
|
|
|
51600 |
* whether it contains video and/or audio
|
|
|
51601 |
*/
|
|
|
51602 |
|
|
|
51603 |
probeTs({
|
|
|
51604 |
data,
|
|
|
51605 |
baseStartTime
|
|
|
51606 |
}) {
|
|
|
51607 |
const tsStartTime = typeof baseStartTime === 'number' && !isNaN(baseStartTime) ? baseStartTime * clock$2.ONE_SECOND_IN_TS : void 0;
|
|
|
51608 |
const timeInfo = tsInspector.inspect(data, tsStartTime);
|
|
|
51609 |
let result = null;
|
|
|
51610 |
if (timeInfo) {
|
|
|
51611 |
result = {
|
|
|
51612 |
// each type's time info comes back as an array of 2 times, start and end
|
|
|
51613 |
hasVideo: timeInfo.video && timeInfo.video.length === 2 || false,
|
|
|
51614 |
hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false
|
|
|
51615 |
};
|
|
|
51616 |
if (result.hasVideo) {
|
|
|
51617 |
result.videoStart = timeInfo.video[0].ptsTime;
|
|
|
51618 |
}
|
|
|
51619 |
if (result.hasAudio) {
|
|
|
51620 |
result.audioStart = timeInfo.audio[0].ptsTime;
|
|
|
51621 |
}
|
|
|
51622 |
}
|
|
|
51623 |
this.self.postMessage({
|
|
|
51624 |
action: 'probeTs',
|
|
|
51625 |
result,
|
|
|
51626 |
data
|
|
|
51627 |
}, [data.buffer]);
|
|
|
51628 |
}
|
|
|
51629 |
clearAllMp4Captions() {
|
|
|
51630 |
if (this.captionParser) {
|
|
|
51631 |
this.captionParser.clearAllCaptions();
|
|
|
51632 |
}
|
|
|
51633 |
}
|
|
|
51634 |
clearParsedMp4Captions() {
|
|
|
51635 |
if (this.captionParser) {
|
|
|
51636 |
this.captionParser.clearParsedCaptions();
|
|
|
51637 |
}
|
|
|
51638 |
}
|
|
|
51639 |
/**
|
|
|
51640 |
* Adds data (a ts segment) to the start of the transmuxer pipeline for
|
|
|
51641 |
* processing.
|
|
|
51642 |
*
|
|
|
51643 |
* @param {ArrayBuffer} data data to push into the muxer
|
|
|
51644 |
*/
|
|
|
51645 |
|
|
|
51646 |
push(data) {
|
|
|
51647 |
// Cast array buffer to correct type for transmuxer
|
|
|
51648 |
const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
|
|
|
51649 |
this.transmuxer.push(segment);
|
|
|
51650 |
}
|
|
|
51651 |
/**
|
|
|
51652 |
* Recreate the transmuxer so that the next segment added via `push`
|
|
|
51653 |
* start with a fresh transmuxer.
|
|
|
51654 |
*/
|
|
|
51655 |
|
|
|
51656 |
reset() {
|
|
|
51657 |
this.transmuxer.reset();
|
|
|
51658 |
}
|
|
|
51659 |
/**
|
|
|
51660 |
* Set the value that will be used as the `baseMediaDecodeTime` time for the
|
|
|
51661 |
* next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime`
|
|
|
51662 |
* set relative to the first based on the PTS values.
|
|
|
51663 |
*
|
|
|
51664 |
* @param {Object} data used to set the timestamp offset in the muxer
|
|
|
51665 |
*/
|
|
|
51666 |
|
|
|
51667 |
setTimestampOffset(data) {
|
|
|
51668 |
const timestampOffset = data.timestampOffset || 0;
|
|
|
51669 |
this.transmuxer.setBaseMediaDecodeTime(Math.round(clock$2.secondsToVideoTs(timestampOffset)));
|
|
|
51670 |
}
|
|
|
51671 |
setAudioAppendStart(data) {
|
|
|
51672 |
this.transmuxer.setAudioAppendStart(Math.ceil(clock$2.secondsToVideoTs(data.appendStart)));
|
|
|
51673 |
}
|
|
|
51674 |
setRemux(data) {
|
|
|
51675 |
this.transmuxer.setRemux(data.remux);
|
|
|
51676 |
}
|
|
|
51677 |
/**
|
|
|
51678 |
* Forces the pipeline to finish processing the last segment and emit it's
|
|
|
51679 |
* results.
|
|
|
51680 |
*
|
|
|
51681 |
* @param {Object} data event data, not really used
|
|
|
51682 |
*/
|
|
|
51683 |
|
|
|
51684 |
flush(data) {
|
|
|
51685 |
this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed
|
|
|
51686 |
|
|
|
51687 |
self.postMessage({
|
|
|
51688 |
action: 'done',
|
|
|
51689 |
type: 'transmuxed'
|
|
|
51690 |
});
|
|
|
51691 |
}
|
|
|
51692 |
endTimeline() {
|
|
|
51693 |
this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their
|
|
|
51694 |
// timelines
|
|
|
51695 |
|
|
|
51696 |
self.postMessage({
|
|
|
51697 |
action: 'endedtimeline',
|
|
|
51698 |
type: 'transmuxed'
|
|
|
51699 |
});
|
|
|
51700 |
}
|
|
|
51701 |
alignGopsWith(data) {
|
|
|
51702 |
this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice());
|
|
|
51703 |
}
|
|
|
51704 |
}
|
|
|
51705 |
/**
|
|
|
51706 |
* Our web worker interface so that things can talk to mux.js
|
|
|
51707 |
* that will be running in a web worker. the scope is passed to this by
|
|
|
51708 |
* webworkify.
|
|
|
51709 |
*
|
|
|
51710 |
* @param {Object} self the scope for the web worker
|
|
|
51711 |
*/
|
|
|
51712 |
|
|
|
51713 |
self.onmessage = function (event) {
|
|
|
51714 |
if (event.data.action === 'init' && event.data.options) {
|
|
|
51715 |
this.messageHandlers = new MessageHandlers(self, event.data.options);
|
|
|
51716 |
return;
|
|
|
51717 |
}
|
|
|
51718 |
if (!this.messageHandlers) {
|
|
|
51719 |
this.messageHandlers = new MessageHandlers(self);
|
|
|
51720 |
}
|
|
|
51721 |
if (event.data && event.data.action && event.data.action !== 'init') {
|
|
|
51722 |
if (this.messageHandlers[event.data.action]) {
|
|
|
51723 |
this.messageHandlers[event.data.action](event.data);
|
|
|
51724 |
}
|
|
|
51725 |
}
|
|
|
51726 |
};
|
|
|
51727 |
}));
|
|
|
51728 |
var TransmuxWorker = factory(workerCode$1);
|
|
|
51729 |
/* rollup-plugin-worker-factory end for worker!/home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */
|
|
|
51730 |
|
|
|
51731 |
const handleData_ = (event, transmuxedData, callback) => {
|
|
|
51732 |
const {
|
|
|
51733 |
type,
|
|
|
51734 |
initSegment,
|
|
|
51735 |
captions,
|
|
|
51736 |
captionStreams,
|
|
|
51737 |
metadata,
|
|
|
51738 |
videoFrameDtsTime,
|
|
|
51739 |
videoFramePtsTime
|
|
|
51740 |
} = event.data.segment;
|
|
|
51741 |
transmuxedData.buffer.push({
|
|
|
51742 |
captions,
|
|
|
51743 |
captionStreams,
|
|
|
51744 |
metadata
|
|
|
51745 |
});
|
|
|
51746 |
const boxes = event.data.segment.boxes || {
|
|
|
51747 |
data: event.data.segment.data
|
|
|
51748 |
};
|
|
|
51749 |
const result = {
|
|
|
51750 |
type,
|
|
|
51751 |
// cast ArrayBuffer to TypedArray
|
|
|
51752 |
data: new Uint8Array(boxes.data, boxes.data.byteOffset, boxes.data.byteLength),
|
|
|
51753 |
initSegment: new Uint8Array(initSegment.data, initSegment.byteOffset, initSegment.byteLength)
|
|
|
51754 |
};
|
|
|
51755 |
if (typeof videoFrameDtsTime !== 'undefined') {
|
|
|
51756 |
result.videoFrameDtsTime = videoFrameDtsTime;
|
|
|
51757 |
}
|
|
|
51758 |
if (typeof videoFramePtsTime !== 'undefined') {
|
|
|
51759 |
result.videoFramePtsTime = videoFramePtsTime;
|
|
|
51760 |
}
|
|
|
51761 |
callback(result);
|
|
|
51762 |
};
|
|
|
51763 |
const handleDone_ = ({
|
|
|
51764 |
transmuxedData,
|
|
|
51765 |
callback
|
|
|
51766 |
}) => {
|
|
|
51767 |
// Previously we only returned data on data events,
|
|
|
51768 |
// not on done events. Clear out the buffer to keep that consistent.
|
|
|
51769 |
transmuxedData.buffer = []; // all buffers should have been flushed from the muxer, so start processing anything we
|
|
|
51770 |
// have received
|
|
|
51771 |
|
|
|
51772 |
callback(transmuxedData);
|
|
|
51773 |
};
|
|
|
51774 |
const handleGopInfo_ = (event, transmuxedData) => {
|
|
|
51775 |
transmuxedData.gopInfo = event.data.gopInfo;
|
|
|
51776 |
};
|
|
|
51777 |
const processTransmux = options => {
|
|
|
51778 |
const {
|
|
|
51779 |
transmuxer,
|
|
|
51780 |
bytes,
|
|
|
51781 |
audioAppendStart,
|
|
|
51782 |
gopsToAlignWith,
|
|
|
51783 |
remux,
|
|
|
51784 |
onData,
|
|
|
51785 |
onTrackInfo,
|
|
|
51786 |
onAudioTimingInfo,
|
|
|
51787 |
onVideoTimingInfo,
|
|
|
51788 |
onVideoSegmentTimingInfo,
|
|
|
51789 |
onAudioSegmentTimingInfo,
|
|
|
51790 |
onId3,
|
|
|
51791 |
onCaptions,
|
|
|
51792 |
onDone,
|
|
|
51793 |
onEndedTimeline,
|
|
|
51794 |
onTransmuxerLog,
|
|
|
51795 |
isEndOfTimeline
|
|
|
51796 |
} = options;
|
|
|
51797 |
const transmuxedData = {
|
|
|
51798 |
buffer: []
|
|
|
51799 |
};
|
|
|
51800 |
let waitForEndedTimelineEvent = isEndOfTimeline;
|
|
|
51801 |
const handleMessage = event => {
|
|
|
51802 |
if (transmuxer.currentTransmux !== options) {
|
|
|
51803 |
// disposed
|
|
|
51804 |
return;
|
|
|
51805 |
}
|
|
|
51806 |
if (event.data.action === 'data') {
|
|
|
51807 |
handleData_(event, transmuxedData, onData);
|
|
|
51808 |
}
|
|
|
51809 |
if (event.data.action === 'trackinfo') {
|
|
|
51810 |
onTrackInfo(event.data.trackInfo);
|
|
|
51811 |
}
|
|
|
51812 |
if (event.data.action === 'gopInfo') {
|
|
|
51813 |
handleGopInfo_(event, transmuxedData);
|
|
|
51814 |
}
|
|
|
51815 |
if (event.data.action === 'audioTimingInfo') {
|
|
|
51816 |
onAudioTimingInfo(event.data.audioTimingInfo);
|
|
|
51817 |
}
|
|
|
51818 |
if (event.data.action === 'videoTimingInfo') {
|
|
|
51819 |
onVideoTimingInfo(event.data.videoTimingInfo);
|
|
|
51820 |
}
|
|
|
51821 |
if (event.data.action === 'videoSegmentTimingInfo') {
|
|
|
51822 |
onVideoSegmentTimingInfo(event.data.videoSegmentTimingInfo);
|
|
|
51823 |
}
|
|
|
51824 |
if (event.data.action === 'audioSegmentTimingInfo') {
|
|
|
51825 |
onAudioSegmentTimingInfo(event.data.audioSegmentTimingInfo);
|
|
|
51826 |
}
|
|
|
51827 |
if (event.data.action === 'id3Frame') {
|
|
|
51828 |
onId3([event.data.id3Frame], event.data.id3Frame.dispatchType);
|
|
|
51829 |
}
|
|
|
51830 |
if (event.data.action === 'caption') {
|
|
|
51831 |
onCaptions(event.data.caption);
|
|
|
51832 |
}
|
|
|
51833 |
if (event.data.action === 'endedtimeline') {
|
|
|
51834 |
waitForEndedTimelineEvent = false;
|
|
|
51835 |
onEndedTimeline();
|
|
|
51836 |
}
|
|
|
51837 |
if (event.data.action === 'log') {
|
|
|
51838 |
onTransmuxerLog(event.data.log);
|
|
|
51839 |
} // wait for the transmuxed event since we may have audio and video
|
|
|
51840 |
|
|
|
51841 |
if (event.data.type !== 'transmuxed') {
|
|
|
51842 |
return;
|
|
|
51843 |
} // If the "endedtimeline" event has not yet fired, and this segment represents the end
|
|
|
51844 |
// of a timeline, that means there may still be data events before the segment
|
|
|
51845 |
// processing can be considerred complete. In that case, the final event should be
|
|
|
51846 |
// an "endedtimeline" event with the type "transmuxed."
|
|
|
51847 |
|
|
|
51848 |
if (waitForEndedTimelineEvent) {
|
|
|
51849 |
return;
|
|
|
51850 |
}
|
|
|
51851 |
transmuxer.onmessage = null;
|
|
|
51852 |
handleDone_({
|
|
|
51853 |
transmuxedData,
|
|
|
51854 |
callback: onDone
|
|
|
51855 |
});
|
|
|
51856 |
/* eslint-disable no-use-before-define */
|
|
|
51857 |
|
|
|
51858 |
dequeue(transmuxer);
|
|
|
51859 |
/* eslint-enable */
|
|
|
51860 |
};
|
|
|
51861 |
|
|
|
51862 |
transmuxer.onmessage = handleMessage;
|
|
|
51863 |
if (audioAppendStart) {
|
|
|
51864 |
transmuxer.postMessage({
|
|
|
51865 |
action: 'setAudioAppendStart',
|
|
|
51866 |
appendStart: audioAppendStart
|
|
|
51867 |
});
|
|
|
51868 |
} // allow empty arrays to be passed to clear out GOPs
|
|
|
51869 |
|
|
|
51870 |
if (Array.isArray(gopsToAlignWith)) {
|
|
|
51871 |
transmuxer.postMessage({
|
|
|
51872 |
action: 'alignGopsWith',
|
|
|
51873 |
gopsToAlignWith
|
|
|
51874 |
});
|
|
|
51875 |
}
|
|
|
51876 |
if (typeof remux !== 'undefined') {
|
|
|
51877 |
transmuxer.postMessage({
|
|
|
51878 |
action: 'setRemux',
|
|
|
51879 |
remux
|
|
|
51880 |
});
|
|
|
51881 |
}
|
|
|
51882 |
if (bytes.byteLength) {
|
|
|
51883 |
const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer;
|
|
|
51884 |
const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset;
|
|
|
51885 |
transmuxer.postMessage({
|
|
|
51886 |
action: 'push',
|
|
|
51887 |
// Send the typed-array of data as an ArrayBuffer so that
|
|
|
51888 |
// it can be sent as a "Transferable" and avoid the costly
|
|
|
51889 |
// memory copy
|
|
|
51890 |
data: buffer,
|
|
|
51891 |
// To recreate the original typed-array, we need information
|
|
|
51892 |
// about what portion of the ArrayBuffer it was a view into
|
|
|
51893 |
byteOffset,
|
|
|
51894 |
byteLength: bytes.byteLength
|
|
|
51895 |
}, [buffer]);
|
|
|
51896 |
}
|
|
|
51897 |
if (isEndOfTimeline) {
|
|
|
51898 |
transmuxer.postMessage({
|
|
|
51899 |
action: 'endTimeline'
|
|
|
51900 |
});
|
|
|
51901 |
} // even if we didn't push any bytes, we have to make sure we flush in case we reached
|
|
|
51902 |
// the end of the segment
|
|
|
51903 |
|
|
|
51904 |
transmuxer.postMessage({
|
|
|
51905 |
action: 'flush'
|
|
|
51906 |
});
|
|
|
51907 |
};
|
|
|
51908 |
const dequeue = transmuxer => {
|
|
|
51909 |
transmuxer.currentTransmux = null;
|
|
|
51910 |
if (transmuxer.transmuxQueue.length) {
|
|
|
51911 |
transmuxer.currentTransmux = transmuxer.transmuxQueue.shift();
|
|
|
51912 |
if (typeof transmuxer.currentTransmux === 'function') {
|
|
|
51913 |
transmuxer.currentTransmux();
|
|
|
51914 |
} else {
|
|
|
51915 |
processTransmux(transmuxer.currentTransmux);
|
|
|
51916 |
}
|
|
|
51917 |
}
|
|
|
51918 |
};
|
|
|
51919 |
const processAction = (transmuxer, action) => {
|
|
|
51920 |
transmuxer.postMessage({
|
|
|
51921 |
action
|
|
|
51922 |
});
|
|
|
51923 |
dequeue(transmuxer);
|
|
|
51924 |
};
|
|
|
51925 |
const enqueueAction = (action, transmuxer) => {
|
|
|
51926 |
if (!transmuxer.currentTransmux) {
|
|
|
51927 |
transmuxer.currentTransmux = action;
|
|
|
51928 |
processAction(transmuxer, action);
|
|
|
51929 |
return;
|
|
|
51930 |
}
|
|
|
51931 |
transmuxer.transmuxQueue.push(processAction.bind(null, transmuxer, action));
|
|
|
51932 |
};
|
|
|
51933 |
const reset = transmuxer => {
|
|
|
51934 |
enqueueAction('reset', transmuxer);
|
|
|
51935 |
};
|
|
|
51936 |
const endTimeline = transmuxer => {
|
|
|
51937 |
enqueueAction('endTimeline', transmuxer);
|
|
|
51938 |
};
|
|
|
51939 |
const transmux = options => {
|
|
|
51940 |
if (!options.transmuxer.currentTransmux) {
|
|
|
51941 |
options.transmuxer.currentTransmux = options;
|
|
|
51942 |
processTransmux(options);
|
|
|
51943 |
return;
|
|
|
51944 |
}
|
|
|
51945 |
options.transmuxer.transmuxQueue.push(options);
|
|
|
51946 |
};
|
|
|
51947 |
const createTransmuxer = options => {
|
|
|
51948 |
const transmuxer = new TransmuxWorker();
|
|
|
51949 |
transmuxer.currentTransmux = null;
|
|
|
51950 |
transmuxer.transmuxQueue = [];
|
|
|
51951 |
const term = transmuxer.terminate;
|
|
|
51952 |
transmuxer.terminate = () => {
|
|
|
51953 |
transmuxer.currentTransmux = null;
|
|
|
51954 |
transmuxer.transmuxQueue.length = 0;
|
|
|
51955 |
return term.call(transmuxer);
|
|
|
51956 |
};
|
|
|
51957 |
transmuxer.postMessage({
|
|
|
51958 |
action: 'init',
|
|
|
51959 |
options
|
|
|
51960 |
});
|
|
|
51961 |
return transmuxer;
|
|
|
51962 |
};
|
|
|
51963 |
var segmentTransmuxer = {
|
|
|
51964 |
reset,
|
|
|
51965 |
endTimeline,
|
|
|
51966 |
transmux,
|
|
|
51967 |
createTransmuxer
|
|
|
51968 |
};
|
|
|
51969 |
const workerCallback = function (options) {
|
|
|
51970 |
const transmuxer = options.transmuxer;
|
|
|
51971 |
const endAction = options.endAction || options.action;
|
|
|
51972 |
const callback = options.callback;
|
|
|
51973 |
const message = _extends$1({}, options, {
|
|
|
51974 |
endAction: null,
|
|
|
51975 |
transmuxer: null,
|
|
|
51976 |
callback: null
|
|
|
51977 |
});
|
|
|
51978 |
const listenForEndEvent = event => {
|
|
|
51979 |
if (event.data.action !== endAction) {
|
|
|
51980 |
return;
|
|
|
51981 |
}
|
|
|
51982 |
transmuxer.removeEventListener('message', listenForEndEvent); // transfer ownership of bytes back to us.
|
|
|
51983 |
|
|
|
51984 |
if (event.data.data) {
|
|
|
51985 |
event.data.data = new Uint8Array(event.data.data, options.byteOffset || 0, options.byteLength || event.data.data.byteLength);
|
|
|
51986 |
if (options.data) {
|
|
|
51987 |
options.data = event.data.data;
|
|
|
51988 |
}
|
|
|
51989 |
}
|
|
|
51990 |
callback(event.data);
|
|
|
51991 |
};
|
|
|
51992 |
transmuxer.addEventListener('message', listenForEndEvent);
|
|
|
51993 |
if (options.data) {
|
|
|
51994 |
const isArrayBuffer = options.data instanceof ArrayBuffer;
|
|
|
51995 |
message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset;
|
|
|
51996 |
message.byteLength = options.data.byteLength;
|
|
|
51997 |
const transfers = [isArrayBuffer ? options.data : options.data.buffer];
|
|
|
51998 |
transmuxer.postMessage(message, transfers);
|
|
|
51999 |
} else {
|
|
|
52000 |
transmuxer.postMessage(message);
|
|
|
52001 |
}
|
|
|
52002 |
};
|
|
|
52003 |
const REQUEST_ERRORS = {
|
|
|
52004 |
FAILURE: 2,
|
|
|
52005 |
TIMEOUT: -101,
|
|
|
52006 |
ABORTED: -102
|
|
|
52007 |
};
|
|
|
52008 |
/**
|
|
|
52009 |
* Abort all requests
|
|
|
52010 |
*
|
|
|
52011 |
* @param {Object} activeXhrs - an object that tracks all XHR requests
|
|
|
52012 |
*/
|
|
|
52013 |
|
|
|
52014 |
const abortAll = activeXhrs => {
|
|
|
52015 |
activeXhrs.forEach(xhr => {
|
|
|
52016 |
xhr.abort();
|
|
|
52017 |
});
|
|
|
52018 |
};
|
|
|
52019 |
/**
|
|
|
52020 |
* Gather important bandwidth stats once a request has completed
|
|
|
52021 |
*
|
|
|
52022 |
* @param {Object} request - the XHR request from which to gather stats
|
|
|
52023 |
*/
|
|
|
52024 |
|
|
|
52025 |
const getRequestStats = request => {
|
|
|
52026 |
return {
|
|
|
52027 |
bandwidth: request.bandwidth,
|
|
|
52028 |
bytesReceived: request.bytesReceived || 0,
|
|
|
52029 |
roundTripTime: request.roundTripTime || 0
|
|
|
52030 |
};
|
|
|
52031 |
};
|
|
|
52032 |
/**
|
|
|
52033 |
* If possible gather bandwidth stats as a request is in
|
|
|
52034 |
* progress
|
|
|
52035 |
*
|
|
|
52036 |
* @param {Event} progressEvent - an event object from an XHR's progress event
|
|
|
52037 |
*/
|
|
|
52038 |
|
|
|
52039 |
const getProgressStats = progressEvent => {
|
|
|
52040 |
const request = progressEvent.target;
|
|
|
52041 |
const roundTripTime = Date.now() - request.requestTime;
|
|
|
52042 |
const stats = {
|
|
|
52043 |
bandwidth: Infinity,
|
|
|
52044 |
bytesReceived: 0,
|
|
|
52045 |
roundTripTime: roundTripTime || 0
|
|
|
52046 |
};
|
|
|
52047 |
stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok
|
|
|
52048 |
// because we should only use bandwidth stats on progress to determine when
|
|
|
52049 |
// abort a request early due to insufficient bandwidth
|
|
|
52050 |
|
|
|
52051 |
stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000);
|
|
|
52052 |
return stats;
|
|
|
52053 |
};
|
|
|
52054 |
/**
|
|
|
52055 |
* Handle all error conditions in one place and return an object
|
|
|
52056 |
* with all the information
|
|
|
52057 |
*
|
|
|
52058 |
* @param {Error|null} error - if non-null signals an error occured with the XHR
|
|
|
52059 |
* @param {Object} request - the XHR request that possibly generated the error
|
|
|
52060 |
*/
|
|
|
52061 |
|
|
|
52062 |
const handleErrors = (error, request) => {
|
|
|
52063 |
if (request.timedout) {
|
|
|
52064 |
return {
|
|
|
52065 |
status: request.status,
|
|
|
52066 |
message: 'HLS request timed-out at URL: ' + request.uri,
|
|
|
52067 |
code: REQUEST_ERRORS.TIMEOUT,
|
|
|
52068 |
xhr: request
|
|
|
52069 |
};
|
|
|
52070 |
}
|
|
|
52071 |
if (request.aborted) {
|
|
|
52072 |
return {
|
|
|
52073 |
status: request.status,
|
|
|
52074 |
message: 'HLS request aborted at URL: ' + request.uri,
|
|
|
52075 |
code: REQUEST_ERRORS.ABORTED,
|
|
|
52076 |
xhr: request
|
|
|
52077 |
};
|
|
|
52078 |
}
|
|
|
52079 |
if (error) {
|
|
|
52080 |
return {
|
|
|
52081 |
status: request.status,
|
|
|
52082 |
message: 'HLS request errored at URL: ' + request.uri,
|
|
|
52083 |
code: REQUEST_ERRORS.FAILURE,
|
|
|
52084 |
xhr: request
|
|
|
52085 |
};
|
|
|
52086 |
}
|
|
|
52087 |
if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) {
|
|
|
52088 |
return {
|
|
|
52089 |
status: request.status,
|
|
|
52090 |
message: 'Empty HLS response at URL: ' + request.uri,
|
|
|
52091 |
code: REQUEST_ERRORS.FAILURE,
|
|
|
52092 |
xhr: request
|
|
|
52093 |
};
|
|
|
52094 |
}
|
|
|
52095 |
return null;
|
|
|
52096 |
};
|
|
|
52097 |
/**
|
|
|
52098 |
* Handle responses for key data and convert the key data to the correct format
|
|
|
52099 |
* for the decryption step later
|
|
|
52100 |
*
|
|
|
52101 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
52102 |
* from SegmentLoader
|
|
|
52103 |
* @param {Array} objects - objects to add the key bytes to.
|
|
|
52104 |
* @param {Function} finishProcessingFn - a callback to execute to continue processing
|
|
|
52105 |
* this request
|
|
|
52106 |
*/
|
|
|
52107 |
|
|
|
52108 |
const handleKeyResponse = (segment, objects, finishProcessingFn) => (error, request) => {
|
|
|
52109 |
const response = request.response;
|
|
|
52110 |
const errorObj = handleErrors(error, request);
|
|
|
52111 |
if (errorObj) {
|
|
|
52112 |
return finishProcessingFn(errorObj, segment);
|
|
|
52113 |
}
|
|
|
52114 |
if (response.byteLength !== 16) {
|
|
|
52115 |
return finishProcessingFn({
|
|
|
52116 |
status: request.status,
|
|
|
52117 |
message: 'Invalid HLS key at URL: ' + request.uri,
|
|
|
52118 |
code: REQUEST_ERRORS.FAILURE,
|
|
|
52119 |
xhr: request
|
|
|
52120 |
}, segment);
|
|
|
52121 |
}
|
|
|
52122 |
const view = new DataView(response);
|
|
|
52123 |
const bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
|
|
|
52124 |
for (let i = 0; i < objects.length; i++) {
|
|
|
52125 |
objects[i].bytes = bytes;
|
|
|
52126 |
}
|
|
|
52127 |
return finishProcessingFn(null, segment);
|
|
|
52128 |
};
|
|
|
52129 |
const parseInitSegment = (segment, callback) => {
|
|
|
52130 |
const type = detectContainerForBytes(segment.map.bytes); // TODO: We should also handle ts init segments here, but we
|
|
|
52131 |
// only know how to parse mp4 init segments at the moment
|
|
|
52132 |
|
|
|
52133 |
if (type !== 'mp4') {
|
|
|
52134 |
const uri = segment.map.resolvedUri || segment.map.uri;
|
|
|
52135 |
return callback({
|
|
|
52136 |
internal: true,
|
|
|
52137 |
message: `Found unsupported ${type || 'unknown'} container for initialization segment at URL: ${uri}`,
|
|
|
52138 |
code: REQUEST_ERRORS.FAILURE
|
|
|
52139 |
});
|
|
|
52140 |
}
|
|
|
52141 |
workerCallback({
|
|
|
52142 |
action: 'probeMp4Tracks',
|
|
|
52143 |
data: segment.map.bytes,
|
|
|
52144 |
transmuxer: segment.transmuxer,
|
|
|
52145 |
callback: ({
|
|
|
52146 |
tracks,
|
|
|
52147 |
data
|
|
|
52148 |
}) => {
|
|
|
52149 |
// transfer bytes back to us
|
|
|
52150 |
segment.map.bytes = data;
|
|
|
52151 |
tracks.forEach(function (track) {
|
|
|
52152 |
segment.map.tracks = segment.map.tracks || {}; // only support one track of each type for now
|
|
|
52153 |
|
|
|
52154 |
if (segment.map.tracks[track.type]) {
|
|
|
52155 |
return;
|
|
|
52156 |
}
|
|
|
52157 |
segment.map.tracks[track.type] = track;
|
|
|
52158 |
if (typeof track.id === 'number' && track.timescale) {
|
|
|
52159 |
segment.map.timescales = segment.map.timescales || {};
|
|
|
52160 |
segment.map.timescales[track.id] = track.timescale;
|
|
|
52161 |
}
|
|
|
52162 |
});
|
|
|
52163 |
return callback(null);
|
|
|
52164 |
}
|
|
|
52165 |
});
|
|
|
52166 |
};
|
|
|
52167 |
/**
|
|
|
52168 |
* Handle init-segment responses
|
|
|
52169 |
*
|
|
|
52170 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
52171 |
* from SegmentLoader
|
|
|
52172 |
* @param {Function} finishProcessingFn - a callback to execute to continue processing
|
|
|
52173 |
* this request
|
|
|
52174 |
*/
|
|
|
52175 |
|
|
|
52176 |
const handleInitSegmentResponse = ({
|
|
|
52177 |
segment,
|
|
|
52178 |
finishProcessingFn
|
|
|
52179 |
}) => (error, request) => {
|
|
|
52180 |
const errorObj = handleErrors(error, request);
|
|
|
52181 |
if (errorObj) {
|
|
|
52182 |
return finishProcessingFn(errorObj, segment);
|
|
|
52183 |
}
|
|
|
52184 |
const bytes = new Uint8Array(request.response); // init segment is encypted, we will have to wait
|
|
|
52185 |
// until the key request is done to decrypt.
|
|
|
52186 |
|
|
|
52187 |
if (segment.map.key) {
|
|
|
52188 |
segment.map.encryptedBytes = bytes;
|
|
|
52189 |
return finishProcessingFn(null, segment);
|
|
|
52190 |
}
|
|
|
52191 |
segment.map.bytes = bytes;
|
|
|
52192 |
parseInitSegment(segment, function (parseError) {
|
|
|
52193 |
if (parseError) {
|
|
|
52194 |
parseError.xhr = request;
|
|
|
52195 |
parseError.status = request.status;
|
|
|
52196 |
return finishProcessingFn(parseError, segment);
|
|
|
52197 |
}
|
|
|
52198 |
finishProcessingFn(null, segment);
|
|
|
52199 |
});
|
|
|
52200 |
};
|
|
|
52201 |
/**
|
|
|
52202 |
* Response handler for segment-requests being sure to set the correct
|
|
|
52203 |
* property depending on whether the segment is encryped or not
|
|
|
52204 |
* Also records and keeps track of stats that are used for ABR purposes
|
|
|
52205 |
*
|
|
|
52206 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
52207 |
* from SegmentLoader
|
|
|
52208 |
* @param {Function} finishProcessingFn - a callback to execute to continue processing
|
|
|
52209 |
* this request
|
|
|
52210 |
*/
|
|
|
52211 |
|
|
|
52212 |
const handleSegmentResponse = ({
|
|
|
52213 |
segment,
|
|
|
52214 |
finishProcessingFn,
|
|
|
52215 |
responseType
|
|
|
52216 |
}) => (error, request) => {
|
|
|
52217 |
const errorObj = handleErrors(error, request);
|
|
|
52218 |
if (errorObj) {
|
|
|
52219 |
return finishProcessingFn(errorObj, segment);
|
|
|
52220 |
}
|
|
|
52221 |
const newBytes =
|
|
|
52222 |
// although responseText "should" exist, this guard serves to prevent an error being
|
|
|
52223 |
// thrown for two primary cases:
|
|
|
52224 |
// 1. the mime type override stops working, or is not implemented for a specific
|
|
|
52225 |
// browser
|
|
|
52226 |
// 2. when using mock XHR libraries like sinon that do not allow the override behavior
|
|
|
52227 |
responseType === 'arraybuffer' || !request.responseText ? request.response : stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0));
|
|
|
52228 |
segment.stats = getRequestStats(request);
|
|
|
52229 |
if (segment.key) {
|
|
|
52230 |
segment.encryptedBytes = new Uint8Array(newBytes);
|
|
|
52231 |
} else {
|
|
|
52232 |
segment.bytes = new Uint8Array(newBytes);
|
|
|
52233 |
}
|
|
|
52234 |
return finishProcessingFn(null, segment);
|
|
|
52235 |
};
|
|
|
52236 |
const transmuxAndNotify = ({
|
|
|
52237 |
segment,
|
|
|
52238 |
bytes,
|
|
|
52239 |
trackInfoFn,
|
|
|
52240 |
timingInfoFn,
|
|
|
52241 |
videoSegmentTimingInfoFn,
|
|
|
52242 |
audioSegmentTimingInfoFn,
|
|
|
52243 |
id3Fn,
|
|
|
52244 |
captionsFn,
|
|
|
52245 |
isEndOfTimeline,
|
|
|
52246 |
endedTimelineFn,
|
|
|
52247 |
dataFn,
|
|
|
52248 |
doneFn,
|
|
|
52249 |
onTransmuxerLog
|
|
|
52250 |
}) => {
|
|
|
52251 |
const fmp4Tracks = segment.map && segment.map.tracks || {};
|
|
|
52252 |
const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); // Keep references to each function so we can null them out after we're done with them.
|
|
|
52253 |
// One reason for this is that in the case of full segments, we want to trust start
|
|
|
52254 |
// times from the probe, rather than the transmuxer.
|
|
|
52255 |
|
|
|
52256 |
let audioStartFn = timingInfoFn.bind(null, segment, 'audio', 'start');
|
|
|
52257 |
const audioEndFn = timingInfoFn.bind(null, segment, 'audio', 'end');
|
|
|
52258 |
let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start');
|
|
|
52259 |
const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end');
|
|
|
52260 |
const finish = () => transmux({
|
|
|
52261 |
bytes,
|
|
|
52262 |
transmuxer: segment.transmuxer,
|
|
|
52263 |
audioAppendStart: segment.audioAppendStart,
|
|
|
52264 |
gopsToAlignWith: segment.gopsToAlignWith,
|
|
|
52265 |
remux: isMuxed,
|
|
|
52266 |
onData: result => {
|
|
|
52267 |
result.type = result.type === 'combined' ? 'video' : result.type;
|
|
|
52268 |
dataFn(segment, result);
|
|
|
52269 |
},
|
|
|
52270 |
onTrackInfo: trackInfo => {
|
|
|
52271 |
if (trackInfoFn) {
|
|
|
52272 |
if (isMuxed) {
|
|
|
52273 |
trackInfo.isMuxed = true;
|
|
|
52274 |
}
|
|
|
52275 |
trackInfoFn(segment, trackInfo);
|
|
|
52276 |
}
|
|
|
52277 |
},
|
|
|
52278 |
onAudioTimingInfo: audioTimingInfo => {
|
|
|
52279 |
// we only want the first start value we encounter
|
|
|
52280 |
if (audioStartFn && typeof audioTimingInfo.start !== 'undefined') {
|
|
|
52281 |
audioStartFn(audioTimingInfo.start);
|
|
|
52282 |
audioStartFn = null;
|
|
|
52283 |
} // we want to continually update the end time
|
|
|
52284 |
|
|
|
52285 |
if (audioEndFn && typeof audioTimingInfo.end !== 'undefined') {
|
|
|
52286 |
audioEndFn(audioTimingInfo.end);
|
|
|
52287 |
}
|
|
|
52288 |
},
|
|
|
52289 |
onVideoTimingInfo: videoTimingInfo => {
|
|
|
52290 |
// we only want the first start value we encounter
|
|
|
52291 |
if (videoStartFn && typeof videoTimingInfo.start !== 'undefined') {
|
|
|
52292 |
videoStartFn(videoTimingInfo.start);
|
|
|
52293 |
videoStartFn = null;
|
|
|
52294 |
} // we want to continually update the end time
|
|
|
52295 |
|
|
|
52296 |
if (videoEndFn && typeof videoTimingInfo.end !== 'undefined') {
|
|
|
52297 |
videoEndFn(videoTimingInfo.end);
|
|
|
52298 |
}
|
|
|
52299 |
},
|
|
|
52300 |
onVideoSegmentTimingInfo: videoSegmentTimingInfo => {
|
|
|
52301 |
videoSegmentTimingInfoFn(videoSegmentTimingInfo);
|
|
|
52302 |
},
|
|
|
52303 |
onAudioSegmentTimingInfo: audioSegmentTimingInfo => {
|
|
|
52304 |
audioSegmentTimingInfoFn(audioSegmentTimingInfo);
|
|
|
52305 |
},
|
|
|
52306 |
onId3: (id3Frames, dispatchType) => {
|
|
|
52307 |
id3Fn(segment, id3Frames, dispatchType);
|
|
|
52308 |
},
|
|
|
52309 |
onCaptions: captions => {
|
|
|
52310 |
captionsFn(segment, [captions]);
|
|
|
52311 |
},
|
|
|
52312 |
isEndOfTimeline,
|
|
|
52313 |
onEndedTimeline: () => {
|
|
|
52314 |
endedTimelineFn();
|
|
|
52315 |
},
|
|
|
52316 |
onTransmuxerLog,
|
|
|
52317 |
onDone: result => {
|
|
|
52318 |
if (!doneFn) {
|
|
|
52319 |
return;
|
|
|
52320 |
}
|
|
|
52321 |
result.type = result.type === 'combined' ? 'video' : result.type;
|
|
|
52322 |
doneFn(null, segment, result);
|
|
|
52323 |
}
|
|
|
52324 |
}); // In the transmuxer, we don't yet have the ability to extract a "proper" start time.
|
|
|
52325 |
// Meaning cached frame data may corrupt our notion of where this segment
|
|
|
52326 |
// really starts. To get around this, probe for the info needed.
|
|
|
52327 |
|
|
|
52328 |
workerCallback({
|
|
|
52329 |
action: 'probeTs',
|
|
|
52330 |
transmuxer: segment.transmuxer,
|
|
|
52331 |
data: bytes,
|
|
|
52332 |
baseStartTime: segment.baseStartTime,
|
|
|
52333 |
callback: data => {
|
|
|
52334 |
segment.bytes = bytes = data.data;
|
|
|
52335 |
const probeResult = data.result;
|
|
|
52336 |
if (probeResult) {
|
|
|
52337 |
trackInfoFn(segment, {
|
|
|
52338 |
hasAudio: probeResult.hasAudio,
|
|
|
52339 |
hasVideo: probeResult.hasVideo,
|
|
|
52340 |
isMuxed
|
|
|
52341 |
});
|
|
|
52342 |
trackInfoFn = null;
|
|
|
52343 |
}
|
|
|
52344 |
finish();
|
|
|
52345 |
}
|
|
|
52346 |
});
|
|
|
52347 |
};
|
|
|
52348 |
const handleSegmentBytes = ({
|
|
|
52349 |
segment,
|
|
|
52350 |
bytes,
|
|
|
52351 |
trackInfoFn,
|
|
|
52352 |
timingInfoFn,
|
|
|
52353 |
videoSegmentTimingInfoFn,
|
|
|
52354 |
audioSegmentTimingInfoFn,
|
|
|
52355 |
id3Fn,
|
|
|
52356 |
captionsFn,
|
|
|
52357 |
isEndOfTimeline,
|
|
|
52358 |
endedTimelineFn,
|
|
|
52359 |
dataFn,
|
|
|
52360 |
doneFn,
|
|
|
52361 |
onTransmuxerLog
|
|
|
52362 |
}) => {
|
|
|
52363 |
let bytesAsUint8Array = new Uint8Array(bytes); // TODO:
|
|
|
52364 |
// We should have a handler that fetches the number of bytes required
|
|
|
52365 |
// to check if something is fmp4. This will allow us to save bandwidth
|
|
|
52366 |
// because we can only exclude a playlist and abort requests
|
|
|
52367 |
// by codec after trackinfo triggers.
|
|
|
52368 |
|
|
|
52369 |
if (isLikelyFmp4MediaSegment(bytesAsUint8Array)) {
|
|
|
52370 |
segment.isFmp4 = true;
|
|
|
52371 |
const {
|
|
|
52372 |
tracks
|
|
|
52373 |
} = segment.map;
|
|
|
52374 |
const trackInfo = {
|
|
|
52375 |
isFmp4: true,
|
|
|
52376 |
hasVideo: !!tracks.video,
|
|
|
52377 |
hasAudio: !!tracks.audio
|
|
|
52378 |
}; // if we have a audio track, with a codec that is not set to
|
|
|
52379 |
// encrypted audio
|
|
|
52380 |
|
|
|
52381 |
if (tracks.audio && tracks.audio.codec && tracks.audio.codec !== 'enca') {
|
|
|
52382 |
trackInfo.audioCodec = tracks.audio.codec;
|
|
|
52383 |
} // if we have a video track, with a codec that is not set to
|
|
|
52384 |
// encrypted video
|
|
|
52385 |
|
|
|
52386 |
if (tracks.video && tracks.video.codec && tracks.video.codec !== 'encv') {
|
|
|
52387 |
trackInfo.videoCodec = tracks.video.codec;
|
|
|
52388 |
}
|
|
|
52389 |
if (tracks.video && tracks.audio) {
|
|
|
52390 |
trackInfo.isMuxed = true;
|
|
|
52391 |
} // since we don't support appending fmp4 data on progress, we know we have the full
|
|
|
52392 |
// segment here
|
|
|
52393 |
|
|
|
52394 |
trackInfoFn(segment, trackInfo); // The probe doesn't provide the segment end time, so only callback with the start
|
|
|
52395 |
// time. The end time can be roughly calculated by the receiver using the duration.
|
|
|
52396 |
//
|
|
|
52397 |
// Note that the start time returned by the probe reflects the baseMediaDecodeTime, as
|
|
|
52398 |
// that is the true start of the segment (where the playback engine should begin
|
|
|
52399 |
// decoding).
|
|
|
52400 |
|
|
|
52401 |
const finishLoading = (captions, id3Frames) => {
|
|
|
52402 |
// if the track still has audio at this point it is only possible
|
|
|
52403 |
// for it to be audio only. See `tracks.video && tracks.audio` if statement
|
|
|
52404 |
// above.
|
|
|
52405 |
// we make sure to use segment.bytes here as that
|
|
|
52406 |
dataFn(segment, {
|
|
|
52407 |
data: bytesAsUint8Array,
|
|
|
52408 |
type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video'
|
|
|
52409 |
});
|
|
|
52410 |
if (id3Frames && id3Frames.length) {
|
|
|
52411 |
id3Fn(segment, id3Frames);
|
|
|
52412 |
}
|
|
|
52413 |
if (captions && captions.length) {
|
|
|
52414 |
captionsFn(segment, captions);
|
|
|
52415 |
}
|
|
|
52416 |
doneFn(null, segment, {});
|
|
|
52417 |
};
|
|
|
52418 |
workerCallback({
|
|
|
52419 |
action: 'probeMp4StartTime',
|
|
|
52420 |
timescales: segment.map.timescales,
|
|
|
52421 |
data: bytesAsUint8Array,
|
|
|
52422 |
transmuxer: segment.transmuxer,
|
|
|
52423 |
callback: ({
|
|
|
52424 |
data,
|
|
|
52425 |
startTime
|
|
|
52426 |
}) => {
|
|
|
52427 |
// transfer bytes back to us
|
|
|
52428 |
bytes = data.buffer;
|
|
|
52429 |
segment.bytes = bytesAsUint8Array = data;
|
|
|
52430 |
if (trackInfo.hasAudio && !trackInfo.isMuxed) {
|
|
|
52431 |
timingInfoFn(segment, 'audio', 'start', startTime);
|
|
|
52432 |
}
|
|
|
52433 |
if (trackInfo.hasVideo) {
|
|
|
52434 |
timingInfoFn(segment, 'video', 'start', startTime);
|
|
|
52435 |
}
|
|
|
52436 |
workerCallback({
|
|
|
52437 |
action: 'probeEmsgID3',
|
|
|
52438 |
data: bytesAsUint8Array,
|
|
|
52439 |
transmuxer: segment.transmuxer,
|
|
|
52440 |
offset: startTime,
|
|
|
52441 |
callback: ({
|
|
|
52442 |
emsgData,
|
|
|
52443 |
id3Frames
|
|
|
52444 |
}) => {
|
|
|
52445 |
// transfer bytes back to us
|
|
|
52446 |
bytes = emsgData.buffer;
|
|
|
52447 |
segment.bytes = bytesAsUint8Array = emsgData; // Run through the CaptionParser in case there are captions.
|
|
|
52448 |
// Initialize CaptionParser if it hasn't been yet
|
|
|
52449 |
|
|
|
52450 |
if (!tracks.video || !emsgData.byteLength || !segment.transmuxer) {
|
|
|
52451 |
finishLoading(undefined, id3Frames);
|
|
|
52452 |
return;
|
|
|
52453 |
}
|
|
|
52454 |
workerCallback({
|
|
|
52455 |
action: 'pushMp4Captions',
|
|
|
52456 |
endAction: 'mp4Captions',
|
|
|
52457 |
transmuxer: segment.transmuxer,
|
|
|
52458 |
data: bytesAsUint8Array,
|
|
|
52459 |
timescales: segment.map.timescales,
|
|
|
52460 |
trackIds: [tracks.video.id],
|
|
|
52461 |
callback: message => {
|
|
|
52462 |
// transfer bytes back to us
|
|
|
52463 |
bytes = message.data.buffer;
|
|
|
52464 |
segment.bytes = bytesAsUint8Array = message.data;
|
|
|
52465 |
message.logs.forEach(function (log) {
|
|
|
52466 |
onTransmuxerLog(merge(log, {
|
|
|
52467 |
stream: 'mp4CaptionParser'
|
|
|
52468 |
}));
|
|
|
52469 |
});
|
|
|
52470 |
finishLoading(message.captions, id3Frames);
|
|
|
52471 |
}
|
|
|
52472 |
});
|
|
|
52473 |
}
|
|
|
52474 |
});
|
|
|
52475 |
}
|
|
|
52476 |
});
|
|
|
52477 |
return;
|
|
|
52478 |
} // VTT or other segments that don't need processing
|
|
|
52479 |
|
|
|
52480 |
if (!segment.transmuxer) {
|
|
|
52481 |
doneFn(null, segment, {});
|
|
|
52482 |
return;
|
|
|
52483 |
}
|
|
|
52484 |
if (typeof segment.container === 'undefined') {
|
|
|
52485 |
segment.container = detectContainerForBytes(bytesAsUint8Array);
|
|
|
52486 |
}
|
|
|
52487 |
if (segment.container !== 'ts' && segment.container !== 'aac') {
|
|
|
52488 |
trackInfoFn(segment, {
|
|
|
52489 |
hasAudio: false,
|
|
|
52490 |
hasVideo: false
|
|
|
52491 |
});
|
|
|
52492 |
doneFn(null, segment, {});
|
|
|
52493 |
return;
|
|
|
52494 |
} // ts or aac
|
|
|
52495 |
|
|
|
52496 |
transmuxAndNotify({
|
|
|
52497 |
segment,
|
|
|
52498 |
bytes,
|
|
|
52499 |
trackInfoFn,
|
|
|
52500 |
timingInfoFn,
|
|
|
52501 |
videoSegmentTimingInfoFn,
|
|
|
52502 |
audioSegmentTimingInfoFn,
|
|
|
52503 |
id3Fn,
|
|
|
52504 |
captionsFn,
|
|
|
52505 |
isEndOfTimeline,
|
|
|
52506 |
endedTimelineFn,
|
|
|
52507 |
dataFn,
|
|
|
52508 |
doneFn,
|
|
|
52509 |
onTransmuxerLog
|
|
|
52510 |
});
|
|
|
52511 |
};
|
|
|
52512 |
const decrypt = function ({
|
|
|
52513 |
id,
|
|
|
52514 |
key,
|
|
|
52515 |
encryptedBytes,
|
|
|
52516 |
decryptionWorker
|
|
|
52517 |
}, callback) {
|
|
|
52518 |
const decryptionHandler = event => {
|
|
|
52519 |
if (event.data.source === id) {
|
|
|
52520 |
decryptionWorker.removeEventListener('message', decryptionHandler);
|
|
|
52521 |
const decrypted = event.data.decrypted;
|
|
|
52522 |
callback(new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength));
|
|
|
52523 |
}
|
|
|
52524 |
};
|
|
|
52525 |
decryptionWorker.addEventListener('message', decryptionHandler);
|
|
|
52526 |
let keyBytes;
|
|
|
52527 |
if (key.bytes.slice) {
|
|
|
52528 |
keyBytes = key.bytes.slice();
|
|
|
52529 |
} else {
|
|
|
52530 |
keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes));
|
|
|
52531 |
} // incrementally decrypt the bytes
|
|
|
52532 |
|
|
|
52533 |
decryptionWorker.postMessage(createTransferableMessage({
|
|
|
52534 |
source: id,
|
|
|
52535 |
encrypted: encryptedBytes,
|
|
|
52536 |
key: keyBytes,
|
|
|
52537 |
iv: key.iv
|
|
|
52538 |
}), [encryptedBytes.buffer, keyBytes.buffer]);
|
|
|
52539 |
};
|
|
|
52540 |
/**
|
|
|
52541 |
* Decrypt the segment via the decryption web worker
|
|
|
52542 |
*
|
|
|
52543 |
* @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption
|
|
|
52544 |
* routines
|
|
|
52545 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
52546 |
* from SegmentLoader
|
|
|
52547 |
* @param {Function} trackInfoFn - a callback that receives track info
|
|
|
52548 |
* @param {Function} timingInfoFn - a callback that receives timing info
|
|
|
52549 |
* @param {Function} videoSegmentTimingInfoFn
|
|
|
52550 |
* a callback that receives video timing info based on media times and
|
|
|
52551 |
* any adjustments made by the transmuxer
|
|
|
52552 |
* @param {Function} audioSegmentTimingInfoFn
|
|
|
52553 |
* a callback that receives audio timing info based on media times and
|
|
|
52554 |
* any adjustments made by the transmuxer
|
|
|
52555 |
* @param {boolean} isEndOfTimeline
|
|
|
52556 |
* true if this segment represents the last segment in a timeline
|
|
|
52557 |
* @param {Function} endedTimelineFn
|
|
|
52558 |
* a callback made when a timeline is ended, will only be called if
|
|
|
52559 |
* isEndOfTimeline is true
|
|
|
52560 |
* @param {Function} dataFn - a callback that is executed when segment bytes are available
|
|
|
52561 |
* and ready to use
|
|
|
52562 |
* @param {Function} doneFn - a callback that is executed after decryption has completed
|
|
|
52563 |
*/
|
|
|
52564 |
|
|
|
52565 |
const decryptSegment = ({
|
|
|
52566 |
decryptionWorker,
|
|
|
52567 |
segment,
|
|
|
52568 |
trackInfoFn,
|
|
|
52569 |
timingInfoFn,
|
|
|
52570 |
videoSegmentTimingInfoFn,
|
|
|
52571 |
audioSegmentTimingInfoFn,
|
|
|
52572 |
id3Fn,
|
|
|
52573 |
captionsFn,
|
|
|
52574 |
isEndOfTimeline,
|
|
|
52575 |
endedTimelineFn,
|
|
|
52576 |
dataFn,
|
|
|
52577 |
doneFn,
|
|
|
52578 |
onTransmuxerLog
|
|
|
52579 |
}) => {
|
|
|
52580 |
decrypt({
|
|
|
52581 |
id: segment.requestId,
|
|
|
52582 |
key: segment.key,
|
|
|
52583 |
encryptedBytes: segment.encryptedBytes,
|
|
|
52584 |
decryptionWorker
|
|
|
52585 |
}, decryptedBytes => {
|
|
|
52586 |
segment.bytes = decryptedBytes;
|
|
|
52587 |
handleSegmentBytes({
|
|
|
52588 |
segment,
|
|
|
52589 |
bytes: segment.bytes,
|
|
|
52590 |
trackInfoFn,
|
|
|
52591 |
timingInfoFn,
|
|
|
52592 |
videoSegmentTimingInfoFn,
|
|
|
52593 |
audioSegmentTimingInfoFn,
|
|
|
52594 |
id3Fn,
|
|
|
52595 |
captionsFn,
|
|
|
52596 |
isEndOfTimeline,
|
|
|
52597 |
endedTimelineFn,
|
|
|
52598 |
dataFn,
|
|
|
52599 |
doneFn,
|
|
|
52600 |
onTransmuxerLog
|
|
|
52601 |
});
|
|
|
52602 |
});
|
|
|
52603 |
};
|
|
|
52604 |
/**
|
|
|
52605 |
* This function waits for all XHRs to finish (with either success or failure)
|
|
|
52606 |
* before continueing processing via it's callback. The function gathers errors
|
|
|
52607 |
* from each request into a single errors array so that the error status for
|
|
|
52608 |
* each request can be examined later.
|
|
|
52609 |
*
|
|
|
52610 |
* @param {Object} activeXhrs - an object that tracks all XHR requests
|
|
|
52611 |
* @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption
|
|
|
52612 |
* routines
|
|
|
52613 |
* @param {Function} trackInfoFn - a callback that receives track info
|
|
|
52614 |
* @param {Function} timingInfoFn - a callback that receives timing info
|
|
|
52615 |
* @param {Function} videoSegmentTimingInfoFn
|
|
|
52616 |
* a callback that receives video timing info based on media times and
|
|
|
52617 |
* any adjustments made by the transmuxer
|
|
|
52618 |
* @param {Function} audioSegmentTimingInfoFn
|
|
|
52619 |
* a callback that receives audio timing info based on media times and
|
|
|
52620 |
* any adjustments made by the transmuxer
|
|
|
52621 |
* @param {Function} id3Fn - a callback that receives ID3 metadata
|
|
|
52622 |
* @param {Function} captionsFn - a callback that receives captions
|
|
|
52623 |
* @param {boolean} isEndOfTimeline
|
|
|
52624 |
* true if this segment represents the last segment in a timeline
|
|
|
52625 |
* @param {Function} endedTimelineFn
|
|
|
52626 |
* a callback made when a timeline is ended, will only be called if
|
|
|
52627 |
* isEndOfTimeline is true
|
|
|
52628 |
* @param {Function} dataFn - a callback that is executed when segment bytes are available
|
|
|
52629 |
* and ready to use
|
|
|
52630 |
* @param {Function} doneFn - a callback that is executed after all resources have been
|
|
|
52631 |
* downloaded and any decryption completed
|
|
|
52632 |
*/
|
|
|
52633 |
|
|
|
52634 |
const waitForCompletion = ({
|
|
|
52635 |
activeXhrs,
|
|
|
52636 |
decryptionWorker,
|
|
|
52637 |
trackInfoFn,
|
|
|
52638 |
timingInfoFn,
|
|
|
52639 |
videoSegmentTimingInfoFn,
|
|
|
52640 |
audioSegmentTimingInfoFn,
|
|
|
52641 |
id3Fn,
|
|
|
52642 |
captionsFn,
|
|
|
52643 |
isEndOfTimeline,
|
|
|
52644 |
endedTimelineFn,
|
|
|
52645 |
dataFn,
|
|
|
52646 |
doneFn,
|
|
|
52647 |
onTransmuxerLog
|
|
|
52648 |
}) => {
|
|
|
52649 |
let count = 0;
|
|
|
52650 |
let didError = false;
|
|
|
52651 |
return (error, segment) => {
|
|
|
52652 |
if (didError) {
|
|
|
52653 |
return;
|
|
|
52654 |
}
|
|
|
52655 |
if (error) {
|
|
|
52656 |
didError = true; // If there are errors, we have to abort any outstanding requests
|
|
|
52657 |
|
|
|
52658 |
abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we
|
|
|
52659 |
// handle the aborted events from those requests, there are some cases where we may
|
|
|
52660 |
// never get an aborted event. For instance, if the network connection is lost and
|
|
|
52661 |
// there were two requests, the first may have triggered an error immediately, while
|
|
|
52662 |
// the second request remains unsent. In that case, the aborted algorithm will not
|
|
|
52663 |
// trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method
|
|
|
52664 |
//
|
|
|
52665 |
// We also can't rely on the ready state of the XHR, since the request that
|
|
|
52666 |
// triggered the connection error may also show as a ready state of 0 (unsent).
|
|
|
52667 |
// Therefore, we have to finish this group of requests immediately after the first
|
|
|
52668 |
// seen error.
|
|
|
52669 |
|
|
|
52670 |
return doneFn(error, segment);
|
|
|
52671 |
}
|
|
|
52672 |
count += 1;
|
|
|
52673 |
if (count === activeXhrs.length) {
|
|
|
52674 |
const segmentFinish = function () {
|
|
|
52675 |
if (segment.encryptedBytes) {
|
|
|
52676 |
return decryptSegment({
|
|
|
52677 |
decryptionWorker,
|
|
|
52678 |
segment,
|
|
|
52679 |
trackInfoFn,
|
|
|
52680 |
timingInfoFn,
|
|
|
52681 |
videoSegmentTimingInfoFn,
|
|
|
52682 |
audioSegmentTimingInfoFn,
|
|
|
52683 |
id3Fn,
|
|
|
52684 |
captionsFn,
|
|
|
52685 |
isEndOfTimeline,
|
|
|
52686 |
endedTimelineFn,
|
|
|
52687 |
dataFn,
|
|
|
52688 |
doneFn,
|
|
|
52689 |
onTransmuxerLog
|
|
|
52690 |
});
|
|
|
52691 |
} // Otherwise, everything is ready just continue
|
|
|
52692 |
|
|
|
52693 |
handleSegmentBytes({
|
|
|
52694 |
segment,
|
|
|
52695 |
bytes: segment.bytes,
|
|
|
52696 |
trackInfoFn,
|
|
|
52697 |
timingInfoFn,
|
|
|
52698 |
videoSegmentTimingInfoFn,
|
|
|
52699 |
audioSegmentTimingInfoFn,
|
|
|
52700 |
id3Fn,
|
|
|
52701 |
captionsFn,
|
|
|
52702 |
isEndOfTimeline,
|
|
|
52703 |
endedTimelineFn,
|
|
|
52704 |
dataFn,
|
|
|
52705 |
doneFn,
|
|
|
52706 |
onTransmuxerLog
|
|
|
52707 |
});
|
|
|
52708 |
}; // Keep track of when *all* of the requests have completed
|
|
|
52709 |
|
|
|
52710 |
segment.endOfAllRequests = Date.now();
|
|
|
52711 |
if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) {
|
|
|
52712 |
return decrypt({
|
|
|
52713 |
decryptionWorker,
|
|
|
52714 |
// add -init to the "id" to differentiate between segment
|
|
|
52715 |
// and init segment decryption, just in case they happen
|
|
|
52716 |
// at the same time at some point in the future.
|
|
|
52717 |
id: segment.requestId + '-init',
|
|
|
52718 |
encryptedBytes: segment.map.encryptedBytes,
|
|
|
52719 |
key: segment.map.key
|
|
|
52720 |
}, decryptedBytes => {
|
|
|
52721 |
segment.map.bytes = decryptedBytes;
|
|
|
52722 |
parseInitSegment(segment, parseError => {
|
|
|
52723 |
if (parseError) {
|
|
|
52724 |
abortAll(activeXhrs);
|
|
|
52725 |
return doneFn(parseError, segment);
|
|
|
52726 |
}
|
|
|
52727 |
segmentFinish();
|
|
|
52728 |
});
|
|
|
52729 |
});
|
|
|
52730 |
}
|
|
|
52731 |
segmentFinish();
|
|
|
52732 |
}
|
|
|
52733 |
};
|
|
|
52734 |
};
|
|
|
52735 |
/**
|
|
|
52736 |
* Calls the abort callback if any request within the batch was aborted. Will only call
|
|
|
52737 |
* the callback once per batch of requests, even if multiple were aborted.
|
|
|
52738 |
*
|
|
|
52739 |
* @param {Object} loadendState - state to check to see if the abort function was called
|
|
|
52740 |
* @param {Function} abortFn - callback to call for abort
|
|
|
52741 |
*/
|
|
|
52742 |
|
|
|
52743 |
const handleLoadEnd = ({
|
|
|
52744 |
loadendState,
|
|
|
52745 |
abortFn
|
|
|
52746 |
}) => event => {
|
|
|
52747 |
const request = event.target;
|
|
|
52748 |
if (request.aborted && abortFn && !loadendState.calledAbortFn) {
|
|
|
52749 |
abortFn();
|
|
|
52750 |
loadendState.calledAbortFn = true;
|
|
|
52751 |
}
|
|
|
52752 |
};
|
|
|
52753 |
/**
|
|
|
52754 |
* Simple progress event callback handler that gathers some stats before
|
|
|
52755 |
* executing a provided callback with the `segment` object
|
|
|
52756 |
*
|
|
|
52757 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
52758 |
* from SegmentLoader
|
|
|
52759 |
* @param {Function} progressFn - a callback that is executed each time a progress event
|
|
|
52760 |
* is received
|
|
|
52761 |
* @param {Function} trackInfoFn - a callback that receives track info
|
|
|
52762 |
* @param {Function} timingInfoFn - a callback that receives timing info
|
|
|
52763 |
* @param {Function} videoSegmentTimingInfoFn
|
|
|
52764 |
* a callback that receives video timing info based on media times and
|
|
|
52765 |
* any adjustments made by the transmuxer
|
|
|
52766 |
* @param {Function} audioSegmentTimingInfoFn
|
|
|
52767 |
* a callback that receives audio timing info based on media times and
|
|
|
52768 |
* any adjustments made by the transmuxer
|
|
|
52769 |
* @param {boolean} isEndOfTimeline
|
|
|
52770 |
* true if this segment represents the last segment in a timeline
|
|
|
52771 |
* @param {Function} endedTimelineFn
|
|
|
52772 |
* a callback made when a timeline is ended, will only be called if
|
|
|
52773 |
* isEndOfTimeline is true
|
|
|
52774 |
* @param {Function} dataFn - a callback that is executed when segment bytes are available
|
|
|
52775 |
* and ready to use
|
|
|
52776 |
* @param {Event} event - the progress event object from XMLHttpRequest
|
|
|
52777 |
*/
|
|
|
52778 |
|
|
|
52779 |
const handleProgress = ({
|
|
|
52780 |
segment,
|
|
|
52781 |
progressFn,
|
|
|
52782 |
trackInfoFn,
|
|
|
52783 |
timingInfoFn,
|
|
|
52784 |
videoSegmentTimingInfoFn,
|
|
|
52785 |
audioSegmentTimingInfoFn,
|
|
|
52786 |
id3Fn,
|
|
|
52787 |
captionsFn,
|
|
|
52788 |
isEndOfTimeline,
|
|
|
52789 |
endedTimelineFn,
|
|
|
52790 |
dataFn
|
|
|
52791 |
}) => event => {
|
|
|
52792 |
const request = event.target;
|
|
|
52793 |
if (request.aborted) {
|
|
|
52794 |
return;
|
|
|
52795 |
}
|
|
|
52796 |
segment.stats = merge(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data
|
|
|
52797 |
|
|
|
52798 |
if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) {
|
|
|
52799 |
segment.stats.firstBytesReceivedAt = Date.now();
|
|
|
52800 |
}
|
|
|
52801 |
return progressFn(event, segment);
|
|
|
52802 |
};
|
|
|
52803 |
/**
|
|
|
52804 |
* Load all resources and does any processing necessary for a media-segment
|
|
|
52805 |
*
|
|
|
52806 |
* Features:
|
|
|
52807 |
* decrypts the media-segment if it has a key uri and an iv
|
|
|
52808 |
* aborts *all* requests if *any* one request fails
|
|
|
52809 |
*
|
|
|
52810 |
* The segment object, at minimum, has the following format:
|
|
|
52811 |
* {
|
|
|
52812 |
* resolvedUri: String,
|
|
|
52813 |
* [transmuxer]: Object,
|
|
|
52814 |
* [byterange]: {
|
|
|
52815 |
* offset: Number,
|
|
|
52816 |
* length: Number
|
|
|
52817 |
* },
|
|
|
52818 |
* [key]: {
|
|
|
52819 |
* resolvedUri: String
|
|
|
52820 |
* [byterange]: {
|
|
|
52821 |
* offset: Number,
|
|
|
52822 |
* length: Number
|
|
|
52823 |
* },
|
|
|
52824 |
* iv: {
|
|
|
52825 |
* bytes: Uint32Array
|
|
|
52826 |
* }
|
|
|
52827 |
* },
|
|
|
52828 |
* [map]: {
|
|
|
52829 |
* resolvedUri: String,
|
|
|
52830 |
* [byterange]: {
|
|
|
52831 |
* offset: Number,
|
|
|
52832 |
* length: Number
|
|
|
52833 |
* },
|
|
|
52834 |
* [bytes]: Uint8Array
|
|
|
52835 |
* }
|
|
|
52836 |
* }
|
|
|
52837 |
* ...where [name] denotes optional properties
|
|
|
52838 |
*
|
|
|
52839 |
* @param {Function} xhr - an instance of the xhr wrapper in xhr.js
|
|
|
52840 |
* @param {Object} xhrOptions - the base options to provide to all xhr requests
|
|
|
52841 |
* @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128
|
|
|
52842 |
* decryption routines
|
|
|
52843 |
* @param {Object} segment - a simplified copy of the segmentInfo object
|
|
|
52844 |
* from SegmentLoader
|
|
|
52845 |
* @param {Function} abortFn - a callback called (only once) if any piece of a request was
|
|
|
52846 |
* aborted
|
|
|
52847 |
* @param {Function} progressFn - a callback that receives progress events from the main
|
|
|
52848 |
* segment's xhr request
|
|
|
52849 |
* @param {Function} trackInfoFn - a callback that receives track info
|
|
|
52850 |
* @param {Function} timingInfoFn - a callback that receives timing info
|
|
|
52851 |
* @param {Function} videoSegmentTimingInfoFn
|
|
|
52852 |
* a callback that receives video timing info based on media times and
|
|
|
52853 |
* any adjustments made by the transmuxer
|
|
|
52854 |
* @param {Function} audioSegmentTimingInfoFn
|
|
|
52855 |
* a callback that receives audio timing info based on media times and
|
|
|
52856 |
* any adjustments made by the transmuxer
|
|
|
52857 |
* @param {Function} id3Fn - a callback that receives ID3 metadata
|
|
|
52858 |
* @param {Function} captionsFn - a callback that receives captions
|
|
|
52859 |
* @param {boolean} isEndOfTimeline
|
|
|
52860 |
* true if this segment represents the last segment in a timeline
|
|
|
52861 |
* @param {Function} endedTimelineFn
|
|
|
52862 |
* a callback made when a timeline is ended, will only be called if
|
|
|
52863 |
* isEndOfTimeline is true
|
|
|
52864 |
* @param {Function} dataFn - a callback that receives data from the main segment's xhr
|
|
|
52865 |
* request, transmuxed if needed
|
|
|
52866 |
* @param {Function} doneFn - a callback that is executed only once all requests have
|
|
|
52867 |
* succeeded or failed
|
|
|
52868 |
* @return {Function} a function that, when invoked, immediately aborts all
|
|
|
52869 |
* outstanding requests
|
|
|
52870 |
*/
|
|
|
52871 |
|
|
|
52872 |
const mediaSegmentRequest = ({
|
|
|
52873 |
xhr,
|
|
|
52874 |
xhrOptions,
|
|
|
52875 |
decryptionWorker,
|
|
|
52876 |
segment,
|
|
|
52877 |
abortFn,
|
|
|
52878 |
progressFn,
|
|
|
52879 |
trackInfoFn,
|
|
|
52880 |
timingInfoFn,
|
|
|
52881 |
videoSegmentTimingInfoFn,
|
|
|
52882 |
audioSegmentTimingInfoFn,
|
|
|
52883 |
id3Fn,
|
|
|
52884 |
captionsFn,
|
|
|
52885 |
isEndOfTimeline,
|
|
|
52886 |
endedTimelineFn,
|
|
|
52887 |
dataFn,
|
|
|
52888 |
doneFn,
|
|
|
52889 |
onTransmuxerLog
|
|
|
52890 |
}) => {
|
|
|
52891 |
const activeXhrs = [];
|
|
|
52892 |
const finishProcessingFn = waitForCompletion({
|
|
|
52893 |
activeXhrs,
|
|
|
52894 |
decryptionWorker,
|
|
|
52895 |
trackInfoFn,
|
|
|
52896 |
timingInfoFn,
|
|
|
52897 |
videoSegmentTimingInfoFn,
|
|
|
52898 |
audioSegmentTimingInfoFn,
|
|
|
52899 |
id3Fn,
|
|
|
52900 |
captionsFn,
|
|
|
52901 |
isEndOfTimeline,
|
|
|
52902 |
endedTimelineFn,
|
|
|
52903 |
dataFn,
|
|
|
52904 |
doneFn,
|
|
|
52905 |
onTransmuxerLog
|
|
|
52906 |
}); // optionally, request the decryption key
|
|
|
52907 |
|
|
|
52908 |
if (segment.key && !segment.key.bytes) {
|
|
|
52909 |
const objects = [segment.key];
|
|
|
52910 |
if (segment.map && !segment.map.bytes && segment.map.key && segment.map.key.resolvedUri === segment.key.resolvedUri) {
|
|
|
52911 |
objects.push(segment.map.key);
|
|
|
52912 |
}
|
|
|
52913 |
const keyRequestOptions = merge(xhrOptions, {
|
|
|
52914 |
uri: segment.key.resolvedUri,
|
|
|
52915 |
responseType: 'arraybuffer'
|
|
|
52916 |
});
|
|
|
52917 |
const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn);
|
|
|
52918 |
const keyXhr = xhr(keyRequestOptions, keyRequestCallback);
|
|
|
52919 |
activeXhrs.push(keyXhr);
|
|
|
52920 |
} // optionally, request the associated media init segment
|
|
|
52921 |
|
|
|
52922 |
if (segment.map && !segment.map.bytes) {
|
|
|
52923 |
const differentMapKey = segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri);
|
|
|
52924 |
if (differentMapKey) {
|
|
|
52925 |
const mapKeyRequestOptions = merge(xhrOptions, {
|
|
|
52926 |
uri: segment.map.key.resolvedUri,
|
|
|
52927 |
responseType: 'arraybuffer'
|
|
|
52928 |
});
|
|
|
52929 |
const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn);
|
|
|
52930 |
const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback);
|
|
|
52931 |
activeXhrs.push(mapKeyXhr);
|
|
|
52932 |
}
|
|
|
52933 |
const initSegmentOptions = merge(xhrOptions, {
|
|
|
52934 |
uri: segment.map.resolvedUri,
|
|
|
52935 |
responseType: 'arraybuffer',
|
|
|
52936 |
headers: segmentXhrHeaders(segment.map)
|
|
|
52937 |
});
|
|
|
52938 |
const initSegmentRequestCallback = handleInitSegmentResponse({
|
|
|
52939 |
segment,
|
|
|
52940 |
finishProcessingFn
|
|
|
52941 |
});
|
|
|
52942 |
const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
|
|
|
52943 |
activeXhrs.push(initSegmentXhr);
|
|
|
52944 |
}
|
|
|
52945 |
const segmentRequestOptions = merge(xhrOptions, {
|
|
|
52946 |
uri: segment.part && segment.part.resolvedUri || segment.resolvedUri,
|
|
|
52947 |
responseType: 'arraybuffer',
|
|
|
52948 |
headers: segmentXhrHeaders(segment)
|
|
|
52949 |
});
|
|
|
52950 |
const segmentRequestCallback = handleSegmentResponse({
|
|
|
52951 |
segment,
|
|
|
52952 |
finishProcessingFn,
|
|
|
52953 |
responseType: segmentRequestOptions.responseType
|
|
|
52954 |
});
|
|
|
52955 |
const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback);
|
|
|
52956 |
segmentXhr.addEventListener('progress', handleProgress({
|
|
|
52957 |
segment,
|
|
|
52958 |
progressFn,
|
|
|
52959 |
trackInfoFn,
|
|
|
52960 |
timingInfoFn,
|
|
|
52961 |
videoSegmentTimingInfoFn,
|
|
|
52962 |
audioSegmentTimingInfoFn,
|
|
|
52963 |
id3Fn,
|
|
|
52964 |
captionsFn,
|
|
|
52965 |
isEndOfTimeline,
|
|
|
52966 |
endedTimelineFn,
|
|
|
52967 |
dataFn
|
|
|
52968 |
}));
|
|
|
52969 |
activeXhrs.push(segmentXhr); // since all parts of the request must be considered, but should not make callbacks
|
|
|
52970 |
// multiple times, provide a shared state object
|
|
|
52971 |
|
|
|
52972 |
const loadendState = {};
|
|
|
52973 |
activeXhrs.forEach(activeXhr => {
|
|
|
52974 |
activeXhr.addEventListener('loadend', handleLoadEnd({
|
|
|
52975 |
loadendState,
|
|
|
52976 |
abortFn
|
|
|
52977 |
}));
|
|
|
52978 |
});
|
|
|
52979 |
return () => abortAll(activeXhrs);
|
|
|
52980 |
};
|
|
|
52981 |
|
|
|
52982 |
/**
|
|
|
52983 |
* @file - codecs.js - Handles tasks regarding codec strings such as translating them to
|
|
|
52984 |
* codec strings, or translating codec strings into objects that can be examined.
|
|
|
52985 |
*/
|
|
|
52986 |
const logFn$1 = logger('CodecUtils');
|
|
|
52987 |
/**
|
|
|
52988 |
* Returns a set of codec strings parsed from the playlist or the default
|
|
|
52989 |
* codec strings if no codecs were specified in the playlist
|
|
|
52990 |
*
|
|
|
52991 |
* @param {Playlist} media the current media playlist
|
|
|
52992 |
* @return {Object} an object with the video and audio codecs
|
|
|
52993 |
*/
|
|
|
52994 |
|
|
|
52995 |
const getCodecs = function (media) {
|
|
|
52996 |
// if the codecs were explicitly specified, use them instead of the
|
|
|
52997 |
// defaults
|
|
|
52998 |
const mediaAttributes = media.attributes || {};
|
|
|
52999 |
if (mediaAttributes.CODECS) {
|
|
|
53000 |
return parseCodecs(mediaAttributes.CODECS);
|
|
|
53001 |
}
|
|
|
53002 |
};
|
|
|
53003 |
const isMaat = (main, media) => {
|
|
|
53004 |
const mediaAttributes = media.attributes || {};
|
|
|
53005 |
return main && main.mediaGroups && main.mediaGroups.AUDIO && mediaAttributes.AUDIO && main.mediaGroups.AUDIO[mediaAttributes.AUDIO];
|
|
|
53006 |
};
|
|
|
53007 |
const isMuxed = (main, media) => {
|
|
|
53008 |
if (!isMaat(main, media)) {
|
|
|
53009 |
return true;
|
|
|
53010 |
}
|
|
|
53011 |
const mediaAttributes = media.attributes || {};
|
|
|
53012 |
const audioGroup = main.mediaGroups.AUDIO[mediaAttributes.AUDIO];
|
|
|
53013 |
for (const groupId in audioGroup) {
|
|
|
53014 |
// If an audio group has a URI (the case for HLS, as HLS will use external playlists),
|
|
|
53015 |
// or there are listed playlists (the case for DASH, as the manifest will have already
|
|
|
53016 |
// provided all of the details necessary to generate the audio playlist, as opposed to
|
|
|
53017 |
// HLS' externally requested playlists), then the content is demuxed.
|
|
|
53018 |
if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) {
|
|
|
53019 |
return true;
|
|
|
53020 |
}
|
|
|
53021 |
}
|
|
|
53022 |
return false;
|
|
|
53023 |
};
|
|
|
53024 |
const unwrapCodecList = function (codecList) {
|
|
|
53025 |
const codecs = {};
|
|
|
53026 |
codecList.forEach(({
|
|
|
53027 |
mediaType,
|
|
|
53028 |
type,
|
|
|
53029 |
details
|
|
|
53030 |
}) => {
|
|
|
53031 |
codecs[mediaType] = codecs[mediaType] || [];
|
|
|
53032 |
codecs[mediaType].push(translateLegacyCodec(`${type}${details}`));
|
|
|
53033 |
});
|
|
|
53034 |
Object.keys(codecs).forEach(function (mediaType) {
|
|
|
53035 |
if (codecs[mediaType].length > 1) {
|
|
|
53036 |
logFn$1(`multiple ${mediaType} codecs found as attributes: ${codecs[mediaType].join(', ')}. Setting playlist codecs to null so that we wait for mux.js to probe segments for real codecs.`);
|
|
|
53037 |
codecs[mediaType] = null;
|
|
|
53038 |
return;
|
|
|
53039 |
}
|
|
|
53040 |
codecs[mediaType] = codecs[mediaType][0];
|
|
|
53041 |
});
|
|
|
53042 |
return codecs;
|
|
|
53043 |
};
|
|
|
53044 |
const codecCount = function (codecObj) {
|
|
|
53045 |
let count = 0;
|
|
|
53046 |
if (codecObj.audio) {
|
|
|
53047 |
count++;
|
|
|
53048 |
}
|
|
|
53049 |
if (codecObj.video) {
|
|
|
53050 |
count++;
|
|
|
53051 |
}
|
|
|
53052 |
return count;
|
|
|
53053 |
};
|
|
|
53054 |
/**
|
|
|
53055 |
* Calculates the codec strings for a working configuration of
|
|
|
53056 |
* SourceBuffers to play variant streams in a main playlist. If
|
|
|
53057 |
* there is no possible working configuration, an empty object will be
|
|
|
53058 |
* returned.
|
|
|
53059 |
*
|
|
|
53060 |
* @param main {Object} the m3u8 object for the main playlist
|
|
|
53061 |
* @param media {Object} the m3u8 object for the variant playlist
|
|
|
53062 |
* @return {Object} the codec strings.
|
|
|
53063 |
*
|
|
|
53064 |
* @private
|
|
|
53065 |
*/
|
|
|
53066 |
|
|
|
53067 |
const codecsForPlaylist = function (main, media) {
|
|
|
53068 |
const mediaAttributes = media.attributes || {};
|
|
|
53069 |
const codecInfo = unwrapCodecList(getCodecs(media) || []); // HLS with multiple-audio tracks must always get an audio codec.
|
|
|
53070 |
// Put another way, there is no way to have a video-only multiple-audio HLS!
|
|
|
53071 |
|
|
|
53072 |
if (isMaat(main, media) && !codecInfo.audio) {
|
|
|
53073 |
if (!isMuxed(main, media)) {
|
|
|
53074 |
// It is possible for codecs to be specified on the audio media group playlist but
|
|
|
53075 |
// not on the rendition playlist. This is mostly the case for DASH, where audio and
|
|
|
53076 |
// video are always separate (and separately specified).
|
|
|
53077 |
const defaultCodecs = unwrapCodecList(codecsFromDefault(main, mediaAttributes.AUDIO) || []);
|
|
|
53078 |
if (defaultCodecs.audio) {
|
|
|
53079 |
codecInfo.audio = defaultCodecs.audio;
|
|
|
53080 |
}
|
|
|
53081 |
}
|
|
|
53082 |
}
|
|
|
53083 |
return codecInfo;
|
|
|
53084 |
};
|
|
|
53085 |
const logFn = logger('PlaylistSelector');
|
|
|
53086 |
const representationToString = function (representation) {
|
|
|
53087 |
if (!representation || !representation.playlist) {
|
|
|
53088 |
return;
|
|
|
53089 |
}
|
|
|
53090 |
const playlist = representation.playlist;
|
|
|
53091 |
return JSON.stringify({
|
|
|
53092 |
id: playlist.id,
|
|
|
53093 |
bandwidth: representation.bandwidth,
|
|
|
53094 |
width: representation.width,
|
|
|
53095 |
height: representation.height,
|
|
|
53096 |
codecs: playlist.attributes && playlist.attributes.CODECS || ''
|
|
|
53097 |
});
|
|
|
53098 |
}; // Utilities
|
|
|
53099 |
|
|
|
53100 |
/**
|
|
|
53101 |
* Returns the CSS value for the specified property on an element
|
|
|
53102 |
* using `getComputedStyle`. Firefox has a long-standing issue where
|
|
|
53103 |
* getComputedStyle() may return null when running in an iframe with
|
|
|
53104 |
* `display: none`.
|
|
|
53105 |
*
|
|
|
53106 |
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
|
|
53107 |
* @param {HTMLElement} el the htmlelement to work on
|
|
|
53108 |
* @param {string} the proprety to get the style for
|
|
|
53109 |
*/
|
|
|
53110 |
|
|
|
53111 |
const safeGetComputedStyle = function (el, property) {
|
|
|
53112 |
if (!el) {
|
|
|
53113 |
return '';
|
|
|
53114 |
}
|
|
|
53115 |
const result = window.getComputedStyle(el);
|
|
|
53116 |
if (!result) {
|
|
|
53117 |
return '';
|
|
|
53118 |
}
|
|
|
53119 |
return result[property];
|
|
|
53120 |
};
|
|
|
53121 |
/**
|
|
|
53122 |
* Resuable stable sort function
|
|
|
53123 |
*
|
|
|
53124 |
* @param {Playlists} array
|
|
|
53125 |
* @param {Function} sortFn Different comparators
|
|
|
53126 |
* @function stableSort
|
|
|
53127 |
*/
|
|
|
53128 |
|
|
|
53129 |
const stableSort = function (array, sortFn) {
|
|
|
53130 |
const newArray = array.slice();
|
|
|
53131 |
array.sort(function (left, right) {
|
|
|
53132 |
const cmp = sortFn(left, right);
|
|
|
53133 |
if (cmp === 0) {
|
|
|
53134 |
return newArray.indexOf(left) - newArray.indexOf(right);
|
|
|
53135 |
}
|
|
|
53136 |
return cmp;
|
|
|
53137 |
});
|
|
|
53138 |
};
|
|
|
53139 |
/**
|
|
|
53140 |
* A comparator function to sort two playlist object by bandwidth.
|
|
|
53141 |
*
|
|
|
53142 |
* @param {Object} left a media playlist object
|
|
|
53143 |
* @param {Object} right a media playlist object
|
|
|
53144 |
* @return {number} Greater than zero if the bandwidth attribute of
|
|
|
53145 |
* left is greater than the corresponding attribute of right. Less
|
|
|
53146 |
* than zero if the bandwidth of right is greater than left and
|
|
|
53147 |
* exactly zero if the two are equal.
|
|
|
53148 |
*/
|
|
|
53149 |
|
|
|
53150 |
const comparePlaylistBandwidth = function (left, right) {
|
|
|
53151 |
let leftBandwidth;
|
|
|
53152 |
let rightBandwidth;
|
|
|
53153 |
if (left.attributes.BANDWIDTH) {
|
|
|
53154 |
leftBandwidth = left.attributes.BANDWIDTH;
|
|
|
53155 |
}
|
|
|
53156 |
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
|
|
|
53157 |
if (right.attributes.BANDWIDTH) {
|
|
|
53158 |
rightBandwidth = right.attributes.BANDWIDTH;
|
|
|
53159 |
}
|
|
|
53160 |
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
|
|
|
53161 |
return leftBandwidth - rightBandwidth;
|
|
|
53162 |
};
|
|
|
53163 |
/**
|
|
|
53164 |
* A comparator function to sort two playlist object by resolution (width).
|
|
|
53165 |
*
|
|
|
53166 |
* @param {Object} left a media playlist object
|
|
|
53167 |
* @param {Object} right a media playlist object
|
|
|
53168 |
* @return {number} Greater than zero if the resolution.width attribute of
|
|
|
53169 |
* left is greater than the corresponding attribute of right. Less
|
|
|
53170 |
* than zero if the resolution.width of right is greater than left and
|
|
|
53171 |
* exactly zero if the two are equal.
|
|
|
53172 |
*/
|
|
|
53173 |
|
|
|
53174 |
const comparePlaylistResolution = function (left, right) {
|
|
|
53175 |
let leftWidth;
|
|
|
53176 |
let rightWidth;
|
|
|
53177 |
if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
|
|
|
53178 |
leftWidth = left.attributes.RESOLUTION.width;
|
|
|
53179 |
}
|
|
|
53180 |
leftWidth = leftWidth || window.Number.MAX_VALUE;
|
|
|
53181 |
if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
|
|
|
53182 |
rightWidth = right.attributes.RESOLUTION.width;
|
|
|
53183 |
}
|
|
|
53184 |
rightWidth = rightWidth || window.Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
|
|
|
53185 |
// have the same media dimensions/ resolution
|
|
|
53186 |
|
|
|
53187 |
if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
|
|
|
53188 |
return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
|
|
|
53189 |
}
|
|
|
53190 |
return leftWidth - rightWidth;
|
|
|
53191 |
};
|
|
|
53192 |
/**
|
|
|
53193 |
* Chooses the appropriate media playlist based on bandwidth and player size
|
|
|
53194 |
*
|
|
|
53195 |
* @param {Object} main
|
|
|
53196 |
* Object representation of the main manifest
|
|
|
53197 |
* @param {number} playerBandwidth
|
|
|
53198 |
* Current calculated bandwidth of the player
|
|
|
53199 |
* @param {number} playerWidth
|
|
|
53200 |
* Current width of the player element (should account for the device pixel ratio)
|
|
|
53201 |
* @param {number} playerHeight
|
|
|
53202 |
* Current height of the player element (should account for the device pixel ratio)
|
|
|
53203 |
* @param {boolean} limitRenditionByPlayerDimensions
|
|
|
53204 |
* True if the player width and height should be used during the selection, false otherwise
|
|
|
53205 |
* @param {Object} playlistController
|
|
|
53206 |
* the current playlistController object
|
|
|
53207 |
* @return {Playlist} the highest bitrate playlist less than the
|
|
|
53208 |
* currently detected bandwidth, accounting for some amount of
|
|
|
53209 |
* bandwidth variance
|
|
|
53210 |
*/
|
|
|
53211 |
|
|
|
53212 |
let simpleSelector = function (main, playerBandwidth, playerWidth, playerHeight, limitRenditionByPlayerDimensions, playlistController) {
|
|
|
53213 |
// If we end up getting called before `main` is available, exit early
|
|
|
53214 |
if (!main) {
|
|
|
53215 |
return;
|
|
|
53216 |
}
|
|
|
53217 |
const options = {
|
|
|
53218 |
bandwidth: playerBandwidth,
|
|
|
53219 |
width: playerWidth,
|
|
|
53220 |
height: playerHeight,
|
|
|
53221 |
limitRenditionByPlayerDimensions
|
|
|
53222 |
};
|
|
|
53223 |
let playlists = main.playlists; // if playlist is audio only, select between currently active audio group playlists.
|
|
|
53224 |
|
|
|
53225 |
if (Playlist.isAudioOnly(main)) {
|
|
|
53226 |
playlists = playlistController.getAudioTrackPlaylists_(); // add audioOnly to options so that we log audioOnly: true
|
|
|
53227 |
// at the buttom of this function for debugging.
|
|
|
53228 |
|
|
|
53229 |
options.audioOnly = true;
|
|
|
53230 |
} // convert the playlists to an intermediary representation to make comparisons easier
|
|
|
53231 |
|
|
|
53232 |
let sortedPlaylistReps = playlists.map(playlist => {
|
|
|
53233 |
let bandwidth;
|
|
|
53234 |
const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
|
|
|
53235 |
const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
|
|
|
53236 |
bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH;
|
|
|
53237 |
bandwidth = bandwidth || window.Number.MAX_VALUE;
|
|
|
53238 |
return {
|
|
|
53239 |
bandwidth,
|
|
|
53240 |
width,
|
|
|
53241 |
height,
|
|
|
53242 |
playlist
|
|
|
53243 |
};
|
|
|
53244 |
});
|
|
|
53245 |
stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); // filter out any playlists that have been excluded due to
|
|
|
53246 |
// incompatible configurations
|
|
|
53247 |
|
|
|
53248 |
sortedPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isIncompatible(rep.playlist)); // filter out any playlists that have been disabled manually through the representations
|
|
|
53249 |
// api or excluded temporarily due to playback errors.
|
|
|
53250 |
|
|
|
53251 |
let enabledPlaylistReps = sortedPlaylistReps.filter(rep => Playlist.isEnabled(rep.playlist));
|
|
|
53252 |
if (!enabledPlaylistReps.length) {
|
|
|
53253 |
// if there are no enabled playlists, then they have all been excluded or disabled
|
|
|
53254 |
// by the user through the representations api. In this case, ignore exclusion and
|
|
|
53255 |
// fallback to what the user wants by using playlists the user has not disabled.
|
|
|
53256 |
enabledPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isDisabled(rep.playlist));
|
|
|
53257 |
} // filter out any variant that has greater effective bitrate
|
|
|
53258 |
// than the current estimated bandwidth
|
|
|
53259 |
|
|
|
53260 |
const bandwidthPlaylistReps = enabledPlaylistReps.filter(rep => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth);
|
|
|
53261 |
let highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth
|
|
|
53262 |
// and then taking the very first element
|
|
|
53263 |
|
|
|
53264 |
const bandwidthBestRep = bandwidthPlaylistReps.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; // if we're not going to limit renditions by player size, make an early decision.
|
|
|
53265 |
|
|
|
53266 |
if (limitRenditionByPlayerDimensions === false) {
|
|
|
53267 |
const chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0];
|
|
|
53268 |
if (chosenRep && chosenRep.playlist) {
|
|
|
53269 |
let type = 'sortedPlaylistReps';
|
|
|
53270 |
if (bandwidthBestRep) {
|
|
|
53271 |
type = 'bandwidthBestRep';
|
|
|
53272 |
}
|
|
|
53273 |
if (enabledPlaylistReps[0]) {
|
|
|
53274 |
type = 'enabledPlaylistReps';
|
|
|
53275 |
}
|
|
|
53276 |
logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options);
|
|
|
53277 |
return chosenRep.playlist;
|
|
|
53278 |
}
|
|
|
53279 |
logFn('could not choose a playlist with options', options);
|
|
|
53280 |
return null;
|
|
|
53281 |
} // filter out playlists without resolution information
|
|
|
53282 |
|
|
|
53283 |
const haveResolution = bandwidthPlaylistReps.filter(rep => rep.width && rep.height); // sort variants by resolution
|
|
|
53284 |
|
|
|
53285 |
stableSort(haveResolution, (left, right) => left.width - right.width); // if we have the exact resolution as the player use it
|
|
|
53286 |
|
|
|
53287 |
const resolutionBestRepList = haveResolution.filter(rep => rep.width === playerWidth && rep.height === playerHeight);
|
|
|
53288 |
highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution
|
|
|
53289 |
|
|
|
53290 |
const resolutionBestRep = resolutionBestRepList.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
|
|
53291 |
let resolutionPlusOneList;
|
|
|
53292 |
let resolutionPlusOneSmallest;
|
|
|
53293 |
let resolutionPlusOneRep; // find the smallest variant that is larger than the player
|
|
|
53294 |
// if there is no match of exact resolution
|
|
|
53295 |
|
|
|
53296 |
if (!resolutionBestRep) {
|
|
|
53297 |
resolutionPlusOneList = haveResolution.filter(rep => rep.width > playerWidth || rep.height > playerHeight); // find all the variants have the same smallest resolution
|
|
|
53298 |
|
|
|
53299 |
resolutionPlusOneSmallest = resolutionPlusOneList.filter(rep => rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height); // ensure that we also pick the highest bandwidth variant that
|
|
|
53300 |
// is just-larger-than the video player
|
|
|
53301 |
|
|
|
53302 |
highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
|
|
|
53303 |
resolutionPlusOneRep = resolutionPlusOneSmallest.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
|
|
|
53304 |
}
|
|
|
53305 |
let leastPixelDiffRep; // If this selector proves to be better than others,
|
|
|
53306 |
// resolutionPlusOneRep and resolutionBestRep and all
|
|
|
53307 |
// the code involving them should be removed.
|
|
|
53308 |
|
|
|
53309 |
if (playlistController.leastPixelDiffSelector) {
|
|
|
53310 |
// find the variant that is closest to the player's pixel size
|
|
|
53311 |
const leastPixelDiffList = haveResolution.map(rep => {
|
|
|
53312 |
rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight);
|
|
|
53313 |
return rep;
|
|
|
53314 |
}); // get the highest bandwidth, closest resolution playlist
|
|
|
53315 |
|
|
|
53316 |
stableSort(leastPixelDiffList, (left, right) => {
|
|
|
53317 |
// sort by highest bandwidth if pixelDiff is the same
|
|
|
53318 |
if (left.pixelDiff === right.pixelDiff) {
|
|
|
53319 |
return right.bandwidth - left.bandwidth;
|
|
|
53320 |
}
|
|
|
53321 |
return left.pixelDiff - right.pixelDiff;
|
|
|
53322 |
});
|
|
|
53323 |
leastPixelDiffRep = leastPixelDiffList[0];
|
|
|
53324 |
} // fallback chain of variants
|
|
|
53325 |
|
|
|
53326 |
const chosenRep = leastPixelDiffRep || resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0];
|
|
|
53327 |
if (chosenRep && chosenRep.playlist) {
|
|
|
53328 |
let type = 'sortedPlaylistReps';
|
|
|
53329 |
if (leastPixelDiffRep) {
|
|
|
53330 |
type = 'leastPixelDiffRep';
|
|
|
53331 |
} else if (resolutionPlusOneRep) {
|
|
|
53332 |
type = 'resolutionPlusOneRep';
|
|
|
53333 |
} else if (resolutionBestRep) {
|
|
|
53334 |
type = 'resolutionBestRep';
|
|
|
53335 |
} else if (bandwidthBestRep) {
|
|
|
53336 |
type = 'bandwidthBestRep';
|
|
|
53337 |
} else if (enabledPlaylistReps[0]) {
|
|
|
53338 |
type = 'enabledPlaylistReps';
|
|
|
53339 |
}
|
|
|
53340 |
logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options);
|
|
|
53341 |
return chosenRep.playlist;
|
|
|
53342 |
}
|
|
|
53343 |
logFn('could not choose a playlist with options', options);
|
|
|
53344 |
return null;
|
|
|
53345 |
};
|
|
|
53346 |
|
|
|
53347 |
/**
|
|
|
53348 |
* Chooses the appropriate media playlist based on the most recent
|
|
|
53349 |
* bandwidth estimate and the player size.
|
|
|
53350 |
*
|
|
|
53351 |
* Expects to be called within the context of an instance of VhsHandler
|
|
|
53352 |
*
|
|
|
53353 |
* @return {Playlist} the highest bitrate playlist less than the
|
|
|
53354 |
* currently detected bandwidth, accounting for some amount of
|
|
|
53355 |
* bandwidth variance
|
|
|
53356 |
*/
|
|
|
53357 |
|
|
|
53358 |
const lastBandwidthSelector = function () {
|
|
|
53359 |
const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
|
|
|
53360 |
return simpleSelector(this.playlists.main, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, this.limitRenditionByPlayerDimensions, this.playlistController_);
|
|
|
53361 |
};
|
|
|
53362 |
/**
|
|
|
53363 |
* Chooses the appropriate media playlist based on an
|
|
|
53364 |
* exponential-weighted moving average of the bandwidth after
|
|
|
53365 |
* filtering for player size.
|
|
|
53366 |
*
|
|
|
53367 |
* Expects to be called within the context of an instance of VhsHandler
|
|
|
53368 |
*
|
|
|
53369 |
* @param {number} decay - a number between 0 and 1. Higher values of
|
|
|
53370 |
* this parameter will cause previous bandwidth estimates to lose
|
|
|
53371 |
* significance more quickly.
|
|
|
53372 |
* @return {Function} a function which can be invoked to create a new
|
|
|
53373 |
* playlist selector function.
|
|
|
53374 |
* @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
|
|
|
53375 |
*/
|
|
|
53376 |
|
|
|
53377 |
const movingAverageBandwidthSelector = function (decay) {
|
|
|
53378 |
let average = -1;
|
|
|
53379 |
let lastSystemBandwidth = -1;
|
|
|
53380 |
if (decay < 0 || decay > 1) {
|
|
|
53381 |
throw new Error('Moving average bandwidth decay must be between 0 and 1.');
|
|
|
53382 |
}
|
|
|
53383 |
return function () {
|
|
|
53384 |
const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
|
|
|
53385 |
if (average < 0) {
|
|
|
53386 |
average = this.systemBandwidth;
|
|
|
53387 |
lastSystemBandwidth = this.systemBandwidth;
|
|
|
53388 |
} // stop the average value from decaying for every 250ms
|
|
|
53389 |
// when the systemBandwidth is constant
|
|
|
53390 |
// and
|
|
|
53391 |
// stop average from setting to a very low value when the
|
|
|
53392 |
// systemBandwidth becomes 0 in case of chunk cancellation
|
|
|
53393 |
|
|
|
53394 |
if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) {
|
|
|
53395 |
average = decay * this.systemBandwidth + (1 - decay) * average;
|
|
|
53396 |
lastSystemBandwidth = this.systemBandwidth;
|
|
|
53397 |
}
|
|
|
53398 |
return simpleSelector(this.playlists.main, average, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, this.limitRenditionByPlayerDimensions, this.playlistController_);
|
|
|
53399 |
};
|
|
|
53400 |
};
|
|
|
53401 |
/**
|
|
|
53402 |
* Chooses the appropriate media playlist based on the potential to rebuffer
|
|
|
53403 |
*
|
|
|
53404 |
* @param {Object} settings
|
|
|
53405 |
* Object of information required to use this selector
|
|
|
53406 |
* @param {Object} settings.main
|
|
|
53407 |
* Object representation of the main manifest
|
|
|
53408 |
* @param {number} settings.currentTime
|
|
|
53409 |
* The current time of the player
|
|
|
53410 |
* @param {number} settings.bandwidth
|
|
|
53411 |
* Current measured bandwidth
|
|
|
53412 |
* @param {number} settings.duration
|
|
|
53413 |
* Duration of the media
|
|
|
53414 |
* @param {number} settings.segmentDuration
|
|
|
53415 |
* Segment duration to be used in round trip time calculations
|
|
|
53416 |
* @param {number} settings.timeUntilRebuffer
|
|
|
53417 |
* Time left in seconds until the player has to rebuffer
|
|
|
53418 |
* @param {number} settings.currentTimeline
|
|
|
53419 |
* The current timeline segments are being loaded from
|
|
|
53420 |
* @param {SyncController} settings.syncController
|
|
|
53421 |
* SyncController for determining if we have a sync point for a given playlist
|
|
|
53422 |
* @return {Object|null}
|
|
|
53423 |
* {Object} return.playlist
|
|
|
53424 |
* The highest bandwidth playlist with the least amount of rebuffering
|
|
|
53425 |
* {Number} return.rebufferingImpact
|
|
|
53426 |
* The amount of time in seconds switching to this playlist will rebuffer. A
|
|
|
53427 |
* negative value means that switching will cause zero rebuffering.
|
|
|
53428 |
*/
|
|
|
53429 |
|
|
|
53430 |
const minRebufferMaxBandwidthSelector = function (settings) {
|
|
|
53431 |
const {
|
|
|
53432 |
main,
|
|
|
53433 |
currentTime,
|
|
|
53434 |
bandwidth,
|
|
|
53435 |
duration,
|
|
|
53436 |
segmentDuration,
|
|
|
53437 |
timeUntilRebuffer,
|
|
|
53438 |
currentTimeline,
|
|
|
53439 |
syncController
|
|
|
53440 |
} = settings; // filter out any playlists that have been excluded due to
|
|
|
53441 |
// incompatible configurations
|
|
|
53442 |
|
|
|
53443 |
const compatiblePlaylists = main.playlists.filter(playlist => !Playlist.isIncompatible(playlist)); // filter out any playlists that have been disabled manually through the representations
|
|
|
53444 |
// api or excluded temporarily due to playback errors.
|
|
|
53445 |
|
|
|
53446 |
let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled);
|
|
|
53447 |
if (!enabledPlaylists.length) {
|
|
|
53448 |
// if there are no enabled playlists, then they have all been excluded or disabled
|
|
|
53449 |
// by the user through the representations api. In this case, ignore exclusion and
|
|
|
53450 |
// fallback to what the user wants by using playlists the user has not disabled.
|
|
|
53451 |
enabledPlaylists = compatiblePlaylists.filter(playlist => !Playlist.isDisabled(playlist));
|
|
|
53452 |
}
|
|
|
53453 |
const bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH'));
|
|
|
53454 |
const rebufferingEstimates = bandwidthPlaylists.map(playlist => {
|
|
|
53455 |
const syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a
|
|
|
53456 |
// sync request first. This will double the request time
|
|
|
53457 |
|
|
|
53458 |
const numRequests = syncPoint ? 1 : 2;
|
|
|
53459 |
const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist);
|
|
|
53460 |
const rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer;
|
|
|
53461 |
return {
|
|
|
53462 |
playlist,
|
|
|
53463 |
rebufferingImpact
|
|
|
53464 |
};
|
|
|
53465 |
});
|
|
|
53466 |
const noRebufferingPlaylists = rebufferingEstimates.filter(estimate => estimate.rebufferingImpact <= 0); // Sort by bandwidth DESC
|
|
|
53467 |
|
|
|
53468 |
stableSort(noRebufferingPlaylists, (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist));
|
|
|
53469 |
if (noRebufferingPlaylists.length) {
|
|
|
53470 |
return noRebufferingPlaylists[0];
|
|
|
53471 |
}
|
|
|
53472 |
stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact);
|
|
|
53473 |
return rebufferingEstimates[0] || null;
|
|
|
53474 |
};
|
|
|
53475 |
/**
|
|
|
53476 |
* Chooses the appropriate media playlist, which in this case is the lowest bitrate
|
|
|
53477 |
* one with video. If no renditions with video exist, return the lowest audio rendition.
|
|
|
53478 |
*
|
|
|
53479 |
* Expects to be called within the context of an instance of VhsHandler
|
|
|
53480 |
*
|
|
|
53481 |
* @return {Object|null}
|
|
|
53482 |
* {Object} return.playlist
|
|
|
53483 |
* The lowest bitrate playlist that contains a video codec. If no such rendition
|
|
|
53484 |
* exists pick the lowest audio rendition.
|
|
|
53485 |
*/
|
|
|
53486 |
|
|
|
53487 |
const lowestBitrateCompatibleVariantSelector = function () {
|
|
|
53488 |
// filter out any playlists that have been excluded due to
|
|
|
53489 |
// incompatible configurations or playback errors
|
|
|
53490 |
const playlists = this.playlists.main.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate
|
|
|
53491 |
|
|
|
53492 |
stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); // Parse and assume that playlists with no video codec have no video
|
|
|
53493 |
// (this is not necessarily true, although it is generally true).
|
|
|
53494 |
//
|
|
|
53495 |
// If an entire manifest has no valid videos everything will get filtered
|
|
|
53496 |
// out.
|
|
|
53497 |
|
|
|
53498 |
const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.main, playlist).video);
|
|
|
53499 |
return playlistsWithVideo[0] || null;
|
|
|
53500 |
};
|
|
|
53501 |
|
|
|
53502 |
/**
|
|
|
53503 |
* Combine all segments into a single Uint8Array
|
|
|
53504 |
*
|
|
|
53505 |
* @param {Object} segmentObj
|
|
|
53506 |
* @return {Uint8Array} concatenated bytes
|
|
|
53507 |
* @private
|
|
|
53508 |
*/
|
|
|
53509 |
const concatSegments = segmentObj => {
|
|
|
53510 |
let offset = 0;
|
|
|
53511 |
let tempBuffer;
|
|
|
53512 |
if (segmentObj.bytes) {
|
|
|
53513 |
tempBuffer = new Uint8Array(segmentObj.bytes); // combine the individual segments into one large typed-array
|
|
|
53514 |
|
|
|
53515 |
segmentObj.segments.forEach(segment => {
|
|
|
53516 |
tempBuffer.set(segment, offset);
|
|
|
53517 |
offset += segment.byteLength;
|
|
|
53518 |
});
|
|
|
53519 |
}
|
|
|
53520 |
return tempBuffer;
|
|
|
53521 |
};
|
|
|
53522 |
|
|
|
53523 |
/**
|
|
|
53524 |
* @file text-tracks.js
|
|
|
53525 |
*/
|
|
|
53526 |
/**
|
|
|
53527 |
* Create captions text tracks on video.js if they do not exist
|
|
|
53528 |
*
|
|
|
53529 |
* @param {Object} inbandTextTracks a reference to current inbandTextTracks
|
|
|
53530 |
* @param {Object} tech the video.js tech
|
|
|
53531 |
* @param {Object} captionStream the caption stream to create
|
|
|
53532 |
* @private
|
|
|
53533 |
*/
|
|
|
53534 |
|
|
|
53535 |
const createCaptionsTrackIfNotExists = function (inbandTextTracks, tech, captionStream) {
|
|
|
53536 |
if (!inbandTextTracks[captionStream]) {
|
|
|
53537 |
tech.trigger({
|
|
|
53538 |
type: 'usage',
|
|
|
53539 |
name: 'vhs-608'
|
|
|
53540 |
});
|
|
|
53541 |
let instreamId = captionStream; // we need to translate SERVICEn for 708 to how mux.js currently labels them
|
|
|
53542 |
|
|
|
53543 |
if (/^cc708_/.test(captionStream)) {
|
|
|
53544 |
instreamId = 'SERVICE' + captionStream.split('_')[1];
|
|
|
53545 |
}
|
|
|
53546 |
const track = tech.textTracks().getTrackById(instreamId);
|
|
|
53547 |
if (track) {
|
|
|
53548 |
// Resuse an existing track with a CC# id because this was
|
|
|
53549 |
// very likely created by videojs-contrib-hls from information
|
|
|
53550 |
// in the m3u8 for us to use
|
|
|
53551 |
inbandTextTracks[captionStream] = track;
|
|
|
53552 |
} else {
|
|
|
53553 |
// This section gets called when we have caption services that aren't specified in the manifest.
|
|
|
53554 |
// Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS.
|
|
|
53555 |
const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {};
|
|
|
53556 |
let label = captionStream;
|
|
|
53557 |
let language = captionStream;
|
|
|
53558 |
let def = false;
|
|
|
53559 |
const captionService = captionServices[instreamId];
|
|
|
53560 |
if (captionService) {
|
|
|
53561 |
label = captionService.label;
|
|
|
53562 |
language = captionService.language;
|
|
|
53563 |
def = captionService.default;
|
|
|
53564 |
} // Otherwise, create a track with the default `CC#` label and
|
|
|
53565 |
// without a language
|
|
|
53566 |
|
|
|
53567 |
inbandTextTracks[captionStream] = tech.addRemoteTextTrack({
|
|
|
53568 |
kind: 'captions',
|
|
|
53569 |
id: instreamId,
|
|
|
53570 |
// TODO: investigate why this doesn't seem to turn the caption on by default
|
|
|
53571 |
default: def,
|
|
|
53572 |
label,
|
|
|
53573 |
language
|
|
|
53574 |
}, false).track;
|
|
|
53575 |
}
|
|
|
53576 |
}
|
|
|
53577 |
};
|
|
|
53578 |
/**
|
|
|
53579 |
* Add caption text track data to a source handler given an array of captions
|
|
|
53580 |
*
|
|
|
53581 |
* @param {Object}
|
|
|
53582 |
* @param {Object} inbandTextTracks the inband text tracks
|
|
|
53583 |
* @param {number} timestampOffset the timestamp offset of the source buffer
|
|
|
53584 |
* @param {Array} captionArray an array of caption data
|
|
|
53585 |
* @private
|
|
|
53586 |
*/
|
|
|
53587 |
|
|
|
53588 |
const addCaptionData = function ({
|
|
|
53589 |
inbandTextTracks,
|
|
|
53590 |
captionArray,
|
|
|
53591 |
timestampOffset
|
|
|
53592 |
}) {
|
|
|
53593 |
if (!captionArray) {
|
|
|
53594 |
return;
|
|
|
53595 |
}
|
|
|
53596 |
const Cue = window.WebKitDataCue || window.VTTCue;
|
|
|
53597 |
captionArray.forEach(caption => {
|
|
|
53598 |
const track = caption.stream; // in CEA 608 captions, video.js/mux.js sends a content array
|
|
|
53599 |
// with positioning data
|
|
|
53600 |
|
|
|
53601 |
if (caption.content) {
|
|
|
53602 |
caption.content.forEach(value => {
|
|
|
53603 |
const cue = new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, value.text);
|
|
|
53604 |
cue.line = value.line;
|
|
|
53605 |
cue.align = 'left';
|
|
|
53606 |
cue.position = value.position;
|
|
|
53607 |
cue.positionAlign = 'line-left';
|
|
|
53608 |
inbandTextTracks[track].addCue(cue);
|
|
|
53609 |
});
|
|
|
53610 |
} else {
|
|
|
53611 |
// otherwise, a text value with combined captions is sent
|
|
|
53612 |
inbandTextTracks[track].addCue(new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, caption.text));
|
|
|
53613 |
}
|
|
|
53614 |
});
|
|
|
53615 |
};
|
|
|
53616 |
/**
|
|
|
53617 |
* Define properties on a cue for backwards compatability,
|
|
|
53618 |
* but warn the user that the way that they are using it
|
|
|
53619 |
* is depricated and will be removed at a later date.
|
|
|
53620 |
*
|
|
|
53621 |
* @param {Cue} cue the cue to add the properties on
|
|
|
53622 |
* @private
|
|
|
53623 |
*/
|
|
|
53624 |
|
|
|
53625 |
const deprecateOldCue = function (cue) {
|
|
|
53626 |
Object.defineProperties(cue.frame, {
|
|
|
53627 |
id: {
|
|
|
53628 |
get() {
|
|
|
53629 |
videojs.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.');
|
|
|
53630 |
return cue.value.key;
|
|
|
53631 |
}
|
|
|
53632 |
},
|
|
|
53633 |
value: {
|
|
|
53634 |
get() {
|
|
|
53635 |
videojs.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.');
|
|
|
53636 |
return cue.value.data;
|
|
|
53637 |
}
|
|
|
53638 |
},
|
|
|
53639 |
privateData: {
|
|
|
53640 |
get() {
|
|
|
53641 |
videojs.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.');
|
|
|
53642 |
return cue.value.data;
|
|
|
53643 |
}
|
|
|
53644 |
}
|
|
|
53645 |
});
|
|
|
53646 |
};
|
|
|
53647 |
/**
|
|
|
53648 |
* Add metadata text track data to a source handler given an array of metadata
|
|
|
53649 |
*
|
|
|
53650 |
* @param {Object}
|
|
|
53651 |
* @param {Object} inbandTextTracks the inband text tracks
|
|
|
53652 |
* @param {Array} metadataArray an array of meta data
|
|
|
53653 |
* @param {number} timestampOffset the timestamp offset of the source buffer
|
|
|
53654 |
* @param {number} videoDuration the duration of the video
|
|
|
53655 |
* @private
|
|
|
53656 |
*/
|
|
|
53657 |
|
|
|
53658 |
const addMetadata = ({
|
|
|
53659 |
inbandTextTracks,
|
|
|
53660 |
metadataArray,
|
|
|
53661 |
timestampOffset,
|
|
|
53662 |
videoDuration
|
|
|
53663 |
}) => {
|
|
|
53664 |
if (!metadataArray) {
|
|
|
53665 |
return;
|
|
|
53666 |
}
|
|
|
53667 |
const Cue = window.WebKitDataCue || window.VTTCue;
|
|
|
53668 |
const metadataTrack = inbandTextTracks.metadataTrack_;
|
|
|
53669 |
if (!metadataTrack) {
|
|
|
53670 |
return;
|
|
|
53671 |
}
|
|
|
53672 |
metadataArray.forEach(metadata => {
|
|
|
53673 |
const time = metadata.cueTime + timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN,
|
|
|
53674 |
// ignore this bit of metadata.
|
|
|
53675 |
// This likely occurs when you have an non-timed ID3 tag like TIT2,
|
|
|
53676 |
// which is the "Title/Songname/Content description" frame
|
|
|
53677 |
|
|
|
53678 |
if (typeof time !== 'number' || window.isNaN(time) || time < 0 || !(time < Infinity)) {
|
|
|
53679 |
return;
|
|
|
53680 |
} // If we have no frames, we can't create a cue.
|
|
|
53681 |
|
|
|
53682 |
if (!metadata.frames || !metadata.frames.length) {
|
|
|
53683 |
return;
|
|
|
53684 |
}
|
|
|
53685 |
metadata.frames.forEach(frame => {
|
|
|
53686 |
const cue = new Cue(time, time, frame.value || frame.url || frame.data || '');
|
|
|
53687 |
cue.frame = frame;
|
|
|
53688 |
cue.value = frame;
|
|
|
53689 |
deprecateOldCue(cue);
|
|
|
53690 |
metadataTrack.addCue(cue);
|
|
|
53691 |
});
|
|
|
53692 |
});
|
|
|
53693 |
if (!metadataTrack.cues || !metadataTrack.cues.length) {
|
|
|
53694 |
return;
|
|
|
53695 |
} // Updating the metadeta cues so that
|
|
|
53696 |
// the endTime of each cue is the startTime of the next cue
|
|
|
53697 |
// the endTime of last cue is the duration of the video
|
|
|
53698 |
|
|
|
53699 |
const cues = metadataTrack.cues;
|
|
|
53700 |
const cuesArray = []; // Create a copy of the TextTrackCueList...
|
|
|
53701 |
// ...disregarding cues with a falsey value
|
|
|
53702 |
|
|
|
53703 |
for (let i = 0; i < cues.length; i++) {
|
|
|
53704 |
if (cues[i]) {
|
|
|
53705 |
cuesArray.push(cues[i]);
|
|
|
53706 |
}
|
|
|
53707 |
} // Group cues by their startTime value
|
|
|
53708 |
|
|
|
53709 |
const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => {
|
|
|
53710 |
const timeSlot = obj[cue.startTime] || [];
|
|
|
53711 |
timeSlot.push(cue);
|
|
|
53712 |
obj[cue.startTime] = timeSlot;
|
|
|
53713 |
return obj;
|
|
|
53714 |
}, {}); // Sort startTimes by ascending order
|
|
|
53715 |
|
|
|
53716 |
const sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort((a, b) => Number(a) - Number(b)); // Map each cue group's endTime to the next group's startTime
|
|
|
53717 |
|
|
|
53718 |
sortedStartTimes.forEach((startTime, idx) => {
|
|
|
53719 |
const cueGroup = cuesGroupedByStartTime[startTime];
|
|
|
53720 |
const finiteDuration = isFinite(videoDuration) ? videoDuration : startTime;
|
|
|
53721 |
const nextTime = Number(sortedStartTimes[idx + 1]) || finiteDuration; // Map each cue's endTime the next group's startTime
|
|
|
53722 |
|
|
|
53723 |
cueGroup.forEach(cue => {
|
|
|
53724 |
cue.endTime = nextTime;
|
|
|
53725 |
});
|
|
|
53726 |
});
|
|
|
53727 |
}; // object for mapping daterange attributes
|
|
|
53728 |
|
|
|
53729 |
const dateRangeAttr = {
|
|
|
53730 |
id: 'ID',
|
|
|
53731 |
class: 'CLASS',
|
|
|
53732 |
startDate: 'START-DATE',
|
|
|
53733 |
duration: 'DURATION',
|
|
|
53734 |
endDate: 'END-DATE',
|
|
|
53735 |
endOnNext: 'END-ON-NEXT',
|
|
|
53736 |
plannedDuration: 'PLANNED-DURATION',
|
|
|
53737 |
scte35Out: 'SCTE35-OUT',
|
|
|
53738 |
scte35In: 'SCTE35-IN'
|
|
|
53739 |
};
|
|
|
53740 |
const dateRangeKeysToOmit = new Set(['id', 'class', 'startDate', 'duration', 'endDate', 'endOnNext', 'startTime', 'endTime', 'processDateRange']);
|
|
|
53741 |
/**
|
|
|
53742 |
* Add DateRange metadata text track to a source handler given an array of metadata
|
|
|
53743 |
*
|
|
|
53744 |
* @param {Object}
|
|
|
53745 |
* @param {Object} inbandTextTracks the inband text tracks
|
|
|
53746 |
* @param {Array} dateRanges parsed media playlist
|
|
|
53747 |
* @private
|
|
|
53748 |
*/
|
|
|
53749 |
|
|
|
53750 |
const addDateRangeMetadata = ({
|
|
|
53751 |
inbandTextTracks,
|
|
|
53752 |
dateRanges
|
|
|
53753 |
}) => {
|
|
|
53754 |
const metadataTrack = inbandTextTracks.metadataTrack_;
|
|
|
53755 |
if (!metadataTrack) {
|
|
|
53756 |
return;
|
|
|
53757 |
}
|
|
|
53758 |
const Cue = window.WebKitDataCue || window.VTTCue;
|
|
|
53759 |
dateRanges.forEach(dateRange => {
|
|
|
53760 |
// we generate multiple cues for each date range with different attributes
|
|
|
53761 |
for (const key of Object.keys(dateRange)) {
|
|
|
53762 |
if (dateRangeKeysToOmit.has(key)) {
|
|
|
53763 |
continue;
|
|
|
53764 |
}
|
|
|
53765 |
const cue = new Cue(dateRange.startTime, dateRange.endTime, '');
|
|
|
53766 |
cue.id = dateRange.id;
|
|
|
53767 |
cue.type = 'com.apple.quicktime.HLS';
|
|
|
53768 |
cue.value = {
|
|
|
53769 |
key: dateRangeAttr[key],
|
|
|
53770 |
data: dateRange[key]
|
|
|
53771 |
};
|
|
|
53772 |
if (key === 'scte35Out' || key === 'scte35In') {
|
|
|
53773 |
cue.value.data = new Uint8Array(cue.value.data.match(/[\da-f]{2}/gi)).buffer;
|
|
|
53774 |
}
|
|
|
53775 |
metadataTrack.addCue(cue);
|
|
|
53776 |
}
|
|
|
53777 |
dateRange.processDateRange();
|
|
|
53778 |
});
|
|
|
53779 |
};
|
|
|
53780 |
/**
|
|
|
53781 |
* Create metadata text track on video.js if it does not exist
|
|
|
53782 |
*
|
|
|
53783 |
* @param {Object} inbandTextTracks a reference to current inbandTextTracks
|
|
|
53784 |
* @param {string} dispatchType the inband metadata track dispatch type
|
|
|
53785 |
* @param {Object} tech the video.js tech
|
|
|
53786 |
* @private
|
|
|
53787 |
*/
|
|
|
53788 |
|
|
|
53789 |
const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => {
|
|
|
53790 |
if (inbandTextTracks.metadataTrack_) {
|
|
|
53791 |
return;
|
|
|
53792 |
}
|
|
|
53793 |
inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack({
|
|
|
53794 |
kind: 'metadata',
|
|
|
53795 |
label: 'Timed Metadata'
|
|
|
53796 |
}, false).track;
|
|
|
53797 |
if (!videojs.browser.IS_ANY_SAFARI) {
|
|
|
53798 |
inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType;
|
|
|
53799 |
}
|
|
|
53800 |
};
|
|
|
53801 |
/**
|
|
|
53802 |
* Remove cues from a track on video.js.
|
|
|
53803 |
*
|
|
|
53804 |
* @param {Double} start start of where we should remove the cue
|
|
|
53805 |
* @param {Double} end end of where the we should remove the cue
|
|
|
53806 |
* @param {Object} track the text track to remove the cues from
|
|
|
53807 |
* @private
|
|
|
53808 |
*/
|
|
|
53809 |
|
|
|
53810 |
const removeCuesFromTrack = function (start, end, track) {
|
|
|
53811 |
let i;
|
|
|
53812 |
let cue;
|
|
|
53813 |
if (!track) {
|
|
|
53814 |
return;
|
|
|
53815 |
}
|
|
|
53816 |
if (!track.cues) {
|
|
|
53817 |
return;
|
|
|
53818 |
}
|
|
|
53819 |
i = track.cues.length;
|
|
|
53820 |
while (i--) {
|
|
|
53821 |
cue = track.cues[i]; // Remove any cue within the provided start and end time
|
|
|
53822 |
|
|
|
53823 |
if (cue.startTime >= start && cue.endTime <= end) {
|
|
|
53824 |
track.removeCue(cue);
|
|
|
53825 |
}
|
|
|
53826 |
}
|
|
|
53827 |
};
|
|
|
53828 |
/**
|
|
|
53829 |
* Remove duplicate cues from a track on video.js (a cue is considered a
|
|
|
53830 |
* duplicate if it has the same time interval and text as another)
|
|
|
53831 |
*
|
|
|
53832 |
* @param {Object} track the text track to remove the duplicate cues from
|
|
|
53833 |
* @private
|
|
|
53834 |
*/
|
|
|
53835 |
|
|
|
53836 |
const removeDuplicateCuesFromTrack = function (track) {
|
|
|
53837 |
const cues = track.cues;
|
|
|
53838 |
if (!cues) {
|
|
|
53839 |
return;
|
|
|
53840 |
}
|
|
|
53841 |
const uniqueCues = {};
|
|
|
53842 |
for (let i = cues.length - 1; i >= 0; i--) {
|
|
|
53843 |
const cue = cues[i];
|
|
|
53844 |
const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`;
|
|
|
53845 |
if (uniqueCues[cueKey]) {
|
|
|
53846 |
track.removeCue(cue);
|
|
|
53847 |
} else {
|
|
|
53848 |
uniqueCues[cueKey] = cue;
|
|
|
53849 |
}
|
|
|
53850 |
}
|
|
|
53851 |
};
|
|
|
53852 |
|
|
|
53853 |
/**
|
|
|
53854 |
* Returns a list of gops in the buffer that have a pts value of 3 seconds or more in
|
|
|
53855 |
* front of current time.
|
|
|
53856 |
*
|
|
|
53857 |
* @param {Array} buffer
|
|
|
53858 |
* The current buffer of gop information
|
|
|
53859 |
* @param {number} currentTime
|
|
|
53860 |
* The current time
|
|
|
53861 |
* @param {Double} mapping
|
|
|
53862 |
* Offset to map display time to stream presentation time
|
|
|
53863 |
* @return {Array}
|
|
|
53864 |
* List of gops considered safe to append over
|
|
|
53865 |
*/
|
|
|
53866 |
|
|
|
53867 |
const gopsSafeToAlignWith = (buffer, currentTime, mapping) => {
|
|
|
53868 |
if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) {
|
|
|
53869 |
return [];
|
|
|
53870 |
} // pts value for current time + 3 seconds to give a bit more wiggle room
|
|
|
53871 |
|
|
|
53872 |
const currentTimePts = Math.ceil((currentTime - mapping + 3) * clock_1);
|
|
|
53873 |
let i;
|
|
|
53874 |
for (i = 0; i < buffer.length; i++) {
|
|
|
53875 |
if (buffer[i].pts > currentTimePts) {
|
|
|
53876 |
break;
|
|
|
53877 |
}
|
|
|
53878 |
}
|
|
|
53879 |
return buffer.slice(i);
|
|
|
53880 |
};
|
|
|
53881 |
/**
|
|
|
53882 |
* Appends gop information (timing and byteLength) received by the transmuxer for the
|
|
|
53883 |
* gops appended in the last call to appendBuffer
|
|
|
53884 |
*
|
|
|
53885 |
* @param {Array} buffer
|
|
|
53886 |
* The current buffer of gop information
|
|
|
53887 |
* @param {Array} gops
|
|
|
53888 |
* List of new gop information
|
|
|
53889 |
* @param {boolean} replace
|
|
|
53890 |
* If true, replace the buffer with the new gop information. If false, append the
|
|
|
53891 |
* new gop information to the buffer in the right location of time.
|
|
|
53892 |
* @return {Array}
|
|
|
53893 |
* Updated list of gop information
|
|
|
53894 |
*/
|
|
|
53895 |
|
|
|
53896 |
const updateGopBuffer = (buffer, gops, replace) => {
|
|
|
53897 |
if (!gops.length) {
|
|
|
53898 |
return buffer;
|
|
|
53899 |
}
|
|
|
53900 |
if (replace) {
|
|
|
53901 |
// If we are in safe append mode, then completely overwrite the gop buffer
|
|
|
53902 |
// with the most recent appeneded data. This will make sure that when appending
|
|
|
53903 |
// future segments, we only try to align with gops that are both ahead of current
|
|
|
53904 |
// time and in the last segment appended.
|
|
|
53905 |
return gops.slice();
|
|
|
53906 |
}
|
|
|
53907 |
const start = gops[0].pts;
|
|
|
53908 |
let i = 0;
|
|
|
53909 |
for (i; i < buffer.length; i++) {
|
|
|
53910 |
if (buffer[i].pts >= start) {
|
|
|
53911 |
break;
|
|
|
53912 |
}
|
|
|
53913 |
}
|
|
|
53914 |
return buffer.slice(0, i).concat(gops);
|
|
|
53915 |
};
|
|
|
53916 |
/**
|
|
|
53917 |
* Removes gop information in buffer that overlaps with provided start and end
|
|
|
53918 |
*
|
|
|
53919 |
* @param {Array} buffer
|
|
|
53920 |
* The current buffer of gop information
|
|
|
53921 |
* @param {Double} start
|
|
|
53922 |
* position to start the remove at
|
|
|
53923 |
* @param {Double} end
|
|
|
53924 |
* position to end the remove at
|
|
|
53925 |
* @param {Double} mapping
|
|
|
53926 |
* Offset to map display time to stream presentation time
|
|
|
53927 |
*/
|
|
|
53928 |
|
|
|
53929 |
const removeGopBuffer = (buffer, start, end, mapping) => {
|
|
|
53930 |
const startPts = Math.ceil((start - mapping) * clock_1);
|
|
|
53931 |
const endPts = Math.ceil((end - mapping) * clock_1);
|
|
|
53932 |
const updatedBuffer = buffer.slice();
|
|
|
53933 |
let i = buffer.length;
|
|
|
53934 |
while (i--) {
|
|
|
53935 |
if (buffer[i].pts <= endPts) {
|
|
|
53936 |
break;
|
|
|
53937 |
}
|
|
|
53938 |
}
|
|
|
53939 |
if (i === -1) {
|
|
|
53940 |
// no removal because end of remove range is before start of buffer
|
|
|
53941 |
return updatedBuffer;
|
|
|
53942 |
}
|
|
|
53943 |
let j = i + 1;
|
|
|
53944 |
while (j--) {
|
|
|
53945 |
if (buffer[j].pts <= startPts) {
|
|
|
53946 |
break;
|
|
|
53947 |
}
|
|
|
53948 |
} // clamp remove range start to 0 index
|
|
|
53949 |
|
|
|
53950 |
j = Math.max(j, 0);
|
|
|
53951 |
updatedBuffer.splice(j, i - j + 1);
|
|
|
53952 |
return updatedBuffer;
|
|
|
53953 |
};
|
|
|
53954 |
const shallowEqual = function (a, b) {
|
|
|
53955 |
// if both are undefined
|
|
|
53956 |
// or one or the other is undefined
|
|
|
53957 |
// they are not equal
|
|
|
53958 |
if (!a && !b || !a && b || a && !b) {
|
|
|
53959 |
return false;
|
|
|
53960 |
} // they are the same object and thus, equal
|
|
|
53961 |
|
|
|
53962 |
if (a === b) {
|
|
|
53963 |
return true;
|
|
|
53964 |
} // sort keys so we can make sure they have
|
|
|
53965 |
// all the same keys later.
|
|
|
53966 |
|
|
|
53967 |
const akeys = Object.keys(a).sort();
|
|
|
53968 |
const bkeys = Object.keys(b).sort(); // different number of keys, not equal
|
|
|
53969 |
|
|
|
53970 |
if (akeys.length !== bkeys.length) {
|
|
|
53971 |
return false;
|
|
|
53972 |
}
|
|
|
53973 |
for (let i = 0; i < akeys.length; i++) {
|
|
|
53974 |
const key = akeys[i]; // different sorted keys, not equal
|
|
|
53975 |
|
|
|
53976 |
if (key !== bkeys[i]) {
|
|
|
53977 |
return false;
|
|
|
53978 |
} // different values, not equal
|
|
|
53979 |
|
|
|
53980 |
if (a[key] !== b[key]) {
|
|
|
53981 |
return false;
|
|
|
53982 |
}
|
|
|
53983 |
}
|
|
|
53984 |
return true;
|
|
|
53985 |
};
|
|
|
53986 |
|
|
|
53987 |
// https://www.w3.org/TR/WebIDL-1/#quotaexceedederror
|
|
|
53988 |
const QUOTA_EXCEEDED_ERR = 22;
|
|
|
53989 |
|
|
|
53990 |
/**
|
|
|
53991 |
* The segment loader has no recourse except to fetch a segment in the
|
|
|
53992 |
* current playlist and use the internal timestamps in that segment to
|
|
|
53993 |
* generate a syncPoint. This function returns a good candidate index
|
|
|
53994 |
* for that process.
|
|
|
53995 |
*
|
|
|
53996 |
* @param {Array} segments - the segments array from a playlist.
|
|
|
53997 |
* @return {number} An index of a segment from the playlist to load
|
|
|
53998 |
*/
|
|
|
53999 |
|
|
|
54000 |
const getSyncSegmentCandidate = function (currentTimeline, segments, targetTime) {
|
|
|
54001 |
segments = segments || [];
|
|
|
54002 |
const timelineSegments = [];
|
|
|
54003 |
let time = 0;
|
|
|
54004 |
for (let i = 0; i < segments.length; i++) {
|
|
|
54005 |
const segment = segments[i];
|
|
|
54006 |
if (currentTimeline === segment.timeline) {
|
|
|
54007 |
timelineSegments.push(i);
|
|
|
54008 |
time += segment.duration;
|
|
|
54009 |
if (time > targetTime) {
|
|
|
54010 |
return i;
|
|
|
54011 |
}
|
|
|
54012 |
}
|
|
|
54013 |
}
|
|
|
54014 |
if (timelineSegments.length === 0) {
|
|
|
54015 |
return 0;
|
|
|
54016 |
} // default to the last timeline segment
|
|
|
54017 |
|
|
|
54018 |
return timelineSegments[timelineSegments.length - 1];
|
|
|
54019 |
}; // In the event of a quota exceeded error, keep at least one second of back buffer. This
|
|
|
54020 |
// number was arbitrarily chosen and may be updated in the future, but seemed reasonable
|
|
|
54021 |
// as a start to prevent any potential issues with removing content too close to the
|
|
|
54022 |
// playhead.
|
|
|
54023 |
|
|
|
54024 |
const MIN_BACK_BUFFER = 1; // in ms
|
|
|
54025 |
|
|
|
54026 |
const CHECK_BUFFER_DELAY = 500;
|
|
|
54027 |
const finite = num => typeof num === 'number' && isFinite(num); // With most content hovering around 30fps, if a segment has a duration less than a half
|
|
|
54028 |
// frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will
|
|
|
54029 |
// not accurately reflect the rest of the content.
|
|
|
54030 |
|
|
|
54031 |
const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60;
|
|
|
54032 |
const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => {
|
|
|
54033 |
// Although these checks should most likely cover non 'main' types, for now it narrows
|
|
|
54034 |
// the scope of our checks.
|
|
|
54035 |
if (loaderType !== 'main' || !startingMedia || !trackInfo) {
|
|
|
54036 |
return null;
|
|
|
54037 |
}
|
|
|
54038 |
if (!trackInfo.hasAudio && !trackInfo.hasVideo) {
|
|
|
54039 |
return 'Neither audio nor video found in segment.';
|
|
|
54040 |
}
|
|
|
54041 |
if (startingMedia.hasVideo && !trackInfo.hasVideo) {
|
|
|
54042 |
return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.';
|
|
|
54043 |
}
|
|
|
54044 |
if (!startingMedia.hasVideo && trackInfo.hasVideo) {
|
|
|
54045 |
return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.';
|
|
|
54046 |
}
|
|
|
54047 |
return null;
|
|
|
54048 |
};
|
|
|
54049 |
/**
|
|
|
54050 |
* Calculates a time value that is safe to remove from the back buffer without interrupting
|
|
|
54051 |
* playback.
|
|
|
54052 |
*
|
|
|
54053 |
* @param {TimeRange} seekable
|
|
|
54054 |
* The current seekable range
|
|
|
54055 |
* @param {number} currentTime
|
|
|
54056 |
* The current time of the player
|
|
|
54057 |
* @param {number} targetDuration
|
|
|
54058 |
* The target duration of the current playlist
|
|
|
54059 |
* @return {number}
|
|
|
54060 |
* Time that is safe to remove from the back buffer without interrupting playback
|
|
|
54061 |
*/
|
|
|
54062 |
|
|
|
54063 |
const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => {
|
|
|
54064 |
// 30 seconds before the playhead provides a safe default for trimming.
|
|
|
54065 |
//
|
|
|
54066 |
// Choosing a reasonable default is particularly important for high bitrate content and
|
|
|
54067 |
// VOD videos/live streams with large windows, as the buffer may end up overfilled and
|
|
|
54068 |
// throw an APPEND_BUFFER_ERR.
|
|
|
54069 |
let trimTime = currentTime - Config.BACK_BUFFER_LENGTH;
|
|
|
54070 |
if (seekable.length) {
|
|
|
54071 |
// Some live playlists may have a shorter window of content than the full allowed back
|
|
|
54072 |
// buffer. For these playlists, don't save content that's no longer within the window.
|
|
|
54073 |
trimTime = Math.max(trimTime, seekable.start(0));
|
|
|
54074 |
} // Don't remove within target duration of the current time to avoid the possibility of
|
|
|
54075 |
// removing the GOP currently being played, as removing it can cause playback stalls.
|
|
|
54076 |
|
|
|
54077 |
const maxTrimTime = currentTime - targetDuration;
|
|
|
54078 |
return Math.min(maxTrimTime, trimTime);
|
|
|
54079 |
};
|
|
|
54080 |
const segmentInfoString = segmentInfo => {
|
|
|
54081 |
const {
|
|
|
54082 |
startOfSegment,
|
|
|
54083 |
duration,
|
|
|
54084 |
segment,
|
|
|
54085 |
part,
|
|
|
54086 |
playlist: {
|
|
|
54087 |
mediaSequence: seq,
|
|
|
54088 |
id,
|
|
|
54089 |
segments = []
|
|
|
54090 |
},
|
|
|
54091 |
mediaIndex: index,
|
|
|
54092 |
partIndex,
|
|
|
54093 |
timeline
|
|
|
54094 |
} = segmentInfo;
|
|
|
54095 |
const segmentLen = segments.length - 1;
|
|
|
54096 |
let selection = 'mediaIndex/partIndex increment';
|
|
|
54097 |
if (segmentInfo.getMediaInfoForTime) {
|
|
|
54098 |
selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`;
|
|
|
54099 |
} else if (segmentInfo.isSyncRequest) {
|
|
|
54100 |
selection = 'getSyncSegmentCandidate (isSyncRequest)';
|
|
|
54101 |
}
|
|
|
54102 |
if (segmentInfo.independent) {
|
|
|
54103 |
selection += ` with independent ${segmentInfo.independent}`;
|
|
|
54104 |
}
|
|
|
54105 |
const hasPartIndex = typeof partIndex === 'number';
|
|
|
54106 |
const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment';
|
|
|
54107 |
const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({
|
|
|
54108 |
preloadSegment: segment
|
|
|
54109 |
}) - 1 : 0;
|
|
|
54110 |
return `${name} [${seq + index}/${seq + segmentLen}]` + (hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') + ` segment start/end [${segment.start} => ${segment.end}]` + (hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') + ` startOfSegment [${startOfSegment}]` + ` duration [${duration}]` + ` timeline [${timeline}]` + ` selected by [${selection}]` + ` playlist [${id}]`;
|
|
|
54111 |
};
|
|
|
54112 |
const timingInfoPropertyForMedia = mediaType => `${mediaType}TimingInfo`;
|
|
|
54113 |
/**
|
|
|
54114 |
* Returns the timestamp offset to use for the segment.
|
|
|
54115 |
*
|
|
|
54116 |
* @param {number} segmentTimeline
|
|
|
54117 |
* The timeline of the segment
|
|
|
54118 |
* @param {number} currentTimeline
|
|
|
54119 |
* The timeline currently being followed by the loader
|
|
|
54120 |
* @param {number} startOfSegment
|
|
|
54121 |
* The estimated segment start
|
|
|
54122 |
* @param {TimeRange[]} buffered
|
|
|
54123 |
* The loader's buffer
|
|
|
54124 |
* @param {boolean} overrideCheck
|
|
|
54125 |
* If true, no checks are made to see if the timestamp offset value should be set,
|
|
|
54126 |
* but sets it directly to a value.
|
|
|
54127 |
*
|
|
|
54128 |
* @return {number|null}
|
|
|
54129 |
* Either a number representing a new timestamp offset, or null if the segment is
|
|
|
54130 |
* part of the same timeline
|
|
|
54131 |
*/
|
|
|
54132 |
|
|
|
54133 |
const timestampOffsetForSegment = ({
|
|
|
54134 |
segmentTimeline,
|
|
|
54135 |
currentTimeline,
|
|
|
54136 |
startOfSegment,
|
|
|
54137 |
buffered,
|
|
|
54138 |
overrideCheck
|
|
|
54139 |
}) => {
|
|
|
54140 |
// Check to see if we are crossing a discontinuity to see if we need to set the
|
|
|
54141 |
// timestamp offset on the transmuxer and source buffer.
|
|
|
54142 |
//
|
|
|
54143 |
// Previously, we changed the timestampOffset if the start of this segment was less than
|
|
|
54144 |
// the currently set timestampOffset, but this isn't desirable as it can produce bad
|
|
|
54145 |
// behavior, especially around long running live streams.
|
|
|
54146 |
if (!overrideCheck && segmentTimeline === currentTimeline) {
|
|
|
54147 |
return null;
|
|
|
54148 |
} // When changing renditions, it's possible to request a segment on an older timeline. For
|
|
|
54149 |
// instance, given two renditions with the following:
|
|
|
54150 |
//
|
|
|
54151 |
// #EXTINF:10
|
|
|
54152 |
// segment1
|
|
|
54153 |
// #EXT-X-DISCONTINUITY
|
|
|
54154 |
// #EXTINF:10
|
|
|
54155 |
// segment2
|
|
|
54156 |
// #EXTINF:10
|
|
|
54157 |
// segment3
|
|
|
54158 |
//
|
|
|
54159 |
// And the current player state:
|
|
|
54160 |
//
|
|
|
54161 |
// current time: 8
|
|
|
54162 |
// buffer: 0 => 20
|
|
|
54163 |
//
|
|
|
54164 |
// The next segment on the current rendition would be segment3, filling the buffer from
|
|
|
54165 |
// 20s onwards. However, if a rendition switch happens after segment2 was requested,
|
|
|
54166 |
// then the next segment to be requested will be segment1 from the new rendition in
|
|
|
54167 |
// order to fill time 8 and onwards. Using the buffered end would result in repeated
|
|
|
54168 |
// content (since it would position segment1 of the new rendition starting at 20s). This
|
|
|
54169 |
// case can be identified when the new segment's timeline is a prior value. Instead of
|
|
|
54170 |
// using the buffered end, the startOfSegment can be used, which, hopefully, will be
|
|
|
54171 |
// more accurate to the actual start time of the segment.
|
|
|
54172 |
|
|
|
54173 |
if (segmentTimeline < currentTimeline) {
|
|
|
54174 |
return startOfSegment;
|
|
|
54175 |
} // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that
|
|
|
54176 |
// value uses the end of the last segment if it is available. While this value
|
|
|
54177 |
// should often be correct, it's better to rely on the buffered end, as the new
|
|
|
54178 |
// content post discontinuity should line up with the buffered end as if it were
|
|
|
54179 |
// time 0 for the new content.
|
|
|
54180 |
|
|
|
54181 |
return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment;
|
|
|
54182 |
};
|
|
|
54183 |
/**
|
|
|
54184 |
* Returns whether or not the loader should wait for a timeline change from the timeline
|
|
|
54185 |
* change controller before processing the segment.
|
|
|
54186 |
*
|
|
|
54187 |
* Primary timing in VHS goes by video. This is different from most media players, as
|
|
|
54188 |
* audio is more often used as the primary timing source. For the foreseeable future, VHS
|
|
|
54189 |
* will continue to use video as the primary timing source, due to the current logic and
|
|
|
54190 |
* expectations built around it.
|
|
|
54191 |
|
|
|
54192 |
* Since the timing follows video, in order to maintain sync, the video loader is
|
|
|
54193 |
* responsible for setting both audio and video source buffer timestamp offsets.
|
|
|
54194 |
*
|
|
|
54195 |
* Setting different values for audio and video source buffers could lead to
|
|
|
54196 |
* desyncing. The following examples demonstrate some of the situations where this
|
|
|
54197 |
* distinction is important. Note that all of these cases involve demuxed content. When
|
|
|
54198 |
* content is muxed, the audio and video are packaged together, therefore syncing
|
|
|
54199 |
* separate media playlists is not an issue.
|
|
|
54200 |
*
|
|
|
54201 |
* CASE 1: Audio prepares to load a new timeline before video:
|
|
|
54202 |
*
|
|
|
54203 |
* Timeline: 0 1
|
|
|
54204 |
* Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
|
|
|
54205 |
* Audio Loader: ^
|
|
|
54206 |
* Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
|
|
|
54207 |
* Video Loader ^
|
|
|
54208 |
*
|
|
|
54209 |
* In the above example, the audio loader is preparing to load the 6th segment, the first
|
|
|
54210 |
* after a discontinuity, while the video loader is still loading the 5th segment, before
|
|
|
54211 |
* the discontinuity.
|
|
|
54212 |
*
|
|
|
54213 |
* If the audio loader goes ahead and loads and appends the 6th segment before the video
|
|
|
54214 |
* loader crosses the discontinuity, then when appended, the 6th audio segment will use
|
|
|
54215 |
* the timestamp offset from timeline 0. This will likely lead to desyncing. In addition,
|
|
|
54216 |
* the audio loader must provide the audioAppendStart value to trim the content in the
|
|
|
54217 |
* transmuxer, and that value relies on the audio timestamp offset. Since the audio
|
|
|
54218 |
* timestamp offset is set by the video (main) loader, the audio loader shouldn't load the
|
|
|
54219 |
* segment until that value is provided.
|
|
|
54220 |
*
|
|
|
54221 |
* CASE 2: Video prepares to load a new timeline before audio:
|
|
|
54222 |
*
|
|
|
54223 |
* Timeline: 0 1
|
|
|
54224 |
* Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
|
|
|
54225 |
* Audio Loader: ^
|
|
|
54226 |
* Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
|
|
|
54227 |
* Video Loader ^
|
|
|
54228 |
*
|
|
|
54229 |
* In the above example, the video loader is preparing to load the 6th segment, the first
|
|
|
54230 |
* after a discontinuity, while the audio loader is still loading the 5th segment, before
|
|
|
54231 |
* the discontinuity.
|
|
|
54232 |
*
|
|
|
54233 |
* If the video loader goes ahead and loads and appends the 6th segment, then once the
|
|
|
54234 |
* segment is loaded and processed, both the video and audio timestamp offsets will be
|
|
|
54235 |
* set, since video is used as the primary timing source. This is to ensure content lines
|
|
|
54236 |
* up appropriately, as any modifications to the video timing are reflected by audio when
|
|
|
54237 |
* the video loader sets the audio and video timestamp offsets to the same value. However,
|
|
|
54238 |
* setting the timestamp offset for audio before audio has had a chance to change
|
|
|
54239 |
* timelines will likely lead to desyncing, as the audio loader will append segment 5 with
|
|
|
54240 |
* a timestamp intended to apply to segments from timeline 1 rather than timeline 0.
|
|
|
54241 |
*
|
|
|
54242 |
* CASE 3: When seeking, audio prepares to load a new timeline before video
|
|
|
54243 |
*
|
|
|
54244 |
* Timeline: 0 1
|
|
|
54245 |
* Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
|
|
|
54246 |
* Audio Loader: ^
|
|
|
54247 |
* Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
|
|
|
54248 |
* Video Loader ^
|
|
|
54249 |
*
|
|
|
54250 |
* In the above example, both audio and video loaders are loading segments from timeline
|
|
|
54251 |
* 0, but imagine that the seek originated from timeline 1.
|
|
|
54252 |
*
|
|
|
54253 |
* When seeking to a new timeline, the timestamp offset will be set based on the expected
|
|
|
54254 |
* segment start of the loaded video segment. In order to maintain sync, the audio loader
|
|
|
54255 |
* must wait for the video loader to load its segment and update both the audio and video
|
|
|
54256 |
* timestamp offsets before it may load and append its own segment. This is the case
|
|
|
54257 |
* whether the seek results in a mismatched segment request (e.g., the audio loader
|
|
|
54258 |
* chooses to load segment 3 and the video loader chooses to load segment 4) or the
|
|
|
54259 |
* loaders choose to load the same segment index from each playlist, as the segments may
|
|
|
54260 |
* not be aligned perfectly, even for matching segment indexes.
|
|
|
54261 |
*
|
|
|
54262 |
* @param {Object} timelinechangeController
|
|
|
54263 |
* @param {number} currentTimeline
|
|
|
54264 |
* The timeline currently being followed by the loader
|
|
|
54265 |
* @param {number} segmentTimeline
|
|
|
54266 |
* The timeline of the segment being loaded
|
|
|
54267 |
* @param {('main'|'audio')} loaderType
|
|
|
54268 |
* The loader type
|
|
|
54269 |
* @param {boolean} audioDisabled
|
|
|
54270 |
* Whether the audio is disabled for the loader. This should only be true when the
|
|
|
54271 |
* loader may have muxed audio in its segment, but should not append it, e.g., for
|
|
|
54272 |
* the main loader when an alternate audio playlist is active.
|
|
|
54273 |
*
|
|
|
54274 |
* @return {boolean}
|
|
|
54275 |
* Whether the loader should wait for a timeline change from the timeline change
|
|
|
54276 |
* controller before processing the segment
|
|
|
54277 |
*/
|
|
|
54278 |
|
|
|
54279 |
const shouldWaitForTimelineChange = ({
|
|
|
54280 |
timelineChangeController,
|
|
|
54281 |
currentTimeline,
|
|
|
54282 |
segmentTimeline,
|
|
|
54283 |
loaderType,
|
|
|
54284 |
audioDisabled
|
|
|
54285 |
}) => {
|
|
|
54286 |
if (currentTimeline === segmentTimeline) {
|
|
|
54287 |
return false;
|
|
|
54288 |
}
|
|
|
54289 |
if (loaderType === 'audio') {
|
|
|
54290 |
const lastMainTimelineChange = timelineChangeController.lastTimelineChange({
|
|
|
54291 |
type: 'main'
|
|
|
54292 |
}); // Audio loader should wait if:
|
|
|
54293 |
//
|
|
|
54294 |
// * main hasn't had a timeline change yet (thus has not loaded its first segment)
|
|
|
54295 |
// * main hasn't yet changed to the timeline audio is looking to load
|
|
|
54296 |
|
|
|
54297 |
return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline;
|
|
|
54298 |
} // The main loader only needs to wait for timeline changes if there's demuxed audio.
|
|
|
54299 |
// Otherwise, there's nothing to wait for, since audio would be muxed into the main
|
|
|
54300 |
// loader's segments (or the content is audio/video only and handled by the main
|
|
|
54301 |
// loader).
|
|
|
54302 |
|
|
|
54303 |
if (loaderType === 'main' && audioDisabled) {
|
|
|
54304 |
const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({
|
|
|
54305 |
type: 'audio'
|
|
|
54306 |
}); // Main loader should wait for the audio loader if audio is not pending a timeline
|
|
|
54307 |
// change to the current timeline.
|
|
|
54308 |
//
|
|
|
54309 |
// Since the main loader is responsible for setting the timestamp offset for both
|
|
|
54310 |
// audio and video, the main loader must wait for audio to be about to change to its
|
|
|
54311 |
// timeline before setting the offset, otherwise, if audio is behind in loading,
|
|
|
54312 |
// segments from the previous timeline would be adjusted by the new timestamp offset.
|
|
|
54313 |
//
|
|
|
54314 |
// This requirement means that video will not cross a timeline until the audio is
|
|
|
54315 |
// about to cross to it, so that way audio and video will always cross the timeline
|
|
|
54316 |
// together.
|
|
|
54317 |
//
|
|
|
54318 |
// In addition to normal timeline changes, these rules also apply to the start of a
|
|
|
54319 |
// stream (going from a non-existent timeline, -1, to timeline 0). It's important
|
|
|
54320 |
// that these rules apply to the first timeline change because if they did not, it's
|
|
|
54321 |
// possible that the main loader will cross two timelines before the audio loader has
|
|
|
54322 |
// crossed one. Logic may be implemented to handle the startup as a special case, but
|
|
|
54323 |
// it's easier to simply treat all timeline changes the same.
|
|
|
54324 |
|
|
|
54325 |
if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) {
|
|
|
54326 |
return false;
|
|
|
54327 |
}
|
|
|
54328 |
return true;
|
|
|
54329 |
}
|
|
|
54330 |
return false;
|
|
|
54331 |
};
|
|
|
54332 |
const mediaDuration = timingInfos => {
|
|
|
54333 |
let maxDuration = 0;
|
|
|
54334 |
['video', 'audio'].forEach(function (type) {
|
|
|
54335 |
const typeTimingInfo = timingInfos[`${type}TimingInfo`];
|
|
|
54336 |
if (!typeTimingInfo) {
|
|
|
54337 |
return;
|
|
|
54338 |
}
|
|
|
54339 |
const {
|
|
|
54340 |
start,
|
|
|
54341 |
end
|
|
|
54342 |
} = typeTimingInfo;
|
|
|
54343 |
let duration;
|
|
|
54344 |
if (typeof start === 'bigint' || typeof end === 'bigint') {
|
|
|
54345 |
duration = window.BigInt(end) - window.BigInt(start);
|
|
|
54346 |
} else if (typeof start === 'number' && typeof end === 'number') {
|
|
|
54347 |
duration = end - start;
|
|
|
54348 |
}
|
|
|
54349 |
if (typeof duration !== 'undefined' && duration > maxDuration) {
|
|
|
54350 |
maxDuration = duration;
|
|
|
54351 |
}
|
|
|
54352 |
}); // convert back to a number if it is lower than MAX_SAFE_INTEGER
|
|
|
54353 |
// as we only need BigInt when we are above that.
|
|
|
54354 |
|
|
|
54355 |
if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) {
|
|
|
54356 |
maxDuration = Number(maxDuration);
|
|
|
54357 |
}
|
|
|
54358 |
return maxDuration;
|
|
|
54359 |
};
|
|
|
54360 |
const segmentTooLong = ({
|
|
|
54361 |
segmentDuration,
|
|
|
54362 |
maxDuration
|
|
|
54363 |
}) => {
|
|
|
54364 |
// 0 duration segments are most likely due to metadata only segments or a lack of
|
|
|
54365 |
// information.
|
|
|
54366 |
if (!segmentDuration) {
|
|
|
54367 |
return false;
|
|
|
54368 |
} // For HLS:
|
|
|
54369 |
//
|
|
|
54370 |
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1
|
|
|
54371 |
// The EXTINF duration of each Media Segment in the Playlist
|
|
|
54372 |
// file, when rounded to the nearest integer, MUST be less than or equal
|
|
|
54373 |
// to the target duration; longer segments can trigger playback stalls
|
|
|
54374 |
// or other errors.
|
|
|
54375 |
//
|
|
|
54376 |
// For DASH, the mpd-parser uses the largest reported segment duration as the target
|
|
|
54377 |
// duration. Although that reported duration is occasionally approximate (i.e., not
|
|
|
54378 |
// exact), a strict check may report that a segment is too long more often in DASH.
|
|
|
54379 |
|
|
|
54380 |
return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR;
|
|
|
54381 |
};
|
|
|
54382 |
const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => {
|
|
|
54383 |
// Right now we aren't following DASH's timing model exactly, so only perform
|
|
|
54384 |
// this check for HLS content.
|
|
|
54385 |
if (sourceType !== 'hls') {
|
|
|
54386 |
return null;
|
|
|
54387 |
}
|
|
|
54388 |
const segmentDuration = mediaDuration({
|
|
|
54389 |
audioTimingInfo: segmentInfo.audioTimingInfo,
|
|
|
54390 |
videoTimingInfo: segmentInfo.videoTimingInfo
|
|
|
54391 |
}); // Don't report if we lack information.
|
|
|
54392 |
//
|
|
|
54393 |
// If the segment has a duration of 0 it is either a lack of information or a
|
|
|
54394 |
// metadata only segment and shouldn't be reported here.
|
|
|
54395 |
|
|
|
54396 |
if (!segmentDuration) {
|
|
|
54397 |
return null;
|
|
|
54398 |
}
|
|
|
54399 |
const targetDuration = segmentInfo.playlist.targetDuration;
|
|
|
54400 |
const isSegmentWayTooLong = segmentTooLong({
|
|
|
54401 |
segmentDuration,
|
|
|
54402 |
maxDuration: targetDuration * 2
|
|
|
54403 |
});
|
|
|
54404 |
const isSegmentSlightlyTooLong = segmentTooLong({
|
|
|
54405 |
segmentDuration,
|
|
|
54406 |
maxDuration: targetDuration
|
|
|
54407 |
});
|
|
|
54408 |
const segmentTooLongMessage = `Segment with index ${segmentInfo.mediaIndex} ` + `from playlist ${segmentInfo.playlist.id} ` + `has a duration of ${segmentDuration} ` + `when the reported duration is ${segmentInfo.duration} ` + `and the target duration is ${targetDuration}. ` + 'For HLS content, a duration in excess of the target duration may result in ' + 'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' + 'more details: ' + 'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1';
|
|
|
54409 |
if (isSegmentWayTooLong || isSegmentSlightlyTooLong) {
|
|
|
54410 |
return {
|
|
|
54411 |
severity: isSegmentWayTooLong ? 'warn' : 'info',
|
|
|
54412 |
message: segmentTooLongMessage
|
|
|
54413 |
};
|
|
|
54414 |
}
|
|
|
54415 |
return null;
|
|
|
54416 |
};
|
|
|
54417 |
/**
|
|
|
54418 |
* An object that manages segment loading and appending.
|
|
|
54419 |
*
|
|
|
54420 |
* @class SegmentLoader
|
|
|
54421 |
* @param {Object} options required and optional options
|
|
|
54422 |
* @extends videojs.EventTarget
|
|
|
54423 |
*/
|
|
|
54424 |
|
|
|
54425 |
class SegmentLoader extends videojs.EventTarget {
|
|
|
54426 |
constructor(settings, options = {}) {
|
|
|
54427 |
super(); // check pre-conditions
|
|
|
54428 |
|
|
|
54429 |
if (!settings) {
|
|
|
54430 |
throw new TypeError('Initialization settings are required');
|
|
|
54431 |
}
|
|
|
54432 |
if (typeof settings.currentTime !== 'function') {
|
|
|
54433 |
throw new TypeError('No currentTime getter specified');
|
|
|
54434 |
}
|
|
|
54435 |
if (!settings.mediaSource) {
|
|
|
54436 |
throw new TypeError('No MediaSource specified');
|
|
|
54437 |
} // public properties
|
|
|
54438 |
|
|
|
54439 |
this.bandwidth = settings.bandwidth;
|
|
|
54440 |
this.throughput = {
|
|
|
54441 |
rate: 0,
|
|
|
54442 |
count: 0
|
|
|
54443 |
};
|
|
|
54444 |
this.roundTrip = NaN;
|
|
|
54445 |
this.resetStats_();
|
|
|
54446 |
this.mediaIndex = null;
|
|
|
54447 |
this.partIndex = null; // private settings
|
|
|
54448 |
|
|
|
54449 |
this.hasPlayed_ = settings.hasPlayed;
|
|
|
54450 |
this.currentTime_ = settings.currentTime;
|
|
|
54451 |
this.seekable_ = settings.seekable;
|
|
|
54452 |
this.seeking_ = settings.seeking;
|
|
|
54453 |
this.duration_ = settings.duration;
|
|
|
54454 |
this.mediaSource_ = settings.mediaSource;
|
|
|
54455 |
this.vhs_ = settings.vhs;
|
|
|
54456 |
this.loaderType_ = settings.loaderType;
|
|
|
54457 |
this.currentMediaInfo_ = void 0;
|
|
|
54458 |
this.startingMediaInfo_ = void 0;
|
|
|
54459 |
this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
|
|
|
54460 |
this.goalBufferLength_ = settings.goalBufferLength;
|
|
|
54461 |
this.sourceType_ = settings.sourceType;
|
|
|
54462 |
this.sourceUpdater_ = settings.sourceUpdater;
|
|
|
54463 |
this.inbandTextTracks_ = settings.inbandTextTracks;
|
|
|
54464 |
this.state_ = 'INIT';
|
|
|
54465 |
this.timelineChangeController_ = settings.timelineChangeController;
|
|
|
54466 |
this.shouldSaveSegmentTimingInfo_ = true;
|
|
|
54467 |
this.parse708captions_ = settings.parse708captions;
|
|
|
54468 |
this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset;
|
|
|
54469 |
this.captionServices_ = settings.captionServices;
|
|
|
54470 |
this.exactManifestTimings = settings.exactManifestTimings;
|
|
|
54471 |
this.addMetadataToTextTrack = settings.addMetadataToTextTrack; // private instance variables
|
|
|
54472 |
|
|
|
54473 |
this.checkBufferTimeout_ = null;
|
|
|
54474 |
this.error_ = void 0;
|
|
|
54475 |
this.currentTimeline_ = -1;
|
|
|
54476 |
this.shouldForceTimestampOffsetAfterResync_ = false;
|
|
|
54477 |
this.pendingSegment_ = null;
|
|
|
54478 |
this.xhrOptions_ = null;
|
|
|
54479 |
this.pendingSegments_ = [];
|
|
|
54480 |
this.audioDisabled_ = false;
|
|
|
54481 |
this.isPendingTimestampOffset_ = false; // TODO possibly move gopBuffer and timeMapping info to a separate controller
|
|
|
54482 |
|
|
|
54483 |
this.gopBuffer_ = [];
|
|
|
54484 |
this.timeMapping_ = 0;
|
|
|
54485 |
this.safeAppend_ = false;
|
|
|
54486 |
this.appendInitSegment_ = {
|
|
|
54487 |
audio: true,
|
|
|
54488 |
video: true
|
|
|
54489 |
};
|
|
|
54490 |
this.playlistOfLastInitSegment_ = {
|
|
|
54491 |
audio: null,
|
|
|
54492 |
video: null
|
|
|
54493 |
};
|
|
|
54494 |
this.callQueue_ = []; // If the segment loader prepares to load a segment, but does not have enough
|
|
|
54495 |
// information yet to start the loading process (e.g., if the audio loader wants to
|
|
|
54496 |
// load a segment from the next timeline but the main loader hasn't yet crossed that
|
|
|
54497 |
// timeline), then the load call will be added to the queue until it is ready to be
|
|
|
54498 |
// processed.
|
|
|
54499 |
|
|
|
54500 |
this.loadQueue_ = [];
|
|
|
54501 |
this.metadataQueue_ = {
|
|
|
54502 |
id3: [],
|
|
|
54503 |
caption: []
|
|
|
54504 |
};
|
|
|
54505 |
this.waitingOnRemove_ = false;
|
|
|
54506 |
this.quotaExceededErrorRetryTimeout_ = null; // Fragmented mp4 playback
|
|
|
54507 |
|
|
|
54508 |
this.activeInitSegmentId_ = null;
|
|
|
54509 |
this.initSegments_ = {}; // HLSe playback
|
|
|
54510 |
|
|
|
54511 |
this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys;
|
|
|
54512 |
this.keyCache_ = {};
|
|
|
54513 |
this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings
|
|
|
54514 |
// between a time in the display time and a segment index within
|
|
|
54515 |
// a playlist
|
|
|
54516 |
|
|
|
54517 |
this.syncController_ = settings.syncController;
|
|
|
54518 |
this.syncPoint_ = {
|
|
|
54519 |
segmentIndex: 0,
|
|
|
54520 |
time: 0
|
|
|
54521 |
};
|
|
|
54522 |
this.transmuxer_ = this.createTransmuxer_();
|
|
|
54523 |
this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate');
|
|
|
54524 |
this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_);
|
|
|
54525 |
this.mediaSource_.addEventListener('sourceopen', () => {
|
|
|
54526 |
if (!this.isEndOfStream_()) {
|
|
|
54527 |
this.ended_ = false;
|
|
|
54528 |
}
|
|
|
54529 |
}); // ...for determining the fetch location
|
|
|
54530 |
|
|
|
54531 |
this.fetchAtBuffer_ = false;
|
|
|
54532 |
this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`);
|
|
|
54533 |
Object.defineProperty(this, 'state', {
|
|
|
54534 |
get() {
|
|
|
54535 |
return this.state_;
|
|
|
54536 |
},
|
|
|
54537 |
set(newState) {
|
|
|
54538 |
if (newState !== this.state_) {
|
|
|
54539 |
this.logger_(`${this.state_} -> ${newState}`);
|
|
|
54540 |
this.state_ = newState;
|
|
|
54541 |
this.trigger('statechange');
|
|
|
54542 |
}
|
|
|
54543 |
}
|
|
|
54544 |
});
|
|
|
54545 |
this.sourceUpdater_.on('ready', () => {
|
|
|
54546 |
if (this.hasEnoughInfoToAppend_()) {
|
|
|
54547 |
this.processCallQueue_();
|
|
|
54548 |
}
|
|
|
54549 |
}); // Only the main loader needs to listen for pending timeline changes, as the main
|
|
|
54550 |
// loader should wait for audio to be ready to change its timeline so that both main
|
|
|
54551 |
// and audio timelines change together. For more details, see the
|
|
|
54552 |
// shouldWaitForTimelineChange function.
|
|
|
54553 |
|
|
|
54554 |
if (this.loaderType_ === 'main') {
|
|
|
54555 |
this.timelineChangeController_.on('pendingtimelinechange', () => {
|
|
|
54556 |
if (this.hasEnoughInfoToAppend_()) {
|
|
|
54557 |
this.processCallQueue_();
|
|
|
54558 |
}
|
|
|
54559 |
});
|
|
|
54560 |
} // The main loader only listens on pending timeline changes, but the audio loader,
|
|
|
54561 |
// since its loads follow main, needs to listen on timeline changes. For more details,
|
|
|
54562 |
// see the shouldWaitForTimelineChange function.
|
|
|
54563 |
|
|
|
54564 |
if (this.loaderType_ === 'audio') {
|
|
|
54565 |
this.timelineChangeController_.on('timelinechange', () => {
|
|
|
54566 |
if (this.hasEnoughInfoToLoad_()) {
|
|
|
54567 |
this.processLoadQueue_();
|
|
|
54568 |
}
|
|
|
54569 |
if (this.hasEnoughInfoToAppend_()) {
|
|
|
54570 |
this.processCallQueue_();
|
|
|
54571 |
}
|
|
|
54572 |
});
|
|
|
54573 |
}
|
|
|
54574 |
}
|
|
|
54575 |
createTransmuxer_() {
|
|
|
54576 |
return segmentTransmuxer.createTransmuxer({
|
|
|
54577 |
remux: false,
|
|
|
54578 |
alignGopsAtEnd: this.safeAppend_,
|
|
|
54579 |
keepOriginalTimestamps: true,
|
|
|
54580 |
parse708captions: this.parse708captions_,
|
|
|
54581 |
captionServices: this.captionServices_
|
|
|
54582 |
});
|
|
|
54583 |
}
|
|
|
54584 |
/**
|
|
|
54585 |
* reset all of our media stats
|
|
|
54586 |
*
|
|
|
54587 |
* @private
|
|
|
54588 |
*/
|
|
|
54589 |
|
|
|
54590 |
resetStats_() {
|
|
|
54591 |
this.mediaBytesTransferred = 0;
|
|
|
54592 |
this.mediaRequests = 0;
|
|
|
54593 |
this.mediaRequestsAborted = 0;
|
|
|
54594 |
this.mediaRequestsTimedout = 0;
|
|
|
54595 |
this.mediaRequestsErrored = 0;
|
|
|
54596 |
this.mediaTransferDuration = 0;
|
|
|
54597 |
this.mediaSecondsLoaded = 0;
|
|
|
54598 |
this.mediaAppends = 0;
|
|
|
54599 |
}
|
|
|
54600 |
/**
|
|
|
54601 |
* dispose of the SegmentLoader and reset to the default state
|
|
|
54602 |
*/
|
|
|
54603 |
|
|
|
54604 |
dispose() {
|
|
|
54605 |
this.trigger('dispose');
|
|
|
54606 |
this.state = 'DISPOSED';
|
|
|
54607 |
this.pause();
|
|
|
54608 |
this.abort_();
|
|
|
54609 |
if (this.transmuxer_) {
|
|
|
54610 |
this.transmuxer_.terminate();
|
|
|
54611 |
}
|
|
|
54612 |
this.resetStats_();
|
|
|
54613 |
if (this.checkBufferTimeout_) {
|
|
|
54614 |
window.clearTimeout(this.checkBufferTimeout_);
|
|
|
54615 |
}
|
|
|
54616 |
if (this.syncController_ && this.triggerSyncInfoUpdate_) {
|
|
|
54617 |
this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_);
|
|
|
54618 |
}
|
|
|
54619 |
this.off();
|
|
|
54620 |
}
|
|
|
54621 |
setAudio(enable) {
|
|
|
54622 |
this.audioDisabled_ = !enable;
|
|
|
54623 |
if (enable) {
|
|
|
54624 |
this.appendInitSegment_.audio = true;
|
|
|
54625 |
} else {
|
|
|
54626 |
// remove current track audio if it gets disabled
|
|
|
54627 |
this.sourceUpdater_.removeAudio(0, this.duration_());
|
|
|
54628 |
}
|
|
|
54629 |
}
|
|
|
54630 |
/**
|
|
|
54631 |
* abort anything that is currently doing on with the SegmentLoader
|
|
|
54632 |
* and reset to a default state
|
|
|
54633 |
*/
|
|
|
54634 |
|
|
|
54635 |
abort() {
|
|
|
54636 |
if (this.state !== 'WAITING') {
|
|
|
54637 |
if (this.pendingSegment_) {
|
|
|
54638 |
this.pendingSegment_ = null;
|
|
|
54639 |
}
|
|
|
54640 |
return;
|
|
|
54641 |
}
|
|
|
54642 |
this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY
|
|
|
54643 |
// since we are no longer "waiting" on any requests. XHR callback is not always run
|
|
|
54644 |
// when the request is aborted. This will prevent the loader from being stuck in the
|
|
|
54645 |
// WAITING state indefinitely.
|
|
|
54646 |
|
|
|
54647 |
this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the
|
|
|
54648 |
// next segment
|
|
|
54649 |
|
|
|
54650 |
if (!this.paused()) {
|
|
|
54651 |
this.monitorBuffer_();
|
|
|
54652 |
}
|
|
|
54653 |
}
|
|
|
54654 |
/**
|
|
|
54655 |
* abort all pending xhr requests and null any pending segements
|
|
|
54656 |
*
|
|
|
54657 |
* @private
|
|
|
54658 |
*/
|
|
|
54659 |
|
|
|
54660 |
abort_() {
|
|
|
54661 |
if (this.pendingSegment_ && this.pendingSegment_.abortRequests) {
|
|
|
54662 |
this.pendingSegment_.abortRequests();
|
|
|
54663 |
} // clear out the segment being processed
|
|
|
54664 |
|
|
|
54665 |
this.pendingSegment_ = null;
|
|
|
54666 |
this.callQueue_ = [];
|
|
|
54667 |
this.loadQueue_ = [];
|
|
|
54668 |
this.metadataQueue_.id3 = [];
|
|
|
54669 |
this.metadataQueue_.caption = [];
|
|
|
54670 |
this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_);
|
|
|
54671 |
this.waitingOnRemove_ = false;
|
|
|
54672 |
window.clearTimeout(this.quotaExceededErrorRetryTimeout_);
|
|
|
54673 |
this.quotaExceededErrorRetryTimeout_ = null;
|
|
|
54674 |
}
|
|
|
54675 |
checkForAbort_(requestId) {
|
|
|
54676 |
// If the state is APPENDING, then aborts will not modify the state, meaning the first
|
|
|
54677 |
// callback that happens should reset the state to READY so that loading can continue.
|
|
|
54678 |
if (this.state === 'APPENDING' && !this.pendingSegment_) {
|
|
|
54679 |
this.state = 'READY';
|
|
|
54680 |
return true;
|
|
|
54681 |
}
|
|
|
54682 |
if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) {
|
|
|
54683 |
return true;
|
|
|
54684 |
}
|
|
|
54685 |
return false;
|
|
|
54686 |
}
|
|
|
54687 |
/**
|
|
|
54688 |
* set an error on the segment loader and null out any pending segements
|
|
|
54689 |
*
|
|
|
54690 |
* @param {Error} error the error to set on the SegmentLoader
|
|
|
54691 |
* @return {Error} the error that was set or that is currently set
|
|
|
54692 |
*/
|
|
|
54693 |
|
|
|
54694 |
error(error) {
|
|
|
54695 |
if (typeof error !== 'undefined') {
|
|
|
54696 |
this.logger_('error occurred:', error);
|
|
|
54697 |
this.error_ = error;
|
|
|
54698 |
}
|
|
|
54699 |
this.pendingSegment_ = null;
|
|
|
54700 |
return this.error_;
|
|
|
54701 |
}
|
|
|
54702 |
endOfStream() {
|
|
|
54703 |
this.ended_ = true;
|
|
|
54704 |
if (this.transmuxer_) {
|
|
|
54705 |
// need to clear out any cached data to prepare for the new segment
|
|
|
54706 |
segmentTransmuxer.reset(this.transmuxer_);
|
|
|
54707 |
}
|
|
|
54708 |
this.gopBuffer_.length = 0;
|
|
|
54709 |
this.pause();
|
|
|
54710 |
this.trigger('ended');
|
|
|
54711 |
}
|
|
|
54712 |
/**
|
|
|
54713 |
* Indicates which time ranges are buffered
|
|
|
54714 |
*
|
|
|
54715 |
* @return {TimeRange}
|
|
|
54716 |
* TimeRange object representing the current buffered ranges
|
|
|
54717 |
*/
|
|
|
54718 |
|
|
|
54719 |
buffered_() {
|
|
|
54720 |
const trackInfo = this.getMediaInfo_();
|
|
|
54721 |
if (!this.sourceUpdater_ || !trackInfo) {
|
|
|
54722 |
return createTimeRanges();
|
|
|
54723 |
}
|
|
|
54724 |
if (this.loaderType_ === 'main') {
|
|
|
54725 |
const {
|
|
|
54726 |
hasAudio,
|
|
|
54727 |
hasVideo,
|
|
|
54728 |
isMuxed
|
|
|
54729 |
} = trackInfo;
|
|
|
54730 |
if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) {
|
|
|
54731 |
return this.sourceUpdater_.buffered();
|
|
|
54732 |
}
|
|
|
54733 |
if (hasVideo) {
|
|
|
54734 |
return this.sourceUpdater_.videoBuffered();
|
|
|
54735 |
}
|
|
|
54736 |
} // One case that can be ignored for now is audio only with alt audio,
|
|
|
54737 |
// as we don't yet have proper support for that.
|
|
|
54738 |
|
|
|
54739 |
return this.sourceUpdater_.audioBuffered();
|
|
|
54740 |
}
|
|
|
54741 |
/**
|
|
|
54742 |
* Gets and sets init segment for the provided map
|
|
|
54743 |
*
|
|
|
54744 |
* @param {Object} map
|
|
|
54745 |
* The map object representing the init segment to get or set
|
|
|
54746 |
* @param {boolean=} set
|
|
|
54747 |
* If true, the init segment for the provided map should be saved
|
|
|
54748 |
* @return {Object}
|
|
|
54749 |
* map object for desired init segment
|
|
|
54750 |
*/
|
|
|
54751 |
|
|
|
54752 |
initSegmentForMap(map, set = false) {
|
|
|
54753 |
if (!map) {
|
|
|
54754 |
return null;
|
|
|
54755 |
}
|
|
|
54756 |
const id = initSegmentId(map);
|
|
|
54757 |
let storedMap = this.initSegments_[id];
|
|
|
54758 |
if (set && !storedMap && map.bytes) {
|
|
|
54759 |
this.initSegments_[id] = storedMap = {
|
|
|
54760 |
resolvedUri: map.resolvedUri,
|
|
|
54761 |
byterange: map.byterange,
|
|
|
54762 |
bytes: map.bytes,
|
|
|
54763 |
tracks: map.tracks,
|
|
|
54764 |
timescales: map.timescales
|
|
|
54765 |
};
|
|
|
54766 |
}
|
|
|
54767 |
return storedMap || map;
|
|
|
54768 |
}
|
|
|
54769 |
/**
|
|
|
54770 |
* Gets and sets key for the provided key
|
|
|
54771 |
*
|
|
|
54772 |
* @param {Object} key
|
|
|
54773 |
* The key object representing the key to get or set
|
|
|
54774 |
* @param {boolean=} set
|
|
|
54775 |
* If true, the key for the provided key should be saved
|
|
|
54776 |
* @return {Object}
|
|
|
54777 |
* Key object for desired key
|
|
|
54778 |
*/
|
|
|
54779 |
|
|
|
54780 |
segmentKey(key, set = false) {
|
|
|
54781 |
if (!key) {
|
|
|
54782 |
return null;
|
|
|
54783 |
}
|
|
|
54784 |
const id = segmentKeyId(key);
|
|
|
54785 |
let storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per
|
|
|
54786 |
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3
|
|
|
54787 |
|
|
|
54788 |
if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) {
|
|
|
54789 |
this.keyCache_[id] = storedKey = {
|
|
|
54790 |
resolvedUri: key.resolvedUri,
|
|
|
54791 |
bytes: key.bytes
|
|
|
54792 |
};
|
|
|
54793 |
}
|
|
|
54794 |
const result = {
|
|
|
54795 |
resolvedUri: (storedKey || key).resolvedUri
|
|
|
54796 |
};
|
|
|
54797 |
if (storedKey) {
|
|
|
54798 |
result.bytes = storedKey.bytes;
|
|
|
54799 |
}
|
|
|
54800 |
return result;
|
|
|
54801 |
}
|
|
|
54802 |
/**
|
|
|
54803 |
* Returns true if all configuration required for loading is present, otherwise false.
|
|
|
54804 |
*
|
|
|
54805 |
* @return {boolean} True if the all configuration is ready for loading
|
|
|
54806 |
* @private
|
|
|
54807 |
*/
|
|
|
54808 |
|
|
|
54809 |
couldBeginLoading_() {
|
|
|
54810 |
return this.playlist_ && !this.paused();
|
|
|
54811 |
}
|
|
|
54812 |
/**
|
|
|
54813 |
* load a playlist and start to fill the buffer
|
|
|
54814 |
*/
|
|
|
54815 |
|
|
|
54816 |
load() {
|
|
|
54817 |
// un-pause
|
|
|
54818 |
this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be
|
|
|
54819 |
// specified
|
|
|
54820 |
|
|
|
54821 |
if (!this.playlist_) {
|
|
|
54822 |
return;
|
|
|
54823 |
} // if all the configuration is ready, initialize and begin loading
|
|
|
54824 |
|
|
|
54825 |
if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
|
|
54826 |
return this.init_();
|
|
|
54827 |
} // if we're in the middle of processing a segment already, don't
|
|
|
54828 |
// kick off an additional segment request
|
|
|
54829 |
|
|
|
54830 |
if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') {
|
|
|
54831 |
return;
|
|
|
54832 |
}
|
|
|
54833 |
this.state = 'READY';
|
|
|
54834 |
}
|
|
|
54835 |
/**
|
|
|
54836 |
* Once all the starting parameters have been specified, begin
|
|
|
54837 |
* operation. This method should only be invoked from the INIT
|
|
|
54838 |
* state.
|
|
|
54839 |
*
|
|
|
54840 |
* @private
|
|
|
54841 |
*/
|
|
|
54842 |
|
|
|
54843 |
init_() {
|
|
|
54844 |
this.state = 'READY'; // if this is the audio segment loader, and it hasn't been inited before, then any old
|
|
|
54845 |
// audio data from the muxed content should be removed
|
|
|
54846 |
|
|
|
54847 |
this.resetEverything();
|
|
|
54848 |
return this.monitorBuffer_();
|
|
|
54849 |
}
|
|
|
54850 |
/**
|
|
|
54851 |
* set a playlist on the segment loader
|
|
|
54852 |
*
|
|
|
54853 |
* @param {PlaylistLoader} media the playlist to set on the segment loader
|
|
|
54854 |
*/
|
|
|
54855 |
|
|
|
54856 |
playlist(newPlaylist, options = {}) {
|
|
|
54857 |
if (!newPlaylist) {
|
|
|
54858 |
return;
|
|
|
54859 |
}
|
|
|
54860 |
const oldPlaylist = this.playlist_;
|
|
|
54861 |
const segmentInfo = this.pendingSegment_;
|
|
|
54862 |
this.playlist_ = newPlaylist;
|
|
|
54863 |
this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist
|
|
|
54864 |
// is always our zero-time so force a sync update each time the playlist
|
|
|
54865 |
// is refreshed from the server
|
|
|
54866 |
//
|
|
|
54867 |
// Use the INIT state to determine if playback has started, as the playlist sync info
|
|
|
54868 |
// should be fixed once requests begin (as sync points are generated based on sync
|
|
|
54869 |
// info), but not before then.
|
|
|
54870 |
|
|
|
54871 |
if (this.state === 'INIT') {
|
|
|
54872 |
newPlaylist.syncInfo = {
|
|
|
54873 |
mediaSequence: newPlaylist.mediaSequence,
|
|
|
54874 |
time: 0
|
|
|
54875 |
}; // Setting the date time mapping means mapping the program date time (if available)
|
|
|
54876 |
// to time 0 on the player's timeline. The playlist's syncInfo serves a similar
|
|
|
54877 |
// purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can
|
|
|
54878 |
// be updated as the playlist is refreshed before the loader starts loading, the
|
|
|
54879 |
// program date time mapping needs to be updated as well.
|
|
|
54880 |
//
|
|
|
54881 |
// This mapping is only done for the main loader because a program date time should
|
|
|
54882 |
// map equivalently between playlists.
|
|
|
54883 |
|
|
|
54884 |
if (this.loaderType_ === 'main') {
|
|
|
54885 |
this.syncController_.setDateTimeMappingForStart(newPlaylist);
|
|
|
54886 |
}
|
|
|
54887 |
}
|
|
|
54888 |
let oldId = null;
|
|
|
54889 |
if (oldPlaylist) {
|
|
|
54890 |
if (oldPlaylist.id) {
|
|
|
54891 |
oldId = oldPlaylist.id;
|
|
|
54892 |
} else if (oldPlaylist.uri) {
|
|
|
54893 |
oldId = oldPlaylist.uri;
|
|
|
54894 |
}
|
|
|
54895 |
}
|
|
|
54896 |
this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`);
|
|
|
54897 |
this.syncController_.updateMediaSequenceMap(newPlaylist, this.currentTime_(), this.loaderType_); // in VOD, this is always a rendition switch (or we updated our syncInfo above)
|
|
|
54898 |
// in LIVE, we always want to update with new playlists (including refreshes)
|
|
|
54899 |
|
|
|
54900 |
this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start
|
|
|
54901 |
// buffering now
|
|
|
54902 |
|
|
|
54903 |
if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
|
|
54904 |
return this.init_();
|
|
|
54905 |
}
|
|
|
54906 |
if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
|
|
|
54907 |
if (this.mediaIndex !== null) {
|
|
|
54908 |
// we must reset/resync the segment loader when we switch renditions and
|
|
|
54909 |
// the segment loader is already synced to the previous rendition
|
|
|
54910 |
// We only want to reset the loader here for LLHLS playback, as resetLoader sets fetchAtBuffer_
|
|
|
54911 |
// to false, resulting in fetching segments at currentTime and causing repeated
|
|
|
54912 |
// same-segment requests on playlist change. This erroneously drives up the playback watcher
|
|
|
54913 |
// stalled segment count, as re-requesting segments at the currentTime or browser cached segments
|
|
|
54914 |
// will not change the buffer.
|
|
|
54915 |
// Reference for LLHLS fixes: https://github.com/videojs/http-streaming/pull/1201
|
|
|
54916 |
const isLLHLS = !newPlaylist.endList && typeof newPlaylist.partTargetDuration === 'number';
|
|
|
54917 |
if (isLLHLS) {
|
|
|
54918 |
this.resetLoader();
|
|
|
54919 |
} else {
|
|
|
54920 |
this.resyncLoader();
|
|
|
54921 |
}
|
|
|
54922 |
}
|
|
|
54923 |
this.currentMediaInfo_ = void 0;
|
|
|
54924 |
this.trigger('playlistupdate'); // the rest of this function depends on `oldPlaylist` being defined
|
|
|
54925 |
|
|
|
54926 |
return;
|
|
|
54927 |
} // we reloaded the same playlist so we are in a live scenario
|
|
|
54928 |
// and we will likely need to adjust the mediaIndex
|
|
|
54929 |
|
|
|
54930 |
const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
|
|
|
54931 |
this.logger_(`live window shift [${mediaSequenceDiff}]`); // update the mediaIndex on the SegmentLoader
|
|
|
54932 |
// this is important because we can abort a request and this value must be
|
|
|
54933 |
// equal to the last appended mediaIndex
|
|
|
54934 |
|
|
|
54935 |
if (this.mediaIndex !== null) {
|
|
|
54936 |
this.mediaIndex -= mediaSequenceDiff; // this can happen if we are going to load the first segment, but get a playlist
|
|
|
54937 |
// update during that. mediaIndex would go from 0 to -1 if mediaSequence in the
|
|
|
54938 |
// new playlist was incremented by 1.
|
|
|
54939 |
|
|
|
54940 |
if (this.mediaIndex < 0) {
|
|
|
54941 |
this.mediaIndex = null;
|
|
|
54942 |
this.partIndex = null;
|
|
|
54943 |
} else {
|
|
|
54944 |
const segment = this.playlist_.segments[this.mediaIndex]; // partIndex should remain the same for the same segment
|
|
|
54945 |
// unless parts fell off of the playlist for this segment.
|
|
|
54946 |
// In that case we need to reset partIndex and resync
|
|
|
54947 |
|
|
|
54948 |
if (this.partIndex && (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex])) {
|
|
|
54949 |
const mediaIndex = this.mediaIndex;
|
|
|
54950 |
this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`);
|
|
|
54951 |
this.resetLoader(); // We want to throw away the partIndex and the data associated with it,
|
|
|
54952 |
// as the part was dropped from our current playlists segment.
|
|
|
54953 |
// The mediaIndex will still be valid so keep that around.
|
|
|
54954 |
|
|
|
54955 |
this.mediaIndex = mediaIndex;
|
|
|
54956 |
}
|
|
|
54957 |
}
|
|
|
54958 |
} // update the mediaIndex on the SegmentInfo object
|
|
|
54959 |
// this is important because we will update this.mediaIndex with this value
|
|
|
54960 |
// in `handleAppendsDone_` after the segment has been successfully appended
|
|
|
54961 |
|
|
|
54962 |
if (segmentInfo) {
|
|
|
54963 |
segmentInfo.mediaIndex -= mediaSequenceDiff;
|
|
|
54964 |
if (segmentInfo.mediaIndex < 0) {
|
|
|
54965 |
segmentInfo.mediaIndex = null;
|
|
|
54966 |
segmentInfo.partIndex = null;
|
|
|
54967 |
} else {
|
|
|
54968 |
// we need to update the referenced segment so that timing information is
|
|
|
54969 |
// saved for the new playlist's segment, however, if the segment fell off the
|
|
|
54970 |
// playlist, we can leave the old reference and just lose the timing info
|
|
|
54971 |
if (segmentInfo.mediaIndex >= 0) {
|
|
|
54972 |
segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
|
|
|
54973 |
}
|
|
|
54974 |
if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) {
|
|
|
54975 |
segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex];
|
|
|
54976 |
}
|
|
|
54977 |
}
|
|
|
54978 |
}
|
|
|
54979 |
this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
|
|
|
54980 |
}
|
|
|
54981 |
/**
|
|
|
54982 |
* Prevent the loader from fetching additional segments. If there
|
|
|
54983 |
* is a segment request outstanding, it will finish processing
|
|
|
54984 |
* before the loader halts. A segment loader can be unpaused by
|
|
|
54985 |
* calling load().
|
|
|
54986 |
*/
|
|
|
54987 |
|
|
|
54988 |
pause() {
|
|
|
54989 |
if (this.checkBufferTimeout_) {
|
|
|
54990 |
window.clearTimeout(this.checkBufferTimeout_);
|
|
|
54991 |
this.checkBufferTimeout_ = null;
|
|
|
54992 |
}
|
|
|
54993 |
}
|
|
|
54994 |
/**
|
|
|
54995 |
* Returns whether the segment loader is fetching additional
|
|
|
54996 |
* segments when given the opportunity. This property can be
|
|
|
54997 |
* modified through calls to pause() and load().
|
|
|
54998 |
*/
|
|
|
54999 |
|
|
|
55000 |
paused() {
|
|
|
55001 |
return this.checkBufferTimeout_ === null;
|
|
|
55002 |
}
|
|
|
55003 |
/**
|
|
|
55004 |
* Delete all the buffered data and reset the SegmentLoader
|
|
|
55005 |
*
|
|
|
55006 |
* @param {Function} [done] an optional callback to be executed when the remove
|
|
|
55007 |
* operation is complete
|
|
|
55008 |
*/
|
|
|
55009 |
|
|
|
55010 |
resetEverything(done) {
|
|
|
55011 |
this.ended_ = false;
|
|
|
55012 |
this.activeInitSegmentId_ = null;
|
|
|
55013 |
this.appendInitSegment_ = {
|
|
|
55014 |
audio: true,
|
|
|
55015 |
video: true
|
|
|
55016 |
};
|
|
|
55017 |
this.resetLoader(); // remove from 0, the earliest point, to Infinity, to signify removal of everything.
|
|
|
55018 |
// VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader,
|
|
|
55019 |
// we then clamp the value to duration if necessary.
|
|
|
55020 |
|
|
|
55021 |
this.remove(0, Infinity, done); // clears fmp4 captions
|
|
|
55022 |
|
|
|
55023 |
if (this.transmuxer_) {
|
|
|
55024 |
this.transmuxer_.postMessage({
|
|
|
55025 |
action: 'clearAllMp4Captions'
|
|
|
55026 |
}); // reset the cache in the transmuxer
|
|
|
55027 |
|
|
|
55028 |
this.transmuxer_.postMessage({
|
|
|
55029 |
action: 'reset'
|
|
|
55030 |
});
|
|
|
55031 |
}
|
|
|
55032 |
}
|
|
|
55033 |
/**
|
|
|
55034 |
* Force the SegmentLoader to resync and start loading around the currentTime instead
|
|
|
55035 |
* of starting at the end of the buffer
|
|
|
55036 |
*
|
|
|
55037 |
* Useful for fast quality changes
|
|
|
55038 |
*/
|
|
|
55039 |
|
|
|
55040 |
resetLoader() {
|
|
|
55041 |
this.fetchAtBuffer_ = false;
|
|
|
55042 |
this.resyncLoader();
|
|
|
55043 |
}
|
|
|
55044 |
/**
|
|
|
55045 |
* Force the SegmentLoader to restart synchronization and make a conservative guess
|
|
|
55046 |
* before returning to the simple walk-forward method
|
|
|
55047 |
*/
|
|
|
55048 |
|
|
|
55049 |
resyncLoader() {
|
|
|
55050 |
if (this.transmuxer_) {
|
|
|
55051 |
// need to clear out any cached data to prepare for the new segment
|
|
|
55052 |
segmentTransmuxer.reset(this.transmuxer_);
|
|
|
55053 |
}
|
|
|
55054 |
this.mediaIndex = null;
|
|
|
55055 |
this.partIndex = null;
|
|
|
55056 |
this.syncPoint_ = null;
|
|
|
55057 |
this.isPendingTimestampOffset_ = false;
|
|
|
55058 |
this.shouldForceTimestampOffsetAfterResync_ = true;
|
|
|
55059 |
this.callQueue_ = [];
|
|
|
55060 |
this.loadQueue_ = [];
|
|
|
55061 |
this.metadataQueue_.id3 = [];
|
|
|
55062 |
this.metadataQueue_.caption = [];
|
|
|
55063 |
this.abort();
|
|
|
55064 |
if (this.transmuxer_) {
|
|
|
55065 |
this.transmuxer_.postMessage({
|
|
|
55066 |
action: 'clearParsedMp4Captions'
|
|
|
55067 |
});
|
|
|
55068 |
}
|
|
|
55069 |
}
|
|
|
55070 |
/**
|
|
|
55071 |
* Remove any data in the source buffer between start and end times
|
|
|
55072 |
*
|
|
|
55073 |
* @param {number} start - the start time of the region to remove from the buffer
|
|
|
55074 |
* @param {number} end - the end time of the region to remove from the buffer
|
|
|
55075 |
* @param {Function} [done] - an optional callback to be executed when the remove
|
|
|
55076 |
* @param {boolean} force - force all remove operations to happen
|
|
|
55077 |
* operation is complete
|
|
|
55078 |
*/
|
|
|
55079 |
|
|
|
55080 |
remove(start, end, done = () => {}, force = false) {
|
|
|
55081 |
// clamp end to duration if we need to remove everything.
|
|
|
55082 |
// This is due to a browser bug that causes issues if we remove to Infinity.
|
|
|
55083 |
// videojs/videojs-contrib-hls#1225
|
|
|
55084 |
if (end === Infinity) {
|
|
|
55085 |
end = this.duration_();
|
|
|
55086 |
} // skip removes that would throw an error
|
|
|
55087 |
// commonly happens during a rendition switch at the start of a video
|
|
|
55088 |
// from start 0 to end 0
|
|
|
55089 |
|
|
|
55090 |
if (end <= start) {
|
|
|
55091 |
this.logger_('skipping remove because end ${end} is <= start ${start}');
|
|
|
55092 |
return;
|
|
|
55093 |
}
|
|
|
55094 |
if (!this.sourceUpdater_ || !this.getMediaInfo_()) {
|
|
|
55095 |
this.logger_('skipping remove because no source updater or starting media info'); // nothing to remove if we haven't processed any media
|
|
|
55096 |
|
|
|
55097 |
return;
|
|
|
55098 |
} // set it to one to complete this function's removes
|
|
|
55099 |
|
|
|
55100 |
let removesRemaining = 1;
|
|
|
55101 |
const removeFinished = () => {
|
|
|
55102 |
removesRemaining--;
|
|
|
55103 |
if (removesRemaining === 0) {
|
|
|
55104 |
done();
|
|
|
55105 |
}
|
|
|
55106 |
};
|
|
|
55107 |
if (force || !this.audioDisabled_) {
|
|
|
55108 |
removesRemaining++;
|
|
|
55109 |
this.sourceUpdater_.removeAudio(start, end, removeFinished);
|
|
|
55110 |
} // While it would be better to only remove video if the main loader has video, this
|
|
|
55111 |
// should be safe with audio only as removeVideo will call back even if there's no
|
|
|
55112 |
// video buffer.
|
|
|
55113 |
//
|
|
|
55114 |
// In theory we can check to see if there's video before calling the remove, but in
|
|
|
55115 |
// the event that we're switching between renditions and from video to audio only
|
|
|
55116 |
// (when we add support for that), we may need to clear the video contents despite
|
|
|
55117 |
// what the new media will contain.
|
|
|
55118 |
|
|
|
55119 |
if (force || this.loaderType_ === 'main') {
|
|
|
55120 |
this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_);
|
|
|
55121 |
removesRemaining++;
|
|
|
55122 |
this.sourceUpdater_.removeVideo(start, end, removeFinished);
|
|
|
55123 |
} // remove any captions and ID3 tags
|
|
|
55124 |
|
|
|
55125 |
for (const track in this.inbandTextTracks_) {
|
|
|
55126 |
removeCuesFromTrack(start, end, this.inbandTextTracks_[track]);
|
|
|
55127 |
}
|
|
|
55128 |
removeCuesFromTrack(start, end, this.segmentMetadataTrack_); // finished this function's removes
|
|
|
55129 |
|
|
|
55130 |
removeFinished();
|
|
|
55131 |
}
|
|
|
55132 |
/**
|
|
|
55133 |
* (re-)schedule monitorBufferTick_ to run as soon as possible
|
|
|
55134 |
*
|
|
|
55135 |
* @private
|
|
|
55136 |
*/
|
|
|
55137 |
|
|
|
55138 |
monitorBuffer_() {
|
|
|
55139 |
if (this.checkBufferTimeout_) {
|
|
|
55140 |
window.clearTimeout(this.checkBufferTimeout_);
|
|
|
55141 |
}
|
|
|
55142 |
this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1);
|
|
|
55143 |
}
|
|
|
55144 |
/**
|
|
|
55145 |
* As long as the SegmentLoader is in the READY state, periodically
|
|
|
55146 |
* invoke fillBuffer_().
|
|
|
55147 |
*
|
|
|
55148 |
* @private
|
|
|
55149 |
*/
|
|
|
55150 |
|
|
|
55151 |
monitorBufferTick_() {
|
|
|
55152 |
if (this.state === 'READY') {
|
|
|
55153 |
this.fillBuffer_();
|
|
|
55154 |
}
|
|
|
55155 |
if (this.checkBufferTimeout_) {
|
|
|
55156 |
window.clearTimeout(this.checkBufferTimeout_);
|
|
|
55157 |
}
|
|
|
55158 |
this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY);
|
|
|
55159 |
}
|
|
|
55160 |
/**
|
|
|
55161 |
* fill the buffer with segements unless the sourceBuffers are
|
|
|
55162 |
* currently updating
|
|
|
55163 |
*
|
|
|
55164 |
* Note: this function should only ever be called by monitorBuffer_
|
|
|
55165 |
* and never directly
|
|
|
55166 |
*
|
|
|
55167 |
* @private
|
|
|
55168 |
*/
|
|
|
55169 |
|
|
|
55170 |
fillBuffer_() {
|
|
|
55171 |
// TODO since the source buffer maintains a queue, and we shouldn't call this function
|
|
|
55172 |
// except when we're ready for the next segment, this check can most likely be removed
|
|
|
55173 |
if (this.sourceUpdater_.updating()) {
|
|
|
55174 |
return;
|
|
|
55175 |
} // see if we need to begin loading immediately
|
|
|
55176 |
|
|
|
55177 |
const segmentInfo = this.chooseNextRequest_();
|
|
|
55178 |
if (!segmentInfo) {
|
|
|
55179 |
return;
|
|
|
55180 |
}
|
|
|
55181 |
if (typeof segmentInfo.timestampOffset === 'number') {
|
|
|
55182 |
this.isPendingTimestampOffset_ = false;
|
|
|
55183 |
this.timelineChangeController_.pendingTimelineChange({
|
|
|
55184 |
type: this.loaderType_,
|
|
|
55185 |
from: this.currentTimeline_,
|
|
|
55186 |
to: segmentInfo.timeline
|
|
|
55187 |
});
|
|
|
55188 |
}
|
|
|
55189 |
this.loadSegment_(segmentInfo);
|
|
|
55190 |
}
|
|
|
55191 |
/**
|
|
|
55192 |
* Determines if we should call endOfStream on the media source based
|
|
|
55193 |
* on the state of the buffer or if appened segment was the final
|
|
|
55194 |
* segment in the playlist.
|
|
|
55195 |
*
|
|
|
55196 |
* @param {number} [mediaIndex] the media index of segment we last appended
|
|
|
55197 |
* @param {Object} [playlist] a media playlist object
|
|
|
55198 |
* @return {boolean} do we need to call endOfStream on the MediaSource
|
|
|
55199 |
*/
|
|
|
55200 |
|
|
|
55201 |
isEndOfStream_(mediaIndex = this.mediaIndex, playlist = this.playlist_, partIndex = this.partIndex) {
|
|
|
55202 |
if (!playlist || !this.mediaSource_) {
|
|
|
55203 |
return false;
|
|
|
55204 |
}
|
|
|
55205 |
const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex]; // mediaIndex is zero based but length is 1 based
|
|
|
55206 |
|
|
|
55207 |
const appendedLastSegment = mediaIndex + 1 === playlist.segments.length; // true if there are no parts, or this is the last part.
|
|
|
55208 |
|
|
|
55209 |
const appendedLastPart = !segment || !segment.parts || partIndex + 1 === segment.parts.length; // if we've buffered to the end of the video, we need to call endOfStream
|
|
|
55210 |
// so that MediaSources can trigger the `ended` event when it runs out of
|
|
|
55211 |
// buffered data instead of waiting for me
|
|
|
55212 |
|
|
|
55213 |
return playlist.endList && this.mediaSource_.readyState === 'open' && appendedLastSegment && appendedLastPart;
|
|
|
55214 |
}
|
|
|
55215 |
/**
|
|
|
55216 |
* Determines what request should be made given current segment loader state.
|
|
|
55217 |
*
|
|
|
55218 |
* @return {Object} a request object that describes the segment/part to load
|
|
|
55219 |
*/
|
|
|
55220 |
|
|
|
55221 |
chooseNextRequest_() {
|
|
|
55222 |
const buffered = this.buffered_();
|
|
|
55223 |
const bufferedEnd = lastBufferedEnd(buffered) || 0;
|
|
|
55224 |
const bufferedTime = timeAheadOf(buffered, this.currentTime_());
|
|
|
55225 |
const preloaded = !this.hasPlayed_() && bufferedTime >= 1;
|
|
|
55226 |
const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_();
|
|
|
55227 |
const segments = this.playlist_.segments; // return no segment if:
|
|
|
55228 |
// 1. we don't have segments
|
|
|
55229 |
// 2. The video has not yet played and we already downloaded a segment
|
|
|
55230 |
// 3. we already have enough buffered time
|
|
|
55231 |
|
|
|
55232 |
if (!segments.length || preloaded || haveEnoughBuffer) {
|
|
|
55233 |
return null;
|
|
|
55234 |
}
|
|
|
55235 |
this.syncPoint_ = this.syncPoint_ || this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_(), this.loaderType_);
|
|
|
55236 |
const next = {
|
|
|
55237 |
partIndex: null,
|
|
|
55238 |
mediaIndex: null,
|
|
|
55239 |
startOfSegment: null,
|
|
|
55240 |
playlist: this.playlist_,
|
|
|
55241 |
isSyncRequest: Boolean(!this.syncPoint_)
|
|
|
55242 |
};
|
|
|
55243 |
if (next.isSyncRequest) {
|
|
|
55244 |
next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd);
|
|
|
55245 |
this.logger_(`choose next request. Can not find sync point. Fallback to media Index: ${next.mediaIndex}`);
|
|
|
55246 |
} else if (this.mediaIndex !== null) {
|
|
|
55247 |
const segment = segments[this.mediaIndex];
|
|
|
55248 |
const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1;
|
|
|
55249 |
next.startOfSegment = segment.end ? segment.end : bufferedEnd;
|
|
|
55250 |
if (segment.parts && segment.parts[partIndex + 1]) {
|
|
|
55251 |
next.mediaIndex = this.mediaIndex;
|
|
|
55252 |
next.partIndex = partIndex + 1;
|
|
|
55253 |
} else {
|
|
|
55254 |
next.mediaIndex = this.mediaIndex + 1;
|
|
|
55255 |
}
|
|
|
55256 |
} else {
|
|
|
55257 |
// Find the segment containing the end of the buffer or current time.
|
|
|
55258 |
const {
|
|
|
55259 |
segmentIndex,
|
|
|
55260 |
startTime,
|
|
|
55261 |
partIndex
|
|
|
55262 |
} = Playlist.getMediaInfoForTime({
|
|
|
55263 |
exactManifestTimings: this.exactManifestTimings,
|
|
|
55264 |
playlist: this.playlist_,
|
|
|
55265 |
currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(),
|
|
|
55266 |
startingPartIndex: this.syncPoint_.partIndex,
|
|
|
55267 |
startingSegmentIndex: this.syncPoint_.segmentIndex,
|
|
|
55268 |
startTime: this.syncPoint_.time
|
|
|
55269 |
});
|
|
|
55270 |
next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`;
|
|
|
55271 |
next.mediaIndex = segmentIndex;
|
|
|
55272 |
next.startOfSegment = startTime;
|
|
|
55273 |
next.partIndex = partIndex;
|
|
|
55274 |
this.logger_(`choose next request. Playlist switched and we have a sync point. Media Index: ${next.mediaIndex} `);
|
|
|
55275 |
}
|
|
|
55276 |
const nextSegment = segments[next.mediaIndex];
|
|
|
55277 |
let nextPart = nextSegment && typeof next.partIndex === 'number' && nextSegment.parts && nextSegment.parts[next.partIndex]; // if the next segment index is invalid or
|
|
|
55278 |
// the next partIndex is invalid do not choose a next segment.
|
|
|
55279 |
|
|
|
55280 |
if (!nextSegment || typeof next.partIndex === 'number' && !nextPart) {
|
|
|
55281 |
return null;
|
|
|
55282 |
} // if the next segment has parts, and we don't have a partIndex.
|
|
|
55283 |
// Set partIndex to 0
|
|
|
55284 |
|
|
|
55285 |
if (typeof next.partIndex !== 'number' && nextSegment.parts) {
|
|
|
55286 |
next.partIndex = 0;
|
|
|
55287 |
nextPart = nextSegment.parts[0];
|
|
|
55288 |
} // independentSegments applies to every segment in a playlist. If independentSegments appears in a main playlist,
|
|
|
55289 |
// it applies to each segment in each media playlist.
|
|
|
55290 |
// https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.5.1
|
|
|
55291 |
|
|
|
55292 |
const hasIndependentSegments = this.vhs_.playlists && this.vhs_.playlists.main && this.vhs_.playlists.main.independentSegments || this.playlist_.independentSegments; // if we have no buffered data then we need to make sure
|
|
|
55293 |
// that the next part we append is "independent" if possible.
|
|
|
55294 |
// So we check if the previous part is independent, and request
|
|
|
55295 |
// it if it is.
|
|
|
55296 |
|
|
|
55297 |
if (!bufferedTime && nextPart && !hasIndependentSegments && !nextPart.independent) {
|
|
|
55298 |
if (next.partIndex === 0) {
|
|
|
55299 |
const lastSegment = segments[next.mediaIndex - 1];
|
|
|
55300 |
const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1];
|
|
|
55301 |
if (lastSegmentLastPart && lastSegmentLastPart.independent) {
|
|
|
55302 |
next.mediaIndex -= 1;
|
|
|
55303 |
next.partIndex = lastSegment.parts.length - 1;
|
|
|
55304 |
next.independent = 'previous segment';
|
|
|
55305 |
}
|
|
|
55306 |
} else if (nextSegment.parts[next.partIndex - 1].independent) {
|
|
|
55307 |
next.partIndex -= 1;
|
|
|
55308 |
next.independent = 'previous part';
|
|
|
55309 |
}
|
|
|
55310 |
}
|
|
|
55311 |
const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended'; // do not choose a next segment if all of the following:
|
|
|
55312 |
// 1. this is the last segment in the playlist
|
|
|
55313 |
// 2. end of stream has been called on the media source already
|
|
|
55314 |
// 3. the player is not seeking
|
|
|
55315 |
|
|
|
55316 |
if (next.mediaIndex >= segments.length - 1 && ended && !this.seeking_()) {
|
|
|
55317 |
return null;
|
|
|
55318 |
}
|
|
|
55319 |
if (this.shouldForceTimestampOffsetAfterResync_) {
|
|
|
55320 |
this.shouldForceTimestampOffsetAfterResync_ = false;
|
|
|
55321 |
next.forceTimestampOffset = true;
|
|
|
55322 |
this.logger_('choose next request. Force timestamp offset after loader resync');
|
|
|
55323 |
}
|
|
|
55324 |
return this.generateSegmentInfo_(next);
|
|
|
55325 |
}
|
|
|
55326 |
generateSegmentInfo_(options) {
|
|
|
55327 |
const {
|
|
|
55328 |
independent,
|
|
|
55329 |
playlist,
|
|
|
55330 |
mediaIndex,
|
|
|
55331 |
startOfSegment,
|
|
|
55332 |
isSyncRequest,
|
|
|
55333 |
partIndex,
|
|
|
55334 |
forceTimestampOffset,
|
|
|
55335 |
getMediaInfoForTime
|
|
|
55336 |
} = options;
|
|
|
55337 |
const segment = playlist.segments[mediaIndex];
|
|
|
55338 |
const part = typeof partIndex === 'number' && segment.parts[partIndex];
|
|
|
55339 |
const segmentInfo = {
|
|
|
55340 |
requestId: 'segment-loader-' + Math.random(),
|
|
|
55341 |
// resolve the segment URL relative to the playlist
|
|
|
55342 |
uri: part && part.resolvedUri || segment.resolvedUri,
|
|
|
55343 |
// the segment's mediaIndex at the time it was requested
|
|
|
55344 |
mediaIndex,
|
|
|
55345 |
partIndex: part ? partIndex : null,
|
|
|
55346 |
// whether or not to update the SegmentLoader's state with this
|
|
|
55347 |
// segment's mediaIndex
|
|
|
55348 |
isSyncRequest,
|
|
|
55349 |
startOfSegment,
|
|
|
55350 |
// the segment's playlist
|
|
|
55351 |
playlist,
|
|
|
55352 |
// unencrypted bytes of the segment
|
|
|
55353 |
bytes: null,
|
|
|
55354 |
// when a key is defined for this segment, the encrypted bytes
|
|
|
55355 |
encryptedBytes: null,
|
|
|
55356 |
// The target timestampOffset for this segment when we append it
|
|
|
55357 |
// to the source buffer
|
|
|
55358 |
timestampOffset: null,
|
|
|
55359 |
// The timeline that the segment is in
|
|
|
55360 |
timeline: segment.timeline,
|
|
|
55361 |
// The expected duration of the segment in seconds
|
|
|
55362 |
duration: part && part.duration || segment.duration,
|
|
|
55363 |
// retain the segment in case the playlist updates while doing an async process
|
|
|
55364 |
segment,
|
|
|
55365 |
part,
|
|
|
55366 |
byteLength: 0,
|
|
|
55367 |
transmuxer: this.transmuxer_,
|
|
|
55368 |
// type of getMediaInfoForTime that was used to get this segment
|
|
|
55369 |
getMediaInfoForTime,
|
|
|
55370 |
independent
|
|
|
55371 |
};
|
|
|
55372 |
const overrideCheck = typeof forceTimestampOffset !== 'undefined' ? forceTimestampOffset : this.isPendingTimestampOffset_;
|
|
|
55373 |
segmentInfo.timestampOffset = this.timestampOffsetForSegment_({
|
|
|
55374 |
segmentTimeline: segment.timeline,
|
|
|
55375 |
currentTimeline: this.currentTimeline_,
|
|
|
55376 |
startOfSegment,
|
|
|
55377 |
buffered: this.buffered_(),
|
|
|
55378 |
overrideCheck
|
|
|
55379 |
});
|
|
|
55380 |
const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered());
|
|
|
55381 |
if (typeof audioBufferedEnd === 'number') {
|
|
|
55382 |
// since the transmuxer is using the actual timing values, but the buffer is
|
|
|
55383 |
// adjusted by the timestamp offset, we must adjust the value here
|
|
|
55384 |
segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset();
|
|
|
55385 |
}
|
|
|
55386 |
if (this.sourceUpdater_.videoBuffered().length) {
|
|
|
55387 |
segmentInfo.gopsToAlignWith = gopsSafeToAlignWith(this.gopBuffer_,
|
|
|
55388 |
// since the transmuxer is using the actual timing values, but the time is
|
|
|
55389 |
// adjusted by the timestmap offset, we must adjust the value here
|
|
|
55390 |
this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(), this.timeMapping_);
|
|
|
55391 |
}
|
|
|
55392 |
return segmentInfo;
|
|
|
55393 |
} // get the timestampoffset for a segment,
|
|
|
55394 |
// added so that vtt segment loader can override and prevent
|
|
|
55395 |
// adding timestamp offsets.
|
|
|
55396 |
|
|
|
55397 |
timestampOffsetForSegment_(options) {
|
|
|
55398 |
return timestampOffsetForSegment(options);
|
|
|
55399 |
}
|
|
|
55400 |
/**
|
|
|
55401 |
* Determines if the network has enough bandwidth to complete the current segment
|
|
|
55402 |
* request in a timely manner. If not, the request will be aborted early and bandwidth
|
|
|
55403 |
* updated to trigger a playlist switch.
|
|
|
55404 |
*
|
|
|
55405 |
* @param {Object} stats
|
|
|
55406 |
* Object containing stats about the request timing and size
|
|
|
55407 |
* @private
|
|
|
55408 |
*/
|
|
|
55409 |
|
|
|
55410 |
earlyAbortWhenNeeded_(stats) {
|
|
|
55411 |
if (this.vhs_.tech_.paused() ||
|
|
|
55412 |
// Don't abort if the current playlist is on the lowestEnabledRendition
|
|
|
55413 |
// TODO: Replace using timeout with a boolean indicating whether this playlist is
|
|
|
55414 |
// the lowestEnabledRendition.
|
|
|
55415 |
!this.xhrOptions_.timeout ||
|
|
|
55416 |
// Don't abort if we have no bandwidth information to estimate segment sizes
|
|
|
55417 |
!this.playlist_.attributes.BANDWIDTH) {
|
|
|
55418 |
return;
|
|
|
55419 |
} // Wait at least 1 second since the first byte of data has been received before
|
|
|
55420 |
// using the calculated bandwidth from the progress event to allow the bitrate
|
|
|
55421 |
// to stabilize
|
|
|
55422 |
|
|
|
55423 |
if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
|
|
|
55424 |
return;
|
|
|
55425 |
}
|
|
|
55426 |
const currentTime = this.currentTime_();
|
|
|
55427 |
const measuredBandwidth = stats.bandwidth;
|
|
|
55428 |
const segmentDuration = this.pendingSegment_.duration;
|
|
|
55429 |
const requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort
|
|
|
55430 |
// if we are only left with less than 1 second when the request completes.
|
|
|
55431 |
// A negative timeUntilRebuffering indicates we are already rebuffering
|
|
|
55432 |
|
|
|
55433 |
const timeUntilRebuffer$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.vhs_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download
|
|
|
55434 |
// is larger than the estimated time until the player runs out of forward buffer
|
|
|
55435 |
|
|
|
55436 |
if (requestTimeRemaining <= timeUntilRebuffer$1) {
|
|
|
55437 |
return;
|
|
|
55438 |
}
|
|
|
55439 |
const switchCandidate = minRebufferMaxBandwidthSelector({
|
|
|
55440 |
main: this.vhs_.playlists.main,
|
|
|
55441 |
currentTime,
|
|
|
55442 |
bandwidth: measuredBandwidth,
|
|
|
55443 |
duration: this.duration_(),
|
|
|
55444 |
segmentDuration,
|
|
|
55445 |
timeUntilRebuffer: timeUntilRebuffer$1,
|
|
|
55446 |
currentTimeline: this.currentTimeline_,
|
|
|
55447 |
syncController: this.syncController_
|
|
|
55448 |
});
|
|
|
55449 |
if (!switchCandidate) {
|
|
|
55450 |
return;
|
|
|
55451 |
}
|
|
|
55452 |
const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$1;
|
|
|
55453 |
const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;
|
|
|
55454 |
let minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the
|
|
|
55455 |
// potential round trip time of the new request so that we are not too aggressive
|
|
|
55456 |
// with switching to a playlist that might save us a fraction of a second.
|
|
|
55457 |
|
|
|
55458 |
if (timeUntilRebuffer$1 <= TIME_FUDGE_FACTOR) {
|
|
|
55459 |
minimumTimeSaving = 1;
|
|
|
55460 |
}
|
|
|
55461 |
if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) {
|
|
|
55462 |
return;
|
|
|
55463 |
} // set the bandwidth to that of the desired playlist being sure to scale by
|
|
|
55464 |
// BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
|
|
|
55465 |
// don't trigger a bandwidthupdate as the bandwidth is artifial
|
|
|
55466 |
|
|
|
55467 |
this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1;
|
|
|
55468 |
this.trigger('earlyabort');
|
|
|
55469 |
}
|
|
|
55470 |
handleAbort_(segmentInfo) {
|
|
|
55471 |
this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`);
|
|
|
55472 |
this.mediaRequestsAborted += 1;
|
|
|
55473 |
}
|
|
|
55474 |
/**
|
|
|
55475 |
* XHR `progress` event handler
|
|
|
55476 |
*
|
|
|
55477 |
* @param {Event}
|
|
|
55478 |
* The XHR `progress` event
|
|
|
55479 |
* @param {Object} simpleSegment
|
|
|
55480 |
* A simplified segment object copy
|
|
|
55481 |
* @private
|
|
|
55482 |
*/
|
|
|
55483 |
|
|
|
55484 |
handleProgress_(event, simpleSegment) {
|
|
|
55485 |
this.earlyAbortWhenNeeded_(simpleSegment.stats);
|
|
|
55486 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55487 |
return;
|
|
|
55488 |
}
|
|
|
55489 |
this.trigger('progress');
|
|
|
55490 |
}
|
|
|
55491 |
handleTrackInfo_(simpleSegment, trackInfo) {
|
|
|
55492 |
this.earlyAbortWhenNeeded_(simpleSegment.stats);
|
|
|
55493 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55494 |
return;
|
|
|
55495 |
}
|
|
|
55496 |
if (this.checkForIllegalMediaSwitch(trackInfo)) {
|
|
|
55497 |
return;
|
|
|
55498 |
}
|
|
|
55499 |
trackInfo = trackInfo || {}; // When we have track info, determine what media types this loader is dealing with.
|
|
|
55500 |
// Guard against cases where we're not getting track info at all until we are
|
|
|
55501 |
// certain that all streams will provide it.
|
|
|
55502 |
|
|
|
55503 |
if (!shallowEqual(this.currentMediaInfo_, trackInfo)) {
|
|
|
55504 |
this.appendInitSegment_ = {
|
|
|
55505 |
audio: true,
|
|
|
55506 |
video: true
|
|
|
55507 |
};
|
|
|
55508 |
this.startingMediaInfo_ = trackInfo;
|
|
|
55509 |
this.currentMediaInfo_ = trackInfo;
|
|
|
55510 |
this.logger_('trackinfo update', trackInfo);
|
|
|
55511 |
this.trigger('trackinfo');
|
|
|
55512 |
} // trackinfo may cause an abort if the trackinfo
|
|
|
55513 |
// causes a codec change to an unsupported codec.
|
|
|
55514 |
|
|
|
55515 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55516 |
return;
|
|
|
55517 |
} // set trackinfo on the pending segment so that
|
|
|
55518 |
// it can append.
|
|
|
55519 |
|
|
|
55520 |
this.pendingSegment_.trackInfo = trackInfo; // check if any calls were waiting on the track info
|
|
|
55521 |
|
|
|
55522 |
if (this.hasEnoughInfoToAppend_()) {
|
|
|
55523 |
this.processCallQueue_();
|
|
|
55524 |
}
|
|
|
55525 |
}
|
|
|
55526 |
handleTimingInfo_(simpleSegment, mediaType, timeType, time) {
|
|
|
55527 |
this.earlyAbortWhenNeeded_(simpleSegment.stats);
|
|
|
55528 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55529 |
return;
|
|
|
55530 |
}
|
|
|
55531 |
const segmentInfo = this.pendingSegment_;
|
|
|
55532 |
const timingInfoProperty = timingInfoPropertyForMedia(mediaType);
|
|
|
55533 |
segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {};
|
|
|
55534 |
segmentInfo[timingInfoProperty][timeType] = time;
|
|
|
55535 |
this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`); // check if any calls were waiting on the timing info
|
|
|
55536 |
|
|
|
55537 |
if (this.hasEnoughInfoToAppend_()) {
|
|
|
55538 |
this.processCallQueue_();
|
|
|
55539 |
}
|
|
|
55540 |
}
|
|
|
55541 |
handleCaptions_(simpleSegment, captionData) {
|
|
|
55542 |
this.earlyAbortWhenNeeded_(simpleSegment.stats);
|
|
|
55543 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55544 |
return;
|
|
|
55545 |
} // This could only happen with fmp4 segments, but
|
|
|
55546 |
// should still not happen in general
|
|
|
55547 |
|
|
|
55548 |
if (captionData.length === 0) {
|
|
|
55549 |
this.logger_('SegmentLoader received no captions from a caption event');
|
|
|
55550 |
return;
|
|
|
55551 |
}
|
|
|
55552 |
const segmentInfo = this.pendingSegment_; // Wait until we have some video data so that caption timing
|
|
|
55553 |
// can be adjusted by the timestamp offset
|
|
|
55554 |
|
|
|
55555 |
if (!segmentInfo.hasAppendedData_) {
|
|
|
55556 |
this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData));
|
|
|
55557 |
return;
|
|
|
55558 |
}
|
|
|
55559 |
const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset();
|
|
|
55560 |
const captionTracks = {}; // get total start/end and captions for each track/stream
|
|
|
55561 |
|
|
|
55562 |
captionData.forEach(caption => {
|
|
|
55563 |
// caption.stream is actually a track name...
|
|
|
55564 |
// set to the existing values in tracks or default values
|
|
|
55565 |
captionTracks[caption.stream] = captionTracks[caption.stream] || {
|
|
|
55566 |
// Infinity, as any other value will be less than this
|
|
|
55567 |
startTime: Infinity,
|
|
|
55568 |
captions: [],
|
|
|
55569 |
// 0 as an other value will be more than this
|
|
|
55570 |
endTime: 0
|
|
|
55571 |
};
|
|
|
55572 |
const captionTrack = captionTracks[caption.stream];
|
|
|
55573 |
captionTrack.startTime = Math.min(captionTrack.startTime, caption.startTime + timestampOffset);
|
|
|
55574 |
captionTrack.endTime = Math.max(captionTrack.endTime, caption.endTime + timestampOffset);
|
|
|
55575 |
captionTrack.captions.push(caption);
|
|
|
55576 |
});
|
|
|
55577 |
Object.keys(captionTracks).forEach(trackName => {
|
|
|
55578 |
const {
|
|
|
55579 |
startTime,
|
|
|
55580 |
endTime,
|
|
|
55581 |
captions
|
|
|
55582 |
} = captionTracks[trackName];
|
|
|
55583 |
const inbandTextTracks = this.inbandTextTracks_;
|
|
|
55584 |
this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`);
|
|
|
55585 |
createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName); // clear out any cues that start and end at the same time period for the same track.
|
|
|
55586 |
// We do this because a rendition change that also changes the timescale for captions
|
|
|
55587 |
// will result in captions being re-parsed for certain segments. If we add them again
|
|
|
55588 |
// without clearing we will have two of the same captions visible.
|
|
|
55589 |
|
|
|
55590 |
removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]);
|
|
|
55591 |
addCaptionData({
|
|
|
55592 |
captionArray: captions,
|
|
|
55593 |
inbandTextTracks,
|
|
|
55594 |
timestampOffset
|
|
|
55595 |
});
|
|
|
55596 |
}); // Reset stored captions since we added parsed
|
|
|
55597 |
// captions to a text track at this point
|
|
|
55598 |
|
|
|
55599 |
if (this.transmuxer_) {
|
|
|
55600 |
this.transmuxer_.postMessage({
|
|
|
55601 |
action: 'clearParsedMp4Captions'
|
|
|
55602 |
});
|
|
|
55603 |
}
|
|
|
55604 |
}
|
|
|
55605 |
handleId3_(simpleSegment, id3Frames, dispatchType) {
|
|
|
55606 |
this.earlyAbortWhenNeeded_(simpleSegment.stats);
|
|
|
55607 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55608 |
return;
|
|
|
55609 |
}
|
|
|
55610 |
const segmentInfo = this.pendingSegment_; // we need to have appended data in order for the timestamp offset to be set
|
|
|
55611 |
|
|
|
55612 |
if (!segmentInfo.hasAppendedData_) {
|
|
|
55613 |
this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType));
|
|
|
55614 |
return;
|
|
|
55615 |
}
|
|
|
55616 |
this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_());
|
|
|
55617 |
}
|
|
|
55618 |
processMetadataQueue_() {
|
|
|
55619 |
this.metadataQueue_.id3.forEach(fn => fn());
|
|
|
55620 |
this.metadataQueue_.caption.forEach(fn => fn());
|
|
|
55621 |
this.metadataQueue_.id3 = [];
|
|
|
55622 |
this.metadataQueue_.caption = [];
|
|
|
55623 |
}
|
|
|
55624 |
processCallQueue_() {
|
|
|
55625 |
const callQueue = this.callQueue_; // Clear out the queue before the queued functions are run, since some of the
|
|
|
55626 |
// functions may check the length of the load queue and default to pushing themselves
|
|
|
55627 |
// back onto the queue.
|
|
|
55628 |
|
|
|
55629 |
this.callQueue_ = [];
|
|
|
55630 |
callQueue.forEach(fun => fun());
|
|
|
55631 |
}
|
|
|
55632 |
processLoadQueue_() {
|
|
|
55633 |
const loadQueue = this.loadQueue_; // Clear out the queue before the queued functions are run, since some of the
|
|
|
55634 |
// functions may check the length of the load queue and default to pushing themselves
|
|
|
55635 |
// back onto the queue.
|
|
|
55636 |
|
|
|
55637 |
this.loadQueue_ = [];
|
|
|
55638 |
loadQueue.forEach(fun => fun());
|
|
|
55639 |
}
|
|
|
55640 |
/**
|
|
|
55641 |
* Determines whether the loader has enough info to load the next segment.
|
|
|
55642 |
*
|
|
|
55643 |
* @return {boolean}
|
|
|
55644 |
* Whether or not the loader has enough info to load the next segment
|
|
|
55645 |
*/
|
|
|
55646 |
|
|
|
55647 |
hasEnoughInfoToLoad_() {
|
|
|
55648 |
// Since primary timing goes by video, only the audio loader potentially needs to wait
|
|
|
55649 |
// to load.
|
|
|
55650 |
if (this.loaderType_ !== 'audio') {
|
|
|
55651 |
return true;
|
|
|
55652 |
}
|
|
|
55653 |
const segmentInfo = this.pendingSegment_; // A fill buffer must have already run to establish a pending segment before there's
|
|
|
55654 |
// enough info to load.
|
|
|
55655 |
|
|
|
55656 |
if (!segmentInfo) {
|
|
|
55657 |
return false;
|
|
|
55658 |
} // The first segment can and should be loaded immediately so that source buffers are
|
|
|
55659 |
// created together (before appending). Source buffer creation uses the presence of
|
|
|
55660 |
// audio and video data to determine whether to create audio/video source buffers, and
|
|
|
55661 |
// uses processed (transmuxed or parsed) media to determine the types required.
|
|
|
55662 |
|
|
|
55663 |
if (!this.getCurrentMediaInfo_()) {
|
|
|
55664 |
return true;
|
|
|
55665 |
}
|
|
|
55666 |
if (
|
|
|
55667 |
// Technically, instead of waiting to load a segment on timeline changes, a segment
|
|
|
55668 |
// can be requested and downloaded and only wait before it is transmuxed or parsed.
|
|
|
55669 |
// But in practice, there are a few reasons why it is better to wait until a loader
|
|
|
55670 |
// is ready to append that segment before requesting and downloading:
|
|
|
55671 |
//
|
|
|
55672 |
// 1. Because audio and main loaders cross discontinuities together, if this loader
|
|
|
55673 |
// is waiting for the other to catch up, then instead of requesting another
|
|
|
55674 |
// segment and using up more bandwidth, by not yet loading, more bandwidth is
|
|
|
55675 |
// allotted to the loader currently behind.
|
|
|
55676 |
// 2. media-segment-request doesn't have to have logic to consider whether a segment
|
|
|
55677 |
// is ready to be processed or not, isolating the queueing behavior to the loader.
|
|
|
55678 |
// 3. The audio loader bases some of its segment properties on timing information
|
|
|
55679 |
// provided by the main loader, meaning that, if the logic for waiting on
|
|
|
55680 |
// processing was in media-segment-request, then it would also need to know how
|
|
|
55681 |
// to re-generate the segment information after the main loader caught up.
|
|
|
55682 |
shouldWaitForTimelineChange({
|
|
|
55683 |
timelineChangeController: this.timelineChangeController_,
|
|
|
55684 |
currentTimeline: this.currentTimeline_,
|
|
|
55685 |
segmentTimeline: segmentInfo.timeline,
|
|
|
55686 |
loaderType: this.loaderType_,
|
|
|
55687 |
audioDisabled: this.audioDisabled_
|
|
|
55688 |
})) {
|
|
|
55689 |
return false;
|
|
|
55690 |
}
|
|
|
55691 |
return true;
|
|
|
55692 |
}
|
|
|
55693 |
getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) {
|
|
|
55694 |
return segmentInfo && segmentInfo.trackInfo || this.currentMediaInfo_;
|
|
|
55695 |
}
|
|
|
55696 |
getMediaInfo_(segmentInfo = this.pendingSegment_) {
|
|
|
55697 |
return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_;
|
|
|
55698 |
}
|
|
|
55699 |
getPendingSegmentPlaylist() {
|
|
|
55700 |
return this.pendingSegment_ ? this.pendingSegment_.playlist : null;
|
|
|
55701 |
}
|
|
|
55702 |
hasEnoughInfoToAppend_() {
|
|
|
55703 |
if (!this.sourceUpdater_.ready()) {
|
|
|
55704 |
return false;
|
|
|
55705 |
} // If content needs to be removed or the loader is waiting on an append reattempt,
|
|
|
55706 |
// then no additional content should be appended until the prior append is resolved.
|
|
|
55707 |
|
|
|
55708 |
if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) {
|
|
|
55709 |
return false;
|
|
|
55710 |
}
|
|
|
55711 |
const segmentInfo = this.pendingSegment_;
|
|
|
55712 |
const trackInfo = this.getCurrentMediaInfo_(); // no segment to append any data for or
|
|
|
55713 |
// we do not have information on this specific
|
|
|
55714 |
// segment yet
|
|
|
55715 |
|
|
|
55716 |
if (!segmentInfo || !trackInfo) {
|
|
|
55717 |
return false;
|
|
|
55718 |
}
|
|
|
55719 |
const {
|
|
|
55720 |
hasAudio,
|
|
|
55721 |
hasVideo,
|
|
|
55722 |
isMuxed
|
|
|
55723 |
} = trackInfo;
|
|
|
55724 |
if (hasVideo && !segmentInfo.videoTimingInfo) {
|
|
|
55725 |
return false;
|
|
|
55726 |
} // muxed content only relies on video timing information for now.
|
|
|
55727 |
|
|
|
55728 |
if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) {
|
|
|
55729 |
return false;
|
|
|
55730 |
}
|
|
|
55731 |
if (shouldWaitForTimelineChange({
|
|
|
55732 |
timelineChangeController: this.timelineChangeController_,
|
|
|
55733 |
currentTimeline: this.currentTimeline_,
|
|
|
55734 |
segmentTimeline: segmentInfo.timeline,
|
|
|
55735 |
loaderType: this.loaderType_,
|
|
|
55736 |
audioDisabled: this.audioDisabled_
|
|
|
55737 |
})) {
|
|
|
55738 |
return false;
|
|
|
55739 |
}
|
|
|
55740 |
return true;
|
|
|
55741 |
}
|
|
|
55742 |
handleData_(simpleSegment, result) {
|
|
|
55743 |
this.earlyAbortWhenNeeded_(simpleSegment.stats);
|
|
|
55744 |
if (this.checkForAbort_(simpleSegment.requestId)) {
|
|
|
55745 |
return;
|
|
|
55746 |
} // If there's anything in the call queue, then this data came later and should be
|
|
|
55747 |
// executed after the calls currently queued.
|
|
|
55748 |
|
|
|
55749 |
if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) {
|
|
|
55750 |
this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result));
|
|
|
55751 |
return;
|
|
|
55752 |
}
|
|
|
55753 |
const segmentInfo = this.pendingSegment_; // update the time mapping so we can translate from display time to media time
|
|
|
55754 |
|
|
|
55755 |
this.setTimeMapping_(segmentInfo.timeline); // for tracking overall stats
|
|
|
55756 |
|
|
|
55757 |
this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment); // Note that the state isn't changed from loading to appending. This is because abort
|
|
|
55758 |
// logic may change behavior depending on the state, and changing state too early may
|
|
|
55759 |
// inflate our estimates of bandwidth. In the future this should be re-examined to
|
|
|
55760 |
// note more granular states.
|
|
|
55761 |
// don't process and append data if the mediaSource is closed
|
|
|
55762 |
|
|
|
55763 |
if (this.mediaSource_.readyState === 'closed') {
|
|
|
55764 |
return;
|
|
|
55765 |
} // if this request included an initialization segment, save that data
|
|
|
55766 |
// to the initSegment cache
|
|
|
55767 |
|
|
|
55768 |
if (simpleSegment.map) {
|
|
|
55769 |
simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true); // move over init segment properties to media request
|
|
|
55770 |
|
|
|
55771 |
segmentInfo.segment.map = simpleSegment.map;
|
|
|
55772 |
} // if this request included a segment key, save that data in the cache
|
|
|
55773 |
|
|
|
55774 |
if (simpleSegment.key) {
|
|
|
55775 |
this.segmentKey(simpleSegment.key, true);
|
|
|
55776 |
}
|
|
|
55777 |
segmentInfo.isFmp4 = simpleSegment.isFmp4;
|
|
|
55778 |
segmentInfo.timingInfo = segmentInfo.timingInfo || {};
|
|
|
55779 |
if (segmentInfo.isFmp4) {
|
|
|
55780 |
this.trigger('fmp4');
|
|
|
55781 |
segmentInfo.timingInfo.start = segmentInfo[timingInfoPropertyForMedia(result.type)].start;
|
|
|
55782 |
} else {
|
|
|
55783 |
const trackInfo = this.getCurrentMediaInfo_();
|
|
|
55784 |
const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo;
|
|
|
55785 |
let firstVideoFrameTimeForData;
|
|
|
55786 |
if (useVideoTimingInfo) {
|
|
|
55787 |
firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start;
|
|
|
55788 |
} // Segment loader knows more about segment timing than the transmuxer (in certain
|
|
|
55789 |
// aspects), so make any changes required for a more accurate start time.
|
|
|
55790 |
// Don't set the end time yet, as the segment may not be finished processing.
|
|
|
55791 |
|
|
|
55792 |
segmentInfo.timingInfo.start = this.trueSegmentStart_({
|
|
|
55793 |
currentStart: segmentInfo.timingInfo.start,
|
|
|
55794 |
playlist: segmentInfo.playlist,
|
|
|
55795 |
mediaIndex: segmentInfo.mediaIndex,
|
|
|
55796 |
currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(),
|
|
|
55797 |
useVideoTimingInfo,
|
|
|
55798 |
firstVideoFrameTimeForData,
|
|
|
55799 |
videoTimingInfo: segmentInfo.videoTimingInfo,
|
|
|
55800 |
audioTimingInfo: segmentInfo.audioTimingInfo
|
|
|
55801 |
});
|
|
|
55802 |
} // Init segments for audio and video only need to be appended in certain cases. Now
|
|
|
55803 |
// that data is about to be appended, we can check the final cases to determine
|
|
|
55804 |
// whether we should append an init segment.
|
|
|
55805 |
|
|
|
55806 |
this.updateAppendInitSegmentStatus(segmentInfo, result.type); // Timestamp offset should be updated once we get new data and have its timing info,
|
|
|
55807 |
// as we use the start of the segment to offset the best guess (playlist provided)
|
|
|
55808 |
// timestamp offset.
|
|
|
55809 |
|
|
|
55810 |
this.updateSourceBufferTimestampOffset_(segmentInfo); // if this is a sync request we need to determine whether it should
|
|
|
55811 |
// be appended or not.
|
|
|
55812 |
|
|
|
55813 |
if (segmentInfo.isSyncRequest) {
|
|
|
55814 |
// first save/update our timing info for this segment.
|
|
|
55815 |
// this is what allows us to choose an accurate segment
|
|
|
55816 |
// and the main reason we make a sync request.
|
|
|
55817 |
this.updateTimingInfoEnd_(segmentInfo);
|
|
|
55818 |
this.syncController_.saveSegmentTimingInfo({
|
|
|
55819 |
segmentInfo,
|
|
|
55820 |
shouldSaveTimelineMapping: this.loaderType_ === 'main'
|
|
|
55821 |
});
|
|
|
55822 |
const next = this.chooseNextRequest_(); // If the sync request isn't the segment that would be requested next
|
|
|
55823 |
// after taking into account its timing info, do not append it.
|
|
|
55824 |
|
|
|
55825 |
if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) {
|
|
|
55826 |
this.logger_('sync segment was incorrect, not appending');
|
|
|
55827 |
return;
|
|
|
55828 |
} // otherwise append it like any other segment as our guess was correct.
|
|
|
55829 |
|
|
|
55830 |
this.logger_('sync segment was correct, appending');
|
|
|
55831 |
} // Save some state so that in the future anything waiting on first append (and/or
|
|
|
55832 |
// timestamp offset(s)) can process immediately. While the extra state isn't optimal,
|
|
|
55833 |
// we need some notion of whether the timestamp offset or other relevant information
|
|
|
55834 |
// has had a chance to be set.
|
|
|
55835 |
|
|
|
55836 |
segmentInfo.hasAppendedData_ = true; // Now that the timestamp offset should be set, we can append any waiting ID3 tags.
|
|
|
55837 |
|
|
|
55838 |
this.processMetadataQueue_();
|
|
|
55839 |
this.appendData_(segmentInfo, result);
|
|
|
55840 |
}
|
|
|
55841 |
updateAppendInitSegmentStatus(segmentInfo, type) {
|
|
|
55842 |
// alt audio doesn't manage timestamp offset
|
|
|
55843 |
if (this.loaderType_ === 'main' && typeof segmentInfo.timestampOffset === 'number' &&
|
|
|
55844 |
// in the case that we're handling partial data, we don't want to append an init
|
|
|
55845 |
// segment for each chunk
|
|
|
55846 |
!segmentInfo.changedTimestampOffset) {
|
|
|
55847 |
// if the timestamp offset changed, the timeline may have changed, so we have to re-
|
|
|
55848 |
// append init segments
|
|
|
55849 |
this.appendInitSegment_ = {
|
|
|
55850 |
audio: true,
|
|
|
55851 |
video: true
|
|
|
55852 |
};
|
|
|
55853 |
}
|
|
|
55854 |
if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) {
|
|
|
55855 |
// make sure we append init segment on playlist changes, in case the media config
|
|
|
55856 |
// changed
|
|
|
55857 |
this.appendInitSegment_[type] = true;
|
|
|
55858 |
}
|
|
|
55859 |
}
|
|
|
55860 |
getInitSegmentAndUpdateState_({
|
|
|
55861 |
type,
|
|
|
55862 |
initSegment,
|
|
|
55863 |
map,
|
|
|
55864 |
playlist
|
|
|
55865 |
}) {
|
|
|
55866 |
// "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section
|
|
|
55867 |
// (Section 3) required to parse the applicable Media Segments. It applies to every
|
|
|
55868 |
// Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag
|
|
|
55869 |
// or until the end of the playlist."
|
|
|
55870 |
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5
|
|
|
55871 |
if (map) {
|
|
|
55872 |
const id = initSegmentId(map);
|
|
|
55873 |
if (this.activeInitSegmentId_ === id) {
|
|
|
55874 |
// don't need to re-append the init segment if the ID matches
|
|
|
55875 |
return null;
|
|
|
55876 |
} // a map-specified init segment takes priority over any transmuxed (or otherwise
|
|
|
55877 |
// obtained) init segment
|
|
|
55878 |
//
|
|
|
55879 |
// this also caches the init segment for later use
|
|
|
55880 |
|
|
|
55881 |
initSegment = this.initSegmentForMap(map, true).bytes;
|
|
|
55882 |
this.activeInitSegmentId_ = id;
|
|
|
55883 |
} // We used to always prepend init segments for video, however, that shouldn't be
|
|
|
55884 |
// necessary. Instead, we should only append on changes, similar to what we've always
|
|
|
55885 |
// done for audio. This is more important (though may not be that important) for
|
|
|
55886 |
// frame-by-frame appending for LHLS, simply because of the increased quantity of
|
|
|
55887 |
// appends.
|
|
|
55888 |
|
|
|
55889 |
if (initSegment && this.appendInitSegment_[type]) {
|
|
|
55890 |
// Make sure we track the playlist that we last used for the init segment, so that
|
|
|
55891 |
// we can re-append the init segment in the event that we get data from a new
|
|
|
55892 |
// playlist. Discontinuities and track changes are handled in other sections.
|
|
|
55893 |
this.playlistOfLastInitSegment_[type] = playlist; // Disable future init segment appends for this type. Until a change is necessary.
|
|
|
55894 |
|
|
|
55895 |
this.appendInitSegment_[type] = false; // we need to clear out the fmp4 active init segment id, since
|
|
|
55896 |
// we are appending the muxer init segment
|
|
|
55897 |
|
|
|
55898 |
this.activeInitSegmentId_ = null;
|
|
|
55899 |
return initSegment;
|
|
|
55900 |
}
|
|
|
55901 |
return null;
|
|
|
55902 |
}
|
|
|
55903 |
handleQuotaExceededError_({
|
|
|
55904 |
segmentInfo,
|
|
|
55905 |
type,
|
|
|
55906 |
bytes
|
|
|
55907 |
}, error) {
|
|
|
55908 |
const audioBuffered = this.sourceUpdater_.audioBuffered();
|
|
|
55909 |
const videoBuffered = this.sourceUpdater_.videoBuffered(); // For now we're ignoring any notion of gaps in the buffer, but they, in theory,
|
|
|
55910 |
// should be cleared out during the buffer removals. However, log in case it helps
|
|
|
55911 |
// debug.
|
|
|
55912 |
|
|
|
55913 |
if (audioBuffered.length > 1) {
|
|
|
55914 |
this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' + timeRangesToArray(audioBuffered).join(', '));
|
|
|
55915 |
}
|
|
|
55916 |
if (videoBuffered.length > 1) {
|
|
|
55917 |
this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' + timeRangesToArray(videoBuffered).join(', '));
|
|
|
55918 |
}
|
|
|
55919 |
const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0;
|
|
|
55920 |
const audioBufferEnd = audioBuffered.length ? audioBuffered.end(audioBuffered.length - 1) : 0;
|
|
|
55921 |
const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0;
|
|
|
55922 |
const videoBufferEnd = videoBuffered.length ? videoBuffered.end(videoBuffered.length - 1) : 0;
|
|
|
55923 |
if (audioBufferEnd - audioBufferStart <= MIN_BACK_BUFFER && videoBufferEnd - videoBufferStart <= MIN_BACK_BUFFER) {
|
|
|
55924 |
// Can't remove enough buffer to make room for new segment (or the browser doesn't
|
|
|
55925 |
// allow for appends of segments this size). In the future, it may be possible to
|
|
|
55926 |
// split up the segment and append in pieces, but for now, error out this playlist
|
|
|
55927 |
// in an attempt to switch to a more manageable rendition.
|
|
|
55928 |
this.logger_('On QUOTA_EXCEEDED_ERR, single segment too large to append to ' + 'buffer, triggering an error. ' + `Appended byte length: ${bytes.byteLength}, ` + `audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` + `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `);
|
|
|
55929 |
this.error({
|
|
|
55930 |
message: 'Quota exceeded error with append of a single segment of content',
|
|
|
55931 |
excludeUntil: Infinity
|
|
|
55932 |
});
|
|
|
55933 |
this.trigger('error');
|
|
|
55934 |
return;
|
|
|
55935 |
} // To try to resolve the quota exceeded error, clear back buffer and retry. This means
|
|
|
55936 |
// that the segment-loader should block on future events until this one is handled, so
|
|
|
55937 |
// that it doesn't keep moving onto further segments. Adding the call to the call
|
|
|
55938 |
// queue will prevent further appends until waitingOnRemove_ and
|
|
|
55939 |
// quotaExceededErrorRetryTimeout_ are cleared.
|
|
|
55940 |
//
|
|
|
55941 |
// Note that this will only block the current loader. In the case of demuxed content,
|
|
|
55942 |
// the other load may keep filling as fast as possible. In practice, this should be
|
|
|
55943 |
// OK, as it is a rare case when either audio has a high enough bitrate to fill up a
|
|
|
55944 |
// source buffer, or video fills without enough room for audio to append (and without
|
|
|
55945 |
// the availability of clearing out seconds of back buffer to make room for audio).
|
|
|
55946 |
// But it might still be good to handle this case in the future as a TODO.
|
|
|
55947 |
|
|
|
55948 |
this.waitingOnRemove_ = true;
|
|
|
55949 |
this.callQueue_.push(this.appendToSourceBuffer_.bind(this, {
|
|
|
55950 |
segmentInfo,
|
|
|
55951 |
type,
|
|
|
55952 |
bytes
|
|
|
55953 |
}));
|
|
|
55954 |
const currentTime = this.currentTime_(); // Try to remove as much audio and video as possible to make room for new content
|
|
|
55955 |
// before retrying.
|
|
|
55956 |
|
|
|
55957 |
const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER;
|
|
|
55958 |
this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`);
|
|
|
55959 |
this.remove(0, timeToRemoveUntil, () => {
|
|
|
55960 |
this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`);
|
|
|
55961 |
this.waitingOnRemove_ = false; // wait the length of time alotted in the back buffer to prevent wasted
|
|
|
55962 |
// attempts (since we can't clear less than the minimum)
|
|
|
55963 |
|
|
|
55964 |
this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => {
|
|
|
55965 |
this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue');
|
|
|
55966 |
this.quotaExceededErrorRetryTimeout_ = null;
|
|
|
55967 |
this.processCallQueue_();
|
|
|
55968 |
}, MIN_BACK_BUFFER * 1000);
|
|
|
55969 |
}, true);
|
|
|
55970 |
}
|
|
|
55971 |
handleAppendError_({
|
|
|
55972 |
segmentInfo,
|
|
|
55973 |
type,
|
|
|
55974 |
bytes
|
|
|
55975 |
}, error) {
|
|
|
55976 |
// if there's no error, nothing to do
|
|
|
55977 |
if (!error) {
|
|
|
55978 |
return;
|
|
|
55979 |
}
|
|
|
55980 |
if (error.code === QUOTA_EXCEEDED_ERR) {
|
|
|
55981 |
this.handleQuotaExceededError_({
|
|
|
55982 |
segmentInfo,
|
|
|
55983 |
type,
|
|
|
55984 |
bytes
|
|
|
55985 |
}); // A quota exceeded error should be recoverable with a future re-append, so no need
|
|
|
55986 |
// to trigger an append error.
|
|
|
55987 |
|
|
|
55988 |
return;
|
|
|
55989 |
}
|
|
|
55990 |
this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error);
|
|
|
55991 |
this.error(`${type} append of ${bytes.length}b failed for segment ` + `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`); // If an append errors, we often can't recover.
|
|
|
55992 |
// (see https://w3c.github.io/media-source/#sourcebuffer-append-error).
|
|
|
55993 |
//
|
|
|
55994 |
// Trigger a special error so that it can be handled separately from normal,
|
|
|
55995 |
// recoverable errors.
|
|
|
55996 |
|
|
|
55997 |
this.trigger('appenderror');
|
|
|
55998 |
}
|
|
|
55999 |
appendToSourceBuffer_({
|
|
|
56000 |
segmentInfo,
|
|
|
56001 |
type,
|
|
|
56002 |
initSegment,
|
|
|
56003 |
data,
|
|
|
56004 |
bytes
|
|
|
56005 |
}) {
|
|
|
56006 |
// If this is a re-append, bytes were already created and don't need to be recreated
|
|
|
56007 |
if (!bytes) {
|
|
|
56008 |
const segments = [data];
|
|
|
56009 |
let byteLength = data.byteLength;
|
|
|
56010 |
if (initSegment) {
|
|
|
56011 |
// if the media initialization segment is changing, append it before the content
|
|
|
56012 |
// segment
|
|
|
56013 |
segments.unshift(initSegment);
|
|
|
56014 |
byteLength += initSegment.byteLength;
|
|
|
56015 |
} // Technically we should be OK appending the init segment separately, however, we
|
|
|
56016 |
// haven't yet tested that, and prepending is how we have always done things.
|
|
|
56017 |
|
|
|
56018 |
bytes = concatSegments({
|
|
|
56019 |
bytes: byteLength,
|
|
|
56020 |
segments
|
|
|
56021 |
});
|
|
|
56022 |
}
|
|
|
56023 |
this.sourceUpdater_.appendBuffer({
|
|
|
56024 |
segmentInfo,
|
|
|
56025 |
type,
|
|
|
56026 |
bytes
|
|
|
56027 |
}, this.handleAppendError_.bind(this, {
|
|
|
56028 |
segmentInfo,
|
|
|
56029 |
type,
|
|
|
56030 |
bytes
|
|
|
56031 |
}));
|
|
|
56032 |
}
|
|
|
56033 |
handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) {
|
|
|
56034 |
if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) {
|
|
|
56035 |
return;
|
|
|
56036 |
}
|
|
|
56037 |
const segment = this.pendingSegment_.segment;
|
|
|
56038 |
const timingInfoProperty = `${type}TimingInfo`;
|
|
|
56039 |
if (!segment[timingInfoProperty]) {
|
|
|
56040 |
segment[timingInfoProperty] = {};
|
|
|
56041 |
}
|
|
|
56042 |
segment[timingInfoProperty].transmuxerPrependedSeconds = segmentTimingInfo.prependedContentDuration || 0;
|
|
|
56043 |
segment[timingInfoProperty].transmuxedPresentationStart = segmentTimingInfo.start.presentation;
|
|
|
56044 |
segment[timingInfoProperty].transmuxedDecodeStart = segmentTimingInfo.start.decode;
|
|
|
56045 |
segment[timingInfoProperty].transmuxedPresentationEnd = segmentTimingInfo.end.presentation;
|
|
|
56046 |
segment[timingInfoProperty].transmuxedDecodeEnd = segmentTimingInfo.end.decode; // mainly used as a reference for debugging
|
|
|
56047 |
|
|
|
56048 |
segment[timingInfoProperty].baseMediaDecodeTime = segmentTimingInfo.baseMediaDecodeTime;
|
|
|
56049 |
}
|
|
|
56050 |
appendData_(segmentInfo, result) {
|
|
|
56051 |
const {
|
|
|
56052 |
type,
|
|
|
56053 |
data
|
|
|
56054 |
} = result;
|
|
|
56055 |
if (!data || !data.byteLength) {
|
|
|
56056 |
return;
|
|
|
56057 |
}
|
|
|
56058 |
if (type === 'audio' && this.audioDisabled_) {
|
|
|
56059 |
return;
|
|
|
56060 |
}
|
|
|
56061 |
const initSegment = this.getInitSegmentAndUpdateState_({
|
|
|
56062 |
type,
|
|
|
56063 |
initSegment: result.initSegment,
|
|
|
56064 |
playlist: segmentInfo.playlist,
|
|
|
56065 |
map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null
|
|
|
56066 |
});
|
|
|
56067 |
this.appendToSourceBuffer_({
|
|
|
56068 |
segmentInfo,
|
|
|
56069 |
type,
|
|
|
56070 |
initSegment,
|
|
|
56071 |
data
|
|
|
56072 |
});
|
|
|
56073 |
}
|
|
|
56074 |
/**
|
|
|
56075 |
* load a specific segment from a request into the buffer
|
|
|
56076 |
*
|
|
|
56077 |
* @private
|
|
|
56078 |
*/
|
|
|
56079 |
|
|
|
56080 |
loadSegment_(segmentInfo) {
|
|
|
56081 |
this.state = 'WAITING';
|
|
|
56082 |
this.pendingSegment_ = segmentInfo;
|
|
|
56083 |
this.trimBackBuffer_(segmentInfo);
|
|
|
56084 |
if (typeof segmentInfo.timestampOffset === 'number') {
|
|
|
56085 |
if (this.transmuxer_) {
|
|
|
56086 |
this.transmuxer_.postMessage({
|
|
|
56087 |
action: 'clearAllMp4Captions'
|
|
|
56088 |
});
|
|
|
56089 |
}
|
|
|
56090 |
}
|
|
|
56091 |
if (!this.hasEnoughInfoToLoad_()) {
|
|
|
56092 |
this.loadQueue_.push(() => {
|
|
|
56093 |
// regenerate the audioAppendStart, timestampOffset, etc as they
|
|
|
56094 |
// may have changed since this function was added to the queue.
|
|
|
56095 |
const options = _extends$1({}, segmentInfo, {
|
|
|
56096 |
forceTimestampOffset: true
|
|
|
56097 |
});
|
|
|
56098 |
_extends$1(segmentInfo, this.generateSegmentInfo_(options));
|
|
|
56099 |
this.isPendingTimestampOffset_ = false;
|
|
|
56100 |
this.updateTransmuxerAndRequestSegment_(segmentInfo);
|
|
|
56101 |
});
|
|
|
56102 |
return;
|
|
|
56103 |
}
|
|
|
56104 |
this.updateTransmuxerAndRequestSegment_(segmentInfo);
|
|
|
56105 |
}
|
|
|
56106 |
updateTransmuxerAndRequestSegment_(segmentInfo) {
|
|
|
56107 |
// We'll update the source buffer's timestamp offset once we have transmuxed data, but
|
|
|
56108 |
// the transmuxer still needs to be updated before then.
|
|
|
56109 |
//
|
|
|
56110 |
// Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp
|
|
|
56111 |
// offset must be passed to the transmuxer for stream correcting adjustments.
|
|
|
56112 |
if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) {
|
|
|
56113 |
this.gopBuffer_.length = 0; // gopsToAlignWith was set before the GOP buffer was cleared
|
|
|
56114 |
|
|
|
56115 |
segmentInfo.gopsToAlignWith = [];
|
|
|
56116 |
this.timeMapping_ = 0; // reset values in the transmuxer since a discontinuity should start fresh
|
|
|
56117 |
|
|
|
56118 |
this.transmuxer_.postMessage({
|
|
|
56119 |
action: 'reset'
|
|
|
56120 |
});
|
|
|
56121 |
this.transmuxer_.postMessage({
|
|
|
56122 |
action: 'setTimestampOffset',
|
|
|
56123 |
timestampOffset: segmentInfo.timestampOffset
|
|
|
56124 |
});
|
|
|
56125 |
}
|
|
|
56126 |
const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo);
|
|
|
56127 |
const isEndOfStream = this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex);
|
|
|
56128 |
const isWalkingForward = this.mediaIndex !== null;
|
|
|
56129 |
const isDiscontinuity = segmentInfo.timeline !== this.currentTimeline_ &&
|
|
|
56130 |
// currentTimeline starts at -1, so we shouldn't end the timeline switching to 0,
|
|
|
56131 |
// the first timeline
|
|
|
56132 |
segmentInfo.timeline > 0;
|
|
|
56133 |
const isEndOfTimeline = isEndOfStream || isWalkingForward && isDiscontinuity;
|
|
|
56134 |
this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes),
|
|
|
56135 |
// then this init segment has never been seen before and should be appended.
|
|
|
56136 |
//
|
|
|
56137 |
// At this point the content type (audio/video or both) is not yet known, but it should be safe to set
|
|
|
56138 |
// both to true and leave the decision of whether to append the init segment to append time.
|
|
|
56139 |
|
|
|
56140 |
if (simpleSegment.map && !simpleSegment.map.bytes) {
|
|
|
56141 |
this.logger_('going to request init segment.');
|
|
|
56142 |
this.appendInitSegment_ = {
|
|
|
56143 |
video: true,
|
|
|
56144 |
audio: true
|
|
|
56145 |
};
|
|
|
56146 |
}
|
|
|
56147 |
segmentInfo.abortRequests = mediaSegmentRequest({
|
|
|
56148 |
xhr: this.vhs_.xhr,
|
|
|
56149 |
xhrOptions: this.xhrOptions_,
|
|
|
56150 |
decryptionWorker: this.decrypter_,
|
|
|
56151 |
segment: simpleSegment,
|
|
|
56152 |
abortFn: this.handleAbort_.bind(this, segmentInfo),
|
|
|
56153 |
progressFn: this.handleProgress_.bind(this),
|
|
|
56154 |
trackInfoFn: this.handleTrackInfo_.bind(this),
|
|
|
56155 |
timingInfoFn: this.handleTimingInfo_.bind(this),
|
|
|
56156 |
videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'video', segmentInfo.requestId),
|
|
|
56157 |
audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'audio', segmentInfo.requestId),
|
|
|
56158 |
captionsFn: this.handleCaptions_.bind(this),
|
|
|
56159 |
isEndOfTimeline,
|
|
|
56160 |
endedTimelineFn: () => {
|
|
|
56161 |
this.logger_('received endedtimeline callback');
|
|
|
56162 |
},
|
|
|
56163 |
id3Fn: this.handleId3_.bind(this),
|
|
|
56164 |
dataFn: this.handleData_.bind(this),
|
|
|
56165 |
doneFn: this.segmentRequestFinished_.bind(this),
|
|
|
56166 |
onTransmuxerLog: ({
|
|
|
56167 |
message,
|
|
|
56168 |
level,
|
|
|
56169 |
stream
|
|
|
56170 |
}) => {
|
|
|
56171 |
this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`);
|
|
|
56172 |
}
|
|
|
56173 |
});
|
|
|
56174 |
}
|
|
|
56175 |
/**
|
|
|
56176 |
* trim the back buffer so that we don't have too much data
|
|
|
56177 |
* in the source buffer
|
|
|
56178 |
*
|
|
|
56179 |
* @private
|
|
|
56180 |
*
|
|
|
56181 |
* @param {Object} segmentInfo - the current segment
|
|
|
56182 |
*/
|
|
|
56183 |
|
|
|
56184 |
trimBackBuffer_(segmentInfo) {
|
|
|
56185 |
const removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); // Chrome has a hard limit of 150MB of
|
|
|
56186 |
// buffer and a very conservative "garbage collector"
|
|
|
56187 |
// We manually clear out the old buffer to ensure
|
|
|
56188 |
// we don't trigger the QuotaExceeded error
|
|
|
56189 |
// on the source buffer during subsequent appends
|
|
|
56190 |
|
|
|
56191 |
if (removeToTime > 0) {
|
|
|
56192 |
this.remove(0, removeToTime);
|
|
|
56193 |
}
|
|
|
56194 |
}
|
|
|
56195 |
/**
|
|
|
56196 |
* created a simplified copy of the segment object with just the
|
|
|
56197 |
* information necessary to perform the XHR and decryption
|
|
|
56198 |
*
|
|
|
56199 |
* @private
|
|
|
56200 |
*
|
|
|
56201 |
* @param {Object} segmentInfo - the current segment
|
|
|
56202 |
* @return {Object} a simplified segment object copy
|
|
|
56203 |
*/
|
|
|
56204 |
|
|
|
56205 |
createSimplifiedSegmentObj_(segmentInfo) {
|
|
|
56206 |
const segment = segmentInfo.segment;
|
|
|
56207 |
const part = segmentInfo.part;
|
|
|
56208 |
const simpleSegment = {
|
|
|
56209 |
resolvedUri: part ? part.resolvedUri : segment.resolvedUri,
|
|
|
56210 |
byterange: part ? part.byterange : segment.byterange,
|
|
|
56211 |
requestId: segmentInfo.requestId,
|
|
|
56212 |
transmuxer: segmentInfo.transmuxer,
|
|
|
56213 |
audioAppendStart: segmentInfo.audioAppendStart,
|
|
|
56214 |
gopsToAlignWith: segmentInfo.gopsToAlignWith,
|
|
|
56215 |
part: segmentInfo.part
|
|
|
56216 |
};
|
|
|
56217 |
const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1];
|
|
|
56218 |
if (previousSegment && previousSegment.timeline === segment.timeline) {
|
|
|
56219 |
// The baseStartTime of a segment is used to handle rollover when probing the TS
|
|
|
56220 |
// segment to retrieve timing information. Since the probe only looks at the media's
|
|
|
56221 |
// times (e.g., PTS and DTS values of the segment), and doesn't consider the
|
|
|
56222 |
// player's time (e.g., player.currentTime()), baseStartTime should reflect the
|
|
|
56223 |
// media time as well. transmuxedDecodeEnd represents the end time of a segment, in
|
|
|
56224 |
// seconds of media time, so should be used here. The previous segment is used since
|
|
|
56225 |
// the end of the previous segment should represent the beginning of the current
|
|
|
56226 |
// segment, so long as they are on the same timeline.
|
|
|
56227 |
if (previousSegment.videoTimingInfo) {
|
|
|
56228 |
simpleSegment.baseStartTime = previousSegment.videoTimingInfo.transmuxedDecodeEnd;
|
|
|
56229 |
} else if (previousSegment.audioTimingInfo) {
|
|
|
56230 |
simpleSegment.baseStartTime = previousSegment.audioTimingInfo.transmuxedDecodeEnd;
|
|
|
56231 |
}
|
|
|
56232 |
}
|
|
|
56233 |
if (segment.key) {
|
|
|
56234 |
// if the media sequence is greater than 2^32, the IV will be incorrect
|
|
|
56235 |
// assuming 10s segments, that would be about 1300 years
|
|
|
56236 |
const iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]);
|
|
|
56237 |
simpleSegment.key = this.segmentKey(segment.key);
|
|
|
56238 |
simpleSegment.key.iv = iv;
|
|
|
56239 |
}
|
|
|
56240 |
if (segment.map) {
|
|
|
56241 |
simpleSegment.map = this.initSegmentForMap(segment.map);
|
|
|
56242 |
}
|
|
|
56243 |
return simpleSegment;
|
|
|
56244 |
}
|
|
|
56245 |
saveTransferStats_(stats) {
|
|
|
56246 |
// every request counts as a media request even if it has been aborted
|
|
|
56247 |
// or canceled due to a timeout
|
|
|
56248 |
this.mediaRequests += 1;
|
|
|
56249 |
if (stats) {
|
|
|
56250 |
this.mediaBytesTransferred += stats.bytesReceived;
|
|
|
56251 |
this.mediaTransferDuration += stats.roundTripTime;
|
|
|
56252 |
}
|
|
|
56253 |
}
|
|
|
56254 |
saveBandwidthRelatedStats_(duration, stats) {
|
|
|
56255 |
// byteLength will be used for throughput, and should be based on bytes receieved,
|
|
|
56256 |
// which we only know at the end of the request and should reflect total bytes
|
|
|
56257 |
// downloaded rather than just bytes processed from components of the segment
|
|
|
56258 |
this.pendingSegment_.byteLength = stats.bytesReceived;
|
|
|
56259 |
if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) {
|
|
|
56260 |
this.logger_(`Ignoring segment's bandwidth because its duration of ${duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`);
|
|
|
56261 |
return;
|
|
|
56262 |
}
|
|
|
56263 |
this.bandwidth = stats.bandwidth;
|
|
|
56264 |
this.roundTrip = stats.roundTripTime;
|
|
|
56265 |
}
|
|
|
56266 |
handleTimeout_() {
|
|
|
56267 |
// although the VTT segment loader bandwidth isn't really used, it's good to
|
|
|
56268 |
// maintain functinality between segment loaders
|
|
|
56269 |
this.mediaRequestsTimedout += 1;
|
|
|
56270 |
this.bandwidth = 1;
|
|
|
56271 |
this.roundTrip = NaN;
|
|
|
56272 |
this.trigger('bandwidthupdate');
|
|
|
56273 |
this.trigger('timeout');
|
|
|
56274 |
}
|
|
|
56275 |
/**
|
|
|
56276 |
* Handle the callback from the segmentRequest function and set the
|
|
|
56277 |
* associated SegmentLoader state and errors if necessary
|
|
|
56278 |
*
|
|
|
56279 |
* @private
|
|
|
56280 |
*/
|
|
|
56281 |
|
|
|
56282 |
segmentRequestFinished_(error, simpleSegment, result) {
|
|
|
56283 |
// TODO handle special cases, e.g., muxed audio/video but only audio in the segment
|
|
|
56284 |
// check the call queue directly since this function doesn't need to deal with any
|
|
|
56285 |
// data, and can continue even if the source buffers are not set up and we didn't get
|
|
|
56286 |
// any data from the segment
|
|
|
56287 |
if (this.callQueue_.length) {
|
|
|
56288 |
this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result));
|
|
|
56289 |
return;
|
|
|
56290 |
}
|
|
|
56291 |
this.saveTransferStats_(simpleSegment.stats); // The request was aborted and the SegmentLoader has already been reset
|
|
|
56292 |
|
|
|
56293 |
if (!this.pendingSegment_) {
|
|
|
56294 |
return;
|
|
|
56295 |
} // the request was aborted and the SegmentLoader has already started
|
|
|
56296 |
// another request. this can happen when the timeout for an aborted
|
|
|
56297 |
// request triggers due to a limitation in the XHR library
|
|
|
56298 |
// do not count this as any sort of request or we risk double-counting
|
|
|
56299 |
|
|
|
56300 |
if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
|
|
|
56301 |
return;
|
|
|
56302 |
} // an error occurred from the active pendingSegment_ so reset everything
|
|
|
56303 |
|
|
|
56304 |
if (error) {
|
|
|
56305 |
this.pendingSegment_ = null;
|
|
|
56306 |
this.state = 'READY'; // aborts are not a true error condition and nothing corrective needs to be done
|
|
|
56307 |
|
|
|
56308 |
if (error.code === REQUEST_ERRORS.ABORTED) {
|
|
|
56309 |
return;
|
|
|
56310 |
}
|
|
|
56311 |
this.pause(); // the error is really just that at least one of the requests timed-out
|
|
|
56312 |
// set the bandwidth to a very low value and trigger an ABR switch to
|
|
|
56313 |
// take emergency action
|
|
|
56314 |
|
|
|
56315 |
if (error.code === REQUEST_ERRORS.TIMEOUT) {
|
|
|
56316 |
this.handleTimeout_();
|
|
|
56317 |
return;
|
|
|
56318 |
} // if control-flow has arrived here, then the error is real
|
|
|
56319 |
// emit an error event to exclude the current playlist
|
|
|
56320 |
|
|
|
56321 |
this.mediaRequestsErrored += 1;
|
|
|
56322 |
this.error(error);
|
|
|
56323 |
this.trigger('error');
|
|
|
56324 |
return;
|
|
|
56325 |
}
|
|
|
56326 |
const segmentInfo = this.pendingSegment_; // the response was a success so set any bandwidth stats the request
|
|
|
56327 |
// generated for ABR purposes
|
|
|
56328 |
|
|
|
56329 |
this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats);
|
|
|
56330 |
segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
|
|
|
56331 |
if (result.gopInfo) {
|
|
|
56332 |
this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_);
|
|
|
56333 |
} // Although we may have already started appending on progress, we shouldn't switch the
|
|
|
56334 |
// state away from loading until we are officially done loading the segment data.
|
|
|
56335 |
|
|
|
56336 |
this.state = 'APPENDING'; // used for testing
|
|
|
56337 |
|
|
|
56338 |
this.trigger('appending');
|
|
|
56339 |
this.waitForAppendsToComplete_(segmentInfo);
|
|
|
56340 |
}
|
|
|
56341 |
setTimeMapping_(timeline) {
|
|
|
56342 |
const timelineMapping = this.syncController_.mappingForTimeline(timeline);
|
|
|
56343 |
if (timelineMapping !== null) {
|
|
|
56344 |
this.timeMapping_ = timelineMapping;
|
|
|
56345 |
}
|
|
|
56346 |
}
|
|
|
56347 |
updateMediaSecondsLoaded_(segment) {
|
|
|
56348 |
if (typeof segment.start === 'number' && typeof segment.end === 'number') {
|
|
|
56349 |
this.mediaSecondsLoaded += segment.end - segment.start;
|
|
|
56350 |
} else {
|
|
|
56351 |
this.mediaSecondsLoaded += segment.duration;
|
|
|
56352 |
}
|
|
|
56353 |
}
|
|
|
56354 |
shouldUpdateTransmuxerTimestampOffset_(timestampOffset) {
|
|
|
56355 |
if (timestampOffset === null) {
|
|
|
56356 |
return false;
|
|
|
56357 |
} // note that we're potentially using the same timestamp offset for both video and
|
|
|
56358 |
// audio
|
|
|
56359 |
|
|
|
56360 |
if (this.loaderType_ === 'main' && timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) {
|
|
|
56361 |
return true;
|
|
|
56362 |
}
|
|
|
56363 |
if (!this.audioDisabled_ && timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) {
|
|
|
56364 |
return true;
|
|
|
56365 |
}
|
|
|
56366 |
return false;
|
|
|
56367 |
}
|
|
|
56368 |
trueSegmentStart_({
|
|
|
56369 |
currentStart,
|
|
|
56370 |
playlist,
|
|
|
56371 |
mediaIndex,
|
|
|
56372 |
firstVideoFrameTimeForData,
|
|
|
56373 |
currentVideoTimestampOffset,
|
|
|
56374 |
useVideoTimingInfo,
|
|
|
56375 |
videoTimingInfo,
|
|
|
56376 |
audioTimingInfo
|
|
|
56377 |
}) {
|
|
|
56378 |
if (typeof currentStart !== 'undefined') {
|
|
|
56379 |
// if start was set once, keep using it
|
|
|
56380 |
return currentStart;
|
|
|
56381 |
}
|
|
|
56382 |
if (!useVideoTimingInfo) {
|
|
|
56383 |
return audioTimingInfo.start;
|
|
|
56384 |
}
|
|
|
56385 |
const previousSegment = playlist.segments[mediaIndex - 1]; // The start of a segment should be the start of the first full frame contained
|
|
|
56386 |
// within that segment. Since the transmuxer maintains a cache of incomplete data
|
|
|
56387 |
// from and/or the last frame seen, the start time may reflect a frame that starts
|
|
|
56388 |
// in the previous segment. Check for that case and ensure the start time is
|
|
|
56389 |
// accurate for the segment.
|
|
|
56390 |
|
|
|
56391 |
if (mediaIndex === 0 || !previousSegment || typeof previousSegment.start === 'undefined' || previousSegment.end !== firstVideoFrameTimeForData + currentVideoTimestampOffset) {
|
|
|
56392 |
return firstVideoFrameTimeForData;
|
|
|
56393 |
}
|
|
|
56394 |
return videoTimingInfo.start;
|
|
|
56395 |
}
|
|
|
56396 |
waitForAppendsToComplete_(segmentInfo) {
|
|
|
56397 |
const trackInfo = this.getCurrentMediaInfo_(segmentInfo);
|
|
|
56398 |
if (!trackInfo) {
|
|
|
56399 |
this.error({
|
|
|
56400 |
message: 'No starting media returned, likely due to an unsupported media format.',
|
|
|
56401 |
playlistExclusionDuration: Infinity
|
|
|
56402 |
});
|
|
|
56403 |
this.trigger('error');
|
|
|
56404 |
return;
|
|
|
56405 |
} // Although transmuxing is done, appends may not yet be finished. Throw a marker
|
|
|
56406 |
// on each queue this loader is responsible for to ensure that the appends are
|
|
|
56407 |
// complete.
|
|
|
56408 |
|
|
|
56409 |
const {
|
|
|
56410 |
hasAudio,
|
|
|
56411 |
hasVideo,
|
|
|
56412 |
isMuxed
|
|
|
56413 |
} = trackInfo;
|
|
|
56414 |
const waitForVideo = this.loaderType_ === 'main' && hasVideo;
|
|
|
56415 |
const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed;
|
|
|
56416 |
segmentInfo.waitingOnAppends = 0; // segments with no data
|
|
|
56417 |
|
|
|
56418 |
if (!segmentInfo.hasAppendedData_) {
|
|
|
56419 |
if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') {
|
|
|
56420 |
// When there's no audio or video data in the segment, there's no audio or video
|
|
|
56421 |
// timing information.
|
|
|
56422 |
//
|
|
|
56423 |
// If there's no audio or video timing information, then the timestamp offset
|
|
|
56424 |
// can't be adjusted to the appropriate value for the transmuxer and source
|
|
|
56425 |
// buffers.
|
|
|
56426 |
//
|
|
|
56427 |
// Therefore, the next segment should be used to set the timestamp offset.
|
|
|
56428 |
this.isPendingTimestampOffset_ = true;
|
|
|
56429 |
} // override settings for metadata only segments
|
|
|
56430 |
|
|
|
56431 |
segmentInfo.timingInfo = {
|
|
|
56432 |
start: 0
|
|
|
56433 |
};
|
|
|
56434 |
segmentInfo.waitingOnAppends++;
|
|
|
56435 |
if (!this.isPendingTimestampOffset_) {
|
|
|
56436 |
// update the timestampoffset
|
|
|
56437 |
this.updateSourceBufferTimestampOffset_(segmentInfo); // make sure the metadata queue is processed even though we have
|
|
|
56438 |
// no video/audio data.
|
|
|
56439 |
|
|
|
56440 |
this.processMetadataQueue_();
|
|
|
56441 |
} // append is "done" instantly with no data.
|
|
|
56442 |
|
|
|
56443 |
this.checkAppendsDone_(segmentInfo);
|
|
|
56444 |
return;
|
|
|
56445 |
} // Since source updater could call back synchronously, do the increments first.
|
|
|
56446 |
|
|
|
56447 |
if (waitForVideo) {
|
|
|
56448 |
segmentInfo.waitingOnAppends++;
|
|
|
56449 |
}
|
|
|
56450 |
if (waitForAudio) {
|
|
|
56451 |
segmentInfo.waitingOnAppends++;
|
|
|
56452 |
}
|
|
|
56453 |
if (waitForVideo) {
|
|
|
56454 |
this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo));
|
|
|
56455 |
}
|
|
|
56456 |
if (waitForAudio) {
|
|
|
56457 |
this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo));
|
|
|
56458 |
}
|
|
|
56459 |
}
|
|
|
56460 |
checkAppendsDone_(segmentInfo) {
|
|
|
56461 |
if (this.checkForAbort_(segmentInfo.requestId)) {
|
|
|
56462 |
return;
|
|
|
56463 |
}
|
|
|
56464 |
segmentInfo.waitingOnAppends--;
|
|
|
56465 |
if (segmentInfo.waitingOnAppends === 0) {
|
|
|
56466 |
this.handleAppendsDone_();
|
|
|
56467 |
}
|
|
|
56468 |
}
|
|
|
56469 |
checkForIllegalMediaSwitch(trackInfo) {
|
|
|
56470 |
const illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.getCurrentMediaInfo_(), trackInfo);
|
|
|
56471 |
if (illegalMediaSwitchError) {
|
|
|
56472 |
this.error({
|
|
|
56473 |
message: illegalMediaSwitchError,
|
|
|
56474 |
playlistExclusionDuration: Infinity
|
|
|
56475 |
});
|
|
|
56476 |
this.trigger('error');
|
|
|
56477 |
return true;
|
|
|
56478 |
}
|
|
|
56479 |
return false;
|
|
|
56480 |
}
|
|
|
56481 |
updateSourceBufferTimestampOffset_(segmentInfo) {
|
|
|
56482 |
if (segmentInfo.timestampOffset === null ||
|
|
|
56483 |
// we don't yet have the start for whatever media type (video or audio) has
|
|
|
56484 |
// priority, timing-wise, so we must wait
|
|
|
56485 |
typeof segmentInfo.timingInfo.start !== 'number' ||
|
|
|
56486 |
// already updated the timestamp offset for this segment
|
|
|
56487 |
segmentInfo.changedTimestampOffset ||
|
|
|
56488 |
// the alt audio loader should not be responsible for setting the timestamp offset
|
|
|
56489 |
this.loaderType_ !== 'main') {
|
|
|
56490 |
return;
|
|
|
56491 |
}
|
|
|
56492 |
let didChange = false; // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that
|
|
|
56493 |
// the timing info here comes from video. In the event that the audio is longer than
|
|
|
56494 |
// the video, this will trim the start of the audio.
|
|
|
56495 |
// This also trims any offset from 0 at the beginning of the media
|
|
|
56496 |
|
|
|
56497 |
segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({
|
|
|
56498 |
videoTimingInfo: segmentInfo.segment.videoTimingInfo,
|
|
|
56499 |
audioTimingInfo: segmentInfo.segment.audioTimingInfo,
|
|
|
56500 |
timingInfo: segmentInfo.timingInfo
|
|
|
56501 |
}); // In the event that there are part segment downloads, each will try to update the
|
|
|
56502 |
// timestamp offset. Retaining this bit of state prevents us from updating in the
|
|
|
56503 |
// future (within the same segment), however, there may be a better way to handle it.
|
|
|
56504 |
|
|
|
56505 |
segmentInfo.changedTimestampOffset = true;
|
|
|
56506 |
if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) {
|
|
|
56507 |
this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset);
|
|
|
56508 |
didChange = true;
|
|
|
56509 |
}
|
|
|
56510 |
if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) {
|
|
|
56511 |
this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset);
|
|
|
56512 |
didChange = true;
|
|
|
56513 |
}
|
|
|
56514 |
if (didChange) {
|
|
|
56515 |
this.trigger('timestampoffset');
|
|
|
56516 |
}
|
|
|
56517 |
}
|
|
|
56518 |
getSegmentStartTimeForTimestampOffsetCalculation_({
|
|
|
56519 |
videoTimingInfo,
|
|
|
56520 |
audioTimingInfo,
|
|
|
56521 |
timingInfo
|
|
|
56522 |
}) {
|
|
|
56523 |
if (!this.useDtsForTimestampOffset_) {
|
|
|
56524 |
return timingInfo.start;
|
|
|
56525 |
}
|
|
|
56526 |
if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') {
|
|
|
56527 |
return videoTimingInfo.transmuxedDecodeStart;
|
|
|
56528 |
} // handle audio only
|
|
|
56529 |
|
|
|
56530 |
if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') {
|
|
|
56531 |
return audioTimingInfo.transmuxedDecodeStart;
|
|
|
56532 |
} // handle content not transmuxed (e.g., MP4)
|
|
|
56533 |
|
|
|
56534 |
return timingInfo.start;
|
|
|
56535 |
}
|
|
|
56536 |
updateTimingInfoEnd_(segmentInfo) {
|
|
|
56537 |
segmentInfo.timingInfo = segmentInfo.timingInfo || {};
|
|
|
56538 |
const trackInfo = this.getMediaInfo_();
|
|
|
56539 |
const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo;
|
|
|
56540 |
const prioritizedTimingInfo = useVideoTimingInfo && segmentInfo.videoTimingInfo ? segmentInfo.videoTimingInfo : segmentInfo.audioTimingInfo;
|
|
|
56541 |
if (!prioritizedTimingInfo) {
|
|
|
56542 |
return;
|
|
|
56543 |
}
|
|
|
56544 |
segmentInfo.timingInfo.end = typeof prioritizedTimingInfo.end === 'number' ?
|
|
|
56545 |
// End time may not exist in a case where we aren't parsing the full segment (one
|
|
|
56546 |
// current example is the case of fmp4), so use the rough duration to calculate an
|
|
|
56547 |
// end time.
|
|
|
56548 |
prioritizedTimingInfo.end : prioritizedTimingInfo.start + segmentInfo.duration;
|
|
|
56549 |
}
|
|
|
56550 |
/**
|
|
|
56551 |
* callback to run when appendBuffer is finished. detects if we are
|
|
|
56552 |
* in a good state to do things with the data we got, or if we need
|
|
|
56553 |
* to wait for more
|
|
|
56554 |
*
|
|
|
56555 |
* @private
|
|
|
56556 |
*/
|
|
|
56557 |
|
|
|
56558 |
handleAppendsDone_() {
|
|
|
56559 |
// appendsdone can cause an abort
|
|
|
56560 |
if (this.pendingSegment_) {
|
|
|
56561 |
this.trigger('appendsdone');
|
|
|
56562 |
}
|
|
|
56563 |
if (!this.pendingSegment_) {
|
|
|
56564 |
this.state = 'READY'; // TODO should this move into this.checkForAbort to speed up requests post abort in
|
|
|
56565 |
// all appending cases?
|
|
|
56566 |
|
|
|
56567 |
if (!this.paused()) {
|
|
|
56568 |
this.monitorBuffer_();
|
|
|
56569 |
}
|
|
|
56570 |
return;
|
|
|
56571 |
}
|
|
|
56572 |
const segmentInfo = this.pendingSegment_; // Now that the end of the segment has been reached, we can set the end time. It's
|
|
|
56573 |
// best to wait until all appends are done so we're sure that the primary media is
|
|
|
56574 |
// finished (and we have its end time).
|
|
|
56575 |
|
|
|
56576 |
this.updateTimingInfoEnd_(segmentInfo);
|
|
|
56577 |
if (this.shouldSaveSegmentTimingInfo_) {
|
|
|
56578 |
// Timeline mappings should only be saved for the main loader. This is for multiple
|
|
|
56579 |
// reasons:
|
|
|
56580 |
//
|
|
|
56581 |
// 1) Only one mapping is saved per timeline, meaning that if both the audio loader
|
|
|
56582 |
// and the main loader try to save the timeline mapping, whichever comes later
|
|
|
56583 |
// will overwrite the first. In theory this is OK, as the mappings should be the
|
|
|
56584 |
// same, however, it breaks for (2)
|
|
|
56585 |
// 2) In the event of a live stream, the initial live point will make for a somewhat
|
|
|
56586 |
// arbitrary mapping. If audio and video streams are not perfectly in-sync, then
|
|
|
56587 |
// the mapping will be off for one of the streams, dependent on which one was
|
|
|
56588 |
// first saved (see (1)).
|
|
|
56589 |
// 3) Primary timing goes by video in VHS, so the mapping should be video.
|
|
|
56590 |
//
|
|
|
56591 |
// Since the audio loader will wait for the main loader to load the first segment,
|
|
|
56592 |
// the main loader will save the first timeline mapping, and ensure that there won't
|
|
|
56593 |
// be a case where audio loads two segments without saving a mapping (thus leading
|
|
|
56594 |
// to missing segment timing info).
|
|
|
56595 |
this.syncController_.saveSegmentTimingInfo({
|
|
|
56596 |
segmentInfo,
|
|
|
56597 |
shouldSaveTimelineMapping: this.loaderType_ === 'main'
|
|
|
56598 |
});
|
|
|
56599 |
}
|
|
|
56600 |
const segmentDurationMessage = getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_);
|
|
|
56601 |
if (segmentDurationMessage) {
|
|
|
56602 |
if (segmentDurationMessage.severity === 'warn') {
|
|
|
56603 |
videojs.log.warn(segmentDurationMessage.message);
|
|
|
56604 |
} else {
|
|
|
56605 |
this.logger_(segmentDurationMessage.message);
|
|
|
56606 |
}
|
|
|
56607 |
}
|
|
|
56608 |
this.recordThroughput_(segmentInfo);
|
|
|
56609 |
this.pendingSegment_ = null;
|
|
|
56610 |
this.state = 'READY';
|
|
|
56611 |
if (segmentInfo.isSyncRequest) {
|
|
|
56612 |
this.trigger('syncinfoupdate'); // if the sync request was not appended
|
|
|
56613 |
// then it was not the correct segment.
|
|
|
56614 |
// throw it away and use the data it gave us
|
|
|
56615 |
// to get the correct one.
|
|
|
56616 |
|
|
|
56617 |
if (!segmentInfo.hasAppendedData_) {
|
|
|
56618 |
this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`);
|
|
|
56619 |
return;
|
|
|
56620 |
}
|
|
|
56621 |
}
|
|
|
56622 |
this.logger_(`Appended ${segmentInfoString(segmentInfo)}`);
|
|
|
56623 |
this.addSegmentMetadataCue_(segmentInfo);
|
|
|
56624 |
this.fetchAtBuffer_ = true;
|
|
|
56625 |
if (this.currentTimeline_ !== segmentInfo.timeline) {
|
|
|
56626 |
this.timelineChangeController_.lastTimelineChange({
|
|
|
56627 |
type: this.loaderType_,
|
|
|
56628 |
from: this.currentTimeline_,
|
|
|
56629 |
to: segmentInfo.timeline
|
|
|
56630 |
}); // If audio is not disabled, the main segment loader is responsible for updating
|
|
|
56631 |
// the audio timeline as well. If the content is video only, this won't have any
|
|
|
56632 |
// impact.
|
|
|
56633 |
|
|
|
56634 |
if (this.loaderType_ === 'main' && !this.audioDisabled_) {
|
|
|
56635 |
this.timelineChangeController_.lastTimelineChange({
|
|
|
56636 |
type: 'audio',
|
|
|
56637 |
from: this.currentTimeline_,
|
|
|
56638 |
to: segmentInfo.timeline
|
|
|
56639 |
});
|
|
|
56640 |
}
|
|
|
56641 |
}
|
|
|
56642 |
this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before
|
|
|
56643 |
// the following conditional otherwise it may consider this a bad "guess"
|
|
|
56644 |
// and attempt to resync when the post-update seekable window and live
|
|
|
56645 |
// point would mean that this was the perfect segment to fetch
|
|
|
56646 |
|
|
|
56647 |
this.trigger('syncinfoupdate');
|
|
|
56648 |
const segment = segmentInfo.segment;
|
|
|
56649 |
const part = segmentInfo.part;
|
|
|
56650 |
const badSegmentGuess = segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3;
|
|
|
56651 |
const badPartGuess = part && part.end && this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3; // If we previously appended a segment/part that ends more than 3 part/targetDurations before
|
|
|
56652 |
// the currentTime_ that means that our conservative guess was too conservative.
|
|
|
56653 |
// In that case, reset the loader state so that we try to use any information gained
|
|
|
56654 |
// from the previous request to create a new, more accurate, sync-point.
|
|
|
56655 |
|
|
|
56656 |
if (badSegmentGuess || badPartGuess) {
|
|
|
56657 |
this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`);
|
|
|
56658 |
this.resetEverything();
|
|
|
56659 |
return;
|
|
|
56660 |
}
|
|
|
56661 |
const isWalkingForward = this.mediaIndex !== null; // Don't do a rendition switch unless we have enough time to get a sync segment
|
|
|
56662 |
// and conservatively guess
|
|
|
56663 |
|
|
|
56664 |
if (isWalkingForward) {
|
|
|
56665 |
this.trigger('bandwidthupdate');
|
|
|
56666 |
}
|
|
|
56667 |
this.trigger('progress');
|
|
|
56668 |
this.mediaIndex = segmentInfo.mediaIndex;
|
|
|
56669 |
this.partIndex = segmentInfo.partIndex; // any time an update finishes and the last segment is in the
|
|
|
56670 |
// buffer, end the stream. this ensures the "ended" event will
|
|
|
56671 |
// fire if playback reaches that point.
|
|
|
56672 |
|
|
|
56673 |
if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) {
|
|
|
56674 |
this.endOfStream();
|
|
|
56675 |
} // used for testing
|
|
|
56676 |
|
|
|
56677 |
this.trigger('appended');
|
|
|
56678 |
if (segmentInfo.hasAppendedData_) {
|
|
|
56679 |
this.mediaAppends++;
|
|
|
56680 |
}
|
|
|
56681 |
if (!this.paused()) {
|
|
|
56682 |
this.monitorBuffer_();
|
|
|
56683 |
}
|
|
|
56684 |
}
|
|
|
56685 |
/**
|
|
|
56686 |
* Records the current throughput of the decrypt, transmux, and append
|
|
|
56687 |
* portion of the semgment pipeline. `throughput.rate` is a the cumulative
|
|
|
56688 |
* moving average of the throughput. `throughput.count` is the number of
|
|
|
56689 |
* data points in the average.
|
|
|
56690 |
*
|
|
|
56691 |
* @private
|
|
|
56692 |
* @param {Object} segmentInfo the object returned by loadSegment
|
|
|
56693 |
*/
|
|
|
56694 |
|
|
|
56695 |
recordThroughput_(segmentInfo) {
|
|
|
56696 |
if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) {
|
|
|
56697 |
this.logger_(`Ignoring segment's throughput because its duration of ${segmentInfo.duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`);
|
|
|
56698 |
return;
|
|
|
56699 |
}
|
|
|
56700 |
const rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide
|
|
|
56701 |
// by zero in the case where the throughput is ridiculously high
|
|
|
56702 |
|
|
|
56703 |
const segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second
|
|
|
56704 |
|
|
|
56705 |
const segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); // This is just a cumulative moving average calculation:
|
|
|
56706 |
// newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
|
|
|
56707 |
|
|
|
56708 |
this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count;
|
|
|
56709 |
}
|
|
|
56710 |
/**
|
|
|
56711 |
* Adds a cue to the segment-metadata track with some metadata information about the
|
|
|
56712 |
* segment
|
|
|
56713 |
*
|
|
|
56714 |
* @private
|
|
|
56715 |
* @param {Object} segmentInfo
|
|
|
56716 |
* the object returned by loadSegment
|
|
|
56717 |
* @method addSegmentMetadataCue_
|
|
|
56718 |
*/
|
|
|
56719 |
|
|
|
56720 |
addSegmentMetadataCue_(segmentInfo) {
|
|
|
56721 |
if (!this.segmentMetadataTrack_) {
|
|
|
56722 |
return;
|
|
|
56723 |
}
|
|
|
56724 |
const segment = segmentInfo.segment;
|
|
|
56725 |
const start = segment.start;
|
|
|
56726 |
const end = segment.end; // Do not try adding the cue if the start and end times are invalid.
|
|
|
56727 |
|
|
|
56728 |
if (!finite(start) || !finite(end)) {
|
|
|
56729 |
return;
|
|
|
56730 |
}
|
|
|
56731 |
removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
|
|
|
56732 |
const Cue = window.WebKitDataCue || window.VTTCue;
|
|
|
56733 |
const value = {
|
|
|
56734 |
custom: segment.custom,
|
|
|
56735 |
dateTimeObject: segment.dateTimeObject,
|
|
|
56736 |
dateTimeString: segment.dateTimeString,
|
|
|
56737 |
programDateTime: segment.programDateTime,
|
|
|
56738 |
bandwidth: segmentInfo.playlist.attributes.BANDWIDTH,
|
|
|
56739 |
resolution: segmentInfo.playlist.attributes.RESOLUTION,
|
|
|
56740 |
codecs: segmentInfo.playlist.attributes.CODECS,
|
|
|
56741 |
byteLength: segmentInfo.byteLength,
|
|
|
56742 |
uri: segmentInfo.uri,
|
|
|
56743 |
timeline: segmentInfo.timeline,
|
|
|
56744 |
playlist: segmentInfo.playlist.id,
|
|
|
56745 |
start,
|
|
|
56746 |
end
|
|
|
56747 |
};
|
|
|
56748 |
const data = JSON.stringify(value);
|
|
|
56749 |
const cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between
|
|
|
56750 |
// the differences of WebKitDataCue in safari and VTTCue in other browsers
|
|
|
56751 |
|
|
|
56752 |
cue.value = value;
|
|
|
56753 |
this.segmentMetadataTrack_.addCue(cue);
|
|
|
56754 |
}
|
|
|
56755 |
}
|
|
|
56756 |
function noop() {}
|
|
|
56757 |
const toTitleCase = function (string) {
|
|
|
56758 |
if (typeof string !== 'string') {
|
|
|
56759 |
return string;
|
|
|
56760 |
}
|
|
|
56761 |
return string.replace(/./, w => w.toUpperCase());
|
|
|
56762 |
};
|
|
|
56763 |
|
|
|
56764 |
/**
|
|
|
56765 |
* @file source-updater.js
|
|
|
56766 |
*/
|
|
|
56767 |
const bufferTypes = ['video', 'audio'];
|
|
|
56768 |
const updating = (type, sourceUpdater) => {
|
|
|
56769 |
const sourceBuffer = sourceUpdater[`${type}Buffer`];
|
|
|
56770 |
return sourceBuffer && sourceBuffer.updating || sourceUpdater.queuePending[type];
|
|
|
56771 |
};
|
|
|
56772 |
const nextQueueIndexOfType = (type, queue) => {
|
|
|
56773 |
for (let i = 0; i < queue.length; i++) {
|
|
|
56774 |
const queueEntry = queue[i];
|
|
|
56775 |
if (queueEntry.type === 'mediaSource') {
|
|
|
56776 |
// If the next entry is a media source entry (uses multiple source buffers), block
|
|
|
56777 |
// processing to allow it to go through first.
|
|
|
56778 |
return null;
|
|
|
56779 |
}
|
|
|
56780 |
if (queueEntry.type === type) {
|
|
|
56781 |
return i;
|
|
|
56782 |
}
|
|
|
56783 |
}
|
|
|
56784 |
return null;
|
|
|
56785 |
};
|
|
|
56786 |
const shiftQueue = (type, sourceUpdater) => {
|
|
|
56787 |
if (sourceUpdater.queue.length === 0) {
|
|
|
56788 |
return;
|
|
|
56789 |
}
|
|
|
56790 |
let queueIndex = 0;
|
|
|
56791 |
let queueEntry = sourceUpdater.queue[queueIndex];
|
|
|
56792 |
if (queueEntry.type === 'mediaSource') {
|
|
|
56793 |
if (!sourceUpdater.updating() && sourceUpdater.mediaSource.readyState !== 'closed') {
|
|
|
56794 |
sourceUpdater.queue.shift();
|
|
|
56795 |
queueEntry.action(sourceUpdater);
|
|
|
56796 |
if (queueEntry.doneFn) {
|
|
|
56797 |
queueEntry.doneFn();
|
|
|
56798 |
} // Only specific source buffer actions must wait for async updateend events. Media
|
|
|
56799 |
// Source actions process synchronously. Therefore, both audio and video source
|
|
|
56800 |
// buffers are now clear to process the next queue entries.
|
|
|
56801 |
|
|
|
56802 |
shiftQueue('audio', sourceUpdater);
|
|
|
56803 |
shiftQueue('video', sourceUpdater);
|
|
|
56804 |
} // Media Source actions require both source buffers, so if the media source action
|
|
|
56805 |
// couldn't process yet (because one or both source buffers are busy), block other
|
|
|
56806 |
// queue actions until both are available and the media source action can process.
|
|
|
56807 |
|
|
|
56808 |
return;
|
|
|
56809 |
}
|
|
|
56810 |
if (type === 'mediaSource') {
|
|
|
56811 |
// If the queue was shifted by a media source action (this happens when pushing a
|
|
|
56812 |
// media source action onto the queue), then it wasn't from an updateend event from an
|
|
|
56813 |
// audio or video source buffer, so there's no change from previous state, and no
|
|
|
56814 |
// processing should be done.
|
|
|
56815 |
return;
|
|
|
56816 |
} // Media source queue entries don't need to consider whether the source updater is
|
|
|
56817 |
// started (i.e., source buffers are created) as they don't need the source buffers, but
|
|
|
56818 |
// source buffer queue entries do.
|
|
|
56819 |
|
|
|
56820 |
if (!sourceUpdater.ready() || sourceUpdater.mediaSource.readyState === 'closed' || updating(type, sourceUpdater)) {
|
|
|
56821 |
return;
|
|
|
56822 |
}
|
|
|
56823 |
if (queueEntry.type !== type) {
|
|
|
56824 |
queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue);
|
|
|
56825 |
if (queueIndex === null) {
|
|
|
56826 |
// Either there's no queue entry that uses this source buffer type in the queue, or
|
|
|
56827 |
// there's a media source queue entry before the next entry of this type, in which
|
|
|
56828 |
// case wait for that action to process first.
|
|
|
56829 |
return;
|
|
|
56830 |
}
|
|
|
56831 |
queueEntry = sourceUpdater.queue[queueIndex];
|
|
|
56832 |
}
|
|
|
56833 |
sourceUpdater.queue.splice(queueIndex, 1); // Keep a record that this source buffer type is in use.
|
|
|
56834 |
//
|
|
|
56835 |
// The queue pending operation must be set before the action is performed in the event
|
|
|
56836 |
// that the action results in a synchronous event that is acted upon. For instance, if
|
|
|
56837 |
// an exception is thrown that can be handled, it's possible that new actions will be
|
|
|
56838 |
// appended to an empty queue and immediately executed, but would not have the correct
|
|
|
56839 |
// pending information if this property was set after the action was performed.
|
|
|
56840 |
|
|
|
56841 |
sourceUpdater.queuePending[type] = queueEntry;
|
|
|
56842 |
queueEntry.action(type, sourceUpdater);
|
|
|
56843 |
if (!queueEntry.doneFn) {
|
|
|
56844 |
// synchronous operation, process next entry
|
|
|
56845 |
sourceUpdater.queuePending[type] = null;
|
|
|
56846 |
shiftQueue(type, sourceUpdater);
|
|
|
56847 |
return;
|
|
|
56848 |
}
|
|
|
56849 |
};
|
|
|
56850 |
const cleanupBuffer = (type, sourceUpdater) => {
|
|
|
56851 |
const buffer = sourceUpdater[`${type}Buffer`];
|
|
|
56852 |
const titleType = toTitleCase(type);
|
|
|
56853 |
if (!buffer) {
|
|
|
56854 |
return;
|
|
|
56855 |
}
|
|
|
56856 |
buffer.removeEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]);
|
|
|
56857 |
buffer.removeEventListener('error', sourceUpdater[`on${titleType}Error_`]);
|
|
|
56858 |
sourceUpdater.codecs[type] = null;
|
|
|
56859 |
sourceUpdater[`${type}Buffer`] = null;
|
|
|
56860 |
};
|
|
|
56861 |
const inSourceBuffers = (mediaSource, sourceBuffer) => mediaSource && sourceBuffer && Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1;
|
|
|
56862 |
const actions = {
|
|
|
56863 |
appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => {
|
|
|
56864 |
const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
|
|
|
56865 |
// or the media source does not contain this source buffer.
|
|
|
56866 |
|
|
|
56867 |
if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
|
|
|
56868 |
return;
|
|
|
56869 |
}
|
|
|
56870 |
sourceUpdater.logger_(`Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer`);
|
|
|
56871 |
try {
|
|
|
56872 |
sourceBuffer.appendBuffer(bytes);
|
|
|
56873 |
} catch (e) {
|
|
|
56874 |
sourceUpdater.logger_(`Error with code ${e.code} ` + (e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') + `when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer`);
|
|
|
56875 |
sourceUpdater.queuePending[type] = null;
|
|
|
56876 |
onError(e);
|
|
|
56877 |
}
|
|
|
56878 |
},
|
|
|
56879 |
remove: (start, end) => (type, sourceUpdater) => {
|
|
|
56880 |
const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
|
|
|
56881 |
// or the media source does not contain this source buffer.
|
|
|
56882 |
|
|
|
56883 |
if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
|
|
|
56884 |
return;
|
|
|
56885 |
}
|
|
|
56886 |
sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`);
|
|
|
56887 |
try {
|
|
|
56888 |
sourceBuffer.remove(start, end);
|
|
|
56889 |
} catch (e) {
|
|
|
56890 |
sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`);
|
|
|
56891 |
}
|
|
|
56892 |
},
|
|
|
56893 |
timestampOffset: offset => (type, sourceUpdater) => {
|
|
|
56894 |
const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
|
|
|
56895 |
// or the media source does not contain this source buffer.
|
|
|
56896 |
|
|
|
56897 |
if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
|
|
|
56898 |
return;
|
|
|
56899 |
}
|
|
|
56900 |
sourceUpdater.logger_(`Setting ${type}timestampOffset to ${offset}`);
|
|
|
56901 |
sourceBuffer.timestampOffset = offset;
|
|
|
56902 |
},
|
|
|
56903 |
callback: callback => (type, sourceUpdater) => {
|
|
|
56904 |
callback();
|
|
|
56905 |
},
|
|
|
56906 |
endOfStream: error => sourceUpdater => {
|
|
|
56907 |
if (sourceUpdater.mediaSource.readyState !== 'open') {
|
|
|
56908 |
return;
|
|
|
56909 |
}
|
|
|
56910 |
sourceUpdater.logger_(`Calling mediaSource endOfStream(${error || ''})`);
|
|
|
56911 |
try {
|
|
|
56912 |
sourceUpdater.mediaSource.endOfStream(error);
|
|
|
56913 |
} catch (e) {
|
|
|
56914 |
videojs.log.warn('Failed to call media source endOfStream', e);
|
|
|
56915 |
}
|
|
|
56916 |
},
|
|
|
56917 |
duration: duration => sourceUpdater => {
|
|
|
56918 |
sourceUpdater.logger_(`Setting mediaSource duration to ${duration}`);
|
|
|
56919 |
try {
|
|
|
56920 |
sourceUpdater.mediaSource.duration = duration;
|
|
|
56921 |
} catch (e) {
|
|
|
56922 |
videojs.log.warn('Failed to set media source duration', e);
|
|
|
56923 |
}
|
|
|
56924 |
},
|
|
|
56925 |
abort: () => (type, sourceUpdater) => {
|
|
|
56926 |
if (sourceUpdater.mediaSource.readyState !== 'open') {
|
|
|
56927 |
return;
|
|
|
56928 |
}
|
|
|
56929 |
const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
|
|
|
56930 |
// or the media source does not contain this source buffer.
|
|
|
56931 |
|
|
|
56932 |
if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
|
|
|
56933 |
return;
|
|
|
56934 |
}
|
|
|
56935 |
sourceUpdater.logger_(`calling abort on ${type}Buffer`);
|
|
|
56936 |
try {
|
|
|
56937 |
sourceBuffer.abort();
|
|
|
56938 |
} catch (e) {
|
|
|
56939 |
videojs.log.warn(`Failed to abort on ${type}Buffer`, e);
|
|
|
56940 |
}
|
|
|
56941 |
},
|
|
|
56942 |
addSourceBuffer: (type, codec) => sourceUpdater => {
|
|
|
56943 |
const titleType = toTitleCase(type);
|
|
|
56944 |
const mime = getMimeForCodec(codec);
|
|
|
56945 |
sourceUpdater.logger_(`Adding ${type}Buffer with codec ${codec} to mediaSource`);
|
|
|
56946 |
const sourceBuffer = sourceUpdater.mediaSource.addSourceBuffer(mime);
|
|
|
56947 |
sourceBuffer.addEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]);
|
|
|
56948 |
sourceBuffer.addEventListener('error', sourceUpdater[`on${titleType}Error_`]);
|
|
|
56949 |
sourceUpdater.codecs[type] = codec;
|
|
|
56950 |
sourceUpdater[`${type}Buffer`] = sourceBuffer;
|
|
|
56951 |
},
|
|
|
56952 |
removeSourceBuffer: type => sourceUpdater => {
|
|
|
56953 |
const sourceBuffer = sourceUpdater[`${type}Buffer`];
|
|
|
56954 |
cleanupBuffer(type, sourceUpdater); // can't do anything if the media source / source buffer is null
|
|
|
56955 |
// or the media source does not contain this source buffer.
|
|
|
56956 |
|
|
|
56957 |
if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
|
|
|
56958 |
return;
|
|
|
56959 |
}
|
|
|
56960 |
sourceUpdater.logger_(`Removing ${type}Buffer with codec ${sourceUpdater.codecs[type]} from mediaSource`);
|
|
|
56961 |
try {
|
|
|
56962 |
sourceUpdater.mediaSource.removeSourceBuffer(sourceBuffer);
|
|
|
56963 |
} catch (e) {
|
|
|
56964 |
videojs.log.warn(`Failed to removeSourceBuffer ${type}Buffer`, e);
|
|
|
56965 |
}
|
|
|
56966 |
},
|
|
|
56967 |
changeType: codec => (type, sourceUpdater) => {
|
|
|
56968 |
const sourceBuffer = sourceUpdater[`${type}Buffer`];
|
|
|
56969 |
const mime = getMimeForCodec(codec); // can't do anything if the media source / source buffer is null
|
|
|
56970 |
// or the media source does not contain this source buffer.
|
|
|
56971 |
|
|
|
56972 |
if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
|
|
|
56973 |
return;
|
|
|
56974 |
} // do not update codec if we don't need to.
|
|
|
56975 |
|
|
|
56976 |
if (sourceUpdater.codecs[type] === codec) {
|
|
|
56977 |
return;
|
|
|
56978 |
}
|
|
|
56979 |
sourceUpdater.logger_(`changing ${type}Buffer codec from ${sourceUpdater.codecs[type]} to ${codec}`); // check if change to the provided type is supported
|
|
|
56980 |
|
|
|
56981 |
try {
|
|
|
56982 |
sourceBuffer.changeType(mime);
|
|
|
56983 |
sourceUpdater.codecs[type] = codec;
|
|
|
56984 |
} catch (e) {
|
|
|
56985 |
videojs.log.warn(`Failed to changeType on ${type}Buffer`, e);
|
|
|
56986 |
}
|
|
|
56987 |
}
|
|
|
56988 |
};
|
|
|
56989 |
const pushQueue = ({
|
|
|
56990 |
type,
|
|
|
56991 |
sourceUpdater,
|
|
|
56992 |
action,
|
|
|
56993 |
doneFn,
|
|
|
56994 |
name
|
|
|
56995 |
}) => {
|
|
|
56996 |
sourceUpdater.queue.push({
|
|
|
56997 |
type,
|
|
|
56998 |
action,
|
|
|
56999 |
doneFn,
|
|
|
57000 |
name
|
|
|
57001 |
});
|
|
|
57002 |
shiftQueue(type, sourceUpdater);
|
|
|
57003 |
};
|
|
|
57004 |
const onUpdateend = (type, sourceUpdater) => e => {
|
|
|
57005 |
// Although there should, in theory, be a pending action for any updateend receieved,
|
|
|
57006 |
// there are some actions that may trigger updateend events without set definitions in
|
|
|
57007 |
// the w3c spec. For instance, setting the duration on the media source may trigger
|
|
|
57008 |
// updateend events on source buffers. This does not appear to be in the spec. As such,
|
|
|
57009 |
// if we encounter an updateend without a corresponding pending action from our queue
|
|
|
57010 |
// for that source buffer type, process the next action.
|
|
|
57011 |
if (sourceUpdater.queuePending[type]) {
|
|
|
57012 |
const doneFn = sourceUpdater.queuePending[type].doneFn;
|
|
|
57013 |
sourceUpdater.queuePending[type] = null;
|
|
|
57014 |
if (doneFn) {
|
|
|
57015 |
// if there's an error, report it
|
|
|
57016 |
doneFn(sourceUpdater[`${type}Error_`]);
|
|
|
57017 |
}
|
|
|
57018 |
}
|
|
|
57019 |
shiftQueue(type, sourceUpdater);
|
|
|
57020 |
};
|
|
|
57021 |
/**
|
|
|
57022 |
* A queue of callbacks to be serialized and applied when a
|
|
|
57023 |
* MediaSource and its associated SourceBuffers are not in the
|
|
|
57024 |
* updating state. It is used by the segment loader to update the
|
|
|
57025 |
* underlying SourceBuffers when new data is loaded, for instance.
|
|
|
57026 |
*
|
|
|
57027 |
* @class SourceUpdater
|
|
|
57028 |
* @param {MediaSource} mediaSource the MediaSource to create the SourceBuffer from
|
|
|
57029 |
* @param {string} mimeType the desired MIME type of the underlying SourceBuffer
|
|
|
57030 |
*/
|
|
|
57031 |
|
|
|
57032 |
class SourceUpdater extends videojs.EventTarget {
|
|
|
57033 |
constructor(mediaSource) {
|
|
|
57034 |
super();
|
|
|
57035 |
this.mediaSource = mediaSource;
|
|
|
57036 |
this.sourceopenListener_ = () => shiftQueue('mediaSource', this);
|
|
|
57037 |
this.mediaSource.addEventListener('sourceopen', this.sourceopenListener_);
|
|
|
57038 |
this.logger_ = logger('SourceUpdater'); // initial timestamp offset is 0
|
|
|
57039 |
|
|
|
57040 |
this.audioTimestampOffset_ = 0;
|
|
|
57041 |
this.videoTimestampOffset_ = 0;
|
|
|
57042 |
this.queue = [];
|
|
|
57043 |
this.queuePending = {
|
|
|
57044 |
audio: null,
|
|
|
57045 |
video: null
|
|
|
57046 |
};
|
|
|
57047 |
this.delayedAudioAppendQueue_ = [];
|
|
|
57048 |
this.videoAppendQueued_ = false;
|
|
|
57049 |
this.codecs = {};
|
|
|
57050 |
this.onVideoUpdateEnd_ = onUpdateend('video', this);
|
|
|
57051 |
this.onAudioUpdateEnd_ = onUpdateend('audio', this);
|
|
|
57052 |
this.onVideoError_ = e => {
|
|
|
57053 |
// used for debugging
|
|
|
57054 |
this.videoError_ = e;
|
|
|
57055 |
};
|
|
|
57056 |
this.onAudioError_ = e => {
|
|
|
57057 |
// used for debugging
|
|
|
57058 |
this.audioError_ = e;
|
|
|
57059 |
};
|
|
|
57060 |
this.createdSourceBuffers_ = false;
|
|
|
57061 |
this.initializedEme_ = false;
|
|
|
57062 |
this.triggeredReady_ = false;
|
|
|
57063 |
}
|
|
|
57064 |
initializedEme() {
|
|
|
57065 |
this.initializedEme_ = true;
|
|
|
57066 |
this.triggerReady();
|
|
|
57067 |
}
|
|
|
57068 |
hasCreatedSourceBuffers() {
|
|
|
57069 |
// if false, likely waiting on one of the segment loaders to get enough data to create
|
|
|
57070 |
// source buffers
|
|
|
57071 |
return this.createdSourceBuffers_;
|
|
|
57072 |
}
|
|
|
57073 |
hasInitializedAnyEme() {
|
|
|
57074 |
return this.initializedEme_;
|
|
|
57075 |
}
|
|
|
57076 |
ready() {
|
|
|
57077 |
return this.hasCreatedSourceBuffers() && this.hasInitializedAnyEme();
|
|
|
57078 |
}
|
|
|
57079 |
createSourceBuffers(codecs) {
|
|
|
57080 |
if (this.hasCreatedSourceBuffers()) {
|
|
|
57081 |
// already created them before
|
|
|
57082 |
return;
|
|
|
57083 |
} // the intial addOrChangeSourceBuffers will always be
|
|
|
57084 |
// two add buffers.
|
|
|
57085 |
|
|
|
57086 |
this.addOrChangeSourceBuffers(codecs);
|
|
|
57087 |
this.createdSourceBuffers_ = true;
|
|
|
57088 |
this.trigger('createdsourcebuffers');
|
|
|
57089 |
this.triggerReady();
|
|
|
57090 |
}
|
|
|
57091 |
triggerReady() {
|
|
|
57092 |
// only allow ready to be triggered once, this prevents the case
|
|
|
57093 |
// where:
|
|
|
57094 |
// 1. we trigger createdsourcebuffers
|
|
|
57095 |
// 2. ie 11 synchronously initializates eme
|
|
|
57096 |
// 3. the synchronous initialization causes us to trigger ready
|
|
|
57097 |
// 4. We go back to the ready check in createSourceBuffers and ready is triggered again.
|
|
|
57098 |
if (this.ready() && !this.triggeredReady_) {
|
|
|
57099 |
this.triggeredReady_ = true;
|
|
|
57100 |
this.trigger('ready');
|
|
|
57101 |
}
|
|
|
57102 |
}
|
|
|
57103 |
/**
|
|
|
57104 |
* Add a type of source buffer to the media source.
|
|
|
57105 |
*
|
|
|
57106 |
* @param {string} type
|
|
|
57107 |
* The type of source buffer to add.
|
|
|
57108 |
*
|
|
|
57109 |
* @param {string} codec
|
|
|
57110 |
* The codec to add the source buffer with.
|
|
|
57111 |
*/
|
|
|
57112 |
|
|
|
57113 |
addSourceBuffer(type, codec) {
|
|
|
57114 |
pushQueue({
|
|
|
57115 |
type: 'mediaSource',
|
|
|
57116 |
sourceUpdater: this,
|
|
|
57117 |
action: actions.addSourceBuffer(type, codec),
|
|
|
57118 |
name: 'addSourceBuffer'
|
|
|
57119 |
});
|
|
|
57120 |
}
|
|
|
57121 |
/**
|
|
|
57122 |
* call abort on a source buffer.
|
|
|
57123 |
*
|
|
|
57124 |
* @param {string} type
|
|
|
57125 |
* The type of source buffer to call abort on.
|
|
|
57126 |
*/
|
|
|
57127 |
|
|
|
57128 |
abort(type) {
|
|
|
57129 |
pushQueue({
|
|
|
57130 |
type,
|
|
|
57131 |
sourceUpdater: this,
|
|
|
57132 |
action: actions.abort(type),
|
|
|
57133 |
name: 'abort'
|
|
|
57134 |
});
|
|
|
57135 |
}
|
|
|
57136 |
/**
|
|
|
57137 |
* Call removeSourceBuffer and remove a specific type
|
|
|
57138 |
* of source buffer on the mediaSource.
|
|
|
57139 |
*
|
|
|
57140 |
* @param {string} type
|
|
|
57141 |
* The type of source buffer to remove.
|
|
|
57142 |
*/
|
|
|
57143 |
|
|
|
57144 |
removeSourceBuffer(type) {
|
|
|
57145 |
if (!this.canRemoveSourceBuffer()) {
|
|
|
57146 |
videojs.log.error('removeSourceBuffer is not supported!');
|
|
|
57147 |
return;
|
|
|
57148 |
}
|
|
|
57149 |
pushQueue({
|
|
|
57150 |
type: 'mediaSource',
|
|
|
57151 |
sourceUpdater: this,
|
|
|
57152 |
action: actions.removeSourceBuffer(type),
|
|
|
57153 |
name: 'removeSourceBuffer'
|
|
|
57154 |
});
|
|
|
57155 |
}
|
|
|
57156 |
/**
|
|
|
57157 |
* Whether or not the removeSourceBuffer function is supported
|
|
|
57158 |
* on the mediaSource.
|
|
|
57159 |
*
|
|
|
57160 |
* @return {boolean}
|
|
|
57161 |
* if removeSourceBuffer can be called.
|
|
|
57162 |
*/
|
|
|
57163 |
|
|
|
57164 |
canRemoveSourceBuffer() {
|
|
|
57165 |
// As of Firefox 83 removeSourceBuffer
|
|
|
57166 |
// throws errors, so we report that it does not support this.
|
|
|
57167 |
return !videojs.browser.IS_FIREFOX && window.MediaSource && window.MediaSource.prototype && typeof window.MediaSource.prototype.removeSourceBuffer === 'function';
|
|
|
57168 |
}
|
|
|
57169 |
/**
|
|
|
57170 |
* Whether or not the changeType function is supported
|
|
|
57171 |
* on our SourceBuffers.
|
|
|
57172 |
*
|
|
|
57173 |
* @return {boolean}
|
|
|
57174 |
* if changeType can be called.
|
|
|
57175 |
*/
|
|
|
57176 |
|
|
|
57177 |
static canChangeType() {
|
|
|
57178 |
return window.SourceBuffer && window.SourceBuffer.prototype && typeof window.SourceBuffer.prototype.changeType === 'function';
|
|
|
57179 |
}
|
|
|
57180 |
/**
|
|
|
57181 |
* Whether or not the changeType function is supported
|
|
|
57182 |
* on our SourceBuffers.
|
|
|
57183 |
*
|
|
|
57184 |
* @return {boolean}
|
|
|
57185 |
* if changeType can be called.
|
|
|
57186 |
*/
|
|
|
57187 |
|
|
|
57188 |
canChangeType() {
|
|
|
57189 |
return this.constructor.canChangeType();
|
|
|
57190 |
}
|
|
|
57191 |
/**
|
|
|
57192 |
* Call the changeType function on a source buffer, given the code and type.
|
|
|
57193 |
*
|
|
|
57194 |
* @param {string} type
|
|
|
57195 |
* The type of source buffer to call changeType on.
|
|
|
57196 |
*
|
|
|
57197 |
* @param {string} codec
|
|
|
57198 |
* The codec string to change type with on the source buffer.
|
|
|
57199 |
*/
|
|
|
57200 |
|
|
|
57201 |
changeType(type, codec) {
|
|
|
57202 |
if (!this.canChangeType()) {
|
|
|
57203 |
videojs.log.error('changeType is not supported!');
|
|
|
57204 |
return;
|
|
|
57205 |
}
|
|
|
57206 |
pushQueue({
|
|
|
57207 |
type,
|
|
|
57208 |
sourceUpdater: this,
|
|
|
57209 |
action: actions.changeType(codec),
|
|
|
57210 |
name: 'changeType'
|
|
|
57211 |
});
|
|
|
57212 |
}
|
|
|
57213 |
/**
|
|
|
57214 |
* Add source buffers with a codec or, if they are already created,
|
|
|
57215 |
* call changeType on source buffers using changeType.
|
|
|
57216 |
*
|
|
|
57217 |
* @param {Object} codecs
|
|
|
57218 |
* Codecs to switch to
|
|
|
57219 |
*/
|
|
|
57220 |
|
|
|
57221 |
addOrChangeSourceBuffers(codecs) {
|
|
|
57222 |
if (!codecs || typeof codecs !== 'object' || Object.keys(codecs).length === 0) {
|
|
|
57223 |
throw new Error('Cannot addOrChangeSourceBuffers to undefined codecs');
|
|
|
57224 |
}
|
|
|
57225 |
Object.keys(codecs).forEach(type => {
|
|
|
57226 |
const codec = codecs[type];
|
|
|
57227 |
if (!this.hasCreatedSourceBuffers()) {
|
|
|
57228 |
return this.addSourceBuffer(type, codec);
|
|
|
57229 |
}
|
|
|
57230 |
if (this.canChangeType()) {
|
|
|
57231 |
this.changeType(type, codec);
|
|
|
57232 |
}
|
|
|
57233 |
});
|
|
|
57234 |
}
|
|
|
57235 |
/**
|
|
|
57236 |
* Queue an update to append an ArrayBuffer.
|
|
|
57237 |
*
|
|
|
57238 |
* @param {MediaObject} object containing audioBytes and/or videoBytes
|
|
|
57239 |
* @param {Function} done the function to call when done
|
|
|
57240 |
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
|
|
|
57241 |
*/
|
|
|
57242 |
|
|
|
57243 |
appendBuffer(options, doneFn) {
|
|
|
57244 |
const {
|
|
|
57245 |
segmentInfo,
|
|
|
57246 |
type,
|
|
|
57247 |
bytes
|
|
|
57248 |
} = options;
|
|
|
57249 |
this.processedAppend_ = true;
|
|
|
57250 |
if (type === 'audio' && this.videoBuffer && !this.videoAppendQueued_) {
|
|
|
57251 |
this.delayedAudioAppendQueue_.push([options, doneFn]);
|
|
|
57252 |
this.logger_(`delayed audio append of ${bytes.length} until video append`);
|
|
|
57253 |
return;
|
|
|
57254 |
} // In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will
|
|
|
57255 |
// not be fired. This means that the queue will be blocked until the next action
|
|
|
57256 |
// taken by the segment-loader. Provide a mechanism for segment-loader to handle
|
|
|
57257 |
// these errors by calling the doneFn with the specific error.
|
|
|
57258 |
|
|
|
57259 |
const onError = doneFn;
|
|
|
57260 |
pushQueue({
|
|
|
57261 |
type,
|
|
|
57262 |
sourceUpdater: this,
|
|
|
57263 |
action: actions.appendBuffer(bytes, segmentInfo || {
|
|
|
57264 |
mediaIndex: -1
|
|
|
57265 |
}, onError),
|
|
|
57266 |
doneFn,
|
|
|
57267 |
name: 'appendBuffer'
|
|
|
57268 |
});
|
|
|
57269 |
if (type === 'video') {
|
|
|
57270 |
this.videoAppendQueued_ = true;
|
|
|
57271 |
if (!this.delayedAudioAppendQueue_.length) {
|
|
|
57272 |
return;
|
|
|
57273 |
}
|
|
|
57274 |
const queue = this.delayedAudioAppendQueue_.slice();
|
|
|
57275 |
this.logger_(`queuing delayed audio ${queue.length} appendBuffers`);
|
|
|
57276 |
this.delayedAudioAppendQueue_.length = 0;
|
|
|
57277 |
queue.forEach(que => {
|
|
|
57278 |
this.appendBuffer.apply(this, que);
|
|
|
57279 |
});
|
|
|
57280 |
}
|
|
|
57281 |
}
|
|
|
57282 |
/**
|
|
|
57283 |
* Get the audio buffer's buffered timerange.
|
|
|
57284 |
*
|
|
|
57285 |
* @return {TimeRange}
|
|
|
57286 |
* The audio buffer's buffered time range
|
|
|
57287 |
*/
|
|
|
57288 |
|
|
|
57289 |
audioBuffered() {
|
|
|
57290 |
// no media source/source buffer or it isn't in the media sources
|
|
|
57291 |
// source buffer list
|
|
|
57292 |
if (!inSourceBuffers(this.mediaSource, this.audioBuffer)) {
|
|
|
57293 |
return createTimeRanges();
|
|
|
57294 |
}
|
|
|
57295 |
return this.audioBuffer.buffered ? this.audioBuffer.buffered : createTimeRanges();
|
|
|
57296 |
}
|
|
|
57297 |
/**
|
|
|
57298 |
* Get the video buffer's buffered timerange.
|
|
|
57299 |
*
|
|
|
57300 |
* @return {TimeRange}
|
|
|
57301 |
* The video buffer's buffered time range
|
|
|
57302 |
*/
|
|
|
57303 |
|
|
|
57304 |
videoBuffered() {
|
|
|
57305 |
// no media source/source buffer or it isn't in the media sources
|
|
|
57306 |
// source buffer list
|
|
|
57307 |
if (!inSourceBuffers(this.mediaSource, this.videoBuffer)) {
|
|
|
57308 |
return createTimeRanges();
|
|
|
57309 |
}
|
|
|
57310 |
return this.videoBuffer.buffered ? this.videoBuffer.buffered : createTimeRanges();
|
|
|
57311 |
}
|
|
|
57312 |
/**
|
|
|
57313 |
* Get a combined video/audio buffer's buffered timerange.
|
|
|
57314 |
*
|
|
|
57315 |
* @return {TimeRange}
|
|
|
57316 |
* the combined time range
|
|
|
57317 |
*/
|
|
|
57318 |
|
|
|
57319 |
buffered() {
|
|
|
57320 |
const video = inSourceBuffers(this.mediaSource, this.videoBuffer) ? this.videoBuffer : null;
|
|
|
57321 |
const audio = inSourceBuffers(this.mediaSource, this.audioBuffer) ? this.audioBuffer : null;
|
|
|
57322 |
if (audio && !video) {
|
|
|
57323 |
return this.audioBuffered();
|
|
|
57324 |
}
|
|
|
57325 |
if (video && !audio) {
|
|
|
57326 |
return this.videoBuffered();
|
|
|
57327 |
}
|
|
|
57328 |
return bufferIntersection(this.audioBuffered(), this.videoBuffered());
|
|
|
57329 |
}
|
|
|
57330 |
/**
|
|
|
57331 |
* Add a callback to the queue that will set duration on the mediaSource.
|
|
|
57332 |
*
|
|
|
57333 |
* @param {number} duration
|
|
|
57334 |
* The duration to set
|
|
|
57335 |
*
|
|
|
57336 |
* @param {Function} [doneFn]
|
|
|
57337 |
* function to run after duration has been set.
|
|
|
57338 |
*/
|
|
|
57339 |
|
|
|
57340 |
setDuration(duration, doneFn = noop) {
|
|
|
57341 |
// In order to set the duration on the media source, it's necessary to wait for all
|
|
|
57342 |
// source buffers to no longer be updating. "If the updating attribute equals true on
|
|
|
57343 |
// any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and
|
|
|
57344 |
// abort these steps." (source: https://www.w3.org/TR/media-source/#attributes).
|
|
|
57345 |
pushQueue({
|
|
|
57346 |
type: 'mediaSource',
|
|
|
57347 |
sourceUpdater: this,
|
|
|
57348 |
action: actions.duration(duration),
|
|
|
57349 |
name: 'duration',
|
|
|
57350 |
doneFn
|
|
|
57351 |
});
|
|
|
57352 |
}
|
|
|
57353 |
/**
|
|
|
57354 |
* Add a mediaSource endOfStream call to the queue
|
|
|
57355 |
*
|
|
|
57356 |
* @param {Error} [error]
|
|
|
57357 |
* Call endOfStream with an error
|
|
|
57358 |
*
|
|
|
57359 |
* @param {Function} [doneFn]
|
|
|
57360 |
* A function that should be called when the
|
|
|
57361 |
* endOfStream call has finished.
|
|
|
57362 |
*/
|
|
|
57363 |
|
|
|
57364 |
endOfStream(error = null, doneFn = noop) {
|
|
|
57365 |
if (typeof error !== 'string') {
|
|
|
57366 |
error = undefined;
|
|
|
57367 |
} // In order to set the duration on the media source, it's necessary to wait for all
|
|
|
57368 |
// source buffers to no longer be updating. "If the updating attribute equals true on
|
|
|
57369 |
// any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and
|
|
|
57370 |
// abort these steps." (source: https://www.w3.org/TR/media-source/#attributes).
|
|
|
57371 |
|
|
|
57372 |
pushQueue({
|
|
|
57373 |
type: 'mediaSource',
|
|
|
57374 |
sourceUpdater: this,
|
|
|
57375 |
action: actions.endOfStream(error),
|
|
|
57376 |
name: 'endOfStream',
|
|
|
57377 |
doneFn
|
|
|
57378 |
});
|
|
|
57379 |
}
|
|
|
57380 |
/**
|
|
|
57381 |
* Queue an update to remove a time range from the buffer.
|
|
|
57382 |
*
|
|
|
57383 |
* @param {number} start where to start the removal
|
|
|
57384 |
* @param {number} end where to end the removal
|
|
|
57385 |
* @param {Function} [done=noop] optional callback to be executed when the remove
|
|
|
57386 |
* operation is complete
|
|
|
57387 |
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
|
|
|
57388 |
*/
|
|
|
57389 |
|
|
|
57390 |
removeAudio(start, end, done = noop) {
|
|
|
57391 |
if (!this.audioBuffered().length || this.audioBuffered().end(0) === 0) {
|
|
|
57392 |
done();
|
|
|
57393 |
return;
|
|
|
57394 |
}
|
|
|
57395 |
pushQueue({
|
|
|
57396 |
type: 'audio',
|
|
|
57397 |
sourceUpdater: this,
|
|
|
57398 |
action: actions.remove(start, end),
|
|
|
57399 |
doneFn: done,
|
|
|
57400 |
name: 'remove'
|
|
|
57401 |
});
|
|
|
57402 |
}
|
|
|
57403 |
/**
|
|
|
57404 |
* Queue an update to remove a time range from the buffer.
|
|
|
57405 |
*
|
|
|
57406 |
* @param {number} start where to start the removal
|
|
|
57407 |
* @param {number} end where to end the removal
|
|
|
57408 |
* @param {Function} [done=noop] optional callback to be executed when the remove
|
|
|
57409 |
* operation is complete
|
|
|
57410 |
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
|
|
|
57411 |
*/
|
|
|
57412 |
|
|
|
57413 |
removeVideo(start, end, done = noop) {
|
|
|
57414 |
if (!this.videoBuffered().length || this.videoBuffered().end(0) === 0) {
|
|
|
57415 |
done();
|
|
|
57416 |
return;
|
|
|
57417 |
}
|
|
|
57418 |
pushQueue({
|
|
|
57419 |
type: 'video',
|
|
|
57420 |
sourceUpdater: this,
|
|
|
57421 |
action: actions.remove(start, end),
|
|
|
57422 |
doneFn: done,
|
|
|
57423 |
name: 'remove'
|
|
|
57424 |
});
|
|
|
57425 |
}
|
|
|
57426 |
/**
|
|
|
57427 |
* Whether the underlying sourceBuffer is updating or not
|
|
|
57428 |
*
|
|
|
57429 |
* @return {boolean} the updating status of the SourceBuffer
|
|
|
57430 |
*/
|
|
|
57431 |
|
|
|
57432 |
updating() {
|
|
|
57433 |
// the audio/video source buffer is updating
|
|
|
57434 |
if (updating('audio', this) || updating('video', this)) {
|
|
|
57435 |
return true;
|
|
|
57436 |
}
|
|
|
57437 |
return false;
|
|
|
57438 |
}
|
|
|
57439 |
/**
|
|
|
57440 |
* Set/get the timestampoffset on the audio SourceBuffer
|
|
|
57441 |
*
|
|
|
57442 |
* @return {number} the timestamp offset
|
|
|
57443 |
*/
|
|
|
57444 |
|
|
|
57445 |
audioTimestampOffset(offset) {
|
|
|
57446 |
if (typeof offset !== 'undefined' && this.audioBuffer &&
|
|
|
57447 |
// no point in updating if it's the same
|
|
|
57448 |
this.audioTimestampOffset_ !== offset) {
|
|
|
57449 |
pushQueue({
|
|
|
57450 |
type: 'audio',
|
|
|
57451 |
sourceUpdater: this,
|
|
|
57452 |
action: actions.timestampOffset(offset),
|
|
|
57453 |
name: 'timestampOffset'
|
|
|
57454 |
});
|
|
|
57455 |
this.audioTimestampOffset_ = offset;
|
|
|
57456 |
}
|
|
|
57457 |
return this.audioTimestampOffset_;
|
|
|
57458 |
}
|
|
|
57459 |
/**
|
|
|
57460 |
* Set/get the timestampoffset on the video SourceBuffer
|
|
|
57461 |
*
|
|
|
57462 |
* @return {number} the timestamp offset
|
|
|
57463 |
*/
|
|
|
57464 |
|
|
|
57465 |
videoTimestampOffset(offset) {
|
|
|
57466 |
if (typeof offset !== 'undefined' && this.videoBuffer &&
|
|
|
57467 |
// no point in updating if it's the same
|
|
|
57468 |
this.videoTimestampOffset !== offset) {
|
|
|
57469 |
pushQueue({
|
|
|
57470 |
type: 'video',
|
|
|
57471 |
sourceUpdater: this,
|
|
|
57472 |
action: actions.timestampOffset(offset),
|
|
|
57473 |
name: 'timestampOffset'
|
|
|
57474 |
});
|
|
|
57475 |
this.videoTimestampOffset_ = offset;
|
|
|
57476 |
}
|
|
|
57477 |
return this.videoTimestampOffset_;
|
|
|
57478 |
}
|
|
|
57479 |
/**
|
|
|
57480 |
* Add a function to the queue that will be called
|
|
|
57481 |
* when it is its turn to run in the audio queue.
|
|
|
57482 |
*
|
|
|
57483 |
* @param {Function} callback
|
|
|
57484 |
* The callback to queue.
|
|
|
57485 |
*/
|
|
|
57486 |
|
|
|
57487 |
audioQueueCallback(callback) {
|
|
|
57488 |
if (!this.audioBuffer) {
|
|
|
57489 |
return;
|
|
|
57490 |
}
|
|
|
57491 |
pushQueue({
|
|
|
57492 |
type: 'audio',
|
|
|
57493 |
sourceUpdater: this,
|
|
|
57494 |
action: actions.callback(callback),
|
|
|
57495 |
name: 'callback'
|
|
|
57496 |
});
|
|
|
57497 |
}
|
|
|
57498 |
/**
|
|
|
57499 |
* Add a function to the queue that will be called
|
|
|
57500 |
* when it is its turn to run in the video queue.
|
|
|
57501 |
*
|
|
|
57502 |
* @param {Function} callback
|
|
|
57503 |
* The callback to queue.
|
|
|
57504 |
*/
|
|
|
57505 |
|
|
|
57506 |
videoQueueCallback(callback) {
|
|
|
57507 |
if (!this.videoBuffer) {
|
|
|
57508 |
return;
|
|
|
57509 |
}
|
|
|
57510 |
pushQueue({
|
|
|
57511 |
type: 'video',
|
|
|
57512 |
sourceUpdater: this,
|
|
|
57513 |
action: actions.callback(callback),
|
|
|
57514 |
name: 'callback'
|
|
|
57515 |
});
|
|
|
57516 |
}
|
|
|
57517 |
/**
|
|
|
57518 |
* dispose of the source updater and the underlying sourceBuffer
|
|
|
57519 |
*/
|
|
|
57520 |
|
|
|
57521 |
dispose() {
|
|
|
57522 |
this.trigger('dispose');
|
|
|
57523 |
bufferTypes.forEach(type => {
|
|
|
57524 |
this.abort(type);
|
|
|
57525 |
if (this.canRemoveSourceBuffer()) {
|
|
|
57526 |
this.removeSourceBuffer(type);
|
|
|
57527 |
} else {
|
|
|
57528 |
this[`${type}QueueCallback`](() => cleanupBuffer(type, this));
|
|
|
57529 |
}
|
|
|
57530 |
});
|
|
|
57531 |
this.videoAppendQueued_ = false;
|
|
|
57532 |
this.delayedAudioAppendQueue_.length = 0;
|
|
|
57533 |
if (this.sourceopenListener_) {
|
|
|
57534 |
this.mediaSource.removeEventListener('sourceopen', this.sourceopenListener_);
|
|
|
57535 |
}
|
|
|
57536 |
this.off();
|
|
|
57537 |
}
|
|
|
57538 |
}
|
|
|
57539 |
const uint8ToUtf8 = uintArray => decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray)));
|
|
|
57540 |
const bufferToHexString = buffer => {
|
|
|
57541 |
const uInt8Buffer = new Uint8Array(buffer);
|
|
|
57542 |
return Array.from(uInt8Buffer).map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
|
57543 |
};
|
|
|
57544 |
|
|
|
57545 |
/**
|
|
|
57546 |
* @file vtt-segment-loader.js
|
|
|
57547 |
*/
|
|
|
57548 |
const VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0)));
|
|
|
57549 |
class NoVttJsError extends Error {
|
|
|
57550 |
constructor() {
|
|
|
57551 |
super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.');
|
|
|
57552 |
}
|
|
|
57553 |
}
|
|
|
57554 |
/**
|
|
|
57555 |
* An object that manages segment loading and appending.
|
|
|
57556 |
*
|
|
|
57557 |
* @class VTTSegmentLoader
|
|
|
57558 |
* @param {Object} options required and optional options
|
|
|
57559 |
* @extends videojs.EventTarget
|
|
|
57560 |
*/
|
|
|
57561 |
|
|
|
57562 |
class VTTSegmentLoader extends SegmentLoader {
|
|
|
57563 |
constructor(settings, options = {}) {
|
|
|
57564 |
super(settings, options); // SegmentLoader requires a MediaSource be specified or it will throw an error;
|
|
|
57565 |
// however, VTTSegmentLoader has no need of a media source, so delete the reference
|
|
|
57566 |
|
|
|
57567 |
this.mediaSource_ = null;
|
|
|
57568 |
this.subtitlesTrack_ = null;
|
|
|
57569 |
this.loaderType_ = 'subtitle';
|
|
|
57570 |
this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks;
|
|
|
57571 |
this.loadVttJs = settings.loadVttJs; // The VTT segment will have its own time mappings. Saving VTT segment timing info in
|
|
|
57572 |
// the sync controller leads to improper behavior.
|
|
|
57573 |
|
|
|
57574 |
this.shouldSaveSegmentTimingInfo_ = false;
|
|
|
57575 |
}
|
|
|
57576 |
createTransmuxer_() {
|
|
|
57577 |
// don't need to transmux any subtitles
|
|
|
57578 |
return null;
|
|
|
57579 |
}
|
|
|
57580 |
/**
|
|
|
57581 |
* Indicates which time ranges are buffered
|
|
|
57582 |
*
|
|
|
57583 |
* @return {TimeRange}
|
|
|
57584 |
* TimeRange object representing the current buffered ranges
|
|
|
57585 |
*/
|
|
|
57586 |
|
|
|
57587 |
buffered_() {
|
|
|
57588 |
if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) {
|
|
|
57589 |
return createTimeRanges();
|
|
|
57590 |
}
|
|
|
57591 |
const cues = this.subtitlesTrack_.cues;
|
|
|
57592 |
const start = cues[0].startTime;
|
|
|
57593 |
const end = cues[cues.length - 1].startTime;
|
|
|
57594 |
return createTimeRanges([[start, end]]);
|
|
|
57595 |
}
|
|
|
57596 |
/**
|
|
|
57597 |
* Gets and sets init segment for the provided map
|
|
|
57598 |
*
|
|
|
57599 |
* @param {Object} map
|
|
|
57600 |
* The map object representing the init segment to get or set
|
|
|
57601 |
* @param {boolean=} set
|
|
|
57602 |
* If true, the init segment for the provided map should be saved
|
|
|
57603 |
* @return {Object}
|
|
|
57604 |
* map object for desired init segment
|
|
|
57605 |
*/
|
|
|
57606 |
|
|
|
57607 |
initSegmentForMap(map, set = false) {
|
|
|
57608 |
if (!map) {
|
|
|
57609 |
return null;
|
|
|
57610 |
}
|
|
|
57611 |
const id = initSegmentId(map);
|
|
|
57612 |
let storedMap = this.initSegments_[id];
|
|
|
57613 |
if (set && !storedMap && map.bytes) {
|
|
|
57614 |
// append WebVTT line terminators to the media initialization segment if it exists
|
|
|
57615 |
// to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that
|
|
|
57616 |
// requires two or more WebVTT line terminators between the WebVTT header and the
|
|
|
57617 |
// rest of the file
|
|
|
57618 |
const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength;
|
|
|
57619 |
const combinedSegment = new Uint8Array(combinedByteLength);
|
|
|
57620 |
combinedSegment.set(map.bytes);
|
|
|
57621 |
combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength);
|
|
|
57622 |
this.initSegments_[id] = storedMap = {
|
|
|
57623 |
resolvedUri: map.resolvedUri,
|
|
|
57624 |
byterange: map.byterange,
|
|
|
57625 |
bytes: combinedSegment
|
|
|
57626 |
};
|
|
|
57627 |
}
|
|
|
57628 |
return storedMap || map;
|
|
|
57629 |
}
|
|
|
57630 |
/**
|
|
|
57631 |
* Returns true if all configuration required for loading is present, otherwise false.
|
|
|
57632 |
*
|
|
|
57633 |
* @return {boolean} True if the all configuration is ready for loading
|
|
|
57634 |
* @private
|
|
|
57635 |
*/
|
|
|
57636 |
|
|
|
57637 |
couldBeginLoading_() {
|
|
|
57638 |
return this.playlist_ && this.subtitlesTrack_ && !this.paused();
|
|
|
57639 |
}
|
|
|
57640 |
/**
|
|
|
57641 |
* Once all the starting parameters have been specified, begin
|
|
|
57642 |
* operation. This method should only be invoked from the INIT
|
|
|
57643 |
* state.
|
|
|
57644 |
*
|
|
|
57645 |
* @private
|
|
|
57646 |
*/
|
|
|
57647 |
|
|
|
57648 |
init_() {
|
|
|
57649 |
this.state = 'READY';
|
|
|
57650 |
this.resetEverything();
|
|
|
57651 |
return this.monitorBuffer_();
|
|
|
57652 |
}
|
|
|
57653 |
/**
|
|
|
57654 |
* Set a subtitle track on the segment loader to add subtitles to
|
|
|
57655 |
*
|
|
|
57656 |
* @param {TextTrack=} track
|
|
|
57657 |
* The text track to add loaded subtitles to
|
|
|
57658 |
* @return {TextTrack}
|
|
|
57659 |
* Returns the subtitles track
|
|
|
57660 |
*/
|
|
|
57661 |
|
|
|
57662 |
track(track) {
|
|
|
57663 |
if (typeof track === 'undefined') {
|
|
|
57664 |
return this.subtitlesTrack_;
|
|
|
57665 |
}
|
|
|
57666 |
this.subtitlesTrack_ = track; // if we were unpaused but waiting for a sourceUpdater, start
|
|
|
57667 |
// buffering now
|
|
|
57668 |
|
|
|
57669 |
if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
|
|
57670 |
this.init_();
|
|
|
57671 |
}
|
|
|
57672 |
return this.subtitlesTrack_;
|
|
|
57673 |
}
|
|
|
57674 |
/**
|
|
|
57675 |
* Remove any data in the source buffer between start and end times
|
|
|
57676 |
*
|
|
|
57677 |
* @param {number} start - the start time of the region to remove from the buffer
|
|
|
57678 |
* @param {number} end - the end time of the region to remove from the buffer
|
|
|
57679 |
*/
|
|
|
57680 |
|
|
|
57681 |
remove(start, end) {
|
|
|
57682 |
removeCuesFromTrack(start, end, this.subtitlesTrack_);
|
|
|
57683 |
}
|
|
|
57684 |
/**
|
|
|
57685 |
* fill the buffer with segements unless the sourceBuffers are
|
|
|
57686 |
* currently updating
|
|
|
57687 |
*
|
|
|
57688 |
* Note: this function should only ever be called by monitorBuffer_
|
|
|
57689 |
* and never directly
|
|
|
57690 |
*
|
|
|
57691 |
* @private
|
|
|
57692 |
*/
|
|
|
57693 |
|
|
|
57694 |
fillBuffer_() {
|
|
|
57695 |
// see if we need to begin loading immediately
|
|
|
57696 |
const segmentInfo = this.chooseNextRequest_();
|
|
|
57697 |
if (!segmentInfo) {
|
|
|
57698 |
return;
|
|
|
57699 |
}
|
|
|
57700 |
if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) {
|
|
|
57701 |
// We don't have the timestamp offset that we need to sync subtitles.
|
|
|
57702 |
// Rerun on a timestamp offset or user interaction.
|
|
|
57703 |
const checkTimestampOffset = () => {
|
|
|
57704 |
this.state = 'READY';
|
|
|
57705 |
if (!this.paused()) {
|
|
|
57706 |
// if not paused, queue a buffer check as soon as possible
|
|
|
57707 |
this.monitorBuffer_();
|
|
|
57708 |
}
|
|
|
57709 |
};
|
|
|
57710 |
this.syncController_.one('timestampoffset', checkTimestampOffset);
|
|
|
57711 |
this.state = 'WAITING_ON_TIMELINE';
|
|
|
57712 |
return;
|
|
|
57713 |
}
|
|
|
57714 |
this.loadSegment_(segmentInfo);
|
|
|
57715 |
} // never set a timestamp offset for vtt segments.
|
|
|
57716 |
|
|
|
57717 |
timestampOffsetForSegment_() {
|
|
|
57718 |
return null;
|
|
|
57719 |
}
|
|
|
57720 |
chooseNextRequest_() {
|
|
|
57721 |
return this.skipEmptySegments_(super.chooseNextRequest_());
|
|
|
57722 |
}
|
|
|
57723 |
/**
|
|
|
57724 |
* Prevents the segment loader from requesting segments we know contain no subtitles
|
|
|
57725 |
* by walking forward until we find the next segment that we don't know whether it is
|
|
|
57726 |
* empty or not.
|
|
|
57727 |
*
|
|
|
57728 |
* @param {Object} segmentInfo
|
|
|
57729 |
* a segment info object that describes the current segment
|
|
|
57730 |
* @return {Object}
|
|
|
57731 |
* a segment info object that describes the current segment
|
|
|
57732 |
*/
|
|
|
57733 |
|
|
|
57734 |
skipEmptySegments_(segmentInfo) {
|
|
|
57735 |
while (segmentInfo && segmentInfo.segment.empty) {
|
|
|
57736 |
// stop at the last possible segmentInfo
|
|
|
57737 |
if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) {
|
|
|
57738 |
segmentInfo = null;
|
|
|
57739 |
break;
|
|
|
57740 |
}
|
|
|
57741 |
segmentInfo = this.generateSegmentInfo_({
|
|
|
57742 |
playlist: segmentInfo.playlist,
|
|
|
57743 |
mediaIndex: segmentInfo.mediaIndex + 1,
|
|
|
57744 |
startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration,
|
|
|
57745 |
isSyncRequest: segmentInfo.isSyncRequest
|
|
|
57746 |
});
|
|
|
57747 |
}
|
|
|
57748 |
return segmentInfo;
|
|
|
57749 |
}
|
|
|
57750 |
stopForError(error) {
|
|
|
57751 |
this.error(error);
|
|
|
57752 |
this.state = 'READY';
|
|
|
57753 |
this.pause();
|
|
|
57754 |
this.trigger('error');
|
|
|
57755 |
}
|
|
|
57756 |
/**
|
|
|
57757 |
* append a decrypted segement to the SourceBuffer through a SourceUpdater
|
|
|
57758 |
*
|
|
|
57759 |
* @private
|
|
|
57760 |
*/
|
|
|
57761 |
|
|
|
57762 |
segmentRequestFinished_(error, simpleSegment, result) {
|
|
|
57763 |
if (!this.subtitlesTrack_) {
|
|
|
57764 |
this.state = 'READY';
|
|
|
57765 |
return;
|
|
|
57766 |
}
|
|
|
57767 |
this.saveTransferStats_(simpleSegment.stats); // the request was aborted
|
|
|
57768 |
|
|
|
57769 |
if (!this.pendingSegment_) {
|
|
|
57770 |
this.state = 'READY';
|
|
|
57771 |
this.mediaRequestsAborted += 1;
|
|
|
57772 |
return;
|
|
|
57773 |
}
|
|
|
57774 |
if (error) {
|
|
|
57775 |
if (error.code === REQUEST_ERRORS.TIMEOUT) {
|
|
|
57776 |
this.handleTimeout_();
|
|
|
57777 |
}
|
|
|
57778 |
if (error.code === REQUEST_ERRORS.ABORTED) {
|
|
|
57779 |
this.mediaRequestsAborted += 1;
|
|
|
57780 |
} else {
|
|
|
57781 |
this.mediaRequestsErrored += 1;
|
|
|
57782 |
}
|
|
|
57783 |
this.stopForError(error);
|
|
|
57784 |
return;
|
|
|
57785 |
}
|
|
|
57786 |
const segmentInfo = this.pendingSegment_; // although the VTT segment loader bandwidth isn't really used, it's good to
|
|
|
57787 |
// maintain functionality between segment loaders
|
|
|
57788 |
|
|
|
57789 |
this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); // if this request included a segment key, save that data in the cache
|
|
|
57790 |
|
|
|
57791 |
if (simpleSegment.key) {
|
|
|
57792 |
this.segmentKey(simpleSegment.key, true);
|
|
|
57793 |
}
|
|
|
57794 |
this.state = 'APPENDING'; // used for tests
|
|
|
57795 |
|
|
|
57796 |
this.trigger('appending');
|
|
|
57797 |
const segment = segmentInfo.segment;
|
|
|
57798 |
if (segment.map) {
|
|
|
57799 |
segment.map.bytes = simpleSegment.map.bytes;
|
|
|
57800 |
}
|
|
|
57801 |
segmentInfo.bytes = simpleSegment.bytes; // Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading
|
|
|
57802 |
|
|
|
57803 |
if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') {
|
|
|
57804 |
this.state = 'WAITING_ON_VTTJS'; // should be fine to call multiple times
|
|
|
57805 |
// script will be loaded once but multiple listeners will be added to the queue, which is expected.
|
|
|
57806 |
|
|
|
57807 |
this.loadVttJs().then(() => this.segmentRequestFinished_(error, simpleSegment, result), () => this.stopForError({
|
|
|
57808 |
message: 'Error loading vtt.js'
|
|
|
57809 |
}));
|
|
|
57810 |
return;
|
|
|
57811 |
}
|
|
|
57812 |
segment.requested = true;
|
|
|
57813 |
try {
|
|
|
57814 |
this.parseVTTCues_(segmentInfo);
|
|
|
57815 |
} catch (e) {
|
|
|
57816 |
this.stopForError({
|
|
|
57817 |
message: e.message
|
|
|
57818 |
});
|
|
|
57819 |
return;
|
|
|
57820 |
}
|
|
|
57821 |
this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_);
|
|
|
57822 |
if (segmentInfo.cues.length) {
|
|
|
57823 |
segmentInfo.timingInfo = {
|
|
|
57824 |
start: segmentInfo.cues[0].startTime,
|
|
|
57825 |
end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime
|
|
|
57826 |
};
|
|
|
57827 |
} else {
|
|
|
57828 |
segmentInfo.timingInfo = {
|
|
|
57829 |
start: segmentInfo.startOfSegment,
|
|
|
57830 |
end: segmentInfo.startOfSegment + segmentInfo.duration
|
|
|
57831 |
};
|
|
|
57832 |
}
|
|
|
57833 |
if (segmentInfo.isSyncRequest) {
|
|
|
57834 |
this.trigger('syncinfoupdate');
|
|
|
57835 |
this.pendingSegment_ = null;
|
|
|
57836 |
this.state = 'READY';
|
|
|
57837 |
return;
|
|
|
57838 |
}
|
|
|
57839 |
segmentInfo.byteLength = segmentInfo.bytes.byteLength;
|
|
|
57840 |
this.mediaSecondsLoaded += segment.duration; // Create VTTCue instances for each cue in the new segment and add them to
|
|
|
57841 |
// the subtitle track
|
|
|
57842 |
|
|
|
57843 |
segmentInfo.cues.forEach(cue => {
|
|
|
57844 |
this.subtitlesTrack_.addCue(this.featuresNativeTextTracks_ ? new window.VTTCue(cue.startTime, cue.endTime, cue.text) : cue);
|
|
|
57845 |
}); // Remove any duplicate cues from the subtitle track. The WebVTT spec allows
|
|
|
57846 |
// cues to have identical time-intervals, but if the text is also identical
|
|
|
57847 |
// we can safely assume it is a duplicate that can be removed (ex. when a cue
|
|
|
57848 |
// "overlaps" VTT segments)
|
|
|
57849 |
|
|
|
57850 |
removeDuplicateCuesFromTrack(this.subtitlesTrack_);
|
|
|
57851 |
this.handleAppendsDone_();
|
|
|
57852 |
}
|
|
|
57853 |
handleData_() {// noop as we shouldn't be getting video/audio data captions
|
|
|
57854 |
// that we do not support here.
|
|
|
57855 |
}
|
|
|
57856 |
updateTimingInfoEnd_() {// noop
|
|
|
57857 |
}
|
|
|
57858 |
/**
|
|
|
57859 |
* Uses the WebVTT parser to parse the segment response
|
|
|
57860 |
*
|
|
|
57861 |
* @throws NoVttJsError
|
|
|
57862 |
*
|
|
|
57863 |
* @param {Object} segmentInfo
|
|
|
57864 |
* a segment info object that describes the current segment
|
|
|
57865 |
* @private
|
|
|
57866 |
*/
|
|
|
57867 |
|
|
|
57868 |
parseVTTCues_(segmentInfo) {
|
|
|
57869 |
let decoder;
|
|
|
57870 |
let decodeBytesToString = false;
|
|
|
57871 |
if (typeof window.WebVTT !== 'function') {
|
|
|
57872 |
// caller is responsible for exception handling.
|
|
|
57873 |
throw new NoVttJsError();
|
|
|
57874 |
}
|
|
|
57875 |
if (typeof window.TextDecoder === 'function') {
|
|
|
57876 |
decoder = new window.TextDecoder('utf8');
|
|
|
57877 |
} else {
|
|
|
57878 |
decoder = window.WebVTT.StringDecoder();
|
|
|
57879 |
decodeBytesToString = true;
|
|
|
57880 |
}
|
|
|
57881 |
const parser = new window.WebVTT.Parser(window, window.vttjs, decoder);
|
|
|
57882 |
segmentInfo.cues = [];
|
|
|
57883 |
segmentInfo.timestampmap = {
|
|
|
57884 |
MPEGTS: 0,
|
|
|
57885 |
LOCAL: 0
|
|
|
57886 |
};
|
|
|
57887 |
parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues);
|
|
|
57888 |
parser.ontimestampmap = map => {
|
|
|
57889 |
segmentInfo.timestampmap = map;
|
|
|
57890 |
};
|
|
|
57891 |
parser.onparsingerror = error => {
|
|
|
57892 |
videojs.log.warn('Error encountered when parsing cues: ' + error.message);
|
|
|
57893 |
};
|
|
|
57894 |
if (segmentInfo.segment.map) {
|
|
|
57895 |
let mapData = segmentInfo.segment.map.bytes;
|
|
|
57896 |
if (decodeBytesToString) {
|
|
|
57897 |
mapData = uint8ToUtf8(mapData);
|
|
|
57898 |
}
|
|
|
57899 |
parser.parse(mapData);
|
|
|
57900 |
}
|
|
|
57901 |
let segmentData = segmentInfo.bytes;
|
|
|
57902 |
if (decodeBytesToString) {
|
|
|
57903 |
segmentData = uint8ToUtf8(segmentData);
|
|
|
57904 |
}
|
|
|
57905 |
parser.parse(segmentData);
|
|
|
57906 |
parser.flush();
|
|
|
57907 |
}
|
|
|
57908 |
/**
|
|
|
57909 |
* Updates the start and end times of any cues parsed by the WebVTT parser using
|
|
|
57910 |
* the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping
|
|
|
57911 |
* from the SyncController
|
|
|
57912 |
*
|
|
|
57913 |
* @param {Object} segmentInfo
|
|
|
57914 |
* a segment info object that describes the current segment
|
|
|
57915 |
* @param {Object} mappingObj
|
|
|
57916 |
* object containing a mapping from TS to media time
|
|
|
57917 |
* @param {Object} playlist
|
|
|
57918 |
* the playlist object containing the segment
|
|
|
57919 |
* @private
|
|
|
57920 |
*/
|
|
|
57921 |
|
|
|
57922 |
updateTimeMapping_(segmentInfo, mappingObj, playlist) {
|
|
|
57923 |
const segment = segmentInfo.segment;
|
|
|
57924 |
if (!mappingObj) {
|
|
|
57925 |
// If the sync controller does not have a mapping of TS to Media Time for the
|
|
|
57926 |
// timeline, then we don't have enough information to update the cue
|
|
|
57927 |
// start/end times
|
|
|
57928 |
return;
|
|
|
57929 |
}
|
|
|
57930 |
if (!segmentInfo.cues.length) {
|
|
|
57931 |
// If there are no cues, we also do not have enough information to figure out
|
|
|
57932 |
// segment timing. Mark that the segment contains no cues so we don't re-request
|
|
|
57933 |
// an empty segment.
|
|
|
57934 |
segment.empty = true;
|
|
|
57935 |
return;
|
|
|
57936 |
}
|
|
|
57937 |
const {
|
|
|
57938 |
MPEGTS,
|
|
|
57939 |
LOCAL
|
|
|
57940 |
} = segmentInfo.timestampmap;
|
|
|
57941 |
/**
|
|
|
57942 |
* From the spec:
|
|
|
57943 |
* The MPEGTS media timestamp MUST use a 90KHz timescale,
|
|
|
57944 |
* even when non-WebVTT Media Segments use a different timescale.
|
|
|
57945 |
*/
|
|
|
57946 |
|
|
|
57947 |
const mpegTsInSeconds = MPEGTS / clock_1;
|
|
|
57948 |
const diff = mpegTsInSeconds - LOCAL + mappingObj.mapping;
|
|
|
57949 |
segmentInfo.cues.forEach(cue => {
|
|
|
57950 |
const duration = cue.endTime - cue.startTime;
|
|
|
57951 |
const startTime = MPEGTS === 0 ? cue.startTime + diff : this.handleRollover_(cue.startTime + diff, mappingObj.time);
|
|
|
57952 |
cue.startTime = Math.max(startTime, 0);
|
|
|
57953 |
cue.endTime = Math.max(startTime + duration, 0);
|
|
|
57954 |
});
|
|
|
57955 |
if (!playlist.syncInfo) {
|
|
|
57956 |
const firstStart = segmentInfo.cues[0].startTime;
|
|
|
57957 |
const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime;
|
|
|
57958 |
playlist.syncInfo = {
|
|
|
57959 |
mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
|
|
|
57960 |
time: Math.min(firstStart, lastStart - segment.duration)
|
|
|
57961 |
};
|
|
|
57962 |
}
|
|
|
57963 |
}
|
|
|
57964 |
/**
|
|
|
57965 |
* MPEG-TS PES timestamps are limited to 2^33.
|
|
|
57966 |
* Once they reach 2^33, they roll over to 0.
|
|
|
57967 |
* mux.js handles PES timestamp rollover for the following scenarios:
|
|
|
57968 |
* [forward rollover(right)] ->
|
|
|
57969 |
* PES timestamps monotonically increase, and once they reach 2^33, they roll over to 0
|
|
|
57970 |
* [backward rollover(left)] -->
|
|
|
57971 |
* we seek back to position before rollover.
|
|
|
57972 |
*
|
|
|
57973 |
* According to the HLS SPEC:
|
|
|
57974 |
* When synchronizing WebVTT with PES timestamps, clients SHOULD account
|
|
|
57975 |
* for cases where the 33-bit PES timestamps have wrapped and the WebVTT
|
|
|
57976 |
* cue times have not. When the PES timestamp wraps, the WebVTT Segment
|
|
|
57977 |
* SHOULD have a X-TIMESTAMP-MAP header that maps the current WebVTT
|
|
|
57978 |
* time to the new (low valued) PES timestamp.
|
|
|
57979 |
*
|
|
|
57980 |
* So we want to handle rollover here and align VTT Cue start/end time to the player's time.
|
|
|
57981 |
*/
|
|
|
57982 |
|
|
|
57983 |
handleRollover_(value, reference) {
|
|
|
57984 |
if (reference === null) {
|
|
|
57985 |
return value;
|
|
|
57986 |
}
|
|
|
57987 |
let valueIn90khz = value * clock_1;
|
|
|
57988 |
const referenceIn90khz = reference * clock_1;
|
|
|
57989 |
let offset;
|
|
|
57990 |
if (referenceIn90khz < valueIn90khz) {
|
|
|
57991 |
// - 2^33
|
|
|
57992 |
offset = -8589934592;
|
|
|
57993 |
} else {
|
|
|
57994 |
// + 2^33
|
|
|
57995 |
offset = 8589934592;
|
|
|
57996 |
} // distance(value - reference) > 2^32
|
|
|
57997 |
|
|
|
57998 |
while (Math.abs(valueIn90khz - referenceIn90khz) > 4294967296) {
|
|
|
57999 |
valueIn90khz += offset;
|
|
|
58000 |
}
|
|
|
58001 |
return valueIn90khz / clock_1;
|
|
|
58002 |
}
|
|
|
58003 |
}
|
|
|
58004 |
|
|
|
58005 |
/**
|
|
|
58006 |
* @file ad-cue-tags.js
|
|
|
58007 |
*/
|
|
|
58008 |
/**
|
|
|
58009 |
* Searches for an ad cue that overlaps with the given mediaTime
|
|
|
58010 |
*
|
|
|
58011 |
* @param {Object} track
|
|
|
58012 |
* the track to find the cue for
|
|
|
58013 |
*
|
|
|
58014 |
* @param {number} mediaTime
|
|
|
58015 |
* the time to find the cue at
|
|
|
58016 |
*
|
|
|
58017 |
* @return {Object|null}
|
|
|
58018 |
* the found cue or null
|
|
|
58019 |
*/
|
|
|
58020 |
|
|
|
58021 |
const findAdCue = function (track, mediaTime) {
|
|
|
58022 |
const cues = track.cues;
|
|
|
58023 |
for (let i = 0; i < cues.length; i++) {
|
|
|
58024 |
const cue = cues[i];
|
|
|
58025 |
if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) {
|
|
|
58026 |
return cue;
|
|
|
58027 |
}
|
|
|
58028 |
}
|
|
|
58029 |
return null;
|
|
|
58030 |
};
|
|
|
58031 |
const updateAdCues = function (media, track, offset = 0) {
|
|
|
58032 |
if (!media.segments) {
|
|
|
58033 |
return;
|
|
|
58034 |
}
|
|
|
58035 |
let mediaTime = offset;
|
|
|
58036 |
let cue;
|
|
|
58037 |
for (let i = 0; i < media.segments.length; i++) {
|
|
|
58038 |
const segment = media.segments[i];
|
|
|
58039 |
if (!cue) {
|
|
|
58040 |
// Since the cues will span for at least the segment duration, adding a fudge
|
|
|
58041 |
// factor of half segment duration will prevent duplicate cues from being
|
|
|
58042 |
// created when timing info is not exact (e.g. cue start time initialized
|
|
|
58043 |
// at 10.006677, but next call mediaTime is 10.003332 )
|
|
|
58044 |
cue = findAdCue(track, mediaTime + segment.duration / 2);
|
|
|
58045 |
}
|
|
|
58046 |
if (cue) {
|
|
|
58047 |
if ('cueIn' in segment) {
|
|
|
58048 |
// Found a CUE-IN so end the cue
|
|
|
58049 |
cue.endTime = mediaTime;
|
|
|
58050 |
cue.adEndTime = mediaTime;
|
|
|
58051 |
mediaTime += segment.duration;
|
|
|
58052 |
cue = null;
|
|
|
58053 |
continue;
|
|
|
58054 |
}
|
|
|
58055 |
if (mediaTime < cue.endTime) {
|
|
|
58056 |
// Already processed this mediaTime for this cue
|
|
|
58057 |
mediaTime += segment.duration;
|
|
|
58058 |
continue;
|
|
|
58059 |
} // otherwise extend cue until a CUE-IN is found
|
|
|
58060 |
|
|
|
58061 |
cue.endTime += segment.duration;
|
|
|
58062 |
} else {
|
|
|
58063 |
if ('cueOut' in segment) {
|
|
|
58064 |
cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut);
|
|
|
58065 |
cue.adStartTime = mediaTime; // Assumes tag format to be
|
|
|
58066 |
// #EXT-X-CUE-OUT:30
|
|
|
58067 |
|
|
|
58068 |
cue.adEndTime = mediaTime + parseFloat(segment.cueOut);
|
|
|
58069 |
track.addCue(cue);
|
|
|
58070 |
}
|
|
|
58071 |
if ('cueOutCont' in segment) {
|
|
|
58072 |
// Entered into the middle of an ad cue
|
|
|
58073 |
// Assumes tag formate to be
|
|
|
58074 |
// #EXT-X-CUE-OUT-CONT:10/30
|
|
|
58075 |
const [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat);
|
|
|
58076 |
cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, '');
|
|
|
58077 |
cue.adStartTime = mediaTime - adOffset;
|
|
|
58078 |
cue.adEndTime = cue.adStartTime + adTotal;
|
|
|
58079 |
track.addCue(cue);
|
|
|
58080 |
}
|
|
|
58081 |
}
|
|
|
58082 |
mediaTime += segment.duration;
|
|
|
58083 |
}
|
|
|
58084 |
};
|
|
|
58085 |
|
|
|
58086 |
/**
|
|
|
58087 |
* @file sync-controller.js
|
|
|
58088 |
*/
|
|
|
58089 |
// synchronize expired playlist segments.
|
|
|
58090 |
// the max media sequence diff is 48 hours of live stream
|
|
|
58091 |
// content with two second segments. Anything larger than that
|
|
|
58092 |
// will likely be invalid.
|
|
|
58093 |
|
|
|
58094 |
const MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC = 86400;
|
|
|
58095 |
const syncPointStrategies = [
|
|
|
58096 |
// Stategy "VOD": Handle the VOD-case where the sync-point is *always*
|
|
|
58097 |
// the equivalence display-time 0 === segment-index 0
|
|
|
58098 |
{
|
|
|
58099 |
name: 'VOD',
|
|
|
58100 |
run: (syncController, playlist, duration, currentTimeline, currentTime) => {
|
|
|
58101 |
if (duration !== Infinity) {
|
|
|
58102 |
const syncPoint = {
|
|
|
58103 |
time: 0,
|
|
|
58104 |
segmentIndex: 0,
|
|
|
58105 |
partIndex: null
|
|
|
58106 |
};
|
|
|
58107 |
return syncPoint;
|
|
|
58108 |
}
|
|
|
58109 |
return null;
|
|
|
58110 |
}
|
|
|
58111 |
}, {
|
|
|
58112 |
name: 'MediaSequence',
|
|
|
58113 |
/**
|
|
|
58114 |
* run media sequence strategy
|
|
|
58115 |
*
|
|
|
58116 |
* @param {SyncController} syncController
|
|
|
58117 |
* @param {Object} playlist
|
|
|
58118 |
* @param {number} duration
|
|
|
58119 |
* @param {number} currentTimeline
|
|
|
58120 |
* @param {number} currentTime
|
|
|
58121 |
* @param {string} type
|
|
|
58122 |
*/
|
|
|
58123 |
run: (syncController, playlist, duration, currentTimeline, currentTime, type) => {
|
|
|
58124 |
if (!type) {
|
|
|
58125 |
return null;
|
|
|
58126 |
}
|
|
|
58127 |
const mediaSequenceMap = syncController.getMediaSequenceMap(type);
|
|
|
58128 |
if (!mediaSequenceMap || mediaSequenceMap.size === 0) {
|
|
|
58129 |
return null;
|
|
|
58130 |
}
|
|
|
58131 |
if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) {
|
|
|
58132 |
return null;
|
|
|
58133 |
}
|
|
|
58134 |
let currentMediaSequence = playlist.mediaSequence;
|
|
|
58135 |
let segmentIndex = 0;
|
|
|
58136 |
for (const segment of playlist.segments) {
|
|
|
58137 |
const range = mediaSequenceMap.get(currentMediaSequence);
|
|
|
58138 |
if (!range) {
|
|
|
58139 |
// unexpected case
|
|
|
58140 |
// we expect this playlist to be the same playlist in the map
|
|
|
58141 |
// just break from the loop and move forward to the next strategy
|
|
|
58142 |
break;
|
|
|
58143 |
}
|
|
|
58144 |
if (currentTime >= range.start && currentTime < range.end) {
|
|
|
58145 |
// we found segment
|
|
|
58146 |
if (Array.isArray(segment.parts) && segment.parts.length) {
|
|
|
58147 |
let currentPartStart = range.start;
|
|
|
58148 |
let partIndex = 0;
|
|
|
58149 |
for (const part of segment.parts) {
|
|
|
58150 |
const start = currentPartStart;
|
|
|
58151 |
const end = start + part.duration;
|
|
|
58152 |
if (currentTime >= start && currentTime < end) {
|
|
|
58153 |
return {
|
|
|
58154 |
time: range.start,
|
|
|
58155 |
segmentIndex,
|
|
|
58156 |
partIndex
|
|
|
58157 |
};
|
|
|
58158 |
}
|
|
|
58159 |
partIndex++;
|
|
|
58160 |
currentPartStart = end;
|
|
|
58161 |
}
|
|
|
58162 |
} // no parts found, return sync point for segment
|
|
|
58163 |
|
|
|
58164 |
return {
|
|
|
58165 |
time: range.start,
|
|
|
58166 |
segmentIndex,
|
|
|
58167 |
partIndex: null
|
|
|
58168 |
};
|
|
|
58169 |
}
|
|
|
58170 |
segmentIndex++;
|
|
|
58171 |
currentMediaSequence++;
|
|
|
58172 |
} // we didn't find any segments for provided current time
|
|
|
58173 |
|
|
|
58174 |
return null;
|
|
|
58175 |
}
|
|
|
58176 |
},
|
|
|
58177 |
// Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
|
|
|
58178 |
{
|
|
|
58179 |
name: 'ProgramDateTime',
|
|
|
58180 |
run: (syncController, playlist, duration, currentTimeline, currentTime) => {
|
|
|
58181 |
if (!Object.keys(syncController.timelineToDatetimeMappings).length) {
|
|
|
58182 |
return null;
|
|
|
58183 |
}
|
|
|
58184 |
let syncPoint = null;
|
|
|
58185 |
let lastDistance = null;
|
|
|
58186 |
const partsAndSegments = getPartsAndSegments(playlist);
|
|
|
58187 |
currentTime = currentTime || 0;
|
|
|
58188 |
for (let i = 0; i < partsAndSegments.length; i++) {
|
|
|
58189 |
// start from the end and loop backwards for live
|
|
|
58190 |
// or start from the front and loop forwards for non-live
|
|
|
58191 |
const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1);
|
|
|
58192 |
const partAndSegment = partsAndSegments[index];
|
|
|
58193 |
const segment = partAndSegment.segment;
|
|
|
58194 |
const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline];
|
|
|
58195 |
if (!datetimeMapping || !segment.dateTimeObject) {
|
|
|
58196 |
continue;
|
|
|
58197 |
}
|
|
|
58198 |
const segmentTime = segment.dateTimeObject.getTime() / 1000;
|
|
|
58199 |
let start = segmentTime + datetimeMapping; // take part duration into account.
|
|
|
58200 |
|
|
|
58201 |
if (segment.parts && typeof partAndSegment.partIndex === 'number') {
|
|
|
58202 |
for (let z = 0; z < partAndSegment.partIndex; z++) {
|
|
|
58203 |
start += segment.parts[z].duration;
|
|
|
58204 |
}
|
|
|
58205 |
}
|
|
|
58206 |
const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed
|
|
|
58207 |
// currentTime and can stop looking for better candidates
|
|
|
58208 |
|
|
|
58209 |
if (lastDistance !== null && (distance === 0 || lastDistance < distance)) {
|
|
|
58210 |
break;
|
|
|
58211 |
}
|
|
|
58212 |
lastDistance = distance;
|
|
|
58213 |
syncPoint = {
|
|
|
58214 |
time: start,
|
|
|
58215 |
segmentIndex: partAndSegment.segmentIndex,
|
|
|
58216 |
partIndex: partAndSegment.partIndex
|
|
|
58217 |
};
|
|
|
58218 |
}
|
|
|
58219 |
return syncPoint;
|
|
|
58220 |
}
|
|
|
58221 |
},
|
|
|
58222 |
// Stategy "Segment": We have a known time mapping for a timeline and a
|
|
|
58223 |
// segment in the current timeline with timing data
|
|
|
58224 |
{
|
|
|
58225 |
name: 'Segment',
|
|
|
58226 |
run: (syncController, playlist, duration, currentTimeline, currentTime) => {
|
|
|
58227 |
let syncPoint = null;
|
|
|
58228 |
let lastDistance = null;
|
|
|
58229 |
currentTime = currentTime || 0;
|
|
|
58230 |
const partsAndSegments = getPartsAndSegments(playlist);
|
|
|
58231 |
for (let i = 0; i < partsAndSegments.length; i++) {
|
|
|
58232 |
// start from the end and loop backwards for live
|
|
|
58233 |
// or start from the front and loop forwards for non-live
|
|
|
58234 |
const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1);
|
|
|
58235 |
const partAndSegment = partsAndSegments[index];
|
|
|
58236 |
const segment = partAndSegment.segment;
|
|
|
58237 |
const start = partAndSegment.part && partAndSegment.part.start || segment && segment.start;
|
|
|
58238 |
if (segment.timeline === currentTimeline && typeof start !== 'undefined') {
|
|
|
58239 |
const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed
|
|
|
58240 |
// currentTime and can stop looking for better candidates
|
|
|
58241 |
|
|
|
58242 |
if (lastDistance !== null && lastDistance < distance) {
|
|
|
58243 |
break;
|
|
|
58244 |
}
|
|
|
58245 |
if (!syncPoint || lastDistance === null || lastDistance >= distance) {
|
|
|
58246 |
lastDistance = distance;
|
|
|
58247 |
syncPoint = {
|
|
|
58248 |
time: start,
|
|
|
58249 |
segmentIndex: partAndSegment.segmentIndex,
|
|
|
58250 |
partIndex: partAndSegment.partIndex
|
|
|
58251 |
};
|
|
|
58252 |
}
|
|
|
58253 |
}
|
|
|
58254 |
}
|
|
|
58255 |
return syncPoint;
|
|
|
58256 |
}
|
|
|
58257 |
},
|
|
|
58258 |
// Stategy "Discontinuity": We have a discontinuity with a known
|
|
|
58259 |
// display-time
|
|
|
58260 |
{
|
|
|
58261 |
name: 'Discontinuity',
|
|
|
58262 |
run: (syncController, playlist, duration, currentTimeline, currentTime) => {
|
|
|
58263 |
let syncPoint = null;
|
|
|
58264 |
currentTime = currentTime || 0;
|
|
|
58265 |
if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
|
|
|
58266 |
let lastDistance = null;
|
|
|
58267 |
for (let i = 0; i < playlist.discontinuityStarts.length; i++) {
|
|
|
58268 |
const segmentIndex = playlist.discontinuityStarts[i];
|
|
|
58269 |
const discontinuity = playlist.discontinuitySequence + i + 1;
|
|
|
58270 |
const discontinuitySync = syncController.discontinuities[discontinuity];
|
|
|
58271 |
if (discontinuitySync) {
|
|
|
58272 |
const distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed
|
|
|
58273 |
// currentTime and can stop looking for better candidates
|
|
|
58274 |
|
|
|
58275 |
if (lastDistance !== null && lastDistance < distance) {
|
|
|
58276 |
break;
|
|
|
58277 |
}
|
|
|
58278 |
if (!syncPoint || lastDistance === null || lastDistance >= distance) {
|
|
|
58279 |
lastDistance = distance;
|
|
|
58280 |
syncPoint = {
|
|
|
58281 |
time: discontinuitySync.time,
|
|
|
58282 |
segmentIndex,
|
|
|
58283 |
partIndex: null
|
|
|
58284 |
};
|
|
|
58285 |
}
|
|
|
58286 |
}
|
|
|
58287 |
}
|
|
|
58288 |
}
|
|
|
58289 |
return syncPoint;
|
|
|
58290 |
}
|
|
|
58291 |
},
|
|
|
58292 |
// Stategy "Playlist": We have a playlist with a known mapping of
|
|
|
58293 |
// segment index to display time
|
|
|
58294 |
{
|
|
|
58295 |
name: 'Playlist',
|
|
|
58296 |
run: (syncController, playlist, duration, currentTimeline, currentTime) => {
|
|
|
58297 |
if (playlist.syncInfo) {
|
|
|
58298 |
const syncPoint = {
|
|
|
58299 |
time: playlist.syncInfo.time,
|
|
|
58300 |
segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence,
|
|
|
58301 |
partIndex: null
|
|
|
58302 |
};
|
|
|
58303 |
return syncPoint;
|
|
|
58304 |
}
|
|
|
58305 |
return null;
|
|
|
58306 |
}
|
|
|
58307 |
}];
|
|
|
58308 |
class SyncController extends videojs.EventTarget {
|
|
|
58309 |
constructor(options = {}) {
|
|
|
58310 |
super(); // ...for synching across variants
|
|
|
58311 |
|
|
|
58312 |
this.timelines = [];
|
|
|
58313 |
this.discontinuities = [];
|
|
|
58314 |
this.timelineToDatetimeMappings = {};
|
|
|
58315 |
/**
|
|
|
58316 |
* @type {Map<string, Map<number, { start: number, end: number }>>}
|
|
|
58317 |
* @private
|
|
|
58318 |
*/
|
|
|
58319 |
|
|
|
58320 |
this.mediaSequenceStorage_ = new Map();
|
|
|
58321 |
this.logger_ = logger('SyncController');
|
|
|
58322 |
}
|
|
|
58323 |
/**
|
|
|
58324 |
* Get media sequence map by type
|
|
|
58325 |
*
|
|
|
58326 |
* @param {string} type - segment loader type
|
|
|
58327 |
* @return {Map<number, { start: number, end: number }> | undefined}
|
|
|
58328 |
*/
|
|
|
58329 |
|
|
|
58330 |
getMediaSequenceMap(type) {
|
|
|
58331 |
return this.mediaSequenceStorage_.get(type);
|
|
|
58332 |
}
|
|
|
58333 |
/**
|
|
|
58334 |
* Update Media Sequence Map -> <MediaSequence, Range>
|
|
|
58335 |
*
|
|
|
58336 |
* @param {Object} playlist - parsed playlist
|
|
|
58337 |
* @param {number} currentTime - current player's time
|
|
|
58338 |
* @param {string} type - segment loader type
|
|
|
58339 |
* @return {void}
|
|
|
58340 |
*/
|
|
|
58341 |
|
|
|
58342 |
updateMediaSequenceMap(playlist, currentTime, type) {
|
|
|
58343 |
// we should not process this playlist if it does not have mediaSequence or segments
|
|
|
58344 |
if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) {
|
|
|
58345 |
return;
|
|
|
58346 |
}
|
|
|
58347 |
const currentMap = this.getMediaSequenceMap(type);
|
|
|
58348 |
const result = new Map();
|
|
|
58349 |
let currentMediaSequence = playlist.mediaSequence;
|
|
|
58350 |
let currentBaseTime;
|
|
|
58351 |
if (!currentMap) {
|
|
|
58352 |
// first playlist setup:
|
|
|
58353 |
currentBaseTime = 0;
|
|
|
58354 |
} else if (currentMap.has(playlist.mediaSequence)) {
|
|
|
58355 |
// further playlists setup:
|
|
|
58356 |
currentBaseTime = currentMap.get(playlist.mediaSequence).start;
|
|
|
58357 |
} else {
|
|
|
58358 |
// it seems like we have a gap between playlists, use current time as a fallback:
|
|
|
58359 |
this.logger_(`MediaSequence sync for ${type} segment loader - received a gap between playlists.
|
|
|
58360 |
Fallback base time to: ${currentTime}.
|
|
|
58361 |
Received media sequence: ${currentMediaSequence}.
|
|
|
58362 |
Current map: `, currentMap);
|
|
|
58363 |
currentBaseTime = currentTime;
|
|
|
58364 |
}
|
|
|
58365 |
this.logger_(`MediaSequence sync for ${type} segment loader.
|
|
|
58366 |
Received media sequence: ${currentMediaSequence}.
|
|
|
58367 |
base time is ${currentBaseTime}
|
|
|
58368 |
Current map: `, currentMap);
|
|
|
58369 |
playlist.segments.forEach(segment => {
|
|
|
58370 |
const start = currentBaseTime;
|
|
|
58371 |
const end = start + segment.duration;
|
|
|
58372 |
const range = {
|
|
|
58373 |
start,
|
|
|
58374 |
end
|
|
|
58375 |
};
|
|
|
58376 |
result.set(currentMediaSequence, range);
|
|
|
58377 |
currentMediaSequence++;
|
|
|
58378 |
currentBaseTime = end;
|
|
|
58379 |
});
|
|
|
58380 |
this.mediaSequenceStorage_.set(type, result);
|
|
|
58381 |
}
|
|
|
58382 |
/**
|
|
|
58383 |
* Find a sync-point for the playlist specified
|
|
|
58384 |
*
|
|
|
58385 |
* A sync-point is defined as a known mapping from display-time to
|
|
|
58386 |
* a segment-index in the current playlist.
|
|
|
58387 |
*
|
|
|
58388 |
* @param {Playlist} playlist
|
|
|
58389 |
* The playlist that needs a sync-point
|
|
|
58390 |
* @param {number} duration
|
|
|
58391 |
* Duration of the MediaSource (Infinite if playing a live source)
|
|
|
58392 |
* @param {number} currentTimeline
|
|
|
58393 |
* The last timeline from which a segment was loaded
|
|
|
58394 |
* @param {number} currentTime
|
|
|
58395 |
* Current player's time
|
|
|
58396 |
* @param {string} type
|
|
|
58397 |
* Segment loader type
|
|
|
58398 |
* @return {Object}
|
|
|
58399 |
* A sync-point object
|
|
|
58400 |
*/
|
|
|
58401 |
|
|
|
58402 |
getSyncPoint(playlist, duration, currentTimeline, currentTime, type) {
|
|
|
58403 |
// Always use VOD sync point for VOD
|
|
|
58404 |
if (duration !== Infinity) {
|
|
|
58405 |
const vodSyncPointStrategy = syncPointStrategies.find(({
|
|
|
58406 |
name
|
|
|
58407 |
}) => name === 'VOD');
|
|
|
58408 |
return vodSyncPointStrategy.run(this, playlist, duration);
|
|
|
58409 |
}
|
|
|
58410 |
const syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime, type);
|
|
|
58411 |
if (!syncPoints.length) {
|
|
|
58412 |
// Signal that we need to attempt to get a sync-point manually
|
|
|
58413 |
// by fetching a segment in the playlist and constructing
|
|
|
58414 |
// a sync-point from that information
|
|
|
58415 |
return null;
|
|
|
58416 |
} // If we have exact match just return it instead of finding the nearest distance
|
|
|
58417 |
|
|
|
58418 |
for (const syncPointInfo of syncPoints) {
|
|
|
58419 |
const {
|
|
|
58420 |
syncPoint,
|
|
|
58421 |
strategy
|
|
|
58422 |
} = syncPointInfo;
|
|
|
58423 |
const {
|
|
|
58424 |
segmentIndex,
|
|
|
58425 |
time
|
|
|
58426 |
} = syncPoint;
|
|
|
58427 |
if (segmentIndex < 0) {
|
|
|
58428 |
continue;
|
|
|
58429 |
}
|
|
|
58430 |
const selectedSegment = playlist.segments[segmentIndex];
|
|
|
58431 |
const start = time;
|
|
|
58432 |
const end = start + selectedSegment.duration;
|
|
|
58433 |
this.logger_(`Strategy: ${strategy}. Current time: ${currentTime}. selected segment: ${segmentIndex}. Time: [${start} -> ${end}]}`);
|
|
|
58434 |
if (currentTime >= start && currentTime < end) {
|
|
|
58435 |
this.logger_('Found sync point with exact match: ', syncPoint);
|
|
|
58436 |
return syncPoint;
|
|
|
58437 |
}
|
|
|
58438 |
} // Now find the sync-point that is closest to the currentTime because
|
|
|
58439 |
// that should result in the most accurate guess about which segment
|
|
|
58440 |
// to fetch
|
|
|
58441 |
|
|
|
58442 |
return this.selectSyncPoint_(syncPoints, {
|
|
|
58443 |
key: 'time',
|
|
|
58444 |
value: currentTime
|
|
|
58445 |
});
|
|
|
58446 |
}
|
|
|
58447 |
/**
|
|
|
58448 |
* Calculate the amount of time that has expired off the playlist during playback
|
|
|
58449 |
*
|
|
|
58450 |
* @param {Playlist} playlist
|
|
|
58451 |
* Playlist object to calculate expired from
|
|
|
58452 |
* @param {number} duration
|
|
|
58453 |
* Duration of the MediaSource (Infinity if playling a live source)
|
|
|
58454 |
* @return {number|null}
|
|
|
58455 |
* The amount of time that has expired off the playlist during playback. Null
|
|
|
58456 |
* if no sync-points for the playlist can be found.
|
|
|
58457 |
*/
|
|
|
58458 |
|
|
|
58459 |
getExpiredTime(playlist, duration) {
|
|
|
58460 |
if (!playlist || !playlist.segments) {
|
|
|
58461 |
return null;
|
|
|
58462 |
}
|
|
|
58463 |
const syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0, 'main'); // Without sync-points, there is not enough information to determine the expired time
|
|
|
58464 |
|
|
|
58465 |
if (!syncPoints.length) {
|
|
|
58466 |
return null;
|
|
|
58467 |
}
|
|
|
58468 |
const syncPoint = this.selectSyncPoint_(syncPoints, {
|
|
|
58469 |
key: 'segmentIndex',
|
|
|
58470 |
value: 0
|
|
|
58471 |
}); // If the sync-point is beyond the start of the playlist, we want to subtract the
|
|
|
58472 |
// duration from index 0 to syncPoint.segmentIndex instead of adding.
|
|
|
58473 |
|
|
|
58474 |
if (syncPoint.segmentIndex > 0) {
|
|
|
58475 |
syncPoint.time *= -1;
|
|
|
58476 |
}
|
|
|
58477 |
return Math.abs(syncPoint.time + sumDurations({
|
|
|
58478 |
defaultDuration: playlist.targetDuration,
|
|
|
58479 |
durationList: playlist.segments,
|
|
|
58480 |
startIndex: syncPoint.segmentIndex,
|
|
|
58481 |
endIndex: 0
|
|
|
58482 |
}));
|
|
|
58483 |
}
|
|
|
58484 |
/**
|
|
|
58485 |
* Runs each sync-point strategy and returns a list of sync-points returned by the
|
|
|
58486 |
* strategies
|
|
|
58487 |
*
|
|
|
58488 |
* @private
|
|
|
58489 |
* @param {Playlist} playlist
|
|
|
58490 |
* The playlist that needs a sync-point
|
|
|
58491 |
* @param {number} duration
|
|
|
58492 |
* Duration of the MediaSource (Infinity if playing a live source)
|
|
|
58493 |
* @param {number} currentTimeline
|
|
|
58494 |
* The last timeline from which a segment was loaded
|
|
|
58495 |
* @param {number} currentTime
|
|
|
58496 |
* Current player's time
|
|
|
58497 |
* @param {string} type
|
|
|
58498 |
* Segment loader type
|
|
|
58499 |
* @return {Array}
|
|
|
58500 |
* A list of sync-point objects
|
|
|
58501 |
*/
|
|
|
58502 |
|
|
|
58503 |
runStrategies_(playlist, duration, currentTimeline, currentTime, type) {
|
|
|
58504 |
const syncPoints = []; // Try to find a sync-point in by utilizing various strategies...
|
|
|
58505 |
|
|
|
58506 |
for (let i = 0; i < syncPointStrategies.length; i++) {
|
|
|
58507 |
const strategy = syncPointStrategies[i];
|
|
|
58508 |
const syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime, type);
|
|
|
58509 |
if (syncPoint) {
|
|
|
58510 |
syncPoint.strategy = strategy.name;
|
|
|
58511 |
syncPoints.push({
|
|
|
58512 |
strategy: strategy.name,
|
|
|
58513 |
syncPoint
|
|
|
58514 |
});
|
|
|
58515 |
}
|
|
|
58516 |
}
|
|
|
58517 |
return syncPoints;
|
|
|
58518 |
}
|
|
|
58519 |
/**
|
|
|
58520 |
* Selects the sync-point nearest the specified target
|
|
|
58521 |
*
|
|
|
58522 |
* @private
|
|
|
58523 |
* @param {Array} syncPoints
|
|
|
58524 |
* List of sync-points to select from
|
|
|
58525 |
* @param {Object} target
|
|
|
58526 |
* Object specifying the property and value we are targeting
|
|
|
58527 |
* @param {string} target.key
|
|
|
58528 |
* Specifies the property to target. Must be either 'time' or 'segmentIndex'
|
|
|
58529 |
* @param {number} target.value
|
|
|
58530 |
* The value to target for the specified key.
|
|
|
58531 |
* @return {Object}
|
|
|
58532 |
* The sync-point nearest the target
|
|
|
58533 |
*/
|
|
|
58534 |
|
|
|
58535 |
selectSyncPoint_(syncPoints, target) {
|
|
|
58536 |
let bestSyncPoint = syncPoints[0].syncPoint;
|
|
|
58537 |
let bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value);
|
|
|
58538 |
let bestStrategy = syncPoints[0].strategy;
|
|
|
58539 |
for (let i = 1; i < syncPoints.length; i++) {
|
|
|
58540 |
const newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value);
|
|
|
58541 |
if (newDistance < bestDistance) {
|
|
|
58542 |
bestDistance = newDistance;
|
|
|
58543 |
bestSyncPoint = syncPoints[i].syncPoint;
|
|
|
58544 |
bestStrategy = syncPoints[i].strategy;
|
|
|
58545 |
}
|
|
|
58546 |
}
|
|
|
58547 |
this.logger_(`syncPoint for [${target.key}: ${target.value}] chosen with strategy` + ` [${bestStrategy}]: [time:${bestSyncPoint.time},` + ` segmentIndex:${bestSyncPoint.segmentIndex}` + (typeof bestSyncPoint.partIndex === 'number' ? `,partIndex:${bestSyncPoint.partIndex}` : '') + ']');
|
|
|
58548 |
return bestSyncPoint;
|
|
|
58549 |
}
|
|
|
58550 |
/**
|
|
|
58551 |
* Save any meta-data present on the segments when segments leave
|
|
|
58552 |
* the live window to the playlist to allow for synchronization at the
|
|
|
58553 |
* playlist level later.
|
|
|
58554 |
*
|
|
|
58555 |
* @param {Playlist} oldPlaylist - The previous active playlist
|
|
|
58556 |
* @param {Playlist} newPlaylist - The updated and most current playlist
|
|
|
58557 |
*/
|
|
|
58558 |
|
|
|
58559 |
saveExpiredSegmentInfo(oldPlaylist, newPlaylist) {
|
|
|
58560 |
const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // Ignore large media sequence gaps
|
|
|
58561 |
|
|
|
58562 |
if (mediaSequenceDiff > MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC) {
|
|
|
58563 |
videojs.log.warn(`Not saving expired segment info. Media sequence gap ${mediaSequenceDiff} is too large.`);
|
|
|
58564 |
return;
|
|
|
58565 |
} // When a segment expires from the playlist and it has a start time
|
|
|
58566 |
// save that information as a possible sync-point reference in future
|
|
|
58567 |
|
|
|
58568 |
for (let i = mediaSequenceDiff - 1; i >= 0; i--) {
|
|
|
58569 |
const lastRemovedSegment = oldPlaylist.segments[i];
|
|
|
58570 |
if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') {
|
|
|
58571 |
newPlaylist.syncInfo = {
|
|
|
58572 |
mediaSequence: oldPlaylist.mediaSequence + i,
|
|
|
58573 |
time: lastRemovedSegment.start
|
|
|
58574 |
};
|
|
|
58575 |
this.logger_(`playlist refresh sync: [time:${newPlaylist.syncInfo.time},` + ` mediaSequence: ${newPlaylist.syncInfo.mediaSequence}]`);
|
|
|
58576 |
this.trigger('syncinfoupdate');
|
|
|
58577 |
break;
|
|
|
58578 |
}
|
|
|
58579 |
}
|
|
|
58580 |
}
|
|
|
58581 |
/**
|
|
|
58582 |
* Save the mapping from playlist's ProgramDateTime to display. This should only happen
|
|
|
58583 |
* before segments start to load.
|
|
|
58584 |
*
|
|
|
58585 |
* @param {Playlist} playlist - The currently active playlist
|
|
|
58586 |
*/
|
|
|
58587 |
|
|
|
58588 |
setDateTimeMappingForStart(playlist) {
|
|
|
58589 |
// It's possible for the playlist to be updated before playback starts, meaning time
|
|
|
58590 |
// zero is not yet set. If, during these playlist refreshes, a discontinuity is
|
|
|
58591 |
// crossed, then the old time zero mapping (for the prior timeline) would be retained
|
|
|
58592 |
// unless the mappings are cleared.
|
|
|
58593 |
this.timelineToDatetimeMappings = {};
|
|
|
58594 |
if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) {
|
|
|
58595 |
const firstSegment = playlist.segments[0];
|
|
|
58596 |
const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000;
|
|
|
58597 |
this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp;
|
|
|
58598 |
}
|
|
|
58599 |
}
|
|
|
58600 |
/**
|
|
|
58601 |
* Calculates and saves timeline mappings, playlist sync info, and segment timing values
|
|
|
58602 |
* based on the latest timing information.
|
|
|
58603 |
*
|
|
|
58604 |
* @param {Object} options
|
|
|
58605 |
* Options object
|
|
|
58606 |
* @param {SegmentInfo} options.segmentInfo
|
|
|
58607 |
* The current active request information
|
|
|
58608 |
* @param {boolean} options.shouldSaveTimelineMapping
|
|
|
58609 |
* If there's a timeline change, determines if the timeline mapping should be
|
|
|
58610 |
* saved for timeline mapping and program date time mappings.
|
|
|
58611 |
*/
|
|
|
58612 |
|
|
|
58613 |
saveSegmentTimingInfo({
|
|
|
58614 |
segmentInfo,
|
|
|
58615 |
shouldSaveTimelineMapping
|
|
|
58616 |
}) {
|
|
|
58617 |
const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_(segmentInfo, segmentInfo.timingInfo, shouldSaveTimelineMapping);
|
|
|
58618 |
const segment = segmentInfo.segment;
|
|
|
58619 |
if (didCalculateSegmentTimeMapping) {
|
|
|
58620 |
this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information
|
|
|
58621 |
// now with segment timing information
|
|
|
58622 |
|
|
|
58623 |
if (!segmentInfo.playlist.syncInfo) {
|
|
|
58624 |
segmentInfo.playlist.syncInfo = {
|
|
|
58625 |
mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex,
|
|
|
58626 |
time: segment.start
|
|
|
58627 |
};
|
|
|
58628 |
}
|
|
|
58629 |
}
|
|
|
58630 |
const dateTime = segment.dateTimeObject;
|
|
|
58631 |
if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) {
|
|
|
58632 |
this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000);
|
|
|
58633 |
}
|
|
|
58634 |
}
|
|
|
58635 |
timestampOffsetForTimeline(timeline) {
|
|
|
58636 |
if (typeof this.timelines[timeline] === 'undefined') {
|
|
|
58637 |
return null;
|
|
|
58638 |
}
|
|
|
58639 |
return this.timelines[timeline].time;
|
|
|
58640 |
}
|
|
|
58641 |
mappingForTimeline(timeline) {
|
|
|
58642 |
if (typeof this.timelines[timeline] === 'undefined') {
|
|
|
58643 |
return null;
|
|
|
58644 |
}
|
|
|
58645 |
return this.timelines[timeline].mapping;
|
|
|
58646 |
}
|
|
|
58647 |
/**
|
|
|
58648 |
* Use the "media time" for a segment to generate a mapping to "display time" and
|
|
|
58649 |
* save that display time to the segment.
|
|
|
58650 |
*
|
|
|
58651 |
* @private
|
|
|
58652 |
* @param {SegmentInfo} segmentInfo
|
|
|
58653 |
* The current active request information
|
|
|
58654 |
* @param {Object} timingInfo
|
|
|
58655 |
* The start and end time of the current segment in "media time"
|
|
|
58656 |
* @param {boolean} shouldSaveTimelineMapping
|
|
|
58657 |
* If there's a timeline change, determines if the timeline mapping should be
|
|
|
58658 |
* saved in timelines.
|
|
|
58659 |
* @return {boolean}
|
|
|
58660 |
* Returns false if segment time mapping could not be calculated
|
|
|
58661 |
*/
|
|
|
58662 |
|
|
|
58663 |
calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) {
|
|
|
58664 |
// TODO: remove side effects
|
|
|
58665 |
const segment = segmentInfo.segment;
|
|
|
58666 |
const part = segmentInfo.part;
|
|
|
58667 |
let mappingObj = this.timelines[segmentInfo.timeline];
|
|
|
58668 |
let start;
|
|
|
58669 |
let end;
|
|
|
58670 |
if (typeof segmentInfo.timestampOffset === 'number') {
|
|
|
58671 |
mappingObj = {
|
|
|
58672 |
time: segmentInfo.startOfSegment,
|
|
|
58673 |
mapping: segmentInfo.startOfSegment - timingInfo.start
|
|
|
58674 |
};
|
|
|
58675 |
if (shouldSaveTimelineMapping) {
|
|
|
58676 |
this.timelines[segmentInfo.timeline] = mappingObj;
|
|
|
58677 |
this.trigger('timestampoffset');
|
|
|
58678 |
this.logger_(`time mapping for timeline ${segmentInfo.timeline}: ` + `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]`);
|
|
|
58679 |
}
|
|
|
58680 |
start = segmentInfo.startOfSegment;
|
|
|
58681 |
end = timingInfo.end + mappingObj.mapping;
|
|
|
58682 |
} else if (mappingObj) {
|
|
|
58683 |
start = timingInfo.start + mappingObj.mapping;
|
|
|
58684 |
end = timingInfo.end + mappingObj.mapping;
|
|
|
58685 |
} else {
|
|
|
58686 |
return false;
|
|
|
58687 |
}
|
|
|
58688 |
if (part) {
|
|
|
58689 |
part.start = start;
|
|
|
58690 |
part.end = end;
|
|
|
58691 |
} // If we don't have a segment start yet or the start value we got
|
|
|
58692 |
// is less than our current segment.start value, save a new start value.
|
|
|
58693 |
// We have to do this because parts will have segment timing info saved
|
|
|
58694 |
// multiple times and we want segment start to be the earliest part start
|
|
|
58695 |
// value for that segment.
|
|
|
58696 |
|
|
|
58697 |
if (!segment.start || start < segment.start) {
|
|
|
58698 |
segment.start = start;
|
|
|
58699 |
}
|
|
|
58700 |
segment.end = end;
|
|
|
58701 |
return true;
|
|
|
58702 |
}
|
|
|
58703 |
/**
|
|
|
58704 |
* Each time we have discontinuity in the playlist, attempt to calculate the location
|
|
|
58705 |
* in display of the start of the discontinuity and save that. We also save an accuracy
|
|
|
58706 |
* value so that we save values with the most accuracy (closest to 0.)
|
|
|
58707 |
*
|
|
|
58708 |
* @private
|
|
|
58709 |
* @param {SegmentInfo} segmentInfo - The current active request information
|
|
|
58710 |
*/
|
|
|
58711 |
|
|
|
58712 |
saveDiscontinuitySyncInfo_(segmentInfo) {
|
|
|
58713 |
const playlist = segmentInfo.playlist;
|
|
|
58714 |
const segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where
|
|
|
58715 |
// the start of the range and it's accuracy is 0 (greater accuracy values
|
|
|
58716 |
// mean more approximation)
|
|
|
58717 |
|
|
|
58718 |
if (segment.discontinuity) {
|
|
|
58719 |
this.discontinuities[segment.timeline] = {
|
|
|
58720 |
time: segment.start,
|
|
|
58721 |
accuracy: 0
|
|
|
58722 |
};
|
|
|
58723 |
} else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
|
|
|
58724 |
// Search for future discontinuities that we can provide better timing
|
|
|
58725 |
// information for and save that information for sync purposes
|
|
|
58726 |
for (let i = 0; i < playlist.discontinuityStarts.length; i++) {
|
|
|
58727 |
const segmentIndex = playlist.discontinuityStarts[i];
|
|
|
58728 |
const discontinuity = playlist.discontinuitySequence + i + 1;
|
|
|
58729 |
const mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex;
|
|
|
58730 |
const accuracy = Math.abs(mediaIndexDiff);
|
|
|
58731 |
if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) {
|
|
|
58732 |
let time;
|
|
|
58733 |
if (mediaIndexDiff < 0) {
|
|
|
58734 |
time = segment.start - sumDurations({
|
|
|
58735 |
defaultDuration: playlist.targetDuration,
|
|
|
58736 |
durationList: playlist.segments,
|
|
|
58737 |
startIndex: segmentInfo.mediaIndex,
|
|
|
58738 |
endIndex: segmentIndex
|
|
|
58739 |
});
|
|
|
58740 |
} else {
|
|
|
58741 |
time = segment.end + sumDurations({
|
|
|
58742 |
defaultDuration: playlist.targetDuration,
|
|
|
58743 |
durationList: playlist.segments,
|
|
|
58744 |
startIndex: segmentInfo.mediaIndex + 1,
|
|
|
58745 |
endIndex: segmentIndex
|
|
|
58746 |
});
|
|
|
58747 |
}
|
|
|
58748 |
this.discontinuities[discontinuity] = {
|
|
|
58749 |
time,
|
|
|
58750 |
accuracy
|
|
|
58751 |
};
|
|
|
58752 |
}
|
|
|
58753 |
}
|
|
|
58754 |
}
|
|
|
58755 |
}
|
|
|
58756 |
dispose() {
|
|
|
58757 |
this.trigger('dispose');
|
|
|
58758 |
this.off();
|
|
|
58759 |
}
|
|
|
58760 |
}
|
|
|
58761 |
|
|
|
58762 |
/**
|
|
|
58763 |
* The TimelineChangeController acts as a source for segment loaders to listen for and
|
|
|
58764 |
* keep track of latest and pending timeline changes. This is useful to ensure proper
|
|
|
58765 |
* sync, as each loader may need to make a consideration for what timeline the other
|
|
|
58766 |
* loader is on before making changes which could impact the other loader's media.
|
|
|
58767 |
*
|
|
|
58768 |
* @class TimelineChangeController
|
|
|
58769 |
* @extends videojs.EventTarget
|
|
|
58770 |
*/
|
|
|
58771 |
|
|
|
58772 |
class TimelineChangeController extends videojs.EventTarget {
|
|
|
58773 |
constructor() {
|
|
|
58774 |
super();
|
|
|
58775 |
this.pendingTimelineChanges_ = {};
|
|
|
58776 |
this.lastTimelineChanges_ = {};
|
|
|
58777 |
}
|
|
|
58778 |
clearPendingTimelineChange(type) {
|
|
|
58779 |
this.pendingTimelineChanges_[type] = null;
|
|
|
58780 |
this.trigger('pendingtimelinechange');
|
|
|
58781 |
}
|
|
|
58782 |
pendingTimelineChange({
|
|
|
58783 |
type,
|
|
|
58784 |
from,
|
|
|
58785 |
to
|
|
|
58786 |
}) {
|
|
|
58787 |
if (typeof from === 'number' && typeof to === 'number') {
|
|
|
58788 |
this.pendingTimelineChanges_[type] = {
|
|
|
58789 |
type,
|
|
|
58790 |
from,
|
|
|
58791 |
to
|
|
|
58792 |
};
|
|
|
58793 |
this.trigger('pendingtimelinechange');
|
|
|
58794 |
}
|
|
|
58795 |
return this.pendingTimelineChanges_[type];
|
|
|
58796 |
}
|
|
|
58797 |
lastTimelineChange({
|
|
|
58798 |
type,
|
|
|
58799 |
from,
|
|
|
58800 |
to
|
|
|
58801 |
}) {
|
|
|
58802 |
if (typeof from === 'number' && typeof to === 'number') {
|
|
|
58803 |
this.lastTimelineChanges_[type] = {
|
|
|
58804 |
type,
|
|
|
58805 |
from,
|
|
|
58806 |
to
|
|
|
58807 |
};
|
|
|
58808 |
delete this.pendingTimelineChanges_[type];
|
|
|
58809 |
this.trigger('timelinechange');
|
|
|
58810 |
}
|
|
|
58811 |
return this.lastTimelineChanges_[type];
|
|
|
58812 |
}
|
|
|
58813 |
dispose() {
|
|
|
58814 |
this.trigger('dispose');
|
|
|
58815 |
this.pendingTimelineChanges_ = {};
|
|
|
58816 |
this.lastTimelineChanges_ = {};
|
|
|
58817 |
this.off();
|
|
|
58818 |
}
|
|
|
58819 |
}
|
|
|
58820 |
|
|
|
58821 |
/* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/decrypter-worker.js */
|
|
|
58822 |
const workerCode = transform(getWorkerString(function () {
|
|
|
58823 |
/**
|
|
|
58824 |
* @file stream.js
|
|
|
58825 |
*/
|
|
|
58826 |
|
|
|
58827 |
/**
|
|
|
58828 |
* A lightweight readable stream implemention that handles event dispatching.
|
|
|
58829 |
*
|
|
|
58830 |
* @class Stream
|
|
|
58831 |
*/
|
|
|
58832 |
|
|
|
58833 |
var Stream = /*#__PURE__*/function () {
|
|
|
58834 |
function Stream() {
|
|
|
58835 |
this.listeners = {};
|
|
|
58836 |
}
|
|
|
58837 |
/**
|
|
|
58838 |
* Add a listener for a specified event type.
|
|
|
58839 |
*
|
|
|
58840 |
* @param {string} type the event name
|
|
|
58841 |
* @param {Function} listener the callback to be invoked when an event of
|
|
|
58842 |
* the specified type occurs
|
|
|
58843 |
*/
|
|
|
58844 |
|
|
|
58845 |
var _proto = Stream.prototype;
|
|
|
58846 |
_proto.on = function on(type, listener) {
|
|
|
58847 |
if (!this.listeners[type]) {
|
|
|
58848 |
this.listeners[type] = [];
|
|
|
58849 |
}
|
|
|
58850 |
this.listeners[type].push(listener);
|
|
|
58851 |
}
|
|
|
58852 |
/**
|
|
|
58853 |
* Remove a listener for a specified event type.
|
|
|
58854 |
*
|
|
|
58855 |
* @param {string} type the event name
|
|
|
58856 |
* @param {Function} listener a function previously registered for this
|
|
|
58857 |
* type of event through `on`
|
|
|
58858 |
* @return {boolean} if we could turn it off or not
|
|
|
58859 |
*/;
|
|
|
58860 |
|
|
|
58861 |
_proto.off = function off(type, listener) {
|
|
|
58862 |
if (!this.listeners[type]) {
|
|
|
58863 |
return false;
|
|
|
58864 |
}
|
|
|
58865 |
var index = this.listeners[type].indexOf(listener); // TODO: which is better?
|
|
|
58866 |
// In Video.js we slice listener functions
|
|
|
58867 |
// on trigger so that it does not mess up the order
|
|
|
58868 |
// while we loop through.
|
|
|
58869 |
//
|
|
|
58870 |
// Here we slice on off so that the loop in trigger
|
|
|
58871 |
// can continue using it's old reference to loop without
|
|
|
58872 |
// messing up the order.
|
|
|
58873 |
|
|
|
58874 |
this.listeners[type] = this.listeners[type].slice(0);
|
|
|
58875 |
this.listeners[type].splice(index, 1);
|
|
|
58876 |
return index > -1;
|
|
|
58877 |
}
|
|
|
58878 |
/**
|
|
|
58879 |
* Trigger an event of the specified type on this stream. Any additional
|
|
|
58880 |
* arguments to this function are passed as parameters to event listeners.
|
|
|
58881 |
*
|
|
|
58882 |
* @param {string} type the event name
|
|
|
58883 |
*/;
|
|
|
58884 |
|
|
|
58885 |
_proto.trigger = function trigger(type) {
|
|
|
58886 |
var callbacks = this.listeners[type];
|
|
|
58887 |
if (!callbacks) {
|
|
|
58888 |
return;
|
|
|
58889 |
} // Slicing the arguments on every invocation of this method
|
|
|
58890 |
// can add a significant amount of overhead. Avoid the
|
|
|
58891 |
// intermediate object creation for the common case of a
|
|
|
58892 |
// single callback argument
|
|
|
58893 |
|
|
|
58894 |
if (arguments.length === 2) {
|
|
|
58895 |
var length = callbacks.length;
|
|
|
58896 |
for (var i = 0; i < length; ++i) {
|
|
|
58897 |
callbacks[i].call(this, arguments[1]);
|
|
|
58898 |
}
|
|
|
58899 |
} else {
|
|
|
58900 |
var args = Array.prototype.slice.call(arguments, 1);
|
|
|
58901 |
var _length = callbacks.length;
|
|
|
58902 |
for (var _i = 0; _i < _length; ++_i) {
|
|
|
58903 |
callbacks[_i].apply(this, args);
|
|
|
58904 |
}
|
|
|
58905 |
}
|
|
|
58906 |
}
|
|
|
58907 |
/**
|
|
|
58908 |
* Destroys the stream and cleans up.
|
|
|
58909 |
*/;
|
|
|
58910 |
|
|
|
58911 |
_proto.dispose = function dispose() {
|
|
|
58912 |
this.listeners = {};
|
|
|
58913 |
}
|
|
|
58914 |
/**
|
|
|
58915 |
* Forwards all `data` events on this stream to the destination stream. The
|
|
|
58916 |
* destination stream should provide a method `push` to receive the data
|
|
|
58917 |
* events as they arrive.
|
|
|
58918 |
*
|
|
|
58919 |
* @param {Stream} destination the stream that will receive all `data` events
|
|
|
58920 |
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
|
|
|
58921 |
*/;
|
|
|
58922 |
|
|
|
58923 |
_proto.pipe = function pipe(destination) {
|
|
|
58924 |
this.on('data', function (data) {
|
|
|
58925 |
destination.push(data);
|
|
|
58926 |
});
|
|
|
58927 |
};
|
|
|
58928 |
return Stream;
|
|
|
58929 |
}();
|
|
|
58930 |
/*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */
|
|
|
58931 |
|
|
|
58932 |
/**
|
|
|
58933 |
* Returns the subarray of a Uint8Array without PKCS#7 padding.
|
|
|
58934 |
*
|
|
|
58935 |
* @param padded {Uint8Array} unencrypted bytes that have been padded
|
|
|
58936 |
* @return {Uint8Array} the unpadded bytes
|
|
|
58937 |
* @see http://tools.ietf.org/html/rfc5652
|
|
|
58938 |
*/
|
|
|
58939 |
|
|
|
58940 |
function unpad(padded) {
|
|
|
58941 |
return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]);
|
|
|
58942 |
}
|
|
|
58943 |
/*! @name aes-decrypter @version 4.0.1 @license Apache-2.0 */
|
|
|
58944 |
|
|
|
58945 |
/**
|
|
|
58946 |
* @file aes.js
|
|
|
58947 |
*
|
|
|
58948 |
* This file contains an adaptation of the AES decryption algorithm
|
|
|
58949 |
* from the Standford Javascript Cryptography Library. That work is
|
|
|
58950 |
* covered by the following copyright and permissions notice:
|
|
|
58951 |
*
|
|
|
58952 |
* Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
|
|
|
58953 |
* All rights reserved.
|
|
|
58954 |
*
|
|
|
58955 |
* Redistribution and use in source and binary forms, with or without
|
|
|
58956 |
* modification, are permitted provided that the following conditions are
|
|
|
58957 |
* met:
|
|
|
58958 |
*
|
|
|
58959 |
* 1. Redistributions of source code must retain the above copyright
|
|
|
58960 |
* notice, this list of conditions and the following disclaimer.
|
|
|
58961 |
*
|
|
|
58962 |
* 2. Redistributions in binary form must reproduce the above
|
|
|
58963 |
* copyright notice, this list of conditions and the following
|
|
|
58964 |
* disclaimer in the documentation and/or other materials provided
|
|
|
58965 |
* with the distribution.
|
|
|
58966 |
*
|
|
|
58967 |
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
|
|
|
58968 |
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
58969 |
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
58970 |
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
|
|
|
58971 |
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
58972 |
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
58973 |
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
|
58974 |
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
|
58975 |
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
|
|
58976 |
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
|
|
58977 |
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
58978 |
*
|
|
|
58979 |
* The views and conclusions contained in the software and documentation
|
|
|
58980 |
* are those of the authors and should not be interpreted as representing
|
|
|
58981 |
* official policies, either expressed or implied, of the authors.
|
|
|
58982 |
*/
|
|
|
58983 |
|
|
|
58984 |
/**
|
|
|
58985 |
* Expand the S-box tables.
|
|
|
58986 |
*
|
|
|
58987 |
* @private
|
|
|
58988 |
*/
|
|
|
58989 |
|
|
|
58990 |
const precompute = function () {
|
|
|
58991 |
const tables = [[[], [], [], [], []], [[], [], [], [], []]];
|
|
|
58992 |
const encTable = tables[0];
|
|
|
58993 |
const decTable = tables[1];
|
|
|
58994 |
const sbox = encTable[4];
|
|
|
58995 |
const sboxInv = decTable[4];
|
|
|
58996 |
let i;
|
|
|
58997 |
let x;
|
|
|
58998 |
let xInv;
|
|
|
58999 |
const d = [];
|
|
|
59000 |
const th = [];
|
|
|
59001 |
let x2;
|
|
|
59002 |
let x4;
|
|
|
59003 |
let x8;
|
|
|
59004 |
let s;
|
|
|
59005 |
let tEnc;
|
|
|
59006 |
let tDec; // Compute double and third tables
|
|
|
59007 |
|
|
|
59008 |
for (i = 0; i < 256; i++) {
|
|
|
59009 |
th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i;
|
|
|
59010 |
}
|
|
|
59011 |
for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
|
|
|
59012 |
// Compute sbox
|
|
|
59013 |
s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4;
|
|
|
59014 |
s = s >> 8 ^ s & 255 ^ 99;
|
|
|
59015 |
sbox[x] = s;
|
|
|
59016 |
sboxInv[s] = x; // Compute MixColumns
|
|
|
59017 |
|
|
|
59018 |
x8 = d[x4 = d[x2 = d[x]]];
|
|
|
59019 |
tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100;
|
|
|
59020 |
tEnc = d[s] * 0x101 ^ s * 0x1010100;
|
|
|
59021 |
for (i = 0; i < 4; i++) {
|
|
|
59022 |
encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8;
|
|
|
59023 |
decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8;
|
|
|
59024 |
}
|
|
|
59025 |
} // Compactify. Considerable speedup on Firefox.
|
|
|
59026 |
|
|
|
59027 |
for (i = 0; i < 5; i++) {
|
|
|
59028 |
encTable[i] = encTable[i].slice(0);
|
|
|
59029 |
decTable[i] = decTable[i].slice(0);
|
|
|
59030 |
}
|
|
|
59031 |
return tables;
|
|
|
59032 |
};
|
|
|
59033 |
let aesTables = null;
|
|
|
59034 |
/**
|
|
|
59035 |
* Schedule out an AES key for both encryption and decryption. This
|
|
|
59036 |
* is a low-level class. Use a cipher mode to do bulk encryption.
|
|
|
59037 |
*
|
|
|
59038 |
* @class AES
|
|
|
59039 |
* @param key {Array} The key as an array of 4, 6 or 8 words.
|
|
|
59040 |
*/
|
|
|
59041 |
|
|
|
59042 |
class AES {
|
|
|
59043 |
constructor(key) {
|
|
|
59044 |
/**
|
|
|
59045 |
* The expanded S-box and inverse S-box tables. These will be computed
|
|
|
59046 |
* on the client so that we don't have to send them down the wire.
|
|
|
59047 |
*
|
|
|
59048 |
* There are two tables, _tables[0] is for encryption and
|
|
|
59049 |
* _tables[1] is for decryption.
|
|
|
59050 |
*
|
|
|
59051 |
* The first 4 sub-tables are the expanded S-box with MixColumns. The
|
|
|
59052 |
* last (_tables[01][4]) is the S-box itself.
|
|
|
59053 |
*
|
|
|
59054 |
* @private
|
|
|
59055 |
*/
|
|
|
59056 |
// if we have yet to precompute the S-box tables
|
|
|
59057 |
// do so now
|
|
|
59058 |
if (!aesTables) {
|
|
|
59059 |
aesTables = precompute();
|
|
|
59060 |
} // then make a copy of that object for use
|
|
|
59061 |
|
|
|
59062 |
this._tables = [[aesTables[0][0].slice(), aesTables[0][1].slice(), aesTables[0][2].slice(), aesTables[0][3].slice(), aesTables[0][4].slice()], [aesTables[1][0].slice(), aesTables[1][1].slice(), aesTables[1][2].slice(), aesTables[1][3].slice(), aesTables[1][4].slice()]];
|
|
|
59063 |
let i;
|
|
|
59064 |
let j;
|
|
|
59065 |
let tmp;
|
|
|
59066 |
const sbox = this._tables[0][4];
|
|
|
59067 |
const decTable = this._tables[1];
|
|
|
59068 |
const keyLen = key.length;
|
|
|
59069 |
let rcon = 1;
|
|
|
59070 |
if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
|
|
|
59071 |
throw new Error('Invalid aes key size');
|
|
|
59072 |
}
|
|
|
59073 |
const encKey = key.slice(0);
|
|
|
59074 |
const decKey = [];
|
|
|
59075 |
this._key = [encKey, decKey]; // schedule encryption keys
|
|
|
59076 |
|
|
|
59077 |
for (i = keyLen; i < 4 * keyLen + 28; i++) {
|
|
|
59078 |
tmp = encKey[i - 1]; // apply sbox
|
|
|
59079 |
|
|
|
59080 |
if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) {
|
|
|
59081 |
tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon
|
|
|
59082 |
|
|
|
59083 |
if (i % keyLen === 0) {
|
|
|
59084 |
tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24;
|
|
|
59085 |
rcon = rcon << 1 ^ (rcon >> 7) * 283;
|
|
|
59086 |
}
|
|
|
59087 |
}
|
|
|
59088 |
encKey[i] = encKey[i - keyLen] ^ tmp;
|
|
|
59089 |
} // schedule decryption keys
|
|
|
59090 |
|
|
|
59091 |
for (j = 0; i; j++, i--) {
|
|
|
59092 |
tmp = encKey[j & 3 ? i : i - 4];
|
|
|
59093 |
if (i <= 4 || j < 4) {
|
|
|
59094 |
decKey[j] = tmp;
|
|
|
59095 |
} else {
|
|
|
59096 |
decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]];
|
|
|
59097 |
}
|
|
|
59098 |
}
|
|
|
59099 |
}
|
|
|
59100 |
/**
|
|
|
59101 |
* Decrypt 16 bytes, specified as four 32-bit words.
|
|
|
59102 |
*
|
|
|
59103 |
* @param {number} encrypted0 the first word to decrypt
|
|
|
59104 |
* @param {number} encrypted1 the second word to decrypt
|
|
|
59105 |
* @param {number} encrypted2 the third word to decrypt
|
|
|
59106 |
* @param {number} encrypted3 the fourth word to decrypt
|
|
|
59107 |
* @param {Int32Array} out the array to write the decrypted words
|
|
|
59108 |
* into
|
|
|
59109 |
* @param {number} offset the offset into the output array to start
|
|
|
59110 |
* writing results
|
|
|
59111 |
* @return {Array} The plaintext.
|
|
|
59112 |
*/
|
|
|
59113 |
|
|
|
59114 |
decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) {
|
|
|
59115 |
const key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data
|
|
|
59116 |
|
|
|
59117 |
let a = encrypted0 ^ key[0];
|
|
|
59118 |
let b = encrypted3 ^ key[1];
|
|
|
59119 |
let c = encrypted2 ^ key[2];
|
|
|
59120 |
let d = encrypted1 ^ key[3];
|
|
|
59121 |
let a2;
|
|
|
59122 |
let b2;
|
|
|
59123 |
let c2; // key.length === 2 ?
|
|
|
59124 |
|
|
|
59125 |
const nInnerRounds = key.length / 4 - 2;
|
|
|
59126 |
let i;
|
|
|
59127 |
let kIndex = 4;
|
|
|
59128 |
const table = this._tables[1]; // load up the tables
|
|
|
59129 |
|
|
|
59130 |
const table0 = table[0];
|
|
|
59131 |
const table1 = table[1];
|
|
|
59132 |
const table2 = table[2];
|
|
|
59133 |
const table3 = table[3];
|
|
|
59134 |
const sbox = table[4]; // Inner rounds. Cribbed from OpenSSL.
|
|
|
59135 |
|
|
|
59136 |
for (i = 0; i < nInnerRounds; i++) {
|
|
|
59137 |
a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex];
|
|
|
59138 |
b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1];
|
|
|
59139 |
c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2];
|
|
|
59140 |
d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3];
|
|
|
59141 |
kIndex += 4;
|
|
|
59142 |
a = a2;
|
|
|
59143 |
b = b2;
|
|
|
59144 |
c = c2;
|
|
|
59145 |
} // Last round.
|
|
|
59146 |
|
|
|
59147 |
for (i = 0; i < 4; i++) {
|
|
|
59148 |
out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++];
|
|
|
59149 |
a2 = a;
|
|
|
59150 |
a = b;
|
|
|
59151 |
b = c;
|
|
|
59152 |
c = d;
|
|
|
59153 |
d = a2;
|
|
|
59154 |
}
|
|
|
59155 |
}
|
|
|
59156 |
}
|
|
|
59157 |
/**
|
|
|
59158 |
* @file async-stream.js
|
|
|
59159 |
*/
|
|
|
59160 |
|
|
|
59161 |
/**
|
|
|
59162 |
* A wrapper around the Stream class to use setTimeout
|
|
|
59163 |
* and run stream "jobs" Asynchronously
|
|
|
59164 |
*
|
|
|
59165 |
* @class AsyncStream
|
|
|
59166 |
* @extends Stream
|
|
|
59167 |
*/
|
|
|
59168 |
|
|
|
59169 |
class AsyncStream extends Stream {
|
|
|
59170 |
constructor() {
|
|
|
59171 |
super(Stream);
|
|
|
59172 |
this.jobs = [];
|
|
|
59173 |
this.delay = 1;
|
|
|
59174 |
this.timeout_ = null;
|
|
|
59175 |
}
|
|
|
59176 |
/**
|
|
|
59177 |
* process an async job
|
|
|
59178 |
*
|
|
|
59179 |
* @private
|
|
|
59180 |
*/
|
|
|
59181 |
|
|
|
59182 |
processJob_() {
|
|
|
59183 |
this.jobs.shift()();
|
|
|
59184 |
if (this.jobs.length) {
|
|
|
59185 |
this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
|
|
|
59186 |
} else {
|
|
|
59187 |
this.timeout_ = null;
|
|
|
59188 |
}
|
|
|
59189 |
}
|
|
|
59190 |
/**
|
|
|
59191 |
* push a job into the stream
|
|
|
59192 |
*
|
|
|
59193 |
* @param {Function} job the job to push into the stream
|
|
|
59194 |
*/
|
|
|
59195 |
|
|
|
59196 |
push(job) {
|
|
|
59197 |
this.jobs.push(job);
|
|
|
59198 |
if (!this.timeout_) {
|
|
|
59199 |
this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
|
|
|
59200 |
}
|
|
|
59201 |
}
|
|
|
59202 |
}
|
|
|
59203 |
/**
|
|
|
59204 |
* @file decrypter.js
|
|
|
59205 |
*
|
|
|
59206 |
* An asynchronous implementation of AES-128 CBC decryption with
|
|
|
59207 |
* PKCS#7 padding.
|
|
|
59208 |
*/
|
|
|
59209 |
|
|
|
59210 |
/**
|
|
|
59211 |
* Convert network-order (big-endian) bytes into their little-endian
|
|
|
59212 |
* representation.
|
|
|
59213 |
*/
|
|
|
59214 |
|
|
|
59215 |
const ntoh = function (word) {
|
|
|
59216 |
return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24;
|
|
|
59217 |
};
|
|
|
59218 |
/**
|
|
|
59219 |
* Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
|
|
|
59220 |
*
|
|
|
59221 |
* @param {Uint8Array} encrypted the encrypted bytes
|
|
|
59222 |
* @param {Uint32Array} key the bytes of the decryption key
|
|
|
59223 |
* @param {Uint32Array} initVector the initialization vector (IV) to
|
|
|
59224 |
* use for the first round of CBC.
|
|
|
59225 |
* @return {Uint8Array} the decrypted bytes
|
|
|
59226 |
*
|
|
|
59227 |
* @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
|
|
|
59228 |
* @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
|
|
|
59229 |
* @see https://tools.ietf.org/html/rfc2315
|
|
|
59230 |
*/
|
|
|
59231 |
|
|
|
59232 |
const decrypt = function (encrypted, key, initVector) {
|
|
|
59233 |
// word-level access to the encrypted bytes
|
|
|
59234 |
const encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2);
|
|
|
59235 |
const decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output
|
|
|
59236 |
|
|
|
59237 |
const decrypted = new Uint8Array(encrypted.byteLength);
|
|
|
59238 |
const decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and
|
|
|
59239 |
// decrypted data
|
|
|
59240 |
|
|
|
59241 |
let init0;
|
|
|
59242 |
let init1;
|
|
|
59243 |
let init2;
|
|
|
59244 |
let init3;
|
|
|
59245 |
let encrypted0;
|
|
|
59246 |
let encrypted1;
|
|
|
59247 |
let encrypted2;
|
|
|
59248 |
let encrypted3; // iteration variable
|
|
|
59249 |
|
|
|
59250 |
let wordIx; // pull out the words of the IV to ensure we don't modify the
|
|
|
59251 |
// passed-in reference and easier access
|
|
|
59252 |
|
|
|
59253 |
init0 = initVector[0];
|
|
|
59254 |
init1 = initVector[1];
|
|
|
59255 |
init2 = initVector[2];
|
|
|
59256 |
init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC)
|
|
|
59257 |
// to each decrypted block
|
|
|
59258 |
|
|
|
59259 |
for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) {
|
|
|
59260 |
// convert big-endian (network order) words into little-endian
|
|
|
59261 |
// (javascript order)
|
|
|
59262 |
encrypted0 = ntoh(encrypted32[wordIx]);
|
|
|
59263 |
encrypted1 = ntoh(encrypted32[wordIx + 1]);
|
|
|
59264 |
encrypted2 = ntoh(encrypted32[wordIx + 2]);
|
|
|
59265 |
encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block
|
|
|
59266 |
|
|
|
59267 |
decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the
|
|
|
59268 |
// plaintext
|
|
|
59269 |
|
|
|
59270 |
decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0);
|
|
|
59271 |
decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1);
|
|
|
59272 |
decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2);
|
|
|
59273 |
decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round
|
|
|
59274 |
|
|
|
59275 |
init0 = encrypted0;
|
|
|
59276 |
init1 = encrypted1;
|
|
|
59277 |
init2 = encrypted2;
|
|
|
59278 |
init3 = encrypted3;
|
|
|
59279 |
}
|
|
|
59280 |
return decrypted;
|
|
|
59281 |
};
|
|
|
59282 |
/**
|
|
|
59283 |
* The `Decrypter` class that manages decryption of AES
|
|
|
59284 |
* data through `AsyncStream` objects and the `decrypt`
|
|
|
59285 |
* function
|
|
|
59286 |
*
|
|
|
59287 |
* @param {Uint8Array} encrypted the encrypted bytes
|
|
|
59288 |
* @param {Uint32Array} key the bytes of the decryption key
|
|
|
59289 |
* @param {Uint32Array} initVector the initialization vector (IV) to
|
|
|
59290 |
* @param {Function} done the function to run when done
|
|
|
59291 |
* @class Decrypter
|
|
|
59292 |
*/
|
|
|
59293 |
|
|
|
59294 |
class Decrypter {
|
|
|
59295 |
constructor(encrypted, key, initVector, done) {
|
|
|
59296 |
const step = Decrypter.STEP;
|
|
|
59297 |
const encrypted32 = new Int32Array(encrypted.buffer);
|
|
|
59298 |
const decrypted = new Uint8Array(encrypted.byteLength);
|
|
|
59299 |
let i = 0;
|
|
|
59300 |
this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously
|
|
|
59301 |
|
|
|
59302 |
this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
|
|
|
59303 |
for (i = step; i < encrypted32.length; i += step) {
|
|
|
59304 |
initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]);
|
|
|
59305 |
this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
|
|
|
59306 |
} // invoke the done() callback when everything is finished
|
|
|
59307 |
|
|
|
59308 |
this.asyncStream_.push(function () {
|
|
|
59309 |
// remove pkcs#7 padding from the decrypted bytes
|
|
|
59310 |
done(null, unpad(decrypted));
|
|
|
59311 |
});
|
|
|
59312 |
}
|
|
|
59313 |
/**
|
|
|
59314 |
* a getter for step the maximum number of bytes to process at one time
|
|
|
59315 |
*
|
|
|
59316 |
* @return {number} the value of step 32000
|
|
|
59317 |
*/
|
|
|
59318 |
|
|
|
59319 |
static get STEP() {
|
|
|
59320 |
// 4 * 8000;
|
|
|
59321 |
return 32000;
|
|
|
59322 |
}
|
|
|
59323 |
/**
|
|
|
59324 |
* @private
|
|
|
59325 |
*/
|
|
|
59326 |
|
|
|
59327 |
decryptChunk_(encrypted, key, initVector, decrypted) {
|
|
|
59328 |
return function () {
|
|
|
59329 |
const bytes = decrypt(encrypted, key, initVector);
|
|
|
59330 |
decrypted.set(bytes, encrypted.byteOffset);
|
|
|
59331 |
};
|
|
|
59332 |
}
|
|
|
59333 |
}
|
|
|
59334 |
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
|
59335 |
var win;
|
|
|
59336 |
if (typeof window !== "undefined") {
|
|
|
59337 |
win = window;
|
|
|
59338 |
} else if (typeof commonjsGlobal !== "undefined") {
|
|
|
59339 |
win = commonjsGlobal;
|
|
|
59340 |
} else if (typeof self !== "undefined") {
|
|
|
59341 |
win = self;
|
|
|
59342 |
} else {
|
|
|
59343 |
win = {};
|
|
|
59344 |
}
|
|
|
59345 |
var window_1 = win;
|
|
|
59346 |
var isArrayBufferView = function isArrayBufferView(obj) {
|
|
|
59347 |
if (ArrayBuffer.isView === 'function') {
|
|
|
59348 |
return ArrayBuffer.isView(obj);
|
|
|
59349 |
}
|
|
|
59350 |
return obj && obj.buffer instanceof ArrayBuffer;
|
|
|
59351 |
};
|
|
|
59352 |
var BigInt = window_1.BigInt || Number;
|
|
|
59353 |
[BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
|
|
|
59354 |
(function () {
|
|
|
59355 |
var a = new Uint16Array([0xFFCC]);
|
|
|
59356 |
var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
|
|
|
59357 |
if (b[0] === 0xFF) {
|
|
|
59358 |
return 'big';
|
|
|
59359 |
}
|
|
|
59360 |
if (b[0] === 0xCC) {
|
|
|
59361 |
return 'little';
|
|
|
59362 |
}
|
|
|
59363 |
return 'unknown';
|
|
|
59364 |
})();
|
|
|
59365 |
/**
|
|
|
59366 |
* Creates an object for sending to a web worker modifying properties that are TypedArrays
|
|
|
59367 |
* into a new object with seperated properties for the buffer, byteOffset, and byteLength.
|
|
|
59368 |
*
|
|
|
59369 |
* @param {Object} message
|
|
|
59370 |
* Object of properties and values to send to the web worker
|
|
|
59371 |
* @return {Object}
|
|
|
59372 |
* Modified message with TypedArray values expanded
|
|
|
59373 |
* @function createTransferableMessage
|
|
|
59374 |
*/
|
|
|
59375 |
|
|
|
59376 |
const createTransferableMessage = function (message) {
|
|
|
59377 |
const transferable = {};
|
|
|
59378 |
Object.keys(message).forEach(key => {
|
|
|
59379 |
const value = message[key];
|
|
|
59380 |
if (isArrayBufferView(value)) {
|
|
|
59381 |
transferable[key] = {
|
|
|
59382 |
bytes: value.buffer,
|
|
|
59383 |
byteOffset: value.byteOffset,
|
|
|
59384 |
byteLength: value.byteLength
|
|
|
59385 |
};
|
|
|
59386 |
} else {
|
|
|
59387 |
transferable[key] = value;
|
|
|
59388 |
}
|
|
|
59389 |
});
|
|
|
59390 |
return transferable;
|
|
|
59391 |
};
|
|
|
59392 |
/* global self */
|
|
|
59393 |
|
|
|
59394 |
/**
|
|
|
59395 |
* Our web worker interface so that things can talk to aes-decrypter
|
|
|
59396 |
* that will be running in a web worker. the scope is passed to this by
|
|
|
59397 |
* webworkify.
|
|
|
59398 |
*/
|
|
|
59399 |
|
|
|
59400 |
self.onmessage = function (event) {
|
|
|
59401 |
const data = event.data;
|
|
|
59402 |
const encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength);
|
|
|
59403 |
const key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4);
|
|
|
59404 |
const iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4);
|
|
|
59405 |
/* eslint-disable no-new, handle-callback-err */
|
|
|
59406 |
|
|
|
59407 |
new Decrypter(encrypted, key, iv, function (err, bytes) {
|
|
|
59408 |
self.postMessage(createTransferableMessage({
|
|
|
59409 |
source: data.source,
|
|
|
59410 |
decrypted: bytes
|
|
|
59411 |
}), [bytes.buffer]);
|
|
|
59412 |
});
|
|
|
59413 |
/* eslint-enable */
|
|
|
59414 |
};
|
|
|
59415 |
}));
|
|
|
59416 |
|
|
|
59417 |
var Decrypter = factory(workerCode);
|
|
|
59418 |
/* rollup-plugin-worker-factory end for worker!/home/runner/work/http-streaming/http-streaming/src/decrypter-worker.js */
|
|
|
59419 |
|
|
|
59420 |
/**
|
|
|
59421 |
* Convert the properties of an HLS track into an audioTrackKind.
|
|
|
59422 |
*
|
|
|
59423 |
* @private
|
|
|
59424 |
*/
|
|
|
59425 |
|
|
|
59426 |
const audioTrackKind_ = properties => {
|
|
|
59427 |
let kind = properties.default ? 'main' : 'alternative';
|
|
|
59428 |
if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
|
|
|
59429 |
kind = 'main-desc';
|
|
|
59430 |
}
|
|
|
59431 |
return kind;
|
|
|
59432 |
};
|
|
|
59433 |
/**
|
|
|
59434 |
* Pause provided segment loader and playlist loader if active
|
|
|
59435 |
*
|
|
|
59436 |
* @param {SegmentLoader} segmentLoader
|
|
|
59437 |
* SegmentLoader to pause
|
|
|
59438 |
* @param {Object} mediaType
|
|
|
59439 |
* Active media type
|
|
|
59440 |
* @function stopLoaders
|
|
|
59441 |
*/
|
|
|
59442 |
|
|
|
59443 |
const stopLoaders = (segmentLoader, mediaType) => {
|
|
|
59444 |
segmentLoader.abort();
|
|
|
59445 |
segmentLoader.pause();
|
|
|
59446 |
if (mediaType && mediaType.activePlaylistLoader) {
|
|
|
59447 |
mediaType.activePlaylistLoader.pause();
|
|
|
59448 |
mediaType.activePlaylistLoader = null;
|
|
|
59449 |
}
|
|
|
59450 |
};
|
|
|
59451 |
/**
|
|
|
59452 |
* Start loading provided segment loader and playlist loader
|
|
|
59453 |
*
|
|
|
59454 |
* @param {PlaylistLoader} playlistLoader
|
|
|
59455 |
* PlaylistLoader to start loading
|
|
|
59456 |
* @param {Object} mediaType
|
|
|
59457 |
* Active media type
|
|
|
59458 |
* @function startLoaders
|
|
|
59459 |
*/
|
|
|
59460 |
|
|
|
59461 |
const startLoaders = (playlistLoader, mediaType) => {
|
|
|
59462 |
// Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the
|
|
|
59463 |
// playlist loader
|
|
|
59464 |
mediaType.activePlaylistLoader = playlistLoader;
|
|
|
59465 |
playlistLoader.load();
|
|
|
59466 |
};
|
|
|
59467 |
/**
|
|
|
59468 |
* Returns a function to be called when the media group changes. It performs a
|
|
|
59469 |
* non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a
|
|
|
59470 |
* change of group is merely a rendition switch of the same content at another encoding,
|
|
|
59471 |
* rather than a change of content, such as switching audio from English to Spanish.
|
|
|
59472 |
*
|
|
|
59473 |
* @param {string} type
|
|
|
59474 |
* MediaGroup type
|
|
|
59475 |
* @param {Object} settings
|
|
|
59476 |
* Object containing required information for media groups
|
|
|
59477 |
* @return {Function}
|
|
|
59478 |
* Handler for a non-destructive resync of SegmentLoader when the active media
|
|
|
59479 |
* group changes.
|
|
|
59480 |
* @function onGroupChanged
|
|
|
59481 |
*/
|
|
|
59482 |
|
|
|
59483 |
const onGroupChanged = (type, settings) => () => {
|
|
|
59484 |
const {
|
|
|
59485 |
segmentLoaders: {
|
|
|
59486 |
[type]: segmentLoader,
|
|
|
59487 |
main: mainSegmentLoader
|
|
|
59488 |
},
|
|
|
59489 |
mediaTypes: {
|
|
|
59490 |
[type]: mediaType
|
|
|
59491 |
}
|
|
|
59492 |
} = settings;
|
|
|
59493 |
const activeTrack = mediaType.activeTrack();
|
|
|
59494 |
const activeGroup = mediaType.getActiveGroup();
|
|
|
59495 |
const previousActiveLoader = mediaType.activePlaylistLoader;
|
|
|
59496 |
const lastGroup = mediaType.lastGroup_; // the group did not change do nothing
|
|
|
59497 |
|
|
|
59498 |
if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) {
|
|
|
59499 |
return;
|
|
|
59500 |
}
|
|
|
59501 |
mediaType.lastGroup_ = activeGroup;
|
|
|
59502 |
mediaType.lastTrack_ = activeTrack;
|
|
|
59503 |
stopLoaders(segmentLoader, mediaType);
|
|
|
59504 |
if (!activeGroup || activeGroup.isMainPlaylist) {
|
|
|
59505 |
// there is no group active or active group is a main playlist and won't change
|
|
|
59506 |
return;
|
|
|
59507 |
}
|
|
|
59508 |
if (!activeGroup.playlistLoader) {
|
|
|
59509 |
if (previousActiveLoader) {
|
|
|
59510 |
// The previous group had a playlist loader but the new active group does not
|
|
|
59511 |
// this means we are switching from demuxed to muxed audio. In this case we want to
|
|
|
59512 |
// do a destructive reset of the main segment loader and not restart the audio
|
|
|
59513 |
// loaders.
|
|
|
59514 |
mainSegmentLoader.resetEverything();
|
|
|
59515 |
}
|
|
|
59516 |
return;
|
|
|
59517 |
} // Non-destructive resync
|
|
|
59518 |
|
|
|
59519 |
segmentLoader.resyncLoader();
|
|
|
59520 |
startLoaders(activeGroup.playlistLoader, mediaType);
|
|
|
59521 |
};
|
|
|
59522 |
const onGroupChanging = (type, settings) => () => {
|
|
|
59523 |
const {
|
|
|
59524 |
segmentLoaders: {
|
|
|
59525 |
[type]: segmentLoader
|
|
|
59526 |
},
|
|
|
59527 |
mediaTypes: {
|
|
|
59528 |
[type]: mediaType
|
|
|
59529 |
}
|
|
|
59530 |
} = settings;
|
|
|
59531 |
mediaType.lastGroup_ = null;
|
|
|
59532 |
segmentLoader.abort();
|
|
|
59533 |
segmentLoader.pause();
|
|
|
59534 |
};
|
|
|
59535 |
/**
|
|
|
59536 |
* Returns a function to be called when the media track changes. It performs a
|
|
|
59537 |
* destructive reset of the SegmentLoader to ensure we start loading as close to
|
|
|
59538 |
* currentTime as possible.
|
|
|
59539 |
*
|
|
|
59540 |
* @param {string} type
|
|
|
59541 |
* MediaGroup type
|
|
|
59542 |
* @param {Object} settings
|
|
|
59543 |
* Object containing required information for media groups
|
|
|
59544 |
* @return {Function}
|
|
|
59545 |
* Handler for a destructive reset of SegmentLoader when the active media
|
|
|
59546 |
* track changes.
|
|
|
59547 |
* @function onTrackChanged
|
|
|
59548 |
*/
|
|
|
59549 |
|
|
|
59550 |
const onTrackChanged = (type, settings) => () => {
|
|
|
59551 |
const {
|
|
|
59552 |
mainPlaylistLoader,
|
|
|
59553 |
segmentLoaders: {
|
|
|
59554 |
[type]: segmentLoader,
|
|
|
59555 |
main: mainSegmentLoader
|
|
|
59556 |
},
|
|
|
59557 |
mediaTypes: {
|
|
|
59558 |
[type]: mediaType
|
|
|
59559 |
}
|
|
|
59560 |
} = settings;
|
|
|
59561 |
const activeTrack = mediaType.activeTrack();
|
|
|
59562 |
const activeGroup = mediaType.getActiveGroup();
|
|
|
59563 |
const previousActiveLoader = mediaType.activePlaylistLoader;
|
|
|
59564 |
const lastTrack = mediaType.lastTrack_; // track did not change, do nothing
|
|
|
59565 |
|
|
|
59566 |
if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) {
|
|
|
59567 |
return;
|
|
|
59568 |
}
|
|
|
59569 |
mediaType.lastGroup_ = activeGroup;
|
|
|
59570 |
mediaType.lastTrack_ = activeTrack;
|
|
|
59571 |
stopLoaders(segmentLoader, mediaType);
|
|
|
59572 |
if (!activeGroup) {
|
|
|
59573 |
// there is no group active so we do not want to restart loaders
|
|
|
59574 |
return;
|
|
|
59575 |
}
|
|
|
59576 |
if (activeGroup.isMainPlaylist) {
|
|
|
59577 |
// track did not change, do nothing
|
|
|
59578 |
if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) {
|
|
|
59579 |
return;
|
|
|
59580 |
}
|
|
|
59581 |
const pc = settings.vhs.playlistController_;
|
|
|
59582 |
const newPlaylist = pc.selectPlaylist(); // media will not change do nothing
|
|
|
59583 |
|
|
|
59584 |
if (pc.media() === newPlaylist) {
|
|
|
59585 |
return;
|
|
|
59586 |
}
|
|
|
59587 |
mediaType.logger_(`track change. Switching main audio from ${lastTrack.id} to ${activeTrack.id}`);
|
|
|
59588 |
mainPlaylistLoader.pause();
|
|
|
59589 |
mainSegmentLoader.resetEverything();
|
|
|
59590 |
pc.fastQualityChange_(newPlaylist);
|
|
|
59591 |
return;
|
|
|
59592 |
}
|
|
|
59593 |
if (type === 'AUDIO') {
|
|
|
59594 |
if (!activeGroup.playlistLoader) {
|
|
|
59595 |
// when switching from demuxed audio/video to muxed audio/video (noted by no
|
|
|
59596 |
// playlist loader for the audio group), we want to do a destructive reset of the
|
|
|
59597 |
// main segment loader and not restart the audio loaders
|
|
|
59598 |
mainSegmentLoader.setAudio(true); // don't have to worry about disabling the audio of the audio segment loader since
|
|
|
59599 |
// it should be stopped
|
|
|
59600 |
|
|
|
59601 |
mainSegmentLoader.resetEverything();
|
|
|
59602 |
return;
|
|
|
59603 |
} // although the segment loader is an audio segment loader, call the setAudio
|
|
|
59604 |
// function to ensure it is prepared to re-append the init segment (or handle other
|
|
|
59605 |
// config changes)
|
|
|
59606 |
|
|
|
59607 |
segmentLoader.setAudio(true);
|
|
|
59608 |
mainSegmentLoader.setAudio(false);
|
|
|
59609 |
}
|
|
|
59610 |
if (previousActiveLoader === activeGroup.playlistLoader) {
|
|
|
59611 |
// Nothing has actually changed. This can happen because track change events can fire
|
|
|
59612 |
// multiple times for a "single" change. One for enabling the new active track, and
|
|
|
59613 |
// one for disabling the track that was active
|
|
|
59614 |
startLoaders(activeGroup.playlistLoader, mediaType);
|
|
|
59615 |
return;
|
|
|
59616 |
}
|
|
|
59617 |
if (segmentLoader.track) {
|
|
|
59618 |
// For WebVTT, set the new text track in the segmentloader
|
|
|
59619 |
segmentLoader.track(activeTrack);
|
|
|
59620 |
} // destructive reset
|
|
|
59621 |
|
|
|
59622 |
segmentLoader.resetEverything();
|
|
|
59623 |
startLoaders(activeGroup.playlistLoader, mediaType);
|
|
|
59624 |
};
|
|
|
59625 |
const onError = {
|
|
|
59626 |
/**
|
|
|
59627 |
* Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
|
|
|
59628 |
* an error.
|
|
|
59629 |
*
|
|
|
59630 |
* @param {string} type
|
|
|
59631 |
* MediaGroup type
|
|
|
59632 |
* @param {Object} settings
|
|
|
59633 |
* Object containing required information for media groups
|
|
|
59634 |
* @return {Function}
|
|
|
59635 |
* Error handler. Logs warning (or error if the playlist is excluded) to
|
|
|
59636 |
* console and switches back to default audio track.
|
|
|
59637 |
* @function onError.AUDIO
|
|
|
59638 |
*/
|
|
|
59639 |
AUDIO: (type, settings) => () => {
|
|
|
59640 |
const {
|
|
|
59641 |
mediaTypes: {
|
|
|
59642 |
[type]: mediaType
|
|
|
59643 |
},
|
|
|
59644 |
excludePlaylist
|
|
|
59645 |
} = settings; // switch back to default audio track
|
|
|
59646 |
|
|
|
59647 |
const activeTrack = mediaType.activeTrack();
|
|
|
59648 |
const activeGroup = mediaType.activeGroup();
|
|
|
59649 |
const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id;
|
|
|
59650 |
const defaultTrack = mediaType.tracks[id];
|
|
|
59651 |
if (activeTrack === defaultTrack) {
|
|
|
59652 |
// Default track encountered an error. All we can do now is exclude the current
|
|
|
59653 |
// rendition and hope another will switch audio groups
|
|
|
59654 |
excludePlaylist({
|
|
|
59655 |
error: {
|
|
|
59656 |
message: 'Problem encountered loading the default audio track.'
|
|
|
59657 |
}
|
|
|
59658 |
});
|
|
|
59659 |
return;
|
|
|
59660 |
}
|
|
|
59661 |
videojs.log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.');
|
|
|
59662 |
for (const trackId in mediaType.tracks) {
|
|
|
59663 |
mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack;
|
|
|
59664 |
}
|
|
|
59665 |
mediaType.onTrackChanged();
|
|
|
59666 |
},
|
|
|
59667 |
/**
|
|
|
59668 |
* Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
|
|
|
59669 |
* an error.
|
|
|
59670 |
*
|
|
|
59671 |
* @param {string} type
|
|
|
59672 |
* MediaGroup type
|
|
|
59673 |
* @param {Object} settings
|
|
|
59674 |
* Object containing required information for media groups
|
|
|
59675 |
* @return {Function}
|
|
|
59676 |
* Error handler. Logs warning to console and disables the active subtitle track
|
|
|
59677 |
* @function onError.SUBTITLES
|
|
|
59678 |
*/
|
|
|
59679 |
SUBTITLES: (type, settings) => () => {
|
|
|
59680 |
const {
|
|
|
59681 |
mediaTypes: {
|
|
|
59682 |
[type]: mediaType
|
|
|
59683 |
}
|
|
|
59684 |
} = settings;
|
|
|
59685 |
videojs.log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.');
|
|
|
59686 |
const track = mediaType.activeTrack();
|
|
|
59687 |
if (track) {
|
|
|
59688 |
track.mode = 'disabled';
|
|
|
59689 |
}
|
|
|
59690 |
mediaType.onTrackChanged();
|
|
|
59691 |
}
|
|
|
59692 |
};
|
|
|
59693 |
const setupListeners = {
|
|
|
59694 |
/**
|
|
|
59695 |
* Setup event listeners for audio playlist loader
|
|
|
59696 |
*
|
|
|
59697 |
* @param {string} type
|
|
|
59698 |
* MediaGroup type
|
|
|
59699 |
* @param {PlaylistLoader|null} playlistLoader
|
|
|
59700 |
* PlaylistLoader to register listeners on
|
|
|
59701 |
* @param {Object} settings
|
|
|
59702 |
* Object containing required information for media groups
|
|
|
59703 |
* @function setupListeners.AUDIO
|
|
|
59704 |
*/
|
|
|
59705 |
AUDIO: (type, playlistLoader, settings) => {
|
|
|
59706 |
if (!playlistLoader) {
|
|
|
59707 |
// no playlist loader means audio will be muxed with the video
|
|
|
59708 |
return;
|
|
|
59709 |
}
|
|
|
59710 |
const {
|
|
|
59711 |
tech,
|
|
|
59712 |
requestOptions,
|
|
|
59713 |
segmentLoaders: {
|
|
|
59714 |
[type]: segmentLoader
|
|
|
59715 |
}
|
|
|
59716 |
} = settings;
|
|
|
59717 |
playlistLoader.on('loadedmetadata', () => {
|
|
|
59718 |
const media = playlistLoader.media();
|
|
|
59719 |
segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload
|
|
|
59720 |
// permits, start downloading segments
|
|
|
59721 |
|
|
|
59722 |
if (!tech.paused() || media.endList && tech.preload() !== 'none') {
|
|
|
59723 |
segmentLoader.load();
|
|
|
59724 |
}
|
|
|
59725 |
});
|
|
|
59726 |
playlistLoader.on('loadedplaylist', () => {
|
|
|
59727 |
segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running
|
|
|
59728 |
|
|
|
59729 |
if (!tech.paused()) {
|
|
|
59730 |
segmentLoader.load();
|
|
|
59731 |
}
|
|
|
59732 |
});
|
|
|
59733 |
playlistLoader.on('error', onError[type](type, settings));
|
|
|
59734 |
},
|
|
|
59735 |
/**
|
|
|
59736 |
* Setup event listeners for subtitle playlist loader
|
|
|
59737 |
*
|
|
|
59738 |
* @param {string} type
|
|
|
59739 |
* MediaGroup type
|
|
|
59740 |
* @param {PlaylistLoader|null} playlistLoader
|
|
|
59741 |
* PlaylistLoader to register listeners on
|
|
|
59742 |
* @param {Object} settings
|
|
|
59743 |
* Object containing required information for media groups
|
|
|
59744 |
* @function setupListeners.SUBTITLES
|
|
|
59745 |
*/
|
|
|
59746 |
SUBTITLES: (type, playlistLoader, settings) => {
|
|
|
59747 |
const {
|
|
|
59748 |
tech,
|
|
|
59749 |
requestOptions,
|
|
|
59750 |
segmentLoaders: {
|
|
|
59751 |
[type]: segmentLoader
|
|
|
59752 |
},
|
|
|
59753 |
mediaTypes: {
|
|
|
59754 |
[type]: mediaType
|
|
|
59755 |
}
|
|
|
59756 |
} = settings;
|
|
|
59757 |
playlistLoader.on('loadedmetadata', () => {
|
|
|
59758 |
const media = playlistLoader.media();
|
|
|
59759 |
segmentLoader.playlist(media, requestOptions);
|
|
|
59760 |
segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload
|
|
|
59761 |
// permits, start downloading segments
|
|
|
59762 |
|
|
|
59763 |
if (!tech.paused() || media.endList && tech.preload() !== 'none') {
|
|
|
59764 |
segmentLoader.load();
|
|
|
59765 |
}
|
|
|
59766 |
});
|
|
|
59767 |
playlistLoader.on('loadedplaylist', () => {
|
|
|
59768 |
segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running
|
|
|
59769 |
|
|
|
59770 |
if (!tech.paused()) {
|
|
|
59771 |
segmentLoader.load();
|
|
|
59772 |
}
|
|
|
59773 |
});
|
|
|
59774 |
playlistLoader.on('error', onError[type](type, settings));
|
|
|
59775 |
}
|
|
|
59776 |
};
|
|
|
59777 |
const initialize = {
|
|
|
59778 |
/**
|
|
|
59779 |
* Setup PlaylistLoaders and AudioTracks for the audio groups
|
|
|
59780 |
*
|
|
|
59781 |
* @param {string} type
|
|
|
59782 |
* MediaGroup type
|
|
|
59783 |
* @param {Object} settings
|
|
|
59784 |
* Object containing required information for media groups
|
|
|
59785 |
* @function initialize.AUDIO
|
|
|
59786 |
*/
|
|
|
59787 |
'AUDIO': (type, settings) => {
|
|
|
59788 |
const {
|
|
|
59789 |
vhs,
|
|
|
59790 |
sourceType,
|
|
|
59791 |
segmentLoaders: {
|
|
|
59792 |
[type]: segmentLoader
|
|
|
59793 |
},
|
|
|
59794 |
requestOptions,
|
|
|
59795 |
main: {
|
|
|
59796 |
mediaGroups
|
|
|
59797 |
},
|
|
|
59798 |
mediaTypes: {
|
|
|
59799 |
[type]: {
|
|
|
59800 |
groups,
|
|
|
59801 |
tracks,
|
|
|
59802 |
logger_
|
|
|
59803 |
}
|
|
|
59804 |
},
|
|
|
59805 |
mainPlaylistLoader
|
|
|
59806 |
} = settings;
|
|
|
59807 |
const audioOnlyMain = isAudioOnly(mainPlaylistLoader.main); // force a default if we have none
|
|
|
59808 |
|
|
|
59809 |
if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) {
|
|
|
59810 |
mediaGroups[type] = {
|
|
|
59811 |
main: {
|
|
|
59812 |
default: {
|
|
|
59813 |
default: true
|
|
|
59814 |
}
|
|
|
59815 |
}
|
|
|
59816 |
};
|
|
|
59817 |
if (audioOnlyMain) {
|
|
|
59818 |
mediaGroups[type].main.default.playlists = mainPlaylistLoader.main.playlists;
|
|
|
59819 |
}
|
|
|
59820 |
}
|
|
|
59821 |
for (const groupId in mediaGroups[type]) {
|
|
|
59822 |
if (!groups[groupId]) {
|
|
|
59823 |
groups[groupId] = [];
|
|
|
59824 |
}
|
|
|
59825 |
for (const variantLabel in mediaGroups[type][groupId]) {
|
|
|
59826 |
let properties = mediaGroups[type][groupId][variantLabel];
|
|
|
59827 |
let playlistLoader;
|
|
|
59828 |
if (audioOnlyMain) {
|
|
|
59829 |
logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a main playlist`);
|
|
|
59830 |
properties.isMainPlaylist = true;
|
|
|
59831 |
playlistLoader = null; // if vhs-json was provided as the source, and the media playlist was resolved,
|
|
|
59832 |
// use the resolved media playlist object
|
|
|
59833 |
} else if (sourceType === 'vhs-json' && properties.playlists) {
|
|
|
59834 |
playlistLoader = new PlaylistLoader(properties.playlists[0], vhs, requestOptions);
|
|
|
59835 |
} else if (properties.resolvedUri) {
|
|
|
59836 |
playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); // TODO: dash isn't the only type with properties.playlists
|
|
|
59837 |
// should we even have properties.playlists in this check.
|
|
|
59838 |
} else if (properties.playlists && sourceType === 'dash') {
|
|
|
59839 |
playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader);
|
|
|
59840 |
} else {
|
|
|
59841 |
// no resolvedUri means the audio is muxed with the video when using this
|
|
|
59842 |
// audio track
|
|
|
59843 |
playlistLoader = null;
|
|
|
59844 |
}
|
|
|
59845 |
properties = merge({
|
|
|
59846 |
id: variantLabel,
|
|
|
59847 |
playlistLoader
|
|
|
59848 |
}, properties);
|
|
|
59849 |
setupListeners[type](type, properties.playlistLoader, settings);
|
|
|
59850 |
groups[groupId].push(properties);
|
|
|
59851 |
if (typeof tracks[variantLabel] === 'undefined') {
|
|
|
59852 |
const track = new videojs.AudioTrack({
|
|
|
59853 |
id: variantLabel,
|
|
|
59854 |
kind: audioTrackKind_(properties),
|
|
|
59855 |
enabled: false,
|
|
|
59856 |
language: properties.language,
|
|
|
59857 |
default: properties.default,
|
|
|
59858 |
label: variantLabel
|
|
|
59859 |
});
|
|
|
59860 |
tracks[variantLabel] = track;
|
|
|
59861 |
}
|
|
|
59862 |
}
|
|
|
59863 |
} // setup single error event handler for the segment loader
|
|
|
59864 |
|
|
|
59865 |
segmentLoader.on('error', onError[type](type, settings));
|
|
|
59866 |
},
|
|
|
59867 |
/**
|
|
|
59868 |
* Setup PlaylistLoaders and TextTracks for the subtitle groups
|
|
|
59869 |
*
|
|
|
59870 |
* @param {string} type
|
|
|
59871 |
* MediaGroup type
|
|
|
59872 |
* @param {Object} settings
|
|
|
59873 |
* Object containing required information for media groups
|
|
|
59874 |
* @function initialize.SUBTITLES
|
|
|
59875 |
*/
|
|
|
59876 |
'SUBTITLES': (type, settings) => {
|
|
|
59877 |
const {
|
|
|
59878 |
tech,
|
|
|
59879 |
vhs,
|
|
|
59880 |
sourceType,
|
|
|
59881 |
segmentLoaders: {
|
|
|
59882 |
[type]: segmentLoader
|
|
|
59883 |
},
|
|
|
59884 |
requestOptions,
|
|
|
59885 |
main: {
|
|
|
59886 |
mediaGroups
|
|
|
59887 |
},
|
|
|
59888 |
mediaTypes: {
|
|
|
59889 |
[type]: {
|
|
|
59890 |
groups,
|
|
|
59891 |
tracks
|
|
|
59892 |
}
|
|
|
59893 |
},
|
|
|
59894 |
mainPlaylistLoader
|
|
|
59895 |
} = settings;
|
|
|
59896 |
for (const groupId in mediaGroups[type]) {
|
|
|
59897 |
if (!groups[groupId]) {
|
|
|
59898 |
groups[groupId] = [];
|
|
|
59899 |
}
|
|
|
59900 |
for (const variantLabel in mediaGroups[type][groupId]) {
|
|
|
59901 |
if (!vhs.options_.useForcedSubtitles && mediaGroups[type][groupId][variantLabel].forced) {
|
|
|
59902 |
// Subtitle playlists with the forced attribute are not selectable in Safari.
|
|
|
59903 |
// According to Apple's HLS Authoring Specification:
|
|
|
59904 |
// If content has forced subtitles and regular subtitles in a given language,
|
|
|
59905 |
// the regular subtitles track in that language MUST contain both the forced
|
|
|
59906 |
// subtitles and the regular subtitles for that language.
|
|
|
59907 |
// Because of this requirement and that Safari does not add forced subtitles,
|
|
|
59908 |
// forced subtitles are skipped here to maintain consistent experience across
|
|
|
59909 |
// all platforms
|
|
|
59910 |
continue;
|
|
|
59911 |
}
|
|
|
59912 |
let properties = mediaGroups[type][groupId][variantLabel];
|
|
|
59913 |
let playlistLoader;
|
|
|
59914 |
if (sourceType === 'hls') {
|
|
|
59915 |
playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions);
|
|
|
59916 |
} else if (sourceType === 'dash') {
|
|
|
59917 |
const playlists = properties.playlists.filter(p => p.excludeUntil !== Infinity);
|
|
|
59918 |
if (!playlists.length) {
|
|
|
59919 |
return;
|
|
|
59920 |
}
|
|
|
59921 |
playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader);
|
|
|
59922 |
} else if (sourceType === 'vhs-json') {
|
|
|
59923 |
playlistLoader = new PlaylistLoader(
|
|
|
59924 |
// if the vhs-json object included the media playlist, use the media playlist
|
|
|
59925 |
// as provided, otherwise use the resolved URI to load the playlist
|
|
|
59926 |
properties.playlists ? properties.playlists[0] : properties.resolvedUri, vhs, requestOptions);
|
|
|
59927 |
}
|
|
|
59928 |
properties = merge({
|
|
|
59929 |
id: variantLabel,
|
|
|
59930 |
playlistLoader
|
|
|
59931 |
}, properties);
|
|
|
59932 |
setupListeners[type](type, properties.playlistLoader, settings);
|
|
|
59933 |
groups[groupId].push(properties);
|
|
|
59934 |
if (typeof tracks[variantLabel] === 'undefined') {
|
|
|
59935 |
const track = tech.addRemoteTextTrack({
|
|
|
59936 |
id: variantLabel,
|
|
|
59937 |
kind: 'subtitles',
|
|
|
59938 |
default: properties.default && properties.autoselect,
|
|
|
59939 |
language: properties.language,
|
|
|
59940 |
label: variantLabel
|
|
|
59941 |
}, false).track;
|
|
|
59942 |
tracks[variantLabel] = track;
|
|
|
59943 |
}
|
|
|
59944 |
}
|
|
|
59945 |
} // setup single error event handler for the segment loader
|
|
|
59946 |
|
|
|
59947 |
segmentLoader.on('error', onError[type](type, settings));
|
|
|
59948 |
},
|
|
|
59949 |
/**
|
|
|
59950 |
* Setup TextTracks for the closed-caption groups
|
|
|
59951 |
*
|
|
|
59952 |
* @param {String} type
|
|
|
59953 |
* MediaGroup type
|
|
|
59954 |
* @param {Object} settings
|
|
|
59955 |
* Object containing required information for media groups
|
|
|
59956 |
* @function initialize['CLOSED-CAPTIONS']
|
|
|
59957 |
*/
|
|
|
59958 |
'CLOSED-CAPTIONS': (type, settings) => {
|
|
|
59959 |
const {
|
|
|
59960 |
tech,
|
|
|
59961 |
main: {
|
|
|
59962 |
mediaGroups
|
|
|
59963 |
},
|
|
|
59964 |
mediaTypes: {
|
|
|
59965 |
[type]: {
|
|
|
59966 |
groups,
|
|
|
59967 |
tracks
|
|
|
59968 |
}
|
|
|
59969 |
}
|
|
|
59970 |
} = settings;
|
|
|
59971 |
for (const groupId in mediaGroups[type]) {
|
|
|
59972 |
if (!groups[groupId]) {
|
|
|
59973 |
groups[groupId] = [];
|
|
|
59974 |
}
|
|
|
59975 |
for (const variantLabel in mediaGroups[type][groupId]) {
|
|
|
59976 |
const properties = mediaGroups[type][groupId][variantLabel]; // Look for either 608 (CCn) or 708 (SERVICEn) caption services
|
|
|
59977 |
|
|
|
59978 |
if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) {
|
|
|
59979 |
continue;
|
|
|
59980 |
}
|
|
|
59981 |
const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {};
|
|
|
59982 |
let newProps = {
|
|
|
59983 |
label: variantLabel,
|
|
|
59984 |
language: properties.language,
|
|
|
59985 |
instreamId: properties.instreamId,
|
|
|
59986 |
default: properties.default && properties.autoselect
|
|
|
59987 |
};
|
|
|
59988 |
if (captionServices[newProps.instreamId]) {
|
|
|
59989 |
newProps = merge(newProps, captionServices[newProps.instreamId]);
|
|
|
59990 |
}
|
|
|
59991 |
if (newProps.default === undefined) {
|
|
|
59992 |
delete newProps.default;
|
|
|
59993 |
} // No PlaylistLoader is required for Closed-Captions because the captions are
|
|
|
59994 |
// embedded within the video stream
|
|
|
59995 |
|
|
|
59996 |
groups[groupId].push(merge({
|
|
|
59997 |
id: variantLabel
|
|
|
59998 |
}, properties));
|
|
|
59999 |
if (typeof tracks[variantLabel] === 'undefined') {
|
|
|
60000 |
const track = tech.addRemoteTextTrack({
|
|
|
60001 |
id: newProps.instreamId,
|
|
|
60002 |
kind: 'captions',
|
|
|
60003 |
default: newProps.default,
|
|
|
60004 |
language: newProps.language,
|
|
|
60005 |
label: newProps.label
|
|
|
60006 |
}, false).track;
|
|
|
60007 |
tracks[variantLabel] = track;
|
|
|
60008 |
}
|
|
|
60009 |
}
|
|
|
60010 |
}
|
|
|
60011 |
}
|
|
|
60012 |
};
|
|
|
60013 |
const groupMatch = (list, media) => {
|
|
|
60014 |
for (let i = 0; i < list.length; i++) {
|
|
|
60015 |
if (playlistMatch(media, list[i])) {
|
|
|
60016 |
return true;
|
|
|
60017 |
}
|
|
|
60018 |
if (list[i].playlists && groupMatch(list[i].playlists, media)) {
|
|
|
60019 |
return true;
|
|
|
60020 |
}
|
|
|
60021 |
}
|
|
|
60022 |
return false;
|
|
|
60023 |
};
|
|
|
60024 |
/**
|
|
|
60025 |
* Returns a function used to get the active group of the provided type
|
|
|
60026 |
*
|
|
|
60027 |
* @param {string} type
|
|
|
60028 |
* MediaGroup type
|
|
|
60029 |
* @param {Object} settings
|
|
|
60030 |
* Object containing required information for media groups
|
|
|
60031 |
* @return {Function}
|
|
|
60032 |
* Function that returns the active media group for the provided type. Takes an
|
|
|
60033 |
* optional parameter {TextTrack} track. If no track is provided, a list of all
|
|
|
60034 |
* variants in the group, otherwise the variant corresponding to the provided
|
|
|
60035 |
* track is returned.
|
|
|
60036 |
* @function activeGroup
|
|
|
60037 |
*/
|
|
|
60038 |
|
|
|
60039 |
const activeGroup = (type, settings) => track => {
|
|
|
60040 |
const {
|
|
|
60041 |
mainPlaylistLoader,
|
|
|
60042 |
mediaTypes: {
|
|
|
60043 |
[type]: {
|
|
|
60044 |
groups
|
|
|
60045 |
}
|
|
|
60046 |
}
|
|
|
60047 |
} = settings;
|
|
|
60048 |
const media = mainPlaylistLoader.media();
|
|
|
60049 |
if (!media) {
|
|
|
60050 |
return null;
|
|
|
60051 |
}
|
|
|
60052 |
let variants = null; // set to variants to main media active group
|
|
|
60053 |
|
|
|
60054 |
if (media.attributes[type]) {
|
|
|
60055 |
variants = groups[media.attributes[type]];
|
|
|
60056 |
}
|
|
|
60057 |
const groupKeys = Object.keys(groups);
|
|
|
60058 |
if (!variants) {
|
|
|
60059 |
// find the mainPlaylistLoader media
|
|
|
60060 |
// that is in a media group if we are dealing
|
|
|
60061 |
// with audio only
|
|
|
60062 |
if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.main)) {
|
|
|
60063 |
for (let i = 0; i < groupKeys.length; i++) {
|
|
|
60064 |
const groupPropertyList = groups[groupKeys[i]];
|
|
|
60065 |
if (groupMatch(groupPropertyList, media)) {
|
|
|
60066 |
variants = groupPropertyList;
|
|
|
60067 |
break;
|
|
|
60068 |
}
|
|
|
60069 |
} // use the main group if it exists
|
|
|
60070 |
} else if (groups.main) {
|
|
|
60071 |
variants = groups.main; // only one group, use that one
|
|
|
60072 |
} else if (groupKeys.length === 1) {
|
|
|
60073 |
variants = groups[groupKeys[0]];
|
|
|
60074 |
}
|
|
|
60075 |
}
|
|
|
60076 |
if (typeof track === 'undefined') {
|
|
|
60077 |
return variants;
|
|
|
60078 |
}
|
|
|
60079 |
if (track === null || !variants) {
|
|
|
60080 |
// An active track was specified so a corresponding group is expected. track === null
|
|
|
60081 |
// means no track is currently active so there is no corresponding group
|
|
|
60082 |
return null;
|
|
|
60083 |
}
|
|
|
60084 |
return variants.filter(props => props.id === track.id)[0] || null;
|
|
|
60085 |
};
|
|
|
60086 |
const activeTrack = {
|
|
|
60087 |
/**
|
|
|
60088 |
* Returns a function used to get the active track of type provided
|
|
|
60089 |
*
|
|
|
60090 |
* @param {string} type
|
|
|
60091 |
* MediaGroup type
|
|
|
60092 |
* @param {Object} settings
|
|
|
60093 |
* Object containing required information for media groups
|
|
|
60094 |
* @return {Function}
|
|
|
60095 |
* Function that returns the active media track for the provided type. Returns
|
|
|
60096 |
* null if no track is active
|
|
|
60097 |
* @function activeTrack.AUDIO
|
|
|
60098 |
*/
|
|
|
60099 |
AUDIO: (type, settings) => () => {
|
|
|
60100 |
const {
|
|
|
60101 |
mediaTypes: {
|
|
|
60102 |
[type]: {
|
|
|
60103 |
tracks
|
|
|
60104 |
}
|
|
|
60105 |
}
|
|
|
60106 |
} = settings;
|
|
|
60107 |
for (const id in tracks) {
|
|
|
60108 |
if (tracks[id].enabled) {
|
|
|
60109 |
return tracks[id];
|
|
|
60110 |
}
|
|
|
60111 |
}
|
|
|
60112 |
return null;
|
|
|
60113 |
},
|
|
|
60114 |
/**
|
|
|
60115 |
* Returns a function used to get the active track of type provided
|
|
|
60116 |
*
|
|
|
60117 |
* @param {string} type
|
|
|
60118 |
* MediaGroup type
|
|
|
60119 |
* @param {Object} settings
|
|
|
60120 |
* Object containing required information for media groups
|
|
|
60121 |
* @return {Function}
|
|
|
60122 |
* Function that returns the active media track for the provided type. Returns
|
|
|
60123 |
* null if no track is active
|
|
|
60124 |
* @function activeTrack.SUBTITLES
|
|
|
60125 |
*/
|
|
|
60126 |
SUBTITLES: (type, settings) => () => {
|
|
|
60127 |
const {
|
|
|
60128 |
mediaTypes: {
|
|
|
60129 |
[type]: {
|
|
|
60130 |
tracks
|
|
|
60131 |
}
|
|
|
60132 |
}
|
|
|
60133 |
} = settings;
|
|
|
60134 |
for (const id in tracks) {
|
|
|
60135 |
if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') {
|
|
|
60136 |
return tracks[id];
|
|
|
60137 |
}
|
|
|
60138 |
}
|
|
|
60139 |
return null;
|
|
|
60140 |
}
|
|
|
60141 |
};
|
|
|
60142 |
const getActiveGroup = (type, {
|
|
|
60143 |
mediaTypes
|
|
|
60144 |
}) => () => {
|
|
|
60145 |
const activeTrack_ = mediaTypes[type].activeTrack();
|
|
|
60146 |
if (!activeTrack_) {
|
|
|
60147 |
return null;
|
|
|
60148 |
}
|
|
|
60149 |
return mediaTypes[type].activeGroup(activeTrack_);
|
|
|
60150 |
};
|
|
|
60151 |
/**
|
|
|
60152 |
* Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles,
|
|
|
60153 |
* Closed-Captions) specified in the main manifest.
|
|
|
60154 |
*
|
|
|
60155 |
* @param {Object} settings
|
|
|
60156 |
* Object containing required information for setting up the media groups
|
|
|
60157 |
* @param {Tech} settings.tech
|
|
|
60158 |
* The tech of the player
|
|
|
60159 |
* @param {Object} settings.requestOptions
|
|
|
60160 |
* XHR request options used by the segment loaders
|
|
|
60161 |
* @param {PlaylistLoader} settings.mainPlaylistLoader
|
|
|
60162 |
* PlaylistLoader for the main source
|
|
|
60163 |
* @param {VhsHandler} settings.vhs
|
|
|
60164 |
* VHS SourceHandler
|
|
|
60165 |
* @param {Object} settings.main
|
|
|
60166 |
* The parsed main manifest
|
|
|
60167 |
* @param {Object} settings.mediaTypes
|
|
|
60168 |
* Object to store the loaders, tracks, and utility methods for each media type
|
|
|
60169 |
* @param {Function} settings.excludePlaylist
|
|
|
60170 |
* Excludes the current rendition and forces a rendition switch.
|
|
|
60171 |
* @function setupMediaGroups
|
|
|
60172 |
*/
|
|
|
60173 |
|
|
|
60174 |
const setupMediaGroups = settings => {
|
|
|
60175 |
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => {
|
|
|
60176 |
initialize[type](type, settings);
|
|
|
60177 |
});
|
|
|
60178 |
const {
|
|
|
60179 |
mediaTypes,
|
|
|
60180 |
mainPlaylistLoader,
|
|
|
60181 |
tech,
|
|
|
60182 |
vhs,
|
|
|
60183 |
segmentLoaders: {
|
|
|
60184 |
['AUDIO']: audioSegmentLoader,
|
|
|
60185 |
main: mainSegmentLoader
|
|
|
60186 |
}
|
|
|
60187 |
} = settings; // setup active group and track getters and change event handlers
|
|
|
60188 |
|
|
|
60189 |
['AUDIO', 'SUBTITLES'].forEach(type => {
|
|
|
60190 |
mediaTypes[type].activeGroup = activeGroup(type, settings);
|
|
|
60191 |
mediaTypes[type].activeTrack = activeTrack[type](type, settings);
|
|
|
60192 |
mediaTypes[type].onGroupChanged = onGroupChanged(type, settings);
|
|
|
60193 |
mediaTypes[type].onGroupChanging = onGroupChanging(type, settings);
|
|
|
60194 |
mediaTypes[type].onTrackChanged = onTrackChanged(type, settings);
|
|
|
60195 |
mediaTypes[type].getActiveGroup = getActiveGroup(type, settings);
|
|
|
60196 |
}); // DO NOT enable the default subtitle or caption track.
|
|
|
60197 |
// DO enable the default audio track
|
|
|
60198 |
|
|
|
60199 |
const audioGroup = mediaTypes.AUDIO.activeGroup();
|
|
|
60200 |
if (audioGroup) {
|
|
|
60201 |
const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id;
|
|
|
60202 |
mediaTypes.AUDIO.tracks[groupId].enabled = true;
|
|
|
60203 |
mediaTypes.AUDIO.onGroupChanged();
|
|
|
60204 |
mediaTypes.AUDIO.onTrackChanged();
|
|
|
60205 |
const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup(); // a similar check for handling setAudio on each loader is run again each time the
|
|
|
60206 |
// track is changed, but needs to be handled here since the track may not be considered
|
|
|
60207 |
// changed on the first call to onTrackChanged
|
|
|
60208 |
|
|
|
60209 |
if (!activeAudioGroup.playlistLoader) {
|
|
|
60210 |
// either audio is muxed with video or the stream is audio only
|
|
|
60211 |
mainSegmentLoader.setAudio(true);
|
|
|
60212 |
} else {
|
|
|
60213 |
// audio is demuxed
|
|
|
60214 |
mainSegmentLoader.setAudio(false);
|
|
|
60215 |
audioSegmentLoader.setAudio(true);
|
|
|
60216 |
}
|
|
|
60217 |
}
|
|
|
60218 |
mainPlaylistLoader.on('mediachange', () => {
|
|
|
60219 |
['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged());
|
|
|
60220 |
});
|
|
|
60221 |
mainPlaylistLoader.on('mediachanging', () => {
|
|
|
60222 |
['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanging());
|
|
|
60223 |
}); // custom audio track change event handler for usage event
|
|
|
60224 |
|
|
|
60225 |
const onAudioTrackChanged = () => {
|
|
|
60226 |
mediaTypes.AUDIO.onTrackChanged();
|
|
|
60227 |
tech.trigger({
|
|
|
60228 |
type: 'usage',
|
|
|
60229 |
name: 'vhs-audio-change'
|
|
|
60230 |
});
|
|
|
60231 |
};
|
|
|
60232 |
tech.audioTracks().addEventListener('change', onAudioTrackChanged);
|
|
|
60233 |
tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged);
|
|
|
60234 |
vhs.on('dispose', () => {
|
|
|
60235 |
tech.audioTracks().removeEventListener('change', onAudioTrackChanged);
|
|
|
60236 |
tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged);
|
|
|
60237 |
}); // clear existing audio tracks and add the ones we just created
|
|
|
60238 |
|
|
|
60239 |
tech.clearTracks('audio');
|
|
|
60240 |
for (const id in mediaTypes.AUDIO.tracks) {
|
|
|
60241 |
tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]);
|
|
|
60242 |
}
|
|
|
60243 |
};
|
|
|
60244 |
/**
|
|
|
60245 |
* Creates skeleton object used to store the loaders, tracks, and utility methods for each
|
|
|
60246 |
* media type
|
|
|
60247 |
*
|
|
|
60248 |
* @return {Object}
|
|
|
60249 |
* Object to store the loaders, tracks, and utility methods for each media type
|
|
|
60250 |
* @function createMediaTypes
|
|
|
60251 |
*/
|
|
|
60252 |
|
|
|
60253 |
const createMediaTypes = () => {
|
|
|
60254 |
const mediaTypes = {};
|
|
|
60255 |
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => {
|
|
|
60256 |
mediaTypes[type] = {
|
|
|
60257 |
groups: {},
|
|
|
60258 |
tracks: {},
|
|
|
60259 |
activePlaylistLoader: null,
|
|
|
60260 |
activeGroup: noop,
|
|
|
60261 |
activeTrack: noop,
|
|
|
60262 |
getActiveGroup: noop,
|
|
|
60263 |
onGroupChanged: noop,
|
|
|
60264 |
onTrackChanged: noop,
|
|
|
60265 |
lastTrack_: null,
|
|
|
60266 |
logger_: logger(`MediaGroups[${type}]`)
|
|
|
60267 |
};
|
|
|
60268 |
});
|
|
|
60269 |
return mediaTypes;
|
|
|
60270 |
};
|
|
|
60271 |
|
|
|
60272 |
/**
|
|
|
60273 |
* A utility class for setting properties and maintaining the state of the content steering manifest.
|
|
|
60274 |
*
|
|
|
60275 |
* Content Steering manifest format:
|
|
|
60276 |
* VERSION: number (required) currently only version 1 is supported.
|
|
|
60277 |
* TTL: number in seconds (optional) until the next content steering manifest reload.
|
|
|
60278 |
* RELOAD-URI: string (optional) uri to fetch the next content steering manifest.
|
|
|
60279 |
* SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values.
|
|
|
60280 |
* PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists.
|
|
|
60281 |
*/
|
|
|
60282 |
|
|
|
60283 |
class SteeringManifest {
|
|
|
60284 |
constructor() {
|
|
|
60285 |
this.priority_ = [];
|
|
|
60286 |
this.pathwayClones_ = new Map();
|
|
|
60287 |
}
|
|
|
60288 |
set version(number) {
|
|
|
60289 |
// Only version 1 is currently supported for both DASH and HLS.
|
|
|
60290 |
if (number === 1) {
|
|
|
60291 |
this.version_ = number;
|
|
|
60292 |
}
|
|
|
60293 |
}
|
|
|
60294 |
set ttl(seconds) {
|
|
|
60295 |
// TTL = time-to-live, default = 300 seconds.
|
|
|
60296 |
this.ttl_ = seconds || 300;
|
|
|
60297 |
}
|
|
|
60298 |
set reloadUri(uri) {
|
|
|
60299 |
if (uri) {
|
|
|
60300 |
// reload URI can be relative to the previous reloadUri.
|
|
|
60301 |
this.reloadUri_ = resolveUrl(this.reloadUri_, uri);
|
|
|
60302 |
}
|
|
|
60303 |
}
|
|
|
60304 |
set priority(array) {
|
|
|
60305 |
// priority must be non-empty and unique values.
|
|
|
60306 |
if (array && array.length) {
|
|
|
60307 |
this.priority_ = array;
|
|
|
60308 |
}
|
|
|
60309 |
}
|
|
|
60310 |
set pathwayClones(array) {
|
|
|
60311 |
// pathwayClones must be non-empty.
|
|
|
60312 |
if (array && array.length) {
|
|
|
60313 |
this.pathwayClones_ = new Map(array.map(clone => [clone.ID, clone]));
|
|
|
60314 |
}
|
|
|
60315 |
}
|
|
|
60316 |
get version() {
|
|
|
60317 |
return this.version_;
|
|
|
60318 |
}
|
|
|
60319 |
get ttl() {
|
|
|
60320 |
return this.ttl_;
|
|
|
60321 |
}
|
|
|
60322 |
get reloadUri() {
|
|
|
60323 |
return this.reloadUri_;
|
|
|
60324 |
}
|
|
|
60325 |
get priority() {
|
|
|
60326 |
return this.priority_;
|
|
|
60327 |
}
|
|
|
60328 |
get pathwayClones() {
|
|
|
60329 |
return this.pathwayClones_;
|
|
|
60330 |
}
|
|
|
60331 |
}
|
|
|
60332 |
/**
|
|
|
60333 |
* This class represents a content steering manifest and associated state. See both HLS and DASH specifications.
|
|
|
60334 |
* HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and
|
|
|
60335 |
* https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6.
|
|
|
60336 |
* DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
|
|
|
60337 |
*
|
|
|
60338 |
* @param {function} xhr for making a network request from the browser.
|
|
|
60339 |
* @param {function} bandwidth for fetching the current bandwidth from the main segment loader.
|
|
|
60340 |
*/
|
|
|
60341 |
|
|
|
60342 |
class ContentSteeringController extends videojs.EventTarget {
|
|
|
60343 |
constructor(xhr, bandwidth) {
|
|
|
60344 |
super();
|
|
|
60345 |
this.currentPathway = null;
|
|
|
60346 |
this.defaultPathway = null;
|
|
|
60347 |
this.queryBeforeStart = false;
|
|
|
60348 |
this.availablePathways_ = new Set();
|
|
|
60349 |
this.steeringManifest = new SteeringManifest();
|
|
|
60350 |
this.proxyServerUrl_ = null;
|
|
|
60351 |
this.manifestType_ = null;
|
|
|
60352 |
this.ttlTimeout_ = null;
|
|
|
60353 |
this.request_ = null;
|
|
|
60354 |
this.currentPathwayClones = new Map();
|
|
|
60355 |
this.nextPathwayClones = new Map();
|
|
|
60356 |
this.excludedSteeringManifestURLs = new Set();
|
|
|
60357 |
this.logger_ = logger('Content Steering');
|
|
|
60358 |
this.xhr_ = xhr;
|
|
|
60359 |
this.getBandwidth_ = bandwidth;
|
|
|
60360 |
}
|
|
|
60361 |
/**
|
|
|
60362 |
* Assigns the content steering tag properties to the steering controller
|
|
|
60363 |
*
|
|
|
60364 |
* @param {string} baseUrl the baseURL from the main manifest for resolving the steering manifest url
|
|
|
60365 |
* @param {Object} steeringTag the content steering tag from the main manifest
|
|
|
60366 |
*/
|
|
|
60367 |
|
|
|
60368 |
assignTagProperties(baseUrl, steeringTag) {
|
|
|
60369 |
this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH'; // serverUri is HLS serverURL is DASH
|
|
|
60370 |
|
|
|
60371 |
const steeringUri = steeringTag.serverUri || steeringTag.serverURL;
|
|
|
60372 |
if (!steeringUri) {
|
|
|
60373 |
this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`);
|
|
|
60374 |
this.trigger('error');
|
|
|
60375 |
return;
|
|
|
60376 |
} // Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case.
|
|
|
60377 |
|
|
|
60378 |
if (steeringUri.startsWith('data:')) {
|
|
|
60379 |
this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1));
|
|
|
60380 |
return;
|
|
|
60381 |
} // reloadUri is the resolution of the main manifest URL and steering URL.
|
|
|
60382 |
|
|
|
60383 |
this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri); // pathwayId is HLS defaultServiceLocation is DASH
|
|
|
60384 |
|
|
|
60385 |
this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation; // currently only DASH supports the following properties on <ContentSteering> tags.
|
|
|
60386 |
|
|
|
60387 |
this.queryBeforeStart = steeringTag.queryBeforeStart;
|
|
|
60388 |
this.proxyServerUrl_ = steeringTag.proxyServerURL; // trigger a steering event if we have a pathway from the content steering tag.
|
|
|
60389 |
// this tells VHS which segment pathway to start with.
|
|
|
60390 |
// If queryBeforeStart is true we need to wait for the steering manifest response.
|
|
|
60391 |
|
|
|
60392 |
if (this.defaultPathway && !this.queryBeforeStart) {
|
|
|
60393 |
this.trigger('content-steering');
|
|
|
60394 |
}
|
|
|
60395 |
}
|
|
|
60396 |
/**
|
|
|
60397 |
* Requests the content steering manifest and parse the response. This should only be called after
|
|
|
60398 |
* assignTagProperties was called with a content steering tag.
|
|
|
60399 |
*
|
|
|
60400 |
* @param {string} initialUri The optional uri to make the request with.
|
|
|
60401 |
* If set, the request should be made with exactly what is passed in this variable.
|
|
|
60402 |
* This scenario should only happen once on initalization.
|
|
|
60403 |
*/
|
|
|
60404 |
|
|
|
60405 |
requestSteeringManifest(initial) {
|
|
|
60406 |
const reloadUri = this.steeringManifest.reloadUri;
|
|
|
60407 |
if (!reloadUri) {
|
|
|
60408 |
return;
|
|
|
60409 |
} // We currently don't support passing MPD query parameters directly to the content steering URL as this requires
|
|
|
60410 |
// ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1.
|
|
|
60411 |
// This request URI accounts for manifest URIs that have been excluded.
|
|
|
60412 |
|
|
|
60413 |
const uri = initial ? reloadUri : this.getRequestURI(reloadUri); // If there are no valid manifest URIs, we should stop content steering.
|
|
|
60414 |
|
|
|
60415 |
if (!uri) {
|
|
|
60416 |
this.logger_('No valid content steering manifest URIs. Stopping content steering.');
|
|
|
60417 |
this.trigger('error');
|
|
|
60418 |
this.dispose();
|
|
|
60419 |
return;
|
|
|
60420 |
}
|
|
|
60421 |
this.request_ = this.xhr_({
|
|
|
60422 |
uri
|
|
|
60423 |
}, (error, errorInfo) => {
|
|
|
60424 |
if (error) {
|
|
|
60425 |
// If the client receives HTTP 410 Gone in response to a manifest request,
|
|
|
60426 |
// it MUST NOT issue another request for that URI for the remainder of the
|
|
|
60427 |
// playback session. It MAY continue to use the most-recently obtained set
|
|
|
60428 |
// of Pathways.
|
|
|
60429 |
if (errorInfo.status === 410) {
|
|
|
60430 |
this.logger_(`manifest request 410 ${error}.`);
|
|
|
60431 |
this.logger_(`There will be no more content steering requests to ${uri} this session.`);
|
|
|
60432 |
this.excludedSteeringManifestURLs.add(uri);
|
|
|
60433 |
return;
|
|
|
60434 |
} // If the client receives HTTP 429 Too Many Requests with a Retry-After
|
|
|
60435 |
// header in response to a manifest request, it SHOULD wait until the time
|
|
|
60436 |
// specified by the Retry-After header to reissue the request.
|
|
|
60437 |
|
|
|
60438 |
if (errorInfo.status === 429) {
|
|
|
60439 |
const retrySeconds = errorInfo.responseHeaders['retry-after'];
|
|
|
60440 |
this.logger_(`manifest request 429 ${error}.`);
|
|
|
60441 |
this.logger_(`content steering will retry in ${retrySeconds} seconds.`);
|
|
|
60442 |
this.startTTLTimeout_(parseInt(retrySeconds, 10));
|
|
|
60443 |
return;
|
|
|
60444 |
} // If the Steering Manifest cannot be loaded and parsed correctly, the
|
|
|
60445 |
// client SHOULD continue to use the previous values and attempt to reload
|
|
|
60446 |
// it after waiting for the previously-specified TTL (or 5 minutes if
|
|
|
60447 |
// none).
|
|
|
60448 |
|
|
|
60449 |
this.logger_(`manifest failed to load ${error}.`);
|
|
|
60450 |
this.startTTLTimeout_();
|
|
|
60451 |
return;
|
|
|
60452 |
}
|
|
|
60453 |
const steeringManifestJson = JSON.parse(this.request_.responseText);
|
|
|
60454 |
this.assignSteeringProperties_(steeringManifestJson);
|
|
|
60455 |
this.startTTLTimeout_();
|
|
|
60456 |
});
|
|
|
60457 |
}
|
|
|
60458 |
/**
|
|
|
60459 |
* Set the proxy server URL and add the steering manifest url as a URI encoded parameter.
|
|
|
60460 |
*
|
|
|
60461 |
* @param {string} steeringUrl the steering manifest url
|
|
|
60462 |
* @return the steering manifest url to a proxy server with all parameters set
|
|
|
60463 |
*/
|
|
|
60464 |
|
|
|
60465 |
setProxyServerUrl_(steeringUrl) {
|
|
|
60466 |
const steeringUrlObject = new window.URL(steeringUrl);
|
|
|
60467 |
const proxyServerUrlObject = new window.URL(this.proxyServerUrl_);
|
|
|
60468 |
proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString()));
|
|
|
60469 |
return this.setSteeringParams_(proxyServerUrlObject.toString());
|
|
|
60470 |
}
|
|
|
60471 |
/**
|
|
|
60472 |
* Decodes and parses the data uri encoded steering manifest
|
|
|
60473 |
*
|
|
|
60474 |
* @param {string} dataUri the data uri to be decoded and parsed.
|
|
|
60475 |
*/
|
|
|
60476 |
|
|
|
60477 |
decodeDataUriManifest_(dataUri) {
|
|
|
60478 |
const steeringManifestJson = JSON.parse(window.atob(dataUri));
|
|
|
60479 |
this.assignSteeringProperties_(steeringManifestJson);
|
|
|
60480 |
}
|
|
|
60481 |
/**
|
|
|
60482 |
* Set the HLS or DASH content steering manifest request query parameters. For example:
|
|
|
60483 |
* _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT>
|
|
|
60484 |
* _DASH_pathway and _DASH_throughput
|
|
|
60485 |
*
|
|
|
60486 |
* @param {string} uri to add content steering server parameters to.
|
|
|
60487 |
* @return a new uri as a string with the added steering query parameters.
|
|
|
60488 |
*/
|
|
|
60489 |
|
|
|
60490 |
setSteeringParams_(url) {
|
|
|
60491 |
const urlObject = new window.URL(url);
|
|
|
60492 |
const path = this.getPathway();
|
|
|
60493 |
const networkThroughput = this.getBandwidth_();
|
|
|
60494 |
if (path) {
|
|
|
60495 |
const pathwayKey = `_${this.manifestType_}_pathway`;
|
|
|
60496 |
urlObject.searchParams.set(pathwayKey, path);
|
|
|
60497 |
}
|
|
|
60498 |
if (networkThroughput) {
|
|
|
60499 |
const throughputKey = `_${this.manifestType_}_throughput`;
|
|
|
60500 |
urlObject.searchParams.set(throughputKey, networkThroughput);
|
|
|
60501 |
}
|
|
|
60502 |
return urlObject.toString();
|
|
|
60503 |
}
|
|
|
60504 |
/**
|
|
|
60505 |
* Assigns the current steering manifest properties and to the SteeringManifest object
|
|
|
60506 |
*
|
|
|
60507 |
* @param {Object} steeringJson the raw JSON steering manifest
|
|
|
60508 |
*/
|
|
|
60509 |
|
|
|
60510 |
assignSteeringProperties_(steeringJson) {
|
|
|
60511 |
this.steeringManifest.version = steeringJson.VERSION;
|
|
|
60512 |
if (!this.steeringManifest.version) {
|
|
|
60513 |
this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`);
|
|
|
60514 |
this.trigger('error');
|
|
|
60515 |
return;
|
|
|
60516 |
}
|
|
|
60517 |
this.steeringManifest.ttl = steeringJson.TTL;
|
|
|
60518 |
this.steeringManifest.reloadUri = steeringJson['RELOAD-URI']; // HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional
|
|
|
60519 |
|
|
|
60520 |
this.steeringManifest.priority = steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY']; // Pathway clones to be created/updated in HLS.
|
|
|
60521 |
// See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
|
|
|
60522 |
|
|
|
60523 |
this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES'];
|
|
|
60524 |
this.nextPathwayClones = this.steeringManifest.pathwayClones; // 1. apply first pathway from the array.
|
|
|
60525 |
// 2. if first pathway doesn't exist in manifest, try next pathway.
|
|
|
60526 |
// a. if all pathways are exhausted, ignore the steering manifest priority.
|
|
|
60527 |
// 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway.
|
|
|
60528 |
// a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response,
|
|
|
60529 |
// the excluded pathway will be ignored.
|
|
|
60530 |
// See excludePathway usage in excludePlaylist().
|
|
|
60531 |
// If there are no available pathways, we need to stop content steering.
|
|
|
60532 |
|
|
|
60533 |
if (!this.availablePathways_.size) {
|
|
|
60534 |
this.logger_('There are no available pathways for content steering. Ending content steering.');
|
|
|
60535 |
this.trigger('error');
|
|
|
60536 |
this.dispose();
|
|
|
60537 |
}
|
|
|
60538 |
const chooseNextPathway = pathwaysByPriority => {
|
|
|
60539 |
for (const path of pathwaysByPriority) {
|
|
|
60540 |
if (this.availablePathways_.has(path)) {
|
|
|
60541 |
return path;
|
|
|
60542 |
}
|
|
|
60543 |
} // If no pathway matches, ignore the manifest and choose the first available.
|
|
|
60544 |
|
|
|
60545 |
return [...this.availablePathways_][0];
|
|
|
60546 |
};
|
|
|
60547 |
const nextPathway = chooseNextPathway(this.steeringManifest.priority);
|
|
|
60548 |
if (this.currentPathway !== nextPathway) {
|
|
|
60549 |
this.currentPathway = nextPathway;
|
|
|
60550 |
this.trigger('content-steering');
|
|
|
60551 |
}
|
|
|
60552 |
}
|
|
|
60553 |
/**
|
|
|
60554 |
* Returns the pathway to use for steering decisions
|
|
|
60555 |
*
|
|
|
60556 |
* @return {string} returns the current pathway or the default
|
|
|
60557 |
*/
|
|
|
60558 |
|
|
|
60559 |
getPathway() {
|
|
|
60560 |
return this.currentPathway || this.defaultPathway;
|
|
|
60561 |
}
|
|
|
60562 |
/**
|
|
|
60563 |
* Chooses the manifest request URI based on proxy URIs and server URLs.
|
|
|
60564 |
* Also accounts for exclusion on certain manifest URIs.
|
|
|
60565 |
*
|
|
|
60566 |
* @param {string} reloadUri the base uri before parameters
|
|
|
60567 |
*
|
|
|
60568 |
* @return {string} the final URI for the request to the manifest server.
|
|
|
60569 |
*/
|
|
|
60570 |
|
|
|
60571 |
getRequestURI(reloadUri) {
|
|
|
60572 |
if (!reloadUri) {
|
|
|
60573 |
return null;
|
|
|
60574 |
}
|
|
|
60575 |
const isExcluded = uri => this.excludedSteeringManifestURLs.has(uri);
|
|
|
60576 |
if (this.proxyServerUrl_) {
|
|
|
60577 |
const proxyURI = this.setProxyServerUrl_(reloadUri);
|
|
|
60578 |
if (!isExcluded(proxyURI)) {
|
|
|
60579 |
return proxyURI;
|
|
|
60580 |
}
|
|
|
60581 |
}
|
|
|
60582 |
const steeringURI = this.setSteeringParams_(reloadUri);
|
|
|
60583 |
if (!isExcluded(steeringURI)) {
|
|
|
60584 |
return steeringURI;
|
|
|
60585 |
} // Return nothing if all valid manifest URIs are excluded.
|
|
|
60586 |
|
|
|
60587 |
return null;
|
|
|
60588 |
}
|
|
|
60589 |
/**
|
|
|
60590 |
* Start the timeout for re-requesting the steering manifest at the TTL interval.
|
|
|
60591 |
*
|
|
|
60592 |
* @param {number} ttl time in seconds of the timeout. Defaults to the
|
|
|
60593 |
* ttl interval in the steering manifest
|
|
|
60594 |
*/
|
|
|
60595 |
|
|
|
60596 |
startTTLTimeout_(ttl = this.steeringManifest.ttl) {
|
|
|
60597 |
// 300 (5 minutes) is the default value.
|
|
|
60598 |
const ttlMS = ttl * 1000;
|
|
|
60599 |
this.ttlTimeout_ = window.setTimeout(() => {
|
|
|
60600 |
this.requestSteeringManifest();
|
|
|
60601 |
}, ttlMS);
|
|
|
60602 |
}
|
|
|
60603 |
/**
|
|
|
60604 |
* Clear the TTL timeout if necessary.
|
|
|
60605 |
*/
|
|
|
60606 |
|
|
|
60607 |
clearTTLTimeout_() {
|
|
|
60608 |
window.clearTimeout(this.ttlTimeout_);
|
|
|
60609 |
this.ttlTimeout_ = null;
|
|
|
60610 |
}
|
|
|
60611 |
/**
|
|
|
60612 |
* aborts any current steering xhr and sets the current request object to null
|
|
|
60613 |
*/
|
|
|
60614 |
|
|
|
60615 |
abort() {
|
|
|
60616 |
if (this.request_) {
|
|
|
60617 |
this.request_.abort();
|
|
|
60618 |
}
|
|
|
60619 |
this.request_ = null;
|
|
|
60620 |
}
|
|
|
60621 |
/**
|
|
|
60622 |
* aborts steering requests clears the ttl timeout and resets all properties.
|
|
|
60623 |
*/
|
|
|
60624 |
|
|
|
60625 |
dispose() {
|
|
|
60626 |
this.off('content-steering');
|
|
|
60627 |
this.off('error');
|
|
|
60628 |
this.abort();
|
|
|
60629 |
this.clearTTLTimeout_();
|
|
|
60630 |
this.currentPathway = null;
|
|
|
60631 |
this.defaultPathway = null;
|
|
|
60632 |
this.queryBeforeStart = null;
|
|
|
60633 |
this.proxyServerUrl_ = null;
|
|
|
60634 |
this.manifestType_ = null;
|
|
|
60635 |
this.ttlTimeout_ = null;
|
|
|
60636 |
this.request_ = null;
|
|
|
60637 |
this.excludedSteeringManifestURLs = new Set();
|
|
|
60638 |
this.availablePathways_ = new Set();
|
|
|
60639 |
this.steeringManifest = new SteeringManifest();
|
|
|
60640 |
}
|
|
|
60641 |
/**
|
|
|
60642 |
* adds a pathway to the available pathways set
|
|
|
60643 |
*
|
|
|
60644 |
* @param {string} pathway the pathway string to add
|
|
|
60645 |
*/
|
|
|
60646 |
|
|
|
60647 |
addAvailablePathway(pathway) {
|
|
|
60648 |
if (pathway) {
|
|
|
60649 |
this.availablePathways_.add(pathway);
|
|
|
60650 |
}
|
|
|
60651 |
}
|
|
|
60652 |
/**
|
|
|
60653 |
* Clears all pathways from the available pathways set
|
|
|
60654 |
*/
|
|
|
60655 |
|
|
|
60656 |
clearAvailablePathways() {
|
|
|
60657 |
this.availablePathways_.clear();
|
|
|
60658 |
}
|
|
|
60659 |
/**
|
|
|
60660 |
* Removes a pathway from the available pathways set.
|
|
|
60661 |
*/
|
|
|
60662 |
|
|
|
60663 |
excludePathway(pathway) {
|
|
|
60664 |
return this.availablePathways_.delete(pathway);
|
|
|
60665 |
}
|
|
|
60666 |
/**
|
|
|
60667 |
* Checks the refreshed DASH manifest content steering tag for changes.
|
|
|
60668 |
*
|
|
|
60669 |
* @param {string} baseURL new steering tag on DASH manifest refresh
|
|
|
60670 |
* @param {Object} newTag the new tag to check for changes
|
|
|
60671 |
* @return a true or false whether the new tag has different values
|
|
|
60672 |
*/
|
|
|
60673 |
|
|
|
60674 |
didDASHTagChange(baseURL, newTag) {
|
|
|
60675 |
return !newTag && this.steeringManifest.reloadUri || newTag && (resolveUrl(baseURL, newTag.serverURL) !== this.steeringManifest.reloadUri || newTag.defaultServiceLocation !== this.defaultPathway || newTag.queryBeforeStart !== this.queryBeforeStart || newTag.proxyServerURL !== this.proxyServerUrl_);
|
|
|
60676 |
}
|
|
|
60677 |
getAvailablePathways() {
|
|
|
60678 |
return this.availablePathways_;
|
|
|
60679 |
}
|
|
|
60680 |
}
|
|
|
60681 |
|
|
|
60682 |
/**
|
|
|
60683 |
* @file playlist-controller.js
|
|
|
60684 |
*/
|
|
|
60685 |
const ABORT_EARLY_EXCLUSION_SECONDS = 10;
|
|
|
60686 |
let Vhs$1; // SegmentLoader stats that need to have each loader's
|
|
|
60687 |
// values summed to calculate the final value
|
|
|
60688 |
|
|
|
60689 |
const loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred', 'mediaAppends'];
|
|
|
60690 |
const sumLoaderStat = function (stat) {
|
|
|
60691 |
return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat];
|
|
|
60692 |
};
|
|
|
60693 |
const shouldSwitchToMedia = function ({
|
|
|
60694 |
currentPlaylist,
|
|
|
60695 |
buffered,
|
|
|
60696 |
currentTime,
|
|
|
60697 |
nextPlaylist,
|
|
|
60698 |
bufferLowWaterLine,
|
|
|
60699 |
bufferHighWaterLine,
|
|
|
60700 |
duration,
|
|
|
60701 |
bufferBasedABR,
|
|
|
60702 |
log
|
|
|
60703 |
}) {
|
|
|
60704 |
// we have no other playlist to switch to
|
|
|
60705 |
if (!nextPlaylist) {
|
|
|
60706 |
videojs.log.warn('We received no playlist to switch to. Please check your stream.');
|
|
|
60707 |
return false;
|
|
|
60708 |
}
|
|
|
60709 |
const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`;
|
|
|
60710 |
if (!currentPlaylist) {
|
|
|
60711 |
log(`${sharedLogLine} as current playlist is not set`);
|
|
|
60712 |
return true;
|
|
|
60713 |
} // no need to switch if playlist is the same
|
|
|
60714 |
|
|
|
60715 |
if (nextPlaylist.id === currentPlaylist.id) {
|
|
|
60716 |
return false;
|
|
|
60717 |
} // determine if current time is in a buffered range.
|
|
|
60718 |
|
|
|
60719 |
const isBuffered = Boolean(findRange(buffered, currentTime).length); // If the playlist is live, then we want to not take low water line into account.
|
|
|
60720 |
// This is because in LIVE, the player plays 3 segments from the end of the
|
|
|
60721 |
// playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
|
|
|
60722 |
// in those segments, a viewer will never experience a rendition upswitch.
|
|
|
60723 |
|
|
|
60724 |
if (!currentPlaylist.endList) {
|
|
|
60725 |
// For LLHLS live streams, don't switch renditions before playback has started, as it almost
|
|
|
60726 |
// doubles the time to first playback.
|
|
|
60727 |
if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') {
|
|
|
60728 |
log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`);
|
|
|
60729 |
return false;
|
|
|
60730 |
}
|
|
|
60731 |
log(`${sharedLogLine} as current playlist is live`);
|
|
|
60732 |
return true;
|
|
|
60733 |
}
|
|
|
60734 |
const forwardBuffer = timeAheadOf(buffered, currentTime);
|
|
|
60735 |
const maxBufferLowWaterLine = bufferBasedABR ? Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE; // For the same reason as LIVE, we ignore the low water line when the VOD
|
|
|
60736 |
// duration is below the max potential low water line
|
|
|
60737 |
|
|
|
60738 |
if (duration < maxBufferLowWaterLine) {
|
|
|
60739 |
log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`);
|
|
|
60740 |
return true;
|
|
|
60741 |
}
|
|
|
60742 |
const nextBandwidth = nextPlaylist.attributes.BANDWIDTH;
|
|
|
60743 |
const currBandwidth = currentPlaylist.attributes.BANDWIDTH; // when switching down, if our buffer is lower than the high water line,
|
|
|
60744 |
// we can switch down
|
|
|
60745 |
|
|
|
60746 |
if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) {
|
|
|
60747 |
let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`;
|
|
|
60748 |
if (bufferBasedABR) {
|
|
|
60749 |
logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`;
|
|
|
60750 |
}
|
|
|
60751 |
log(logLine);
|
|
|
60752 |
return true;
|
|
|
60753 |
} // and if our buffer is higher than the low water line,
|
|
|
60754 |
// we can switch up
|
|
|
60755 |
|
|
|
60756 |
if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) {
|
|
|
60757 |
let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`;
|
|
|
60758 |
if (bufferBasedABR) {
|
|
|
60759 |
logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`;
|
|
|
60760 |
}
|
|
|
60761 |
log(logLine);
|
|
|
60762 |
return true;
|
|
|
60763 |
}
|
|
|
60764 |
log(`not ${sharedLogLine} as no switching criteria met`);
|
|
|
60765 |
return false;
|
|
|
60766 |
};
|
|
|
60767 |
/**
|
|
|
60768 |
* the main playlist controller controller all interactons
|
|
|
60769 |
* between playlists and segmentloaders. At this time this mainly
|
|
|
60770 |
* involves a main playlist and a series of audio playlists
|
|
|
60771 |
* if they are available
|
|
|
60772 |
*
|
|
|
60773 |
* @class PlaylistController
|
|
|
60774 |
* @extends videojs.EventTarget
|
|
|
60775 |
*/
|
|
|
60776 |
|
|
|
60777 |
class PlaylistController extends videojs.EventTarget {
|
|
|
60778 |
constructor(options) {
|
|
|
60779 |
super();
|
|
|
60780 |
const {
|
|
|
60781 |
src,
|
|
|
60782 |
withCredentials,
|
|
|
60783 |
tech,
|
|
|
60784 |
bandwidth,
|
|
|
60785 |
externVhs,
|
|
|
60786 |
useCueTags,
|
|
|
60787 |
playlistExclusionDuration,
|
|
|
60788 |
enableLowInitialPlaylist,
|
|
|
60789 |
sourceType,
|
|
|
60790 |
cacheEncryptionKeys,
|
|
|
60791 |
bufferBasedABR,
|
|
|
60792 |
leastPixelDiffSelector,
|
|
|
60793 |
captionServices
|
|
|
60794 |
} = options;
|
|
|
60795 |
if (!src) {
|
|
|
60796 |
throw new Error('A non-empty playlist URL or JSON manifest string is required');
|
|
|
60797 |
}
|
|
|
60798 |
let {
|
|
|
60799 |
maxPlaylistRetries
|
|
|
60800 |
} = options;
|
|
|
60801 |
if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') {
|
|
|
60802 |
maxPlaylistRetries = Infinity;
|
|
|
60803 |
}
|
|
|
60804 |
Vhs$1 = externVhs;
|
|
|
60805 |
this.bufferBasedABR = Boolean(bufferBasedABR);
|
|
|
60806 |
this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector);
|
|
|
60807 |
this.withCredentials = withCredentials;
|
|
|
60808 |
this.tech_ = tech;
|
|
|
60809 |
this.vhs_ = tech.vhs;
|
|
|
60810 |
this.sourceType_ = sourceType;
|
|
|
60811 |
this.useCueTags_ = useCueTags;
|
|
|
60812 |
this.playlistExclusionDuration = playlistExclusionDuration;
|
|
|
60813 |
this.maxPlaylistRetries = maxPlaylistRetries;
|
|
|
60814 |
this.enableLowInitialPlaylist = enableLowInitialPlaylist;
|
|
|
60815 |
if (this.useCueTags_) {
|
|
|
60816 |
this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues');
|
|
|
60817 |
this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
|
|
|
60818 |
}
|
|
|
60819 |
this.requestOptions_ = {
|
|
|
60820 |
withCredentials,
|
|
|
60821 |
maxPlaylistRetries,
|
|
|
60822 |
timeout: null
|
|
|
60823 |
};
|
|
|
60824 |
this.on('error', this.pauseLoading);
|
|
|
60825 |
this.mediaTypes_ = createMediaTypes();
|
|
|
60826 |
this.mediaSource = new window.MediaSource();
|
|
|
60827 |
this.handleDurationChange_ = this.handleDurationChange_.bind(this);
|
|
|
60828 |
this.handleSourceOpen_ = this.handleSourceOpen_.bind(this);
|
|
|
60829 |
this.handleSourceEnded_ = this.handleSourceEnded_.bind(this);
|
|
|
60830 |
this.mediaSource.addEventListener('durationchange', this.handleDurationChange_); // load the media source into the player
|
|
|
60831 |
|
|
|
60832 |
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_);
|
|
|
60833 |
this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_); // we don't have to handle sourceclose since dispose will handle termination of
|
|
|
60834 |
// everything, and the MediaSource should not be detached without a proper disposal
|
|
|
60835 |
|
|
|
60836 |
this.seekable_ = createTimeRanges();
|
|
|
60837 |
this.hasPlayed_ = false;
|
|
|
60838 |
this.syncController_ = new SyncController(options);
|
|
|
60839 |
this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
|
|
|
60840 |
kind: 'metadata',
|
|
|
60841 |
label: 'segment-metadata'
|
|
|
60842 |
}, false).track;
|
|
|
60843 |
this.decrypter_ = new Decrypter();
|
|
|
60844 |
this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
|
|
|
60845 |
this.inbandTextTracks_ = {};
|
|
|
60846 |
this.timelineChangeController_ = new TimelineChangeController();
|
|
|
60847 |
this.keyStatusMap_ = new Map();
|
|
|
60848 |
const segmentLoaderSettings = {
|
|
|
60849 |
vhs: this.vhs_,
|
|
|
60850 |
parse708captions: options.parse708captions,
|
|
|
60851 |
useDtsForTimestampOffset: options.useDtsForTimestampOffset,
|
|
|
60852 |
captionServices,
|
|
|
60853 |
mediaSource: this.mediaSource,
|
|
|
60854 |
currentTime: this.tech_.currentTime.bind(this.tech_),
|
|
|
60855 |
seekable: () => this.seekable(),
|
|
|
60856 |
seeking: () => this.tech_.seeking(),
|
|
|
60857 |
duration: () => this.duration(),
|
|
|
60858 |
hasPlayed: () => this.hasPlayed_,
|
|
|
60859 |
goalBufferLength: () => this.goalBufferLength(),
|
|
|
60860 |
bandwidth,
|
|
|
60861 |
syncController: this.syncController_,
|
|
|
60862 |
decrypter: this.decrypter_,
|
|
|
60863 |
sourceType: this.sourceType_,
|
|
|
60864 |
inbandTextTracks: this.inbandTextTracks_,
|
|
|
60865 |
cacheEncryptionKeys,
|
|
|
60866 |
sourceUpdater: this.sourceUpdater_,
|
|
|
60867 |
timelineChangeController: this.timelineChangeController_,
|
|
|
60868 |
exactManifestTimings: options.exactManifestTimings,
|
|
|
60869 |
addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this)
|
|
|
60870 |
}; // The source type check not only determines whether a special DASH playlist loader
|
|
|
60871 |
// should be used, but also covers the case where the provided src is a vhs-json
|
|
|
60872 |
// manifest object (instead of a URL). In the case of vhs-json, the default
|
|
|
60873 |
// PlaylistLoader should be used.
|
|
|
60874 |
|
|
|
60875 |
this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ? new DashPlaylistLoader(src, this.vhs_, merge(this.requestOptions_, {
|
|
|
60876 |
addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this)
|
|
|
60877 |
})) : new PlaylistLoader(src, this.vhs_, merge(this.requestOptions_, {
|
|
|
60878 |
addDateRangesToTextTrack: this.addDateRangesToTextTrack_.bind(this)
|
|
|
60879 |
}));
|
|
|
60880 |
this.setupMainPlaylistLoaderListeners_(); // setup segment loaders
|
|
|
60881 |
// combined audio/video or just video when alternate audio track is selected
|
|
|
60882 |
|
|
|
60883 |
this.mainSegmentLoader_ = new SegmentLoader(merge(segmentLoaderSettings, {
|
|
|
60884 |
segmentMetadataTrack: this.segmentMetadataTrack_,
|
|
|
60885 |
loaderType: 'main'
|
|
|
60886 |
}), options); // alternate audio track
|
|
|
60887 |
|
|
|
60888 |
this.audioSegmentLoader_ = new SegmentLoader(merge(segmentLoaderSettings, {
|
|
|
60889 |
loaderType: 'audio'
|
|
|
60890 |
}), options);
|
|
|
60891 |
this.subtitleSegmentLoader_ = new VTTSegmentLoader(merge(segmentLoaderSettings, {
|
|
|
60892 |
loaderType: 'vtt',
|
|
|
60893 |
featuresNativeTextTracks: this.tech_.featuresNativeTextTracks,
|
|
|
60894 |
loadVttJs: () => new Promise((resolve, reject) => {
|
|
|
60895 |
function onLoad() {
|
|
|
60896 |
tech.off('vttjserror', onError);
|
|
|
60897 |
resolve();
|
|
|
60898 |
}
|
|
|
60899 |
function onError() {
|
|
|
60900 |
tech.off('vttjsloaded', onLoad);
|
|
|
60901 |
reject();
|
|
|
60902 |
}
|
|
|
60903 |
tech.one('vttjsloaded', onLoad);
|
|
|
60904 |
tech.one('vttjserror', onError); // safe to call multiple times, script will be loaded only once:
|
|
|
60905 |
|
|
|
60906 |
tech.addWebVttScript_();
|
|
|
60907 |
})
|
|
|
60908 |
}), options);
|
|
|
60909 |
const getBandwidth = () => {
|
|
|
60910 |
return this.mainSegmentLoader_.bandwidth;
|
|
|
60911 |
};
|
|
|
60912 |
this.contentSteeringController_ = new ContentSteeringController(this.vhs_.xhr, getBandwidth);
|
|
|
60913 |
this.setupSegmentLoaderListeners_();
|
|
|
60914 |
if (this.bufferBasedABR) {
|
|
|
60915 |
this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_());
|
|
|
60916 |
this.tech_.on('pause', () => this.stopABRTimer_());
|
|
|
60917 |
this.tech_.on('play', () => this.startABRTimer_());
|
|
|
60918 |
} // Create SegmentLoader stat-getters
|
|
|
60919 |
// mediaRequests_
|
|
|
60920 |
// mediaRequestsAborted_
|
|
|
60921 |
// mediaRequestsTimedout_
|
|
|
60922 |
// mediaRequestsErrored_
|
|
|
60923 |
// mediaTransferDuration_
|
|
|
60924 |
// mediaBytesTransferred_
|
|
|
60925 |
// mediaAppends_
|
|
|
60926 |
|
|
|
60927 |
loaderStats.forEach(stat => {
|
|
|
60928 |
this[stat + '_'] = sumLoaderStat.bind(this, stat);
|
|
|
60929 |
});
|
|
|
60930 |
this.logger_ = logger('pc');
|
|
|
60931 |
this.triggeredFmp4Usage = false;
|
|
|
60932 |
if (this.tech_.preload() === 'none') {
|
|
|
60933 |
this.loadOnPlay_ = () => {
|
|
|
60934 |
this.loadOnPlay_ = null;
|
|
|
60935 |
this.mainPlaylistLoader_.load();
|
|
|
60936 |
};
|
|
|
60937 |
this.tech_.one('play', this.loadOnPlay_);
|
|
|
60938 |
} else {
|
|
|
60939 |
this.mainPlaylistLoader_.load();
|
|
|
60940 |
}
|
|
|
60941 |
this.timeToLoadedData__ = -1;
|
|
|
60942 |
this.mainAppendsToLoadedData__ = -1;
|
|
|
60943 |
this.audioAppendsToLoadedData__ = -1;
|
|
|
60944 |
const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart'; // start the first frame timer on loadstart or play (for preload none)
|
|
|
60945 |
|
|
|
60946 |
this.tech_.one(event, () => {
|
|
|
60947 |
const timeToLoadedDataStart = Date.now();
|
|
|
60948 |
this.tech_.one('loadeddata', () => {
|
|
|
60949 |
this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart;
|
|
|
60950 |
this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends;
|
|
|
60951 |
this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends;
|
|
|
60952 |
});
|
|
|
60953 |
});
|
|
|
60954 |
}
|
|
|
60955 |
mainAppendsToLoadedData_() {
|
|
|
60956 |
return this.mainAppendsToLoadedData__;
|
|
|
60957 |
}
|
|
|
60958 |
audioAppendsToLoadedData_() {
|
|
|
60959 |
return this.audioAppendsToLoadedData__;
|
|
|
60960 |
}
|
|
|
60961 |
appendsToLoadedData_() {
|
|
|
60962 |
const main = this.mainAppendsToLoadedData_();
|
|
|
60963 |
const audio = this.audioAppendsToLoadedData_();
|
|
|
60964 |
if (main === -1 || audio === -1) {
|
|
|
60965 |
return -1;
|
|
|
60966 |
}
|
|
|
60967 |
return main + audio;
|
|
|
60968 |
}
|
|
|
60969 |
timeToLoadedData_() {
|
|
|
60970 |
return this.timeToLoadedData__;
|
|
|
60971 |
}
|
|
|
60972 |
/**
|
|
|
60973 |
* Run selectPlaylist and switch to the new playlist if we should
|
|
|
60974 |
*
|
|
|
60975 |
* @param {string} [reason=abr] a reason for why the ABR check is made
|
|
|
60976 |
* @private
|
|
|
60977 |
*/
|
|
|
60978 |
|
|
|
60979 |
checkABR_(reason = 'abr') {
|
|
|
60980 |
const nextPlaylist = this.selectPlaylist();
|
|
|
60981 |
if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) {
|
|
|
60982 |
this.switchMedia_(nextPlaylist, reason);
|
|
|
60983 |
}
|
|
|
60984 |
}
|
|
|
60985 |
switchMedia_(playlist, cause, delay) {
|
|
|
60986 |
const oldMedia = this.media();
|
|
|
60987 |
const oldId = oldMedia && (oldMedia.id || oldMedia.uri);
|
|
|
60988 |
const newId = playlist && (playlist.id || playlist.uri);
|
|
|
60989 |
if (oldId && oldId !== newId) {
|
|
|
60990 |
this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`);
|
|
|
60991 |
this.tech_.trigger({
|
|
|
60992 |
type: 'usage',
|
|
|
60993 |
name: `vhs-rendition-change-${cause}`
|
|
|
60994 |
});
|
|
|
60995 |
}
|
|
|
60996 |
this.mainPlaylistLoader_.media(playlist, delay);
|
|
|
60997 |
}
|
|
|
60998 |
/**
|
|
|
60999 |
* A function that ensures we switch our playlists inside of `mediaTypes`
|
|
|
61000 |
* to match the current `serviceLocation` provided by the contentSteering controller.
|
|
|
61001 |
* We want to check media types of `AUDIO`, `SUBTITLES`, and `CLOSED-CAPTIONS`.
|
|
|
61002 |
*
|
|
|
61003 |
* This should only be called on a DASH playback scenario while using content steering.
|
|
|
61004 |
* This is necessary due to differences in how media in HLS manifests are generally tied to
|
|
|
61005 |
* a video playlist, where in DASH that is not always the case.
|
|
|
61006 |
*/
|
|
|
61007 |
|
|
|
61008 |
switchMediaForDASHContentSteering_() {
|
|
|
61009 |
['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => {
|
|
|
61010 |
const mediaType = this.mediaTypes_[type];
|
|
|
61011 |
const activeGroup = mediaType ? mediaType.activeGroup() : null;
|
|
|
61012 |
const pathway = this.contentSteeringController_.getPathway();
|
|
|
61013 |
if (activeGroup && pathway) {
|
|
|
61014 |
// activeGroup can be an array or a single group
|
|
|
61015 |
const mediaPlaylists = activeGroup.length ? activeGroup[0].playlists : activeGroup.playlists;
|
|
|
61016 |
const dashMediaPlaylists = mediaPlaylists.filter(p => p.attributes.serviceLocation === pathway); // Switch the current active playlist to the correct CDN
|
|
|
61017 |
|
|
|
61018 |
if (dashMediaPlaylists.length) {
|
|
|
61019 |
this.mediaTypes_[type].activePlaylistLoader.media(dashMediaPlaylists[0]);
|
|
|
61020 |
}
|
|
|
61021 |
}
|
|
|
61022 |
});
|
|
|
61023 |
}
|
|
|
61024 |
/**
|
|
|
61025 |
* Start a timer that periodically calls checkABR_
|
|
|
61026 |
*
|
|
|
61027 |
* @private
|
|
|
61028 |
*/
|
|
|
61029 |
|
|
|
61030 |
startABRTimer_() {
|
|
|
61031 |
this.stopABRTimer_();
|
|
|
61032 |
this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250);
|
|
|
61033 |
}
|
|
|
61034 |
/**
|
|
|
61035 |
* Stop the timer that periodically calls checkABR_
|
|
|
61036 |
*
|
|
|
61037 |
* @private
|
|
|
61038 |
*/
|
|
|
61039 |
|
|
|
61040 |
stopABRTimer_() {
|
|
|
61041 |
// if we're scrubbing, we don't need to pause.
|
|
|
61042 |
// This getter will be added to Video.js in version 7.11.
|
|
|
61043 |
if (this.tech_.scrubbing && this.tech_.scrubbing()) {
|
|
|
61044 |
return;
|
|
|
61045 |
}
|
|
|
61046 |
window.clearInterval(this.abrTimer_);
|
|
|
61047 |
this.abrTimer_ = null;
|
|
|
61048 |
}
|
|
|
61049 |
/**
|
|
|
61050 |
* Get a list of playlists for the currently selected audio playlist
|
|
|
61051 |
*
|
|
|
61052 |
* @return {Array} the array of audio playlists
|
|
|
61053 |
*/
|
|
|
61054 |
|
|
|
61055 |
getAudioTrackPlaylists_() {
|
|
|
61056 |
const main = this.main();
|
|
|
61057 |
const defaultPlaylists = main && main.playlists || []; // if we don't have any audio groups then we can only
|
|
|
61058 |
// assume that the audio tracks are contained in main
|
|
|
61059 |
// playlist array, use that or an empty array.
|
|
|
61060 |
|
|
|
61061 |
if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) {
|
|
|
61062 |
return defaultPlaylists;
|
|
|
61063 |
}
|
|
|
61064 |
const AUDIO = main.mediaGroups.AUDIO;
|
|
|
61065 |
const groupKeys = Object.keys(AUDIO);
|
|
|
61066 |
let track; // get the current active track
|
|
|
61067 |
|
|
|
61068 |
if (Object.keys(this.mediaTypes_.AUDIO.groups).length) {
|
|
|
61069 |
track = this.mediaTypes_.AUDIO.activeTrack(); // or get the default track from main if mediaTypes_ isn't setup yet
|
|
|
61070 |
} else {
|
|
|
61071 |
// default group is `main` or just the first group.
|
|
|
61072 |
const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]];
|
|
|
61073 |
for (const label in defaultGroup) {
|
|
|
61074 |
if (defaultGroup[label].default) {
|
|
|
61075 |
track = {
|
|
|
61076 |
label
|
|
|
61077 |
};
|
|
|
61078 |
break;
|
|
|
61079 |
}
|
|
|
61080 |
}
|
|
|
61081 |
} // no active track no playlists.
|
|
|
61082 |
|
|
|
61083 |
if (!track) {
|
|
|
61084 |
return defaultPlaylists;
|
|
|
61085 |
}
|
|
|
61086 |
const playlists = []; // get all of the playlists that are possible for the
|
|
|
61087 |
// active track.
|
|
|
61088 |
|
|
|
61089 |
for (const group in AUDIO) {
|
|
|
61090 |
if (AUDIO[group][track.label]) {
|
|
|
61091 |
const properties = AUDIO[group][track.label];
|
|
|
61092 |
if (properties.playlists && properties.playlists.length) {
|
|
|
61093 |
playlists.push.apply(playlists, properties.playlists);
|
|
|
61094 |
} else if (properties.uri) {
|
|
|
61095 |
playlists.push(properties);
|
|
|
61096 |
} else if (main.playlists.length) {
|
|
|
61097 |
// if an audio group does not have a uri
|
|
|
61098 |
// see if we have main playlists that use it as a group.
|
|
|
61099 |
// if we do then add those to the playlists list.
|
|
|
61100 |
for (let i = 0; i < main.playlists.length; i++) {
|
|
|
61101 |
const playlist = main.playlists[i];
|
|
|
61102 |
if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) {
|
|
|
61103 |
playlists.push(playlist);
|
|
|
61104 |
}
|
|
|
61105 |
}
|
|
|
61106 |
}
|
|
|
61107 |
}
|
|
|
61108 |
}
|
|
|
61109 |
if (!playlists.length) {
|
|
|
61110 |
return defaultPlaylists;
|
|
|
61111 |
}
|
|
|
61112 |
return playlists;
|
|
|
61113 |
}
|
|
|
61114 |
/**
|
|
|
61115 |
* Register event handlers on the main playlist loader. A helper
|
|
|
61116 |
* function for construction time.
|
|
|
61117 |
*
|
|
|
61118 |
* @private
|
|
|
61119 |
*/
|
|
|
61120 |
|
|
|
61121 |
setupMainPlaylistLoaderListeners_() {
|
|
|
61122 |
this.mainPlaylistLoader_.on('loadedmetadata', () => {
|
|
|
61123 |
const media = this.mainPlaylistLoader_.media();
|
|
|
61124 |
const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to
|
|
|
61125 |
// timeout the request.
|
|
|
61126 |
|
|
|
61127 |
if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) {
|
|
|
61128 |
this.requestOptions_.timeout = 0;
|
|
|
61129 |
} else {
|
|
|
61130 |
this.requestOptions_.timeout = requestTimeout;
|
|
|
61131 |
} // if this isn't a live video and preload permits, start
|
|
|
61132 |
// downloading segments
|
|
|
61133 |
|
|
|
61134 |
if (media.endList && this.tech_.preload() !== 'none') {
|
|
|
61135 |
this.mainSegmentLoader_.playlist(media, this.requestOptions_);
|
|
|
61136 |
this.mainSegmentLoader_.load();
|
|
|
61137 |
}
|
|
|
61138 |
setupMediaGroups({
|
|
|
61139 |
sourceType: this.sourceType_,
|
|
|
61140 |
segmentLoaders: {
|
|
|
61141 |
AUDIO: this.audioSegmentLoader_,
|
|
|
61142 |
SUBTITLES: this.subtitleSegmentLoader_,
|
|
|
61143 |
main: this.mainSegmentLoader_
|
|
|
61144 |
},
|
|
|
61145 |
tech: this.tech_,
|
|
|
61146 |
requestOptions: this.requestOptions_,
|
|
|
61147 |
mainPlaylistLoader: this.mainPlaylistLoader_,
|
|
|
61148 |
vhs: this.vhs_,
|
|
|
61149 |
main: this.main(),
|
|
|
61150 |
mediaTypes: this.mediaTypes_,
|
|
|
61151 |
excludePlaylist: this.excludePlaylist.bind(this)
|
|
|
61152 |
});
|
|
|
61153 |
this.triggerPresenceUsage_(this.main(), media);
|
|
|
61154 |
this.setupFirstPlay();
|
|
|
61155 |
if (!this.mediaTypes_.AUDIO.activePlaylistLoader || this.mediaTypes_.AUDIO.activePlaylistLoader.media()) {
|
|
|
61156 |
this.trigger('selectedinitialmedia');
|
|
|
61157 |
} else {
|
|
|
61158 |
// We must wait for the active audio playlist loader to
|
|
|
61159 |
// finish setting up before triggering this event so the
|
|
|
61160 |
// representations API and EME setup is correct
|
|
|
61161 |
this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => {
|
|
|
61162 |
this.trigger('selectedinitialmedia');
|
|
|
61163 |
});
|
|
|
61164 |
}
|
|
|
61165 |
});
|
|
|
61166 |
this.mainPlaylistLoader_.on('loadedplaylist', () => {
|
|
|
61167 |
if (this.loadOnPlay_) {
|
|
|
61168 |
this.tech_.off('play', this.loadOnPlay_);
|
|
|
61169 |
}
|
|
|
61170 |
let updatedPlaylist = this.mainPlaylistLoader_.media();
|
|
|
61171 |
if (!updatedPlaylist) {
|
|
|
61172 |
// Add content steering listeners on first load and init.
|
|
|
61173 |
this.attachContentSteeringListeners_();
|
|
|
61174 |
this.initContentSteeringController_(); // exclude any variants that are not supported by the browser before selecting
|
|
|
61175 |
// an initial media as the playlist selectors do not consider browser support
|
|
|
61176 |
|
|
|
61177 |
this.excludeUnsupportedVariants_();
|
|
|
61178 |
let selectedMedia;
|
|
|
61179 |
if (this.enableLowInitialPlaylist) {
|
|
|
61180 |
selectedMedia = this.selectInitialPlaylist();
|
|
|
61181 |
}
|
|
|
61182 |
if (!selectedMedia) {
|
|
|
61183 |
selectedMedia = this.selectPlaylist();
|
|
|
61184 |
}
|
|
|
61185 |
if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) {
|
|
|
61186 |
return;
|
|
|
61187 |
}
|
|
|
61188 |
this.initialMedia_ = selectedMedia;
|
|
|
61189 |
this.switchMedia_(this.initialMedia_, 'initial'); // Under the standard case where a source URL is provided, loadedplaylist will
|
|
|
61190 |
// fire again since the playlist will be requested. In the case of vhs-json
|
|
|
61191 |
// (where the manifest object is provided as the source), when the media
|
|
|
61192 |
// playlist's `segments` list is already available, a media playlist won't be
|
|
|
61193 |
// requested, and loadedplaylist won't fire again, so the playlist handler must be
|
|
|
61194 |
// called on its own here.
|
|
|
61195 |
|
|
|
61196 |
const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments;
|
|
|
61197 |
if (!haveJsonSource) {
|
|
|
61198 |
return;
|
|
|
61199 |
}
|
|
|
61200 |
updatedPlaylist = this.initialMedia_;
|
|
|
61201 |
}
|
|
|
61202 |
this.handleUpdatedMediaPlaylist(updatedPlaylist);
|
|
|
61203 |
});
|
|
|
61204 |
this.mainPlaylistLoader_.on('error', () => {
|
|
|
61205 |
const error = this.mainPlaylistLoader_.error;
|
|
|
61206 |
this.excludePlaylist({
|
|
|
61207 |
playlistToExclude: error.playlist,
|
|
|
61208 |
error
|
|
|
61209 |
});
|
|
|
61210 |
});
|
|
|
61211 |
this.mainPlaylistLoader_.on('mediachanging', () => {
|
|
|
61212 |
this.mainSegmentLoader_.abort();
|
|
|
61213 |
this.mainSegmentLoader_.pause();
|
|
|
61214 |
});
|
|
|
61215 |
this.mainPlaylistLoader_.on('mediachange', () => {
|
|
|
61216 |
const media = this.mainPlaylistLoader_.media();
|
|
|
61217 |
const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to
|
|
|
61218 |
// timeout the request.
|
|
|
61219 |
|
|
|
61220 |
if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) {
|
|
|
61221 |
this.requestOptions_.timeout = 0;
|
|
|
61222 |
} else {
|
|
|
61223 |
this.requestOptions_.timeout = requestTimeout;
|
|
|
61224 |
}
|
|
|
61225 |
if (this.sourceType_ === 'dash') {
|
|
|
61226 |
// we don't want to re-request the same hls playlist right after it was changed
|
|
|
61227 |
this.mainPlaylistLoader_.load();
|
|
|
61228 |
} // TODO: Create a new event on the PlaylistLoader that signals
|
|
|
61229 |
// that the segments have changed in some way and use that to
|
|
|
61230 |
// update the SegmentLoader instead of doing it twice here and
|
|
|
61231 |
// on `loadedplaylist`
|
|
|
61232 |
|
|
|
61233 |
this.mainSegmentLoader_.pause();
|
|
|
61234 |
this.mainSegmentLoader_.playlist(media, this.requestOptions_);
|
|
|
61235 |
if (this.waitingForFastQualityPlaylistReceived_) {
|
|
|
61236 |
this.runFastQualitySwitch_();
|
|
|
61237 |
} else {
|
|
|
61238 |
this.mainSegmentLoader_.load();
|
|
|
61239 |
}
|
|
|
61240 |
this.tech_.trigger({
|
|
|
61241 |
type: 'mediachange',
|
|
|
61242 |
bubbles: true
|
|
|
61243 |
});
|
|
|
61244 |
});
|
|
|
61245 |
this.mainPlaylistLoader_.on('playlistunchanged', () => {
|
|
|
61246 |
const updatedPlaylist = this.mainPlaylistLoader_.media(); // ignore unchanged playlists that have already been
|
|
|
61247 |
// excluded for not-changing. We likely just have a really slowly updating
|
|
|
61248 |
// playlist.
|
|
|
61249 |
|
|
|
61250 |
if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') {
|
|
|
61251 |
return;
|
|
|
61252 |
}
|
|
|
61253 |
const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist);
|
|
|
61254 |
if (playlistOutdated) {
|
|
|
61255 |
// Playlist has stopped updating and we're stuck at its end. Try to
|
|
|
61256 |
// exclude it and switch to another playlist in the hope that that
|
|
|
61257 |
// one is updating (and give the player a chance to re-adjust to the
|
|
|
61258 |
// safe live point).
|
|
|
61259 |
this.excludePlaylist({
|
|
|
61260 |
error: {
|
|
|
61261 |
message: 'Playlist no longer updating.',
|
|
|
61262 |
reason: 'playlist-unchanged'
|
|
|
61263 |
}
|
|
|
61264 |
}); // useful for monitoring QoS
|
|
|
61265 |
|
|
|
61266 |
this.tech_.trigger('playliststuck');
|
|
|
61267 |
}
|
|
|
61268 |
});
|
|
|
61269 |
this.mainPlaylistLoader_.on('renditiondisabled', () => {
|
|
|
61270 |
this.tech_.trigger({
|
|
|
61271 |
type: 'usage',
|
|
|
61272 |
name: 'vhs-rendition-disabled'
|
|
|
61273 |
});
|
|
|
61274 |
});
|
|
|
61275 |
this.mainPlaylistLoader_.on('renditionenabled', () => {
|
|
|
61276 |
this.tech_.trigger({
|
|
|
61277 |
type: 'usage',
|
|
|
61278 |
name: 'vhs-rendition-enabled'
|
|
|
61279 |
});
|
|
|
61280 |
});
|
|
|
61281 |
}
|
|
|
61282 |
/**
|
|
|
61283 |
* Given an updated media playlist (whether it was loaded for the first time, or
|
|
|
61284 |
* refreshed for live playlists), update any relevant properties and state to reflect
|
|
|
61285 |
* changes in the media that should be accounted for (e.g., cues and duration).
|
|
|
61286 |
*
|
|
|
61287 |
* @param {Object} updatedPlaylist the updated media playlist object
|
|
|
61288 |
*
|
|
|
61289 |
* @private
|
|
|
61290 |
*/
|
|
|
61291 |
|
|
|
61292 |
handleUpdatedMediaPlaylist(updatedPlaylist) {
|
|
|
61293 |
if (this.useCueTags_) {
|
|
|
61294 |
this.updateAdCues_(updatedPlaylist);
|
|
|
61295 |
} // TODO: Create a new event on the PlaylistLoader that signals
|
|
|
61296 |
// that the segments have changed in some way and use that to
|
|
|
61297 |
// update the SegmentLoader instead of doing it twice here and
|
|
|
61298 |
// on `mediachange`
|
|
|
61299 |
|
|
|
61300 |
this.mainSegmentLoader_.pause();
|
|
|
61301 |
this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
|
|
|
61302 |
if (this.waitingForFastQualityPlaylistReceived_) {
|
|
|
61303 |
this.runFastQualitySwitch_();
|
|
|
61304 |
}
|
|
|
61305 |
this.updateDuration(!updatedPlaylist.endList); // If the player isn't paused, ensure that the segment loader is running,
|
|
|
61306 |
// as it is possible that it was temporarily stopped while waiting for
|
|
|
61307 |
// a playlist (e.g., in case the playlist errored and we re-requested it).
|
|
|
61308 |
|
|
|
61309 |
if (!this.tech_.paused()) {
|
|
|
61310 |
this.mainSegmentLoader_.load();
|
|
|
61311 |
if (this.audioSegmentLoader_) {
|
|
|
61312 |
this.audioSegmentLoader_.load();
|
|
|
61313 |
}
|
|
|
61314 |
}
|
|
|
61315 |
}
|
|
|
61316 |
/**
|
|
|
61317 |
* A helper function for triggerring presence usage events once per source
|
|
|
61318 |
*
|
|
|
61319 |
* @private
|
|
|
61320 |
*/
|
|
|
61321 |
|
|
|
61322 |
triggerPresenceUsage_(main, media) {
|
|
|
61323 |
const mediaGroups = main.mediaGroups || {};
|
|
|
61324 |
let defaultDemuxed = true;
|
|
|
61325 |
const audioGroupKeys = Object.keys(mediaGroups.AUDIO);
|
|
|
61326 |
for (const mediaGroup in mediaGroups.AUDIO) {
|
|
|
61327 |
for (const label in mediaGroups.AUDIO[mediaGroup]) {
|
|
|
61328 |
const properties = mediaGroups.AUDIO[mediaGroup][label];
|
|
|
61329 |
if (!properties.uri) {
|
|
|
61330 |
defaultDemuxed = false;
|
|
|
61331 |
}
|
|
|
61332 |
}
|
|
|
61333 |
}
|
|
|
61334 |
if (defaultDemuxed) {
|
|
|
61335 |
this.tech_.trigger({
|
|
|
61336 |
type: 'usage',
|
|
|
61337 |
name: 'vhs-demuxed'
|
|
|
61338 |
});
|
|
|
61339 |
}
|
|
|
61340 |
if (Object.keys(mediaGroups.SUBTITLES).length) {
|
|
|
61341 |
this.tech_.trigger({
|
|
|
61342 |
type: 'usage',
|
|
|
61343 |
name: 'vhs-webvtt'
|
|
|
61344 |
});
|
|
|
61345 |
}
|
|
|
61346 |
if (Vhs$1.Playlist.isAes(media)) {
|
|
|
61347 |
this.tech_.trigger({
|
|
|
61348 |
type: 'usage',
|
|
|
61349 |
name: 'vhs-aes'
|
|
|
61350 |
});
|
|
|
61351 |
}
|
|
|
61352 |
if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
|
|
|
61353 |
this.tech_.trigger({
|
|
|
61354 |
type: 'usage',
|
|
|
61355 |
name: 'vhs-alternate-audio'
|
|
|
61356 |
});
|
|
|
61357 |
}
|
|
|
61358 |
if (this.useCueTags_) {
|
|
|
61359 |
this.tech_.trigger({
|
|
|
61360 |
type: 'usage',
|
|
|
61361 |
name: 'vhs-playlist-cue-tags'
|
|
|
61362 |
});
|
|
|
61363 |
}
|
|
|
61364 |
}
|
|
|
61365 |
shouldSwitchToMedia_(nextPlaylist) {
|
|
|
61366 |
const currentPlaylist = this.mainPlaylistLoader_.media() || this.mainPlaylistLoader_.pendingMedia_;
|
|
|
61367 |
const currentTime = this.tech_.currentTime();
|
|
|
61368 |
const bufferLowWaterLine = this.bufferLowWaterLine();
|
|
|
61369 |
const bufferHighWaterLine = this.bufferHighWaterLine();
|
|
|
61370 |
const buffered = this.tech_.buffered();
|
|
|
61371 |
return shouldSwitchToMedia({
|
|
|
61372 |
buffered,
|
|
|
61373 |
currentTime,
|
|
|
61374 |
currentPlaylist,
|
|
|
61375 |
nextPlaylist,
|
|
|
61376 |
bufferLowWaterLine,
|
|
|
61377 |
bufferHighWaterLine,
|
|
|
61378 |
duration: this.duration(),
|
|
|
61379 |
bufferBasedABR: this.bufferBasedABR,
|
|
|
61380 |
log: this.logger_
|
|
|
61381 |
});
|
|
|
61382 |
}
|
|
|
61383 |
/**
|
|
|
61384 |
* Register event handlers on the segment loaders. A helper function
|
|
|
61385 |
* for construction time.
|
|
|
61386 |
*
|
|
|
61387 |
* @private
|
|
|
61388 |
*/
|
|
|
61389 |
|
|
|
61390 |
setupSegmentLoaderListeners_() {
|
|
|
61391 |
this.mainSegmentLoader_.on('bandwidthupdate', () => {
|
|
|
61392 |
// Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's
|
|
|
61393 |
// useful to check to see if a rendition switch should be made.
|
|
|
61394 |
this.checkABR_('bandwidthupdate');
|
|
|
61395 |
this.tech_.trigger('bandwidthupdate');
|
|
|
61396 |
});
|
|
|
61397 |
this.mainSegmentLoader_.on('timeout', () => {
|
|
|
61398 |
if (this.bufferBasedABR) {
|
|
|
61399 |
// If a rendition change is needed, then it would've be done on `bandwidthupdate`.
|
|
|
61400 |
// Here the only consideration is that for buffer based ABR there's no guarantee
|
|
|
61401 |
// of an immediate switch (since the bandwidth is averaged with a timeout
|
|
|
61402 |
// bandwidth value of 1), so force a load on the segment loader to keep it going.
|
|
|
61403 |
this.mainSegmentLoader_.load();
|
|
|
61404 |
}
|
|
|
61405 |
}); // `progress` events are not reliable enough of a bandwidth measure to trigger buffer
|
|
|
61406 |
// based ABR.
|
|
|
61407 |
|
|
|
61408 |
if (!this.bufferBasedABR) {
|
|
|
61409 |
this.mainSegmentLoader_.on('progress', () => {
|
|
|
61410 |
this.trigger('progress');
|
|
|
61411 |
});
|
|
|
61412 |
}
|
|
|
61413 |
this.mainSegmentLoader_.on('error', () => {
|
|
|
61414 |
const error = this.mainSegmentLoader_.error();
|
|
|
61415 |
this.excludePlaylist({
|
|
|
61416 |
playlistToExclude: error.playlist,
|
|
|
61417 |
error
|
|
|
61418 |
});
|
|
|
61419 |
});
|
|
|
61420 |
this.mainSegmentLoader_.on('appenderror', () => {
|
|
|
61421 |
this.error = this.mainSegmentLoader_.error_;
|
|
|
61422 |
this.trigger('error');
|
|
|
61423 |
});
|
|
|
61424 |
this.mainSegmentLoader_.on('syncinfoupdate', () => {
|
|
|
61425 |
this.onSyncInfoUpdate_();
|
|
|
61426 |
});
|
|
|
61427 |
this.mainSegmentLoader_.on('timestampoffset', () => {
|
|
|
61428 |
this.tech_.trigger({
|
|
|
61429 |
type: 'usage',
|
|
|
61430 |
name: 'vhs-timestamp-offset'
|
|
|
61431 |
});
|
|
|
61432 |
});
|
|
|
61433 |
this.audioSegmentLoader_.on('syncinfoupdate', () => {
|
|
|
61434 |
this.onSyncInfoUpdate_();
|
|
|
61435 |
});
|
|
|
61436 |
this.audioSegmentLoader_.on('appenderror', () => {
|
|
|
61437 |
this.error = this.audioSegmentLoader_.error_;
|
|
|
61438 |
this.trigger('error');
|
|
|
61439 |
});
|
|
|
61440 |
this.mainSegmentLoader_.on('ended', () => {
|
|
|
61441 |
this.logger_('main segment loader ended');
|
|
|
61442 |
this.onEndOfStream();
|
|
|
61443 |
});
|
|
|
61444 |
this.mainSegmentLoader_.on('earlyabort', event => {
|
|
|
61445 |
// never try to early abort with the new ABR algorithm
|
|
|
61446 |
if (this.bufferBasedABR) {
|
|
|
61447 |
return;
|
|
|
61448 |
}
|
|
|
61449 |
this.delegateLoaders_('all', ['abort']);
|
|
|
61450 |
this.excludePlaylist({
|
|
|
61451 |
error: {
|
|
|
61452 |
message: 'Aborted early because there isn\'t enough bandwidth to complete ' + 'the request without rebuffering.'
|
|
|
61453 |
},
|
|
|
61454 |
playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS
|
|
|
61455 |
});
|
|
|
61456 |
});
|
|
|
61457 |
const updateCodecs = () => {
|
|
|
61458 |
if (!this.sourceUpdater_.hasCreatedSourceBuffers()) {
|
|
|
61459 |
return this.tryToCreateSourceBuffers_();
|
|
|
61460 |
}
|
|
|
61461 |
const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded
|
|
|
61462 |
|
|
|
61463 |
if (!codecs) {
|
|
|
61464 |
return;
|
|
|
61465 |
}
|
|
|
61466 |
this.sourceUpdater_.addOrChangeSourceBuffers(codecs);
|
|
|
61467 |
};
|
|
|
61468 |
this.mainSegmentLoader_.on('trackinfo', updateCodecs);
|
|
|
61469 |
this.audioSegmentLoader_.on('trackinfo', updateCodecs);
|
|
|
61470 |
this.mainSegmentLoader_.on('fmp4', () => {
|
|
|
61471 |
if (!this.triggeredFmp4Usage) {
|
|
|
61472 |
this.tech_.trigger({
|
|
|
61473 |
type: 'usage',
|
|
|
61474 |
name: 'vhs-fmp4'
|
|
|
61475 |
});
|
|
|
61476 |
this.triggeredFmp4Usage = true;
|
|
|
61477 |
}
|
|
|
61478 |
});
|
|
|
61479 |
this.audioSegmentLoader_.on('fmp4', () => {
|
|
|
61480 |
if (!this.triggeredFmp4Usage) {
|
|
|
61481 |
this.tech_.trigger({
|
|
|
61482 |
type: 'usage',
|
|
|
61483 |
name: 'vhs-fmp4'
|
|
|
61484 |
});
|
|
|
61485 |
this.triggeredFmp4Usage = true;
|
|
|
61486 |
}
|
|
|
61487 |
});
|
|
|
61488 |
this.audioSegmentLoader_.on('ended', () => {
|
|
|
61489 |
this.logger_('audioSegmentLoader ended');
|
|
|
61490 |
this.onEndOfStream();
|
|
|
61491 |
});
|
|
|
61492 |
}
|
|
|
61493 |
mediaSecondsLoaded_() {
|
|
|
61494 |
return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded);
|
|
|
61495 |
}
|
|
|
61496 |
/**
|
|
|
61497 |
* Call load on our SegmentLoaders
|
|
|
61498 |
*/
|
|
|
61499 |
|
|
|
61500 |
load() {
|
|
|
61501 |
this.mainSegmentLoader_.load();
|
|
|
61502 |
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
|
|
|
61503 |
this.audioSegmentLoader_.load();
|
|
|
61504 |
}
|
|
|
61505 |
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
|
|
|
61506 |
this.subtitleSegmentLoader_.load();
|
|
|
61507 |
}
|
|
|
61508 |
}
|
|
|
61509 |
/**
|
|
|
61510 |
* Re-tune playback quality level for the current player
|
|
|
61511 |
* conditions. This method will perform destructive actions like removing
|
|
|
61512 |
* already buffered content in order to readjust the currently active
|
|
|
61513 |
* playlist quickly. This is good for manual quality changes
|
|
|
61514 |
*
|
|
|
61515 |
* @private
|
|
|
61516 |
*/
|
|
|
61517 |
|
|
|
61518 |
fastQualityChange_(media = this.selectPlaylist()) {
|
|
|
61519 |
if (media && media === this.mainPlaylistLoader_.media()) {
|
|
|
61520 |
this.logger_('skipping fastQualityChange because new media is same as old');
|
|
|
61521 |
return;
|
|
|
61522 |
}
|
|
|
61523 |
this.switchMedia_(media, 'fast-quality'); // we would like to avoid race condition when we call fastQuality,
|
|
|
61524 |
// reset everything and start loading segments from prev segments instead of new because new playlist is not received yet
|
|
|
61525 |
|
|
|
61526 |
this.waitingForFastQualityPlaylistReceived_ = true;
|
|
|
61527 |
}
|
|
|
61528 |
runFastQualitySwitch_() {
|
|
|
61529 |
this.waitingForFastQualityPlaylistReceived_ = false; // Delete all buffered data to allow an immediate quality switch, then seek to give
|
|
|
61530 |
// the browser a kick to remove any cached frames from the previous rendtion (.04 seconds
|
|
|
61531 |
// ahead was roughly the minimum that will accomplish this across a variety of content
|
|
|
61532 |
// in IE and Edge, but seeking in place is sufficient on all other browsers)
|
|
|
61533 |
// Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/
|
|
|
61534 |
// Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904
|
|
|
61535 |
|
|
|
61536 |
this.mainSegmentLoader_.pause();
|
|
|
61537 |
this.mainSegmentLoader_.resetEverything(() => {
|
|
|
61538 |
this.tech_.setCurrentTime(this.tech_.currentTime());
|
|
|
61539 |
}); // don't need to reset audio as it is reset when media changes
|
|
|
61540 |
}
|
|
|
61541 |
/**
|
|
|
61542 |
* Begin playback.
|
|
|
61543 |
*/
|
|
|
61544 |
|
|
|
61545 |
play() {
|
|
|
61546 |
if (this.setupFirstPlay()) {
|
|
|
61547 |
return;
|
|
|
61548 |
}
|
|
|
61549 |
if (this.tech_.ended()) {
|
|
|
61550 |
this.tech_.setCurrentTime(0);
|
|
|
61551 |
}
|
|
|
61552 |
if (this.hasPlayed_) {
|
|
|
61553 |
this.load();
|
|
|
61554 |
}
|
|
|
61555 |
const seekable = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window,
|
|
|
61556 |
// seek forward to the live point
|
|
|
61557 |
|
|
|
61558 |
if (this.tech_.duration() === Infinity) {
|
|
|
61559 |
if (this.tech_.currentTime() < seekable.start(0)) {
|
|
|
61560 |
return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
|
|
|
61561 |
}
|
|
|
61562 |
}
|
|
|
61563 |
}
|
|
|
61564 |
/**
|
|
|
61565 |
* Seek to the latest media position if this is a live video and the
|
|
|
61566 |
* player and video are loaded and initialized.
|
|
|
61567 |
*/
|
|
|
61568 |
|
|
|
61569 |
setupFirstPlay() {
|
|
|
61570 |
const media = this.mainPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play
|
|
|
61571 |
// If 1) there is no active media
|
|
|
61572 |
// 2) the player is paused
|
|
|
61573 |
// 3) the first play has already been setup
|
|
|
61574 |
// then exit early
|
|
|
61575 |
|
|
|
61576 |
if (!media || this.tech_.paused() || this.hasPlayed_) {
|
|
|
61577 |
return false;
|
|
|
61578 |
} // when the video is a live stream and/or has a start time
|
|
|
61579 |
|
|
|
61580 |
if (!media.endList || media.start) {
|
|
|
61581 |
const seekable = this.seekable();
|
|
|
61582 |
if (!seekable.length) {
|
|
|
61583 |
// without a seekable range, the player cannot seek to begin buffering at the
|
|
|
61584 |
// live or start point
|
|
|
61585 |
return false;
|
|
|
61586 |
}
|
|
|
61587 |
const seekableEnd = seekable.end(0);
|
|
|
61588 |
let startPoint = seekableEnd;
|
|
|
61589 |
if (media.start) {
|
|
|
61590 |
const offset = media.start.timeOffset;
|
|
|
61591 |
if (offset < 0) {
|
|
|
61592 |
startPoint = Math.max(seekableEnd + offset, seekable.start(0));
|
|
|
61593 |
} else {
|
|
|
61594 |
startPoint = Math.min(seekableEnd, offset);
|
|
|
61595 |
}
|
|
|
61596 |
} // trigger firstplay to inform the source handler to ignore the next seek event
|
|
|
61597 |
|
|
|
61598 |
this.trigger('firstplay'); // seek to the live point
|
|
|
61599 |
|
|
|
61600 |
this.tech_.setCurrentTime(startPoint);
|
|
|
61601 |
}
|
|
|
61602 |
this.hasPlayed_ = true; // we can begin loading now that everything is ready
|
|
|
61603 |
|
|
|
61604 |
this.load();
|
|
|
61605 |
return true;
|
|
|
61606 |
}
|
|
|
61607 |
/**
|
|
|
61608 |
* handle the sourceopen event on the MediaSource
|
|
|
61609 |
*
|
|
|
61610 |
* @private
|
|
|
61611 |
*/
|
|
|
61612 |
|
|
|
61613 |
handleSourceOpen_() {
|
|
|
61614 |
// Only attempt to create the source buffer if none already exist.
|
|
|
61615 |
// handleSourceOpen is also called when we are "re-opening" a source buffer
|
|
|
61616 |
// after `endOfStream` has been called (in response to a seek for instance)
|
|
|
61617 |
this.tryToCreateSourceBuffers_(); // if autoplay is enabled, begin playback. This is duplicative of
|
|
|
61618 |
// code in video.js but is required because play() must be invoked
|
|
|
61619 |
// *after* the media source has opened.
|
|
|
61620 |
|
|
|
61621 |
if (this.tech_.autoplay()) {
|
|
|
61622 |
const playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request
|
|
|
61623 |
// on browsers which return a promise
|
|
|
61624 |
|
|
|
61625 |
if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') {
|
|
|
61626 |
playPromise.then(null, e => {});
|
|
|
61627 |
}
|
|
|
61628 |
}
|
|
|
61629 |
this.trigger('sourceopen');
|
|
|
61630 |
}
|
|
|
61631 |
/**
|
|
|
61632 |
* handle the sourceended event on the MediaSource
|
|
|
61633 |
*
|
|
|
61634 |
* @private
|
|
|
61635 |
*/
|
|
|
61636 |
|
|
|
61637 |
handleSourceEnded_() {
|
|
|
61638 |
if (!this.inbandTextTracks_.metadataTrack_) {
|
|
|
61639 |
return;
|
|
|
61640 |
}
|
|
|
61641 |
const cues = this.inbandTextTracks_.metadataTrack_.cues;
|
|
|
61642 |
if (!cues || !cues.length) {
|
|
|
61643 |
return;
|
|
|
61644 |
}
|
|
|
61645 |
const duration = this.duration();
|
|
|
61646 |
cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ? Number.MAX_VALUE : duration;
|
|
|
61647 |
}
|
|
|
61648 |
/**
|
|
|
61649 |
* handle the durationchange event on the MediaSource
|
|
|
61650 |
*
|
|
|
61651 |
* @private
|
|
|
61652 |
*/
|
|
|
61653 |
|
|
|
61654 |
handleDurationChange_() {
|
|
|
61655 |
this.tech_.trigger('durationchange');
|
|
|
61656 |
}
|
|
|
61657 |
/**
|
|
|
61658 |
* Calls endOfStream on the media source when all active stream types have called
|
|
|
61659 |
* endOfStream
|
|
|
61660 |
*
|
|
|
61661 |
* @param {string} streamType
|
|
|
61662 |
* Stream type of the segment loader that called endOfStream
|
|
|
61663 |
* @private
|
|
|
61664 |
*/
|
|
|
61665 |
|
|
|
61666 |
onEndOfStream() {
|
|
|
61667 |
let isEndOfStream = this.mainSegmentLoader_.ended_;
|
|
|
61668 |
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
|
|
|
61669 |
const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_(); // if the audio playlist loader exists, then alternate audio is active
|
|
|
61670 |
|
|
|
61671 |
if (!mainMediaInfo || mainMediaInfo.hasVideo) {
|
|
|
61672 |
// if we do not know if the main segment loader contains video yet or if we
|
|
|
61673 |
// definitively know the main segment loader contains video, then we need to wait
|
|
|
61674 |
// for both main and audio segment loaders to call endOfStream
|
|
|
61675 |
isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
|
|
|
61676 |
} else {
|
|
|
61677 |
// otherwise just rely on the audio loader
|
|
|
61678 |
isEndOfStream = this.audioSegmentLoader_.ended_;
|
|
|
61679 |
}
|
|
|
61680 |
}
|
|
|
61681 |
if (!isEndOfStream) {
|
|
|
61682 |
return;
|
|
|
61683 |
}
|
|
|
61684 |
this.stopABRTimer_();
|
|
|
61685 |
this.sourceUpdater_.endOfStream();
|
|
|
61686 |
}
|
|
|
61687 |
/**
|
|
|
61688 |
* Check if a playlist has stopped being updated
|
|
|
61689 |
*
|
|
|
61690 |
* @param {Object} playlist the media playlist object
|
|
|
61691 |
* @return {boolean} whether the playlist has stopped being updated or not
|
|
|
61692 |
*/
|
|
|
61693 |
|
|
|
61694 |
stuckAtPlaylistEnd_(playlist) {
|
|
|
61695 |
const seekable = this.seekable();
|
|
|
61696 |
if (!seekable.length) {
|
|
|
61697 |
// playlist doesn't have enough information to determine whether we are stuck
|
|
|
61698 |
return false;
|
|
|
61699 |
}
|
|
|
61700 |
const expired = this.syncController_.getExpiredTime(playlist, this.duration());
|
|
|
61701 |
if (expired === null) {
|
|
|
61702 |
return false;
|
|
|
61703 |
} // does not use the safe live end to calculate playlist end, since we
|
|
|
61704 |
// don't want to say we are stuck while there is still content
|
|
|
61705 |
|
|
|
61706 |
const absolutePlaylistEnd = Vhs$1.Playlist.playlistEnd(playlist, expired);
|
|
|
61707 |
const currentTime = this.tech_.currentTime();
|
|
|
61708 |
const buffered = this.tech_.buffered();
|
|
|
61709 |
if (!buffered.length) {
|
|
|
61710 |
// return true if the playhead reached the absolute end of the playlist
|
|
|
61711 |
return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA;
|
|
|
61712 |
}
|
|
|
61713 |
const bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute
|
|
|
61714 |
// end of playlist
|
|
|
61715 |
|
|
|
61716 |
return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA;
|
|
|
61717 |
}
|
|
|
61718 |
/**
|
|
|
61719 |
* Exclude a playlist for a set amount of time, making it unavailable for selection by
|
|
|
61720 |
* the rendition selection algorithm, then force a new playlist (rendition) selection.
|
|
|
61721 |
*
|
|
|
61722 |
* @param {Object=} playlistToExclude
|
|
|
61723 |
* the playlist to exclude, defaults to the currently selected playlist
|
|
|
61724 |
* @param {Object=} error
|
|
|
61725 |
* an optional error
|
|
|
61726 |
* @param {number=} playlistExclusionDuration
|
|
|
61727 |
* an optional number of seconds to exclude the playlist
|
|
|
61728 |
*/
|
|
|
61729 |
|
|
|
61730 |
excludePlaylist({
|
|
|
61731 |
playlistToExclude = this.mainPlaylistLoader_.media(),
|
|
|
61732 |
error = {},
|
|
|
61733 |
playlistExclusionDuration
|
|
|
61734 |
}) {
|
|
|
61735 |
// If the `error` was generated by the playlist loader, it will contain
|
|
|
61736 |
// the playlist we were trying to load (but failed) and that should be
|
|
|
61737 |
// excluded instead of the currently selected playlist which is likely
|
|
|
61738 |
// out-of-date in this scenario
|
|
|
61739 |
playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media();
|
|
|
61740 |
playlistExclusionDuration = playlistExclusionDuration || error.playlistExclusionDuration || this.playlistExclusionDuration; // If there is no current playlist, then an error occurred while we were
|
|
|
61741 |
// trying to load the main OR while we were disposing of the tech
|
|
|
61742 |
|
|
|
61743 |
if (!playlistToExclude) {
|
|
|
61744 |
this.error = error;
|
|
|
61745 |
if (this.mediaSource.readyState !== 'open') {
|
|
|
61746 |
this.trigger('error');
|
|
|
61747 |
} else {
|
|
|
61748 |
this.sourceUpdater_.endOfStream('network');
|
|
|
61749 |
}
|
|
|
61750 |
return;
|
|
|
61751 |
}
|
|
|
61752 |
playlistToExclude.playlistErrors_++;
|
|
|
61753 |
const playlists = this.mainPlaylistLoader_.main.playlists;
|
|
|
61754 |
const enabledPlaylists = playlists.filter(isEnabled);
|
|
|
61755 |
const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded
|
|
|
61756 |
// forever
|
|
|
61757 |
|
|
|
61758 |
if (playlists.length === 1 && playlistExclusionDuration !== Infinity) {
|
|
|
61759 |
videojs.log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` + 'Trying again since it is the only playlist.');
|
|
|
61760 |
this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay
|
|
|
61761 |
|
|
|
61762 |
return this.mainPlaylistLoader_.load(isFinalRendition);
|
|
|
61763 |
}
|
|
|
61764 |
if (isFinalRendition) {
|
|
|
61765 |
// If we're content steering, try other pathways.
|
|
|
61766 |
if (this.main().contentSteering) {
|
|
|
61767 |
const pathway = this.pathwayAttribute_(playlistToExclude); // Ignore at least 1 steering manifest refresh.
|
|
|
61768 |
|
|
|
61769 |
const reIncludeDelay = this.contentSteeringController_.steeringManifest.ttl * 1000;
|
|
|
61770 |
this.contentSteeringController_.excludePathway(pathway);
|
|
|
61771 |
this.excludeThenChangePathway_();
|
|
|
61772 |
setTimeout(() => {
|
|
|
61773 |
this.contentSteeringController_.addAvailablePathway(pathway);
|
|
|
61774 |
}, reIncludeDelay);
|
|
|
61775 |
return;
|
|
|
61776 |
} // Since we're on the final non-excluded playlist, and we're about to exclude
|
|
|
61777 |
// it, instead of erring the player or retrying this playlist, clear out the current
|
|
|
61778 |
// exclusion list. This allows other playlists to be attempted in case any have been
|
|
|
61779 |
// fixed.
|
|
|
61780 |
|
|
|
61781 |
let reincluded = false;
|
|
|
61782 |
playlists.forEach(playlist => {
|
|
|
61783 |
// skip current playlist which is about to be excluded
|
|
|
61784 |
if (playlist === playlistToExclude) {
|
|
|
61785 |
return;
|
|
|
61786 |
}
|
|
|
61787 |
const excludeUntil = playlist.excludeUntil; // a playlist cannot be reincluded if it wasn't excluded to begin with.
|
|
|
61788 |
|
|
|
61789 |
if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) {
|
|
|
61790 |
reincluded = true;
|
|
|
61791 |
delete playlist.excludeUntil;
|
|
|
61792 |
}
|
|
|
61793 |
});
|
|
|
61794 |
if (reincluded) {
|
|
|
61795 |
videojs.log.warn('Removing other playlists from the exclusion list because the last ' + 'rendition is about to be excluded.'); // Technically we are retrying a playlist, in that we are simply retrying a previous
|
|
|
61796 |
// playlist. This is needed for users relying on the retryplaylist event to catch a
|
|
|
61797 |
// case where the player might be stuck and looping through "dead" playlists.
|
|
|
61798 |
|
|
|
61799 |
this.tech_.trigger('retryplaylist');
|
|
|
61800 |
}
|
|
|
61801 |
} // Exclude this playlist
|
|
|
61802 |
|
|
|
61803 |
let excludeUntil;
|
|
|
61804 |
if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) {
|
|
|
61805 |
excludeUntil = Infinity;
|
|
|
61806 |
} else {
|
|
|
61807 |
excludeUntil = Date.now() + playlistExclusionDuration * 1000;
|
|
|
61808 |
}
|
|
|
61809 |
playlistToExclude.excludeUntil = excludeUntil;
|
|
|
61810 |
if (error.reason) {
|
|
|
61811 |
playlistToExclude.lastExcludeReason_ = error.reason;
|
|
|
61812 |
}
|
|
|
61813 |
this.tech_.trigger('excludeplaylist');
|
|
|
61814 |
this.tech_.trigger({
|
|
|
61815 |
type: 'usage',
|
|
|
61816 |
name: 'vhs-rendition-excluded'
|
|
|
61817 |
}); // TODO: only load a new playlist if we're excluding the current playlist
|
|
|
61818 |
// If this function was called with a playlist that's not the current active playlist
|
|
|
61819 |
// (e.g., media().id !== playlistToExclude.id),
|
|
|
61820 |
// then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist.
|
|
|
61821 |
|
|
|
61822 |
const nextPlaylist = this.selectPlaylist();
|
|
|
61823 |
if (!nextPlaylist) {
|
|
|
61824 |
this.error = 'Playback cannot continue. No available working or supported playlists.';
|
|
|
61825 |
this.trigger('error');
|
|
|
61826 |
return;
|
|
|
61827 |
}
|
|
|
61828 |
const logFn = error.internal ? this.logger_ : videojs.log.warn;
|
|
|
61829 |
const errorMessage = error.message ? ' ' + error.message : '';
|
|
|
61830 |
logFn(`${error.internal ? 'Internal problem' : 'Problem'} encountered with playlist ${playlistToExclude.id}.` + `${errorMessage} Switching to playlist ${nextPlaylist.id}.`); // if audio group changed reset audio loaders
|
|
|
61831 |
|
|
|
61832 |
if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) {
|
|
|
61833 |
this.delegateLoaders_('audio', ['abort', 'pause']);
|
|
|
61834 |
} // if subtitle group changed reset subtitle loaders
|
|
|
61835 |
|
|
|
61836 |
if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) {
|
|
|
61837 |
this.delegateLoaders_('subtitle', ['abort', 'pause']);
|
|
|
61838 |
}
|
|
|
61839 |
this.delegateLoaders_('main', ['abort', 'pause']);
|
|
|
61840 |
const delayDuration = nextPlaylist.targetDuration / 2 * 1000 || 5 * 1000;
|
|
|
61841 |
const shouldDelay = typeof nextPlaylist.lastRequest === 'number' && Date.now() - nextPlaylist.lastRequest <= delayDuration; // delay if it's a final rendition or if the last refresh is sooner than half targetDuration
|
|
|
61842 |
|
|
|
61843 |
return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay);
|
|
|
61844 |
}
|
|
|
61845 |
/**
|
|
|
61846 |
* Pause all segment/playlist loaders
|
|
|
61847 |
*/
|
|
|
61848 |
|
|
|
61849 |
pauseLoading() {
|
|
|
61850 |
this.delegateLoaders_('all', ['abort', 'pause']);
|
|
|
61851 |
this.stopABRTimer_();
|
|
|
61852 |
}
|
|
|
61853 |
/**
|
|
|
61854 |
* Call a set of functions in order on playlist loaders, segment loaders,
|
|
|
61855 |
* or both types of loaders.
|
|
|
61856 |
*
|
|
|
61857 |
* @param {string} filter
|
|
|
61858 |
* Filter loaders that should call fnNames using a string. Can be:
|
|
|
61859 |
* * all - run on all loaders
|
|
|
61860 |
* * audio - run on all audio loaders
|
|
|
61861 |
* * subtitle - run on all subtitle loaders
|
|
|
61862 |
* * main - run on the main loaders
|
|
|
61863 |
*
|
|
|
61864 |
* @param {Array|string} fnNames
|
|
|
61865 |
* A string or array of function names to call.
|
|
|
61866 |
*/
|
|
|
61867 |
|
|
|
61868 |
delegateLoaders_(filter, fnNames) {
|
|
|
61869 |
const loaders = [];
|
|
|
61870 |
const dontFilterPlaylist = filter === 'all';
|
|
|
61871 |
if (dontFilterPlaylist || filter === 'main') {
|
|
|
61872 |
loaders.push(this.mainPlaylistLoader_);
|
|
|
61873 |
}
|
|
|
61874 |
const mediaTypes = [];
|
|
|
61875 |
if (dontFilterPlaylist || filter === 'audio') {
|
|
|
61876 |
mediaTypes.push('AUDIO');
|
|
|
61877 |
}
|
|
|
61878 |
if (dontFilterPlaylist || filter === 'subtitle') {
|
|
|
61879 |
mediaTypes.push('CLOSED-CAPTIONS');
|
|
|
61880 |
mediaTypes.push('SUBTITLES');
|
|
|
61881 |
}
|
|
|
61882 |
mediaTypes.forEach(mediaType => {
|
|
|
61883 |
const loader = this.mediaTypes_[mediaType] && this.mediaTypes_[mediaType].activePlaylistLoader;
|
|
|
61884 |
if (loader) {
|
|
|
61885 |
loaders.push(loader);
|
|
|
61886 |
}
|
|
|
61887 |
});
|
|
|
61888 |
['main', 'audio', 'subtitle'].forEach(name => {
|
|
|
61889 |
const loader = this[`${name}SegmentLoader_`];
|
|
|
61890 |
if (loader && (filter === name || filter === 'all')) {
|
|
|
61891 |
loaders.push(loader);
|
|
|
61892 |
}
|
|
|
61893 |
});
|
|
|
61894 |
loaders.forEach(loader => fnNames.forEach(fnName => {
|
|
|
61895 |
if (typeof loader[fnName] === 'function') {
|
|
|
61896 |
loader[fnName]();
|
|
|
61897 |
}
|
|
|
61898 |
}));
|
|
|
61899 |
}
|
|
|
61900 |
/**
|
|
|
61901 |
* set the current time on all segment loaders
|
|
|
61902 |
*
|
|
|
61903 |
* @param {TimeRange} currentTime the current time to set
|
|
|
61904 |
* @return {TimeRange} the current time
|
|
|
61905 |
*/
|
|
|
61906 |
|
|
|
61907 |
setCurrentTime(currentTime) {
|
|
|
61908 |
const buffered = findRange(this.tech_.buffered(), currentTime);
|
|
|
61909 |
if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) {
|
|
|
61910 |
// return immediately if the metadata is not ready yet
|
|
|
61911 |
return 0;
|
|
|
61912 |
} // it's clearly an edge-case but don't thrown an error if asked to
|
|
|
61913 |
// seek within an empty playlist
|
|
|
61914 |
|
|
|
61915 |
if (!this.mainPlaylistLoader_.media().segments) {
|
|
|
61916 |
return 0;
|
|
|
61917 |
} // if the seek location is already buffered, continue buffering as usual
|
|
|
61918 |
|
|
|
61919 |
if (buffered && buffered.length) {
|
|
|
61920 |
return currentTime;
|
|
|
61921 |
} // cancel outstanding requests so we begin buffering at the new
|
|
|
61922 |
// location
|
|
|
61923 |
|
|
|
61924 |
this.mainSegmentLoader_.pause();
|
|
|
61925 |
this.mainSegmentLoader_.resetEverything();
|
|
|
61926 |
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
|
|
|
61927 |
this.audioSegmentLoader_.pause();
|
|
|
61928 |
this.audioSegmentLoader_.resetEverything();
|
|
|
61929 |
}
|
|
|
61930 |
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
|
|
|
61931 |
this.subtitleSegmentLoader_.pause();
|
|
|
61932 |
this.subtitleSegmentLoader_.resetEverything();
|
|
|
61933 |
} // start segment loader loading in case they are paused
|
|
|
61934 |
|
|
|
61935 |
this.load();
|
|
|
61936 |
}
|
|
|
61937 |
/**
|
|
|
61938 |
* get the current duration
|
|
|
61939 |
*
|
|
|
61940 |
* @return {TimeRange} the duration
|
|
|
61941 |
*/
|
|
|
61942 |
|
|
|
61943 |
duration() {
|
|
|
61944 |
if (!this.mainPlaylistLoader_) {
|
|
|
61945 |
return 0;
|
|
|
61946 |
}
|
|
|
61947 |
const media = this.mainPlaylistLoader_.media();
|
|
|
61948 |
if (!media) {
|
|
|
61949 |
// no playlists loaded yet, so can't determine a duration
|
|
|
61950 |
return 0;
|
|
|
61951 |
} // Don't rely on the media source for duration in the case of a live playlist since
|
|
|
61952 |
// setting the native MediaSource's duration to infinity ends up with consequences to
|
|
|
61953 |
// seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
|
|
|
61954 |
//
|
|
|
61955 |
// This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
|
|
|
61956 |
// however, few browsers have support for setLiveSeekableRange()
|
|
|
61957 |
// https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
|
|
|
61958 |
//
|
|
|
61959 |
// Until a time when the duration of the media source can be set to infinity, and a
|
|
|
61960 |
// seekable range specified across browsers, just return Infinity.
|
|
|
61961 |
|
|
|
61962 |
if (!media.endList) {
|
|
|
61963 |
return Infinity;
|
|
|
61964 |
} // Since this is a VOD video, it is safe to rely on the media source's duration (if
|
|
|
61965 |
// available). If it's not available, fall back to a playlist-calculated estimate.
|
|
|
61966 |
|
|
|
61967 |
if (this.mediaSource) {
|
|
|
61968 |
return this.mediaSource.duration;
|
|
|
61969 |
}
|
|
|
61970 |
return Vhs$1.Playlist.duration(media);
|
|
|
61971 |
}
|
|
|
61972 |
/**
|
|
|
61973 |
* check the seekable range
|
|
|
61974 |
*
|
|
|
61975 |
* @return {TimeRange} the seekable range
|
|
|
61976 |
*/
|
|
|
61977 |
|
|
|
61978 |
seekable() {
|
|
|
61979 |
return this.seekable_;
|
|
|
61980 |
}
|
|
|
61981 |
onSyncInfoUpdate_() {
|
|
|
61982 |
let audioSeekable; // TODO check for creation of both source buffers before updating seekable
|
|
|
61983 |
//
|
|
|
61984 |
// A fix was made to this function where a check for
|
|
|
61985 |
// this.sourceUpdater_.hasCreatedSourceBuffers
|
|
|
61986 |
// was added to ensure that both source buffers were created before seekable was
|
|
|
61987 |
// updated. However, it originally had a bug where it was checking for a true and
|
|
|
61988 |
// returning early instead of checking for false. Setting it to check for false to
|
|
|
61989 |
// return early though created other issues. A call to play() would check for seekable
|
|
|
61990 |
// end without verifying that a seekable range was present. In addition, even checking
|
|
|
61991 |
// for that didn't solve some issues, as handleFirstPlay is sometimes worked around
|
|
|
61992 |
// due to a media update calling load on the segment loaders, skipping a seek to live,
|
|
|
61993 |
// thereby starting live streams at the beginning of the stream rather than at the end.
|
|
|
61994 |
//
|
|
|
61995 |
// This conditional should be fixed to wait for the creation of two source buffers at
|
|
|
61996 |
// the same time as the other sections of code are fixed to properly seek to live and
|
|
|
61997 |
// not throw an error due to checking for a seekable end when no seekable range exists.
|
|
|
61998 |
//
|
|
|
61999 |
// For now, fall back to the older behavior, with the understanding that the seekable
|
|
|
62000 |
// range may not be completely correct, leading to a suboptimal initial live point.
|
|
|
62001 |
|
|
|
62002 |
if (!this.mainPlaylistLoader_) {
|
|
|
62003 |
return;
|
|
|
62004 |
}
|
|
|
62005 |
let media = this.mainPlaylistLoader_.media();
|
|
|
62006 |
if (!media) {
|
|
|
62007 |
return;
|
|
|
62008 |
}
|
|
|
62009 |
let expired = this.syncController_.getExpiredTime(media, this.duration());
|
|
|
62010 |
if (expired === null) {
|
|
|
62011 |
// not enough information to update seekable
|
|
|
62012 |
return;
|
|
|
62013 |
}
|
|
|
62014 |
const main = this.mainPlaylistLoader_.main;
|
|
|
62015 |
const mainSeekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(main, media));
|
|
|
62016 |
if (mainSeekable.length === 0) {
|
|
|
62017 |
return;
|
|
|
62018 |
}
|
|
|
62019 |
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
|
|
|
62020 |
media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
|
|
|
62021 |
expired = this.syncController_.getExpiredTime(media, this.duration());
|
|
|
62022 |
if (expired === null) {
|
|
|
62023 |
return;
|
|
|
62024 |
}
|
|
|
62025 |
audioSeekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(main, media));
|
|
|
62026 |
if (audioSeekable.length === 0) {
|
|
|
62027 |
return;
|
|
|
62028 |
}
|
|
|
62029 |
}
|
|
|
62030 |
let oldEnd;
|
|
|
62031 |
let oldStart;
|
|
|
62032 |
if (this.seekable_ && this.seekable_.length) {
|
|
|
62033 |
oldEnd = this.seekable_.end(0);
|
|
|
62034 |
oldStart = this.seekable_.start(0);
|
|
|
62035 |
}
|
|
|
62036 |
if (!audioSeekable) {
|
|
|
62037 |
// seekable has been calculated based on buffering video data so it
|
|
|
62038 |
// can be returned directly
|
|
|
62039 |
this.seekable_ = mainSeekable;
|
|
|
62040 |
} else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) {
|
|
|
62041 |
// seekables are pretty far off, rely on main
|
|
|
62042 |
this.seekable_ = mainSeekable;
|
|
|
62043 |
} else {
|
|
|
62044 |
this.seekable_ = createTimeRanges([[audioSeekable.start(0) > mainSeekable.start(0) ? audioSeekable.start(0) : mainSeekable.start(0), audioSeekable.end(0) < mainSeekable.end(0) ? audioSeekable.end(0) : mainSeekable.end(0)]]);
|
|
|
62045 |
} // seekable is the same as last time
|
|
|
62046 |
|
|
|
62047 |
if (this.seekable_ && this.seekable_.length) {
|
|
|
62048 |
if (this.seekable_.end(0) === oldEnd && this.seekable_.start(0) === oldStart) {
|
|
|
62049 |
return;
|
|
|
62050 |
}
|
|
|
62051 |
}
|
|
|
62052 |
this.logger_(`seekable updated [${printableRange(this.seekable_)}]`);
|
|
|
62053 |
this.tech_.trigger('seekablechanged');
|
|
|
62054 |
}
|
|
|
62055 |
/**
|
|
|
62056 |
* Update the player duration
|
|
|
62057 |
*/
|
|
|
62058 |
|
|
|
62059 |
updateDuration(isLive) {
|
|
|
62060 |
if (this.updateDuration_) {
|
|
|
62061 |
this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
|
|
|
62062 |
this.updateDuration_ = null;
|
|
|
62063 |
}
|
|
|
62064 |
if (this.mediaSource.readyState !== 'open') {
|
|
|
62065 |
this.updateDuration_ = this.updateDuration.bind(this, isLive);
|
|
|
62066 |
this.mediaSource.addEventListener('sourceopen', this.updateDuration_);
|
|
|
62067 |
return;
|
|
|
62068 |
}
|
|
|
62069 |
if (isLive) {
|
|
|
62070 |
const seekable = this.seekable();
|
|
|
62071 |
if (!seekable.length) {
|
|
|
62072 |
return;
|
|
|
62073 |
} // Even in the case of a live playlist, the native MediaSource's duration should not
|
|
|
62074 |
// be set to Infinity (even though this would be expected for a live playlist), since
|
|
|
62075 |
// setting the native MediaSource's duration to infinity ends up with consequences to
|
|
|
62076 |
// seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
|
|
|
62077 |
//
|
|
|
62078 |
// This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
|
|
|
62079 |
// however, few browsers have support for setLiveSeekableRange()
|
|
|
62080 |
// https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
|
|
|
62081 |
//
|
|
|
62082 |
// Until a time when the duration of the media source can be set to infinity, and a
|
|
|
62083 |
// seekable range specified across browsers, the duration should be greater than or
|
|
|
62084 |
// equal to the last possible seekable value.
|
|
|
62085 |
// MediaSource duration starts as NaN
|
|
|
62086 |
// It is possible (and probable) that this case will never be reached for many
|
|
|
62087 |
// sources, since the MediaSource reports duration as the highest value without
|
|
|
62088 |
// accounting for timestamp offset. For example, if the timestamp offset is -100 and
|
|
|
62089 |
// we buffered times 0 to 100 with real times of 100 to 200, even though current
|
|
|
62090 |
// time will be between 0 and 100, the native media source may report the duration
|
|
|
62091 |
// as 200. However, since we report duration separate from the media source (as
|
|
|
62092 |
// Infinity), and as long as the native media source duration value is greater than
|
|
|
62093 |
// our reported seekable range, seeks will work as expected. The large number as
|
|
|
62094 |
// duration for live is actually a strategy used by some players to work around the
|
|
|
62095 |
// issue of live seekable ranges cited above.
|
|
|
62096 |
|
|
|
62097 |
if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) {
|
|
|
62098 |
this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1));
|
|
|
62099 |
}
|
|
|
62100 |
return;
|
|
|
62101 |
}
|
|
|
62102 |
const buffered = this.tech_.buffered();
|
|
|
62103 |
let duration = Vhs$1.Playlist.duration(this.mainPlaylistLoader_.media());
|
|
|
62104 |
if (buffered.length > 0) {
|
|
|
62105 |
duration = Math.max(duration, buffered.end(buffered.length - 1));
|
|
|
62106 |
}
|
|
|
62107 |
if (this.mediaSource.duration !== duration) {
|
|
|
62108 |
this.sourceUpdater_.setDuration(duration);
|
|
|
62109 |
}
|
|
|
62110 |
}
|
|
|
62111 |
/**
|
|
|
62112 |
* dispose of the PlaylistController and everything
|
|
|
62113 |
* that it controls
|
|
|
62114 |
*/
|
|
|
62115 |
|
|
|
62116 |
dispose() {
|
|
|
62117 |
this.trigger('dispose');
|
|
|
62118 |
this.decrypter_.terminate();
|
|
|
62119 |
this.mainPlaylistLoader_.dispose();
|
|
|
62120 |
this.mainSegmentLoader_.dispose();
|
|
|
62121 |
this.contentSteeringController_.dispose();
|
|
|
62122 |
this.keyStatusMap_.clear();
|
|
|
62123 |
if (this.loadOnPlay_) {
|
|
|
62124 |
this.tech_.off('play', this.loadOnPlay_);
|
|
|
62125 |
}
|
|
|
62126 |
['AUDIO', 'SUBTITLES'].forEach(type => {
|
|
|
62127 |
const groups = this.mediaTypes_[type].groups;
|
|
|
62128 |
for (const id in groups) {
|
|
|
62129 |
groups[id].forEach(group => {
|
|
|
62130 |
if (group.playlistLoader) {
|
|
|
62131 |
group.playlistLoader.dispose();
|
|
|
62132 |
}
|
|
|
62133 |
});
|
|
|
62134 |
}
|
|
|
62135 |
});
|
|
|
62136 |
this.audioSegmentLoader_.dispose();
|
|
|
62137 |
this.subtitleSegmentLoader_.dispose();
|
|
|
62138 |
this.sourceUpdater_.dispose();
|
|
|
62139 |
this.timelineChangeController_.dispose();
|
|
|
62140 |
this.stopABRTimer_();
|
|
|
62141 |
if (this.updateDuration_) {
|
|
|
62142 |
this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
|
|
|
62143 |
}
|
|
|
62144 |
this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_); // load the media source into the player
|
|
|
62145 |
|
|
|
62146 |
this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_);
|
|
|
62147 |
this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_);
|
|
|
62148 |
this.off();
|
|
|
62149 |
}
|
|
|
62150 |
/**
|
|
|
62151 |
* return the main playlist object if we have one
|
|
|
62152 |
*
|
|
|
62153 |
* @return {Object} the main playlist object that we parsed
|
|
|
62154 |
*/
|
|
|
62155 |
|
|
|
62156 |
main() {
|
|
|
62157 |
return this.mainPlaylistLoader_.main;
|
|
|
62158 |
}
|
|
|
62159 |
/**
|
|
|
62160 |
* return the currently selected playlist
|
|
|
62161 |
*
|
|
|
62162 |
* @return {Object} the currently selected playlist object that we parsed
|
|
|
62163 |
*/
|
|
|
62164 |
|
|
|
62165 |
media() {
|
|
|
62166 |
// playlist loader will not return media if it has not been fully loaded
|
|
|
62167 |
return this.mainPlaylistLoader_.media() || this.initialMedia_;
|
|
|
62168 |
}
|
|
|
62169 |
areMediaTypesKnown_() {
|
|
|
62170 |
const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
|
|
|
62171 |
const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_(); // if we are not using an audio loader, then we have audio media info
|
|
|
62172 |
// otherwise check on the segment loader.
|
|
|
62173 |
|
|
|
62174 |
const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_(); // one or both loaders has not loaded sufficently to get codecs
|
|
|
62175 |
|
|
|
62176 |
if (!hasMainMediaInfo || !hasAudioMediaInfo) {
|
|
|
62177 |
return false;
|
|
|
62178 |
}
|
|
|
62179 |
return true;
|
|
|
62180 |
}
|
|
|
62181 |
getCodecsOrExclude_() {
|
|
|
62182 |
const media = {
|
|
|
62183 |
main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {},
|
|
|
62184 |
audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {}
|
|
|
62185 |
};
|
|
|
62186 |
const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); // set "main" media equal to video
|
|
|
62187 |
|
|
|
62188 |
media.video = media.main;
|
|
|
62189 |
const playlistCodecs = codecsForPlaylist(this.main(), playlist);
|
|
|
62190 |
const codecs = {};
|
|
|
62191 |
const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
|
|
|
62192 |
if (media.main.hasVideo) {
|
|
|
62193 |
codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC;
|
|
|
62194 |
}
|
|
|
62195 |
if (media.main.isMuxed) {
|
|
|
62196 |
codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`;
|
|
|
62197 |
}
|
|
|
62198 |
if (media.main.hasAudio && !media.main.isMuxed || media.audio.hasAudio || usingAudioLoader) {
|
|
|
62199 |
codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC; // set audio isFmp4 so we use the correct "supports" function below
|
|
|
62200 |
|
|
|
62201 |
media.audio.isFmp4 = media.main.hasAudio && !media.main.isMuxed ? media.main.isFmp4 : media.audio.isFmp4;
|
|
|
62202 |
} // no codecs, no playback.
|
|
|
62203 |
|
|
|
62204 |
if (!codecs.audio && !codecs.video) {
|
|
|
62205 |
this.excludePlaylist({
|
|
|
62206 |
playlistToExclude: playlist,
|
|
|
62207 |
error: {
|
|
|
62208 |
message: 'Could not determine codecs for playlist.'
|
|
|
62209 |
},
|
|
|
62210 |
playlistExclusionDuration: Infinity
|
|
|
62211 |
});
|
|
|
62212 |
return;
|
|
|
62213 |
} // fmp4 relies on browser support, while ts relies on muxer support
|
|
|
62214 |
|
|
|
62215 |
const supportFunction = (isFmp4, codec) => isFmp4 ? browserSupportsCodec(codec) : muxerSupportsCodec(codec);
|
|
|
62216 |
const unsupportedCodecs = {};
|
|
|
62217 |
let unsupportedAudio;
|
|
|
62218 |
['video', 'audio'].forEach(function (type) {
|
|
|
62219 |
if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) {
|
|
|
62220 |
const supporter = media[type].isFmp4 ? 'browser' : 'muxer';
|
|
|
62221 |
unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || [];
|
|
|
62222 |
unsupportedCodecs[supporter].push(codecs[type]);
|
|
|
62223 |
if (type === 'audio') {
|
|
|
62224 |
unsupportedAudio = supporter;
|
|
|
62225 |
}
|
|
|
62226 |
}
|
|
|
62227 |
});
|
|
|
62228 |
if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) {
|
|
|
62229 |
const audioGroup = playlist.attributes.AUDIO;
|
|
|
62230 |
this.main().playlists.forEach(variant => {
|
|
|
62231 |
const variantAudioGroup = variant.attributes && variant.attributes.AUDIO;
|
|
|
62232 |
if (variantAudioGroup === audioGroup && variant !== playlist) {
|
|
|
62233 |
variant.excludeUntil = Infinity;
|
|
|
62234 |
}
|
|
|
62235 |
});
|
|
|
62236 |
this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`);
|
|
|
62237 |
} // if we have any unsupported codecs exclude this playlist.
|
|
|
62238 |
|
|
|
62239 |
if (Object.keys(unsupportedCodecs).length) {
|
|
|
62240 |
const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => {
|
|
|
62241 |
if (acc) {
|
|
|
62242 |
acc += ', ';
|
|
|
62243 |
}
|
|
|
62244 |
acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`;
|
|
|
62245 |
return acc;
|
|
|
62246 |
}, '') + '.';
|
|
|
62247 |
this.excludePlaylist({
|
|
|
62248 |
playlistToExclude: playlist,
|
|
|
62249 |
error: {
|
|
|
62250 |
internal: true,
|
|
|
62251 |
message
|
|
|
62252 |
},
|
|
|
62253 |
playlistExclusionDuration: Infinity
|
|
|
62254 |
});
|
|
|
62255 |
return;
|
|
|
62256 |
} // check if codec switching is happening
|
|
|
62257 |
|
|
|
62258 |
if (this.sourceUpdater_.hasCreatedSourceBuffers() && !this.sourceUpdater_.canChangeType()) {
|
|
|
62259 |
const switchMessages = [];
|
|
|
62260 |
['video', 'audio'].forEach(type => {
|
|
|
62261 |
const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type;
|
|
|
62262 |
const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type;
|
|
|
62263 |
if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) {
|
|
|
62264 |
switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`);
|
|
|
62265 |
}
|
|
|
62266 |
});
|
|
|
62267 |
if (switchMessages.length) {
|
|
|
62268 |
this.excludePlaylist({
|
|
|
62269 |
playlistToExclude: playlist,
|
|
|
62270 |
error: {
|
|
|
62271 |
message: `Codec switching not supported: ${switchMessages.join(', ')}.`,
|
|
|
62272 |
internal: true
|
|
|
62273 |
},
|
|
|
62274 |
playlistExclusionDuration: Infinity
|
|
|
62275 |
});
|
|
|
62276 |
return;
|
|
|
62277 |
}
|
|
|
62278 |
} // TODO: when using the muxer shouldn't we just return
|
|
|
62279 |
// the codecs that the muxer outputs?
|
|
|
62280 |
|
|
|
62281 |
return codecs;
|
|
|
62282 |
}
|
|
|
62283 |
/**
|
|
|
62284 |
* Create source buffers and exlude any incompatible renditions.
|
|
|
62285 |
*
|
|
|
62286 |
* @private
|
|
|
62287 |
*/
|
|
|
62288 |
|
|
|
62289 |
tryToCreateSourceBuffers_() {
|
|
|
62290 |
// media source is not ready yet or sourceBuffers are already
|
|
|
62291 |
// created.
|
|
|
62292 |
if (this.mediaSource.readyState !== 'open' || this.sourceUpdater_.hasCreatedSourceBuffers()) {
|
|
|
62293 |
return;
|
|
|
62294 |
}
|
|
|
62295 |
if (!this.areMediaTypesKnown_()) {
|
|
|
62296 |
return;
|
|
|
62297 |
}
|
|
|
62298 |
const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded
|
|
|
62299 |
|
|
|
62300 |
if (!codecs) {
|
|
|
62301 |
return;
|
|
|
62302 |
}
|
|
|
62303 |
this.sourceUpdater_.createSourceBuffers(codecs);
|
|
|
62304 |
const codecString = [codecs.video, codecs.audio].filter(Boolean).join(',');
|
|
|
62305 |
this.excludeIncompatibleVariants_(codecString);
|
|
|
62306 |
}
|
|
|
62307 |
/**
|
|
|
62308 |
* Excludes playlists with codecs that are unsupported by the muxer and browser.
|
|
|
62309 |
*/
|
|
|
62310 |
|
|
|
62311 |
excludeUnsupportedVariants_() {
|
|
|
62312 |
const playlists = this.main().playlists;
|
|
|
62313 |
const ids = []; // TODO: why don't we have a property to loop through all
|
|
|
62314 |
// playlist? Why did we ever mix indexes and keys?
|
|
|
62315 |
|
|
|
62316 |
Object.keys(playlists).forEach(key => {
|
|
|
62317 |
const variant = playlists[key]; // check if we already processed this playlist.
|
|
|
62318 |
|
|
|
62319 |
if (ids.indexOf(variant.id) !== -1) {
|
|
|
62320 |
return;
|
|
|
62321 |
}
|
|
|
62322 |
ids.push(variant.id);
|
|
|
62323 |
const codecs = codecsForPlaylist(this.main, variant);
|
|
|
62324 |
const unsupported = [];
|
|
|
62325 |
if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio)) {
|
|
|
62326 |
unsupported.push(`audio codec ${codecs.audio}`);
|
|
|
62327 |
}
|
|
|
62328 |
if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video)) {
|
|
|
62329 |
unsupported.push(`video codec ${codecs.video}`);
|
|
|
62330 |
}
|
|
|
62331 |
if (codecs.text && codecs.text === 'stpp.ttml.im1t') {
|
|
|
62332 |
unsupported.push(`text codec ${codecs.text}`);
|
|
|
62333 |
}
|
|
|
62334 |
if (unsupported.length) {
|
|
|
62335 |
variant.excludeUntil = Infinity;
|
|
|
62336 |
this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`);
|
|
|
62337 |
}
|
|
|
62338 |
});
|
|
|
62339 |
}
|
|
|
62340 |
/**
|
|
|
62341 |
* Exclude playlists that are known to be codec or
|
|
|
62342 |
* stream-incompatible with the SourceBuffer configuration. For
|
|
|
62343 |
* instance, Media Source Extensions would cause the video element to
|
|
|
62344 |
* stall waiting for video data if you switched from a variant with
|
|
|
62345 |
* video and audio to an audio-only one.
|
|
|
62346 |
*
|
|
|
62347 |
* @param {Object} media a media playlist compatible with the current
|
|
|
62348 |
* set of SourceBuffers. Variants in the current main playlist that
|
|
|
62349 |
* do not appear to have compatible codec or stream configurations
|
|
|
62350 |
* will be excluded from the default playlist selection algorithm
|
|
|
62351 |
* indefinitely.
|
|
|
62352 |
* @private
|
|
|
62353 |
*/
|
|
|
62354 |
|
|
|
62355 |
excludeIncompatibleVariants_(codecString) {
|
|
|
62356 |
const ids = [];
|
|
|
62357 |
const playlists = this.main().playlists;
|
|
|
62358 |
const codecs = unwrapCodecList(parseCodecs(codecString));
|
|
|
62359 |
const codecCount_ = codecCount(codecs);
|
|
|
62360 |
const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null;
|
|
|
62361 |
const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null;
|
|
|
62362 |
Object.keys(playlists).forEach(key => {
|
|
|
62363 |
const variant = playlists[key]; // check if we already processed this playlist.
|
|
|
62364 |
// or it if it is already excluded forever.
|
|
|
62365 |
|
|
|
62366 |
if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) {
|
|
|
62367 |
return;
|
|
|
62368 |
}
|
|
|
62369 |
ids.push(variant.id);
|
|
|
62370 |
const exclusionReasons = []; // get codecs from the playlist for this variant
|
|
|
62371 |
|
|
|
62372 |
const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant);
|
|
|
62373 |
const variantCodecCount = codecCount(variantCodecs); // if no codecs are listed, we cannot determine that this
|
|
|
62374 |
// variant is incompatible. Wait for mux.js to probe
|
|
|
62375 |
|
|
|
62376 |
if (!variantCodecs.audio && !variantCodecs.video) {
|
|
|
62377 |
return;
|
|
|
62378 |
} // TODO: we can support this by removing the
|
|
|
62379 |
// old media source and creating a new one, but it will take some work.
|
|
|
62380 |
// The number of streams cannot change
|
|
|
62381 |
|
|
|
62382 |
if (variantCodecCount !== codecCount_) {
|
|
|
62383 |
exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`);
|
|
|
62384 |
} // only exclude playlists by codec change, if codecs cannot switch
|
|
|
62385 |
// during playback.
|
|
|
62386 |
|
|
|
62387 |
if (!this.sourceUpdater_.canChangeType()) {
|
|
|
62388 |
const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null;
|
|
|
62389 |
const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null; // the video codec cannot change
|
|
|
62390 |
|
|
|
62391 |
if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) {
|
|
|
62392 |
exclusionReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`);
|
|
|
62393 |
} // the audio codec cannot change
|
|
|
62394 |
|
|
|
62395 |
if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) {
|
|
|
62396 |
exclusionReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`);
|
|
|
62397 |
}
|
|
|
62398 |
}
|
|
|
62399 |
if (exclusionReasons.length) {
|
|
|
62400 |
variant.excludeUntil = Infinity;
|
|
|
62401 |
this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`);
|
|
|
62402 |
}
|
|
|
62403 |
});
|
|
|
62404 |
}
|
|
|
62405 |
updateAdCues_(media) {
|
|
|
62406 |
let offset = 0;
|
|
|
62407 |
const seekable = this.seekable();
|
|
|
62408 |
if (seekable.length) {
|
|
|
62409 |
offset = seekable.start(0);
|
|
|
62410 |
}
|
|
|
62411 |
updateAdCues(media, this.cueTagsTrack_, offset);
|
|
|
62412 |
}
|
|
|
62413 |
/**
|
|
|
62414 |
* Calculates the desired forward buffer length based on current time
|
|
|
62415 |
*
|
|
|
62416 |
* @return {number} Desired forward buffer length in seconds
|
|
|
62417 |
*/
|
|
|
62418 |
|
|
|
62419 |
goalBufferLength() {
|
|
|
62420 |
const currentTime = this.tech_.currentTime();
|
|
|
62421 |
const initial = Config.GOAL_BUFFER_LENGTH;
|
|
|
62422 |
const rate = Config.GOAL_BUFFER_LENGTH_RATE;
|
|
|
62423 |
const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH);
|
|
|
62424 |
return Math.min(initial + currentTime * rate, max);
|
|
|
62425 |
}
|
|
|
62426 |
/**
|
|
|
62427 |
* Calculates the desired buffer low water line based on current time
|
|
|
62428 |
*
|
|
|
62429 |
* @return {number} Desired buffer low water line in seconds
|
|
|
62430 |
*/
|
|
|
62431 |
|
|
|
62432 |
bufferLowWaterLine() {
|
|
|
62433 |
const currentTime = this.tech_.currentTime();
|
|
|
62434 |
const initial = Config.BUFFER_LOW_WATER_LINE;
|
|
|
62435 |
const rate = Config.BUFFER_LOW_WATER_LINE_RATE;
|
|
|
62436 |
const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE);
|
|
|
62437 |
const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE);
|
|
|
62438 |
return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max);
|
|
|
62439 |
}
|
|
|
62440 |
bufferHighWaterLine() {
|
|
|
62441 |
return Config.BUFFER_HIGH_WATER_LINE;
|
|
|
62442 |
}
|
|
|
62443 |
addDateRangesToTextTrack_(dateRanges) {
|
|
|
62444 |
createMetadataTrackIfNotExists(this.inbandTextTracks_, 'com.apple.streaming', this.tech_);
|
|
|
62445 |
addDateRangeMetadata({
|
|
|
62446 |
inbandTextTracks: this.inbandTextTracks_,
|
|
|
62447 |
dateRanges
|
|
|
62448 |
});
|
|
|
62449 |
}
|
|
|
62450 |
addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) {
|
|
|
62451 |
const timestampOffset = this.sourceUpdater_.videoBuffer ? this.sourceUpdater_.videoTimestampOffset() : this.sourceUpdater_.audioTimestampOffset(); // There's potentially an issue where we could double add metadata if there's a muxed
|
|
|
62452 |
// audio/video source with a metadata track, and an alt audio with a metadata track.
|
|
|
62453 |
// However, this probably won't happen, and if it does it can be handled then.
|
|
|
62454 |
|
|
|
62455 |
createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_);
|
|
|
62456 |
addMetadata({
|
|
|
62457 |
inbandTextTracks: this.inbandTextTracks_,
|
|
|
62458 |
metadataArray,
|
|
|
62459 |
timestampOffset,
|
|
|
62460 |
videoDuration
|
|
|
62461 |
});
|
|
|
62462 |
}
|
|
|
62463 |
/**
|
|
|
62464 |
* Utility for getting the pathway or service location from an HLS or DASH playlist.
|
|
|
62465 |
*
|
|
|
62466 |
* @param {Object} playlist for getting pathway from.
|
|
|
62467 |
* @return the pathway attribute of a playlist
|
|
|
62468 |
*/
|
|
|
62469 |
|
|
|
62470 |
pathwayAttribute_(playlist) {
|
|
|
62471 |
return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation;
|
|
|
62472 |
}
|
|
|
62473 |
/**
|
|
|
62474 |
* Initialize available pathways and apply the tag properties.
|
|
|
62475 |
*/
|
|
|
62476 |
|
|
|
62477 |
initContentSteeringController_() {
|
|
|
62478 |
const main = this.main();
|
|
|
62479 |
if (!main.contentSteering) {
|
|
|
62480 |
return;
|
|
|
62481 |
}
|
|
|
62482 |
for (const playlist of main.playlists) {
|
|
|
62483 |
this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist));
|
|
|
62484 |
}
|
|
|
62485 |
this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering); // request the steering manifest immediately if queryBeforeStart is set.
|
|
|
62486 |
|
|
|
62487 |
if (this.contentSteeringController_.queryBeforeStart) {
|
|
|
62488 |
// When queryBeforeStart is true, initial request should omit steering parameters.
|
|
|
62489 |
this.contentSteeringController_.requestSteeringManifest(true);
|
|
|
62490 |
return;
|
|
|
62491 |
} // otherwise start content steering after playback starts
|
|
|
62492 |
|
|
|
62493 |
this.tech_.one('canplay', () => {
|
|
|
62494 |
this.contentSteeringController_.requestSteeringManifest();
|
|
|
62495 |
});
|
|
|
62496 |
}
|
|
|
62497 |
/**
|
|
|
62498 |
* Reset the content steering controller and re-init.
|
|
|
62499 |
*/
|
|
|
62500 |
|
|
|
62501 |
resetContentSteeringController_() {
|
|
|
62502 |
this.contentSteeringController_.clearAvailablePathways();
|
|
|
62503 |
this.contentSteeringController_.dispose();
|
|
|
62504 |
this.initContentSteeringController_();
|
|
|
62505 |
}
|
|
|
62506 |
/**
|
|
|
62507 |
* Attaches the listeners for content steering.
|
|
|
62508 |
*/
|
|
|
62509 |
|
|
|
62510 |
attachContentSteeringListeners_() {
|
|
|
62511 |
this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this));
|
|
|
62512 |
if (this.sourceType_ === 'dash') {
|
|
|
62513 |
this.mainPlaylistLoader_.on('loadedplaylist', () => {
|
|
|
62514 |
const main = this.main(); // check if steering tag or pathways changed.
|
|
|
62515 |
|
|
|
62516 |
const didDashTagChange = this.contentSteeringController_.didDASHTagChange(main.uri, main.contentSteering);
|
|
|
62517 |
const didPathwaysChange = () => {
|
|
|
62518 |
const availablePathways = this.contentSteeringController_.getAvailablePathways();
|
|
|
62519 |
const newPathways = [];
|
|
|
62520 |
for (const playlist of main.playlists) {
|
|
|
62521 |
const serviceLocation = playlist.attributes.serviceLocation;
|
|
|
62522 |
if (serviceLocation) {
|
|
|
62523 |
newPathways.push(serviceLocation);
|
|
|
62524 |
if (!availablePathways.has(serviceLocation)) {
|
|
|
62525 |
return true;
|
|
|
62526 |
}
|
|
|
62527 |
}
|
|
|
62528 |
} // If we have no new serviceLocations and previously had availablePathways
|
|
|
62529 |
|
|
|
62530 |
if (!newPathways.length && availablePathways.size) {
|
|
|
62531 |
return true;
|
|
|
62532 |
}
|
|
|
62533 |
return false;
|
|
|
62534 |
};
|
|
|
62535 |
if (didDashTagChange || didPathwaysChange()) {
|
|
|
62536 |
this.resetContentSteeringController_();
|
|
|
62537 |
}
|
|
|
62538 |
});
|
|
|
62539 |
}
|
|
|
62540 |
}
|
|
|
62541 |
/**
|
|
|
62542 |
* Simple exclude and change playlist logic for content steering.
|
|
|
62543 |
*/
|
|
|
62544 |
|
|
|
62545 |
excludeThenChangePathway_() {
|
|
|
62546 |
const currentPathway = this.contentSteeringController_.getPathway();
|
|
|
62547 |
if (!currentPathway) {
|
|
|
62548 |
return;
|
|
|
62549 |
}
|
|
|
62550 |
this.handlePathwayClones_();
|
|
|
62551 |
const main = this.main();
|
|
|
62552 |
const playlists = main.playlists;
|
|
|
62553 |
const ids = new Set();
|
|
|
62554 |
let didEnablePlaylists = false;
|
|
|
62555 |
Object.keys(playlists).forEach(key => {
|
|
|
62556 |
const variant = playlists[key];
|
|
|
62557 |
const pathwayId = this.pathwayAttribute_(variant);
|
|
|
62558 |
const differentPathwayId = pathwayId && currentPathway !== pathwayId;
|
|
|
62559 |
const steeringExclusion = variant.excludeUntil === Infinity && variant.lastExcludeReason_ === 'content-steering';
|
|
|
62560 |
if (steeringExclusion && !differentPathwayId) {
|
|
|
62561 |
delete variant.excludeUntil;
|
|
|
62562 |
delete variant.lastExcludeReason_;
|
|
|
62563 |
didEnablePlaylists = true;
|
|
|
62564 |
}
|
|
|
62565 |
const noExcludeUntil = !variant.excludeUntil && variant.excludeUntil !== Infinity;
|
|
|
62566 |
const shouldExclude = !ids.has(variant.id) && differentPathwayId && noExcludeUntil;
|
|
|
62567 |
if (!shouldExclude) {
|
|
|
62568 |
return;
|
|
|
62569 |
}
|
|
|
62570 |
ids.add(variant.id);
|
|
|
62571 |
variant.excludeUntil = Infinity;
|
|
|
62572 |
variant.lastExcludeReason_ = 'content-steering'; // TODO: kind of spammy, maybe move this.
|
|
|
62573 |
|
|
|
62574 |
this.logger_(`excluding ${variant.id} for ${variant.lastExcludeReason_}`);
|
|
|
62575 |
});
|
|
|
62576 |
if (this.contentSteeringController_.manifestType_ === 'DASH') {
|
|
|
62577 |
Object.keys(this.mediaTypes_).forEach(key => {
|
|
|
62578 |
const type = this.mediaTypes_[key];
|
|
|
62579 |
if (type.activePlaylistLoader) {
|
|
|
62580 |
const currentPlaylist = type.activePlaylistLoader.media_; // Check if the current media playlist matches the current CDN
|
|
|
62581 |
|
|
|
62582 |
if (currentPlaylist && currentPlaylist.attributes.serviceLocation !== currentPathway) {
|
|
|
62583 |
didEnablePlaylists = true;
|
|
|
62584 |
}
|
|
|
62585 |
}
|
|
|
62586 |
});
|
|
|
62587 |
}
|
|
|
62588 |
if (didEnablePlaylists) {
|
|
|
62589 |
this.changeSegmentPathway_();
|
|
|
62590 |
}
|
|
|
62591 |
}
|
|
|
62592 |
/**
|
|
|
62593 |
* Add, update, or delete playlists and media groups for
|
|
|
62594 |
* the pathway clones for HLS Content Steering.
|
|
|
62595 |
*
|
|
|
62596 |
* See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
|
|
|
62597 |
*
|
|
|
62598 |
* NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and
|
|
|
62599 |
* `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or
|
|
|
62600 |
* `STABLE-RENDITION-ID` values.
|
|
|
62601 |
*/
|
|
|
62602 |
|
|
|
62603 |
handlePathwayClones_() {
|
|
|
62604 |
const main = this.main();
|
|
|
62605 |
const playlists = main.playlists;
|
|
|
62606 |
const currentPathwayClones = this.contentSteeringController_.currentPathwayClones;
|
|
|
62607 |
const nextPathwayClones = this.contentSteeringController_.nextPathwayClones;
|
|
|
62608 |
const hasClones = currentPathwayClones && currentPathwayClones.size || nextPathwayClones && nextPathwayClones.size;
|
|
|
62609 |
if (!hasClones) {
|
|
|
62610 |
return;
|
|
|
62611 |
}
|
|
|
62612 |
for (const [id, clone] of currentPathwayClones.entries()) {
|
|
|
62613 |
const newClone = nextPathwayClones.get(id); // Delete the old pathway clone.
|
|
|
62614 |
|
|
|
62615 |
if (!newClone) {
|
|
|
62616 |
this.mainPlaylistLoader_.updateOrDeleteClone(clone);
|
|
|
62617 |
this.contentSteeringController_.excludePathway(id);
|
|
|
62618 |
}
|
|
|
62619 |
}
|
|
|
62620 |
for (const [id, clone] of nextPathwayClones.entries()) {
|
|
|
62621 |
const oldClone = currentPathwayClones.get(id); // Create a new pathway if it is a new pathway clone object.
|
|
|
62622 |
|
|
|
62623 |
if (!oldClone) {
|
|
|
62624 |
const playlistsToClone = playlists.filter(p => {
|
|
|
62625 |
return p.attributes['PATHWAY-ID'] === clone['BASE-ID'];
|
|
|
62626 |
});
|
|
|
62627 |
playlistsToClone.forEach(p => {
|
|
|
62628 |
this.mainPlaylistLoader_.addClonePathway(clone, p);
|
|
|
62629 |
});
|
|
|
62630 |
this.contentSteeringController_.addAvailablePathway(id);
|
|
|
62631 |
continue;
|
|
|
62632 |
} // There have not been changes to the pathway clone object, so skip.
|
|
|
62633 |
|
|
|
62634 |
if (this.equalPathwayClones_(oldClone, clone)) {
|
|
|
62635 |
continue;
|
|
|
62636 |
} // Update a preexisting cloned pathway.
|
|
|
62637 |
// True is set for the update flag.
|
|
|
62638 |
|
|
|
62639 |
this.mainPlaylistLoader_.updateOrDeleteClone(clone, true);
|
|
|
62640 |
this.contentSteeringController_.addAvailablePathway(id);
|
|
|
62641 |
} // Deep copy contents of next to current pathways.
|
|
|
62642 |
|
|
|
62643 |
this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones])));
|
|
|
62644 |
}
|
|
|
62645 |
/**
|
|
|
62646 |
* Determines whether two pathway clone objects are equivalent.
|
|
|
62647 |
*
|
|
|
62648 |
* @param {Object} a The first pathway clone object.
|
|
|
62649 |
* @param {Object} b The second pathway clone object.
|
|
|
62650 |
* @return {boolean} True if the pathway clone objects are equal, false otherwise.
|
|
|
62651 |
*/
|
|
|
62652 |
|
|
|
62653 |
equalPathwayClones_(a, b) {
|
|
|
62654 |
if (a['BASE-ID'] !== b['BASE-ID'] || a.ID !== b.ID || a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST) {
|
|
|
62655 |
return false;
|
|
|
62656 |
}
|
|
|
62657 |
const aParams = a['URI-REPLACEMENT'].PARAMS;
|
|
|
62658 |
const bParams = b['URI-REPLACEMENT'].PARAMS; // We need to iterate through both lists of params because one could be
|
|
|
62659 |
// missing a parameter that the other has.
|
|
|
62660 |
|
|
|
62661 |
for (const p in aParams) {
|
|
|
62662 |
if (aParams[p] !== bParams[p]) {
|
|
|
62663 |
return false;
|
|
|
62664 |
}
|
|
|
62665 |
}
|
|
|
62666 |
for (const p in bParams) {
|
|
|
62667 |
if (aParams[p] !== bParams[p]) {
|
|
|
62668 |
return false;
|
|
|
62669 |
}
|
|
|
62670 |
}
|
|
|
62671 |
return true;
|
|
|
62672 |
}
|
|
|
62673 |
/**
|
|
|
62674 |
* Changes the current playlists for audio, video and subtitles after a new pathway
|
|
|
62675 |
* is chosen from content steering.
|
|
|
62676 |
*/
|
|
|
62677 |
|
|
|
62678 |
changeSegmentPathway_() {
|
|
|
62679 |
const nextPlaylist = this.selectPlaylist();
|
|
|
62680 |
this.pauseLoading(); // Switch audio and text track playlists if necessary in DASH
|
|
|
62681 |
|
|
|
62682 |
if (this.contentSteeringController_.manifestType_ === 'DASH') {
|
|
|
62683 |
this.switchMediaForDASHContentSteering_();
|
|
|
62684 |
}
|
|
|
62685 |
this.switchMedia_(nextPlaylist, 'content-steering');
|
|
|
62686 |
}
|
|
|
62687 |
/**
|
|
|
62688 |
* Iterates through playlists and check their keyId set and compare with the
|
|
|
62689 |
* keyStatusMap, only enable playlists that have a usable key. If the playlist
|
|
|
62690 |
* has no keyId leave it enabled by default.
|
|
|
62691 |
*/
|
|
|
62692 |
|
|
|
62693 |
excludeNonUsablePlaylistsByKeyId_() {
|
|
|
62694 |
if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) {
|
|
|
62695 |
return;
|
|
|
62696 |
}
|
|
|
62697 |
let nonUsableKeyStatusCount = 0;
|
|
|
62698 |
const NON_USABLE = 'non-usable';
|
|
|
62699 |
this.mainPlaylistLoader_.main.playlists.forEach(playlist => {
|
|
|
62700 |
const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); // If the playlist doesn't have keyIDs lets not exclude it.
|
|
|
62701 |
|
|
|
62702 |
if (!keyIdSet || !keyIdSet.size) {
|
|
|
62703 |
return;
|
|
|
62704 |
}
|
|
|
62705 |
keyIdSet.forEach(key => {
|
|
|
62706 |
const USABLE = 'usable';
|
|
|
62707 |
const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE;
|
|
|
62708 |
const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity;
|
|
|
62709 |
if (!hasUsableKeyStatus) {
|
|
|
62710 |
// Only exclude playlists that haven't already been excluded as non-usable.
|
|
|
62711 |
if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) {
|
|
|
62712 |
playlist.excludeUntil = Infinity;
|
|
|
62713 |
playlist.lastExcludeReason_ = NON_USABLE;
|
|
|
62714 |
this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`);
|
|
|
62715 |
} // count all nonUsableKeyStatus
|
|
|
62716 |
|
|
|
62717 |
nonUsableKeyStatusCount++;
|
|
|
62718 |
} else if (hasUsableKeyStatus && nonUsableExclusion) {
|
|
|
62719 |
delete playlist.excludeUntil;
|
|
|
62720 |
delete playlist.lastExcludeReason_;
|
|
|
62721 |
this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`);
|
|
|
62722 |
}
|
|
|
62723 |
});
|
|
|
62724 |
}); // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe.
|
|
|
62725 |
|
|
|
62726 |
if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) {
|
|
|
62727 |
this.mainPlaylistLoader_.main.playlists.forEach(playlist => {
|
|
|
62728 |
const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720;
|
|
|
62729 |
const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE;
|
|
|
62730 |
if (isNonHD && excludedForNonUsableKey) {
|
|
|
62731 |
// Only delete the excludeUntil so we don't try and re-exclude these playlists.
|
|
|
62732 |
delete playlist.excludeUntil;
|
|
|
62733 |
videojs.log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`);
|
|
|
62734 |
}
|
|
|
62735 |
});
|
|
|
62736 |
}
|
|
|
62737 |
}
|
|
|
62738 |
/**
|
|
|
62739 |
* Adds a keystatus to the keystatus map, tries to convert to string if necessary.
|
|
|
62740 |
*
|
|
|
62741 |
* @param {any} keyId the keyId to add a status for
|
|
|
62742 |
* @param {string} status the status of the keyId
|
|
|
62743 |
*/
|
|
|
62744 |
|
|
|
62745 |
addKeyStatus_(keyId, status) {
|
|
|
62746 |
const isString = typeof keyId === 'string';
|
|
|
62747 |
const keyIdHexString = isString ? keyId : bufferToHexString(keyId);
|
|
|
62748 |
const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase();
|
|
|
62749 |
this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`);
|
|
|
62750 |
this.keyStatusMap_.set(formattedKeyIdString, status);
|
|
|
62751 |
}
|
|
|
62752 |
/**
|
|
|
62753 |
* Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists.
|
|
|
62754 |
*
|
|
|
62755 |
* @param {any} keyId the keyId from the keystatuschange event
|
|
|
62756 |
* @param {string} status the key status string
|
|
|
62757 |
*/
|
|
|
62758 |
|
|
|
62759 |
updatePlaylistByKeyStatus(keyId, status) {
|
|
|
62760 |
this.addKeyStatus_(keyId, status);
|
|
|
62761 |
if (!this.waitingForFastQualityPlaylistReceived_) {
|
|
|
62762 |
this.excludeNonUsableThenChangePlaylist_();
|
|
|
62763 |
} // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated.
|
|
|
62764 |
|
|
|
62765 |
this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this));
|
|
|
62766 |
this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this));
|
|
|
62767 |
}
|
|
|
62768 |
excludeNonUsableThenChangePlaylist_() {
|
|
|
62769 |
this.excludeNonUsablePlaylistsByKeyId_();
|
|
|
62770 |
this.fastQualityChange_();
|
|
|
62771 |
}
|
|
|
62772 |
}
|
|
|
62773 |
|
|
|
62774 |
/**
|
|
|
62775 |
* Returns a function that acts as the Enable/disable playlist function.
|
|
|
62776 |
*
|
|
|
62777 |
* @param {PlaylistLoader} loader - The main playlist loader
|
|
|
62778 |
* @param {string} playlistID - id of the playlist
|
|
|
62779 |
* @param {Function} changePlaylistFn - A function to be called after a
|
|
|
62780 |
* playlist's enabled-state has been changed. Will NOT be called if a
|
|
|
62781 |
* playlist's enabled-state is unchanged
|
|
|
62782 |
* @param {boolean=} enable - Value to set the playlist enabled-state to
|
|
|
62783 |
* or if undefined returns the current enabled-state for the playlist
|
|
|
62784 |
* @return {Function} Function for setting/getting enabled
|
|
|
62785 |
*/
|
|
|
62786 |
|
|
|
62787 |
const enableFunction = (loader, playlistID, changePlaylistFn) => enable => {
|
|
|
62788 |
const playlist = loader.main.playlists[playlistID];
|
|
|
62789 |
const incompatible = isIncompatible(playlist);
|
|
|
62790 |
const currentlyEnabled = isEnabled(playlist);
|
|
|
62791 |
if (typeof enable === 'undefined') {
|
|
|
62792 |
return currentlyEnabled;
|
|
|
62793 |
}
|
|
|
62794 |
if (enable) {
|
|
|
62795 |
delete playlist.disabled;
|
|
|
62796 |
} else {
|
|
|
62797 |
playlist.disabled = true;
|
|
|
62798 |
}
|
|
|
62799 |
if (enable !== currentlyEnabled && !incompatible) {
|
|
|
62800 |
// Ensure the outside world knows about our changes
|
|
|
62801 |
changePlaylistFn();
|
|
|
62802 |
if (enable) {
|
|
|
62803 |
loader.trigger('renditionenabled');
|
|
|
62804 |
} else {
|
|
|
62805 |
loader.trigger('renditiondisabled');
|
|
|
62806 |
}
|
|
|
62807 |
}
|
|
|
62808 |
return enable;
|
|
|
62809 |
};
|
|
|
62810 |
/**
|
|
|
62811 |
* The representation object encapsulates the publicly visible information
|
|
|
62812 |
* in a media playlist along with a setter/getter-type function (enabled)
|
|
|
62813 |
* for changing the enabled-state of a particular playlist entry
|
|
|
62814 |
*
|
|
|
62815 |
* @class Representation
|
|
|
62816 |
*/
|
|
|
62817 |
|
|
|
62818 |
class Representation {
|
|
|
62819 |
constructor(vhsHandler, playlist, id) {
|
|
|
62820 |
const {
|
|
|
62821 |
playlistController_: pc
|
|
|
62822 |
} = vhsHandler;
|
|
|
62823 |
const qualityChangeFunction = pc.fastQualityChange_.bind(pc); // some playlist attributes are optional
|
|
|
62824 |
|
|
|
62825 |
if (playlist.attributes) {
|
|
|
62826 |
const resolution = playlist.attributes.RESOLUTION;
|
|
|
62827 |
this.width = resolution && resolution.width;
|
|
|
62828 |
this.height = resolution && resolution.height;
|
|
|
62829 |
this.bandwidth = playlist.attributes.BANDWIDTH;
|
|
|
62830 |
this.frameRate = playlist.attributes['FRAME-RATE'];
|
|
|
62831 |
}
|
|
|
62832 |
this.codecs = codecsForPlaylist(pc.main(), playlist);
|
|
|
62833 |
this.playlist = playlist; // The id is simply the ordinality of the media playlist
|
|
|
62834 |
// within the main playlist
|
|
|
62835 |
|
|
|
62836 |
this.id = id; // Partially-apply the enableFunction to create a playlist-
|
|
|
62837 |
// specific variant
|
|
|
62838 |
|
|
|
62839 |
this.enabled = enableFunction(vhsHandler.playlists, playlist.id, qualityChangeFunction);
|
|
|
62840 |
}
|
|
|
62841 |
}
|
|
|
62842 |
/**
|
|
|
62843 |
* A mixin function that adds the `representations` api to an instance
|
|
|
62844 |
* of the VhsHandler class
|
|
|
62845 |
*
|
|
|
62846 |
* @param {VhsHandler} vhsHandler - An instance of VhsHandler to add the
|
|
|
62847 |
* representation API into
|
|
|
62848 |
*/
|
|
|
62849 |
|
|
|
62850 |
const renditionSelectionMixin = function (vhsHandler) {
|
|
|
62851 |
// Add a single API-specific function to the VhsHandler instance
|
|
|
62852 |
vhsHandler.representations = () => {
|
|
|
62853 |
const main = vhsHandler.playlistController_.main();
|
|
|
62854 |
const playlists = isAudioOnly(main) ? vhsHandler.playlistController_.getAudioTrackPlaylists_() : main.playlists;
|
|
|
62855 |
if (!playlists) {
|
|
|
62856 |
return [];
|
|
|
62857 |
}
|
|
|
62858 |
return playlists.filter(media => !isIncompatible(media)).map((e, i) => new Representation(vhsHandler, e, e.id));
|
|
|
62859 |
};
|
|
|
62860 |
};
|
|
|
62861 |
|
|
|
62862 |
/**
|
|
|
62863 |
* @file playback-watcher.js
|
|
|
62864 |
*
|
|
|
62865 |
* Playback starts, and now my watch begins. It shall not end until my death. I shall
|
|
|
62866 |
* take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns
|
|
|
62867 |
* and win no glory. I shall live and die at my post. I am the corrector of the underflow.
|
|
|
62868 |
* I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge
|
|
|
62869 |
* my life and honor to the Playback Watch, for this Player and all the Players to come.
|
|
|
62870 |
*/
|
|
|
62871 |
|
|
|
62872 |
const timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error'];
|
|
|
62873 |
/**
|
|
|
62874 |
* @class PlaybackWatcher
|
|
|
62875 |
*/
|
|
|
62876 |
|
|
|
62877 |
class PlaybackWatcher {
|
|
|
62878 |
/**
|
|
|
62879 |
* Represents an PlaybackWatcher object.
|
|
|
62880 |
*
|
|
|
62881 |
* @class
|
|
|
62882 |
* @param {Object} options an object that includes the tech and settings
|
|
|
62883 |
*/
|
|
|
62884 |
constructor(options) {
|
|
|
62885 |
this.playlistController_ = options.playlistController;
|
|
|
62886 |
this.tech_ = options.tech;
|
|
|
62887 |
this.seekable = options.seekable;
|
|
|
62888 |
this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow;
|
|
|
62889 |
this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta;
|
|
|
62890 |
this.media = options.media;
|
|
|
62891 |
this.consecutiveUpdates = 0;
|
|
|
62892 |
this.lastRecordedTime = null;
|
|
|
62893 |
this.checkCurrentTimeTimeout_ = null;
|
|
|
62894 |
this.logger_ = logger('PlaybackWatcher');
|
|
|
62895 |
this.logger_('initialize');
|
|
|
62896 |
const playHandler = () => this.monitorCurrentTime_();
|
|
|
62897 |
const canPlayHandler = () => this.monitorCurrentTime_();
|
|
|
62898 |
const waitingHandler = () => this.techWaiting_();
|
|
|
62899 |
const cancelTimerHandler = () => this.resetTimeUpdate_();
|
|
|
62900 |
const pc = this.playlistController_;
|
|
|
62901 |
const loaderTypes = ['main', 'subtitle', 'audio'];
|
|
|
62902 |
const loaderChecks = {};
|
|
|
62903 |
loaderTypes.forEach(type => {
|
|
|
62904 |
loaderChecks[type] = {
|
|
|
62905 |
reset: () => this.resetSegmentDownloads_(type),
|
|
|
62906 |
updateend: () => this.checkSegmentDownloads_(type)
|
|
|
62907 |
};
|
|
|
62908 |
pc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend); // If a rendition switch happens during a playback stall where the buffer
|
|
|
62909 |
// isn't changing we want to reset. We cannot assume that the new rendition
|
|
|
62910 |
// will also be stalled, until after new appends.
|
|
|
62911 |
|
|
|
62912 |
pc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset); // Playback stalls should not be detected right after seeking.
|
|
|
62913 |
// This prevents one segment playlists (single vtt or single segment content)
|
|
|
62914 |
// from being detected as stalling. As the buffer will not change in those cases, since
|
|
|
62915 |
// the buffer is the entire video duration.
|
|
|
62916 |
|
|
|
62917 |
this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset);
|
|
|
62918 |
});
|
|
|
62919 |
/**
|
|
|
62920 |
* We check if a seek was into a gap through the following steps:
|
|
|
62921 |
* 1. We get a seeking event and we do not get a seeked event. This means that
|
|
|
62922 |
* a seek was attempted but not completed.
|
|
|
62923 |
* 2. We run `fixesBadSeeks_` on segment loader appends. This means that we already
|
|
|
62924 |
* removed everything from our buffer and appended a segment, and should be ready
|
|
|
62925 |
* to check for gaps.
|
|
|
62926 |
*/
|
|
|
62927 |
|
|
|
62928 |
const setSeekingHandlers = fn => {
|
|
|
62929 |
['main', 'audio'].forEach(type => {
|
|
|
62930 |
pc[`${type}SegmentLoader_`][fn]('appended', this.seekingAppendCheck_);
|
|
|
62931 |
});
|
|
|
62932 |
};
|
|
|
62933 |
this.seekingAppendCheck_ = () => {
|
|
|
62934 |
if (this.fixesBadSeeks_()) {
|
|
|
62935 |
this.consecutiveUpdates = 0;
|
|
|
62936 |
this.lastRecordedTime = this.tech_.currentTime();
|
|
|
62937 |
setSeekingHandlers('off');
|
|
|
62938 |
}
|
|
|
62939 |
};
|
|
|
62940 |
this.clearSeekingAppendCheck_ = () => setSeekingHandlers('off');
|
|
|
62941 |
this.watchForBadSeeking_ = () => {
|
|
|
62942 |
this.clearSeekingAppendCheck_();
|
|
|
62943 |
setSeekingHandlers('on');
|
|
|
62944 |
};
|
|
|
62945 |
this.tech_.on('seeked', this.clearSeekingAppendCheck_);
|
|
|
62946 |
this.tech_.on('seeking', this.watchForBadSeeking_);
|
|
|
62947 |
this.tech_.on('waiting', waitingHandler);
|
|
|
62948 |
this.tech_.on(timerCancelEvents, cancelTimerHandler);
|
|
|
62949 |
this.tech_.on('canplay', canPlayHandler);
|
|
|
62950 |
/*
|
|
|
62951 |
An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case
|
|
|
62952 |
is surfaced in one of two ways:
|
|
|
62953 |
1) The `waiting` event is fired before the player has buffered content, making it impossible
|
|
|
62954 |
to find or skip the gap. The `waiting` event is followed by a `play` event. On first play
|
|
|
62955 |
we can check if playback is stalled due to a gap, and skip the gap if necessary.
|
|
|
62956 |
2) A source with a gap at the beginning of the stream is loaded programatically while the player
|
|
|
62957 |
is in a playing state. To catch this case, it's important that our one-time play listener is setup
|
|
|
62958 |
even if the player is in a playing state
|
|
|
62959 |
*/
|
|
|
62960 |
|
|
|
62961 |
this.tech_.one('play', playHandler); // Define the dispose function to clean up our events
|
|
|
62962 |
|
|
|
62963 |
this.dispose = () => {
|
|
|
62964 |
this.clearSeekingAppendCheck_();
|
|
|
62965 |
this.logger_('dispose');
|
|
|
62966 |
this.tech_.off('waiting', waitingHandler);
|
|
|
62967 |
this.tech_.off(timerCancelEvents, cancelTimerHandler);
|
|
|
62968 |
this.tech_.off('canplay', canPlayHandler);
|
|
|
62969 |
this.tech_.off('play', playHandler);
|
|
|
62970 |
this.tech_.off('seeking', this.watchForBadSeeking_);
|
|
|
62971 |
this.tech_.off('seeked', this.clearSeekingAppendCheck_);
|
|
|
62972 |
loaderTypes.forEach(type => {
|
|
|
62973 |
pc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend);
|
|
|
62974 |
pc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset);
|
|
|
62975 |
this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset);
|
|
|
62976 |
});
|
|
|
62977 |
if (this.checkCurrentTimeTimeout_) {
|
|
|
62978 |
window.clearTimeout(this.checkCurrentTimeTimeout_);
|
|
|
62979 |
}
|
|
|
62980 |
this.resetTimeUpdate_();
|
|
|
62981 |
};
|
|
|
62982 |
}
|
|
|
62983 |
/**
|
|
|
62984 |
* Periodically check current time to see if playback stopped
|
|
|
62985 |
*
|
|
|
62986 |
* @private
|
|
|
62987 |
*/
|
|
|
62988 |
|
|
|
62989 |
monitorCurrentTime_() {
|
|
|
62990 |
this.checkCurrentTime_();
|
|
|
62991 |
if (this.checkCurrentTimeTimeout_) {
|
|
|
62992 |
window.clearTimeout(this.checkCurrentTimeTimeout_);
|
|
|
62993 |
} // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
|
|
|
62994 |
|
|
|
62995 |
this.checkCurrentTimeTimeout_ = window.setTimeout(this.monitorCurrentTime_.bind(this), 250);
|
|
|
62996 |
}
|
|
|
62997 |
/**
|
|
|
62998 |
* Reset stalled download stats for a specific type of loader
|
|
|
62999 |
*
|
|
|
63000 |
* @param {string} type
|
|
|
63001 |
* The segment loader type to check.
|
|
|
63002 |
*
|
|
|
63003 |
* @listens SegmentLoader#playlistupdate
|
|
|
63004 |
* @listens Tech#seeking
|
|
|
63005 |
* @listens Tech#seeked
|
|
|
63006 |
*/
|
|
|
63007 |
|
|
|
63008 |
resetSegmentDownloads_(type) {
|
|
|
63009 |
const loader = this.playlistController_[`${type}SegmentLoader_`];
|
|
|
63010 |
if (this[`${type}StalledDownloads_`] > 0) {
|
|
|
63011 |
this.logger_(`resetting possible stalled download count for ${type} loader`);
|
|
|
63012 |
}
|
|
|
63013 |
this[`${type}StalledDownloads_`] = 0;
|
|
|
63014 |
this[`${type}Buffered_`] = loader.buffered_();
|
|
|
63015 |
}
|
|
|
63016 |
/**
|
|
|
63017 |
* Checks on every segment `appendsdone` to see
|
|
|
63018 |
* if segment appends are making progress. If they are not
|
|
|
63019 |
* and we are still downloading bytes. We exclude the playlist.
|
|
|
63020 |
*
|
|
|
63021 |
* @param {string} type
|
|
|
63022 |
* The segment loader type to check.
|
|
|
63023 |
*
|
|
|
63024 |
* @listens SegmentLoader#appendsdone
|
|
|
63025 |
*/
|
|
|
63026 |
|
|
|
63027 |
checkSegmentDownloads_(type) {
|
|
|
63028 |
const pc = this.playlistController_;
|
|
|
63029 |
const loader = pc[`${type}SegmentLoader_`];
|
|
|
63030 |
const buffered = loader.buffered_();
|
|
|
63031 |
const isBufferedDifferent = isRangeDifferent(this[`${type}Buffered_`], buffered);
|
|
|
63032 |
this[`${type}Buffered_`] = buffered; // if another watcher is going to fix the issue or
|
|
|
63033 |
// the buffered value for this loader changed
|
|
|
63034 |
// appends are working
|
|
|
63035 |
|
|
|
63036 |
if (isBufferedDifferent) {
|
|
|
63037 |
this.resetSegmentDownloads_(type);
|
|
|
63038 |
return;
|
|
|
63039 |
}
|
|
|
63040 |
this[`${type}StalledDownloads_`]++;
|
|
|
63041 |
this.logger_(`found #${this[`${type}StalledDownloads_`]} ${type} appends that did not increase buffer (possible stalled download)`, {
|
|
|
63042 |
playlistId: loader.playlist_ && loader.playlist_.id,
|
|
|
63043 |
buffered: timeRangesToArray(buffered)
|
|
|
63044 |
}); // after 10 possibly stalled appends with no reset, exclude
|
|
|
63045 |
|
|
|
63046 |
if (this[`${type}StalledDownloads_`] < 10) {
|
|
|
63047 |
return;
|
|
|
63048 |
}
|
|
|
63049 |
this.logger_(`${type} loader stalled download exclusion`);
|
|
|
63050 |
this.resetSegmentDownloads_(type);
|
|
|
63051 |
this.tech_.trigger({
|
|
|
63052 |
type: 'usage',
|
|
|
63053 |
name: `vhs-${type}-download-exclusion`
|
|
|
63054 |
});
|
|
|
63055 |
if (type === 'subtitle') {
|
|
|
63056 |
return;
|
|
|
63057 |
} // TODO: should we exclude audio tracks rather than main tracks
|
|
|
63058 |
// when type is audio?
|
|
|
63059 |
|
|
|
63060 |
pc.excludePlaylist({
|
|
|
63061 |
error: {
|
|
|
63062 |
message: `Excessive ${type} segment downloading detected.`
|
|
|
63063 |
},
|
|
|
63064 |
playlistExclusionDuration: Infinity
|
|
|
63065 |
});
|
|
|
63066 |
}
|
|
|
63067 |
/**
|
|
|
63068 |
* The purpose of this function is to emulate the "waiting" event on
|
|
|
63069 |
* browsers that do not emit it when they are waiting for more
|
|
|
63070 |
* data to continue playback
|
|
|
63071 |
*
|
|
|
63072 |
* @private
|
|
|
63073 |
*/
|
|
|
63074 |
|
|
|
63075 |
checkCurrentTime_() {
|
|
|
63076 |
if (this.tech_.paused() || this.tech_.seeking()) {
|
|
|
63077 |
return;
|
|
|
63078 |
}
|
|
|
63079 |
const currentTime = this.tech_.currentTime();
|
|
|
63080 |
const buffered = this.tech_.buffered();
|
|
|
63081 |
if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) {
|
|
|
63082 |
// If current time is at the end of the final buffered region, then any playback
|
|
|
63083 |
// stall is most likely caused by buffering in a low bandwidth environment. The tech
|
|
|
63084 |
// should fire a `waiting` event in this scenario, but due to browser and tech
|
|
|
63085 |
// inconsistencies. Calling `techWaiting_` here allows us to simulate
|
|
|
63086 |
// responding to a native `waiting` event when the tech fails to emit one.
|
|
|
63087 |
return this.techWaiting_();
|
|
|
63088 |
}
|
|
|
63089 |
if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) {
|
|
|
63090 |
this.consecutiveUpdates++;
|
|
|
63091 |
this.waiting_();
|
|
|
63092 |
} else if (currentTime === this.lastRecordedTime) {
|
|
|
63093 |
this.consecutiveUpdates++;
|
|
|
63094 |
} else {
|
|
|
63095 |
this.consecutiveUpdates = 0;
|
|
|
63096 |
this.lastRecordedTime = currentTime;
|
|
|
63097 |
}
|
|
|
63098 |
}
|
|
|
63099 |
/**
|
|
|
63100 |
* Resets the 'timeupdate' mechanism designed to detect that we are stalled
|
|
|
63101 |
*
|
|
|
63102 |
* @private
|
|
|
63103 |
*/
|
|
|
63104 |
|
|
|
63105 |
resetTimeUpdate_() {
|
|
|
63106 |
this.consecutiveUpdates = 0;
|
|
|
63107 |
}
|
|
|
63108 |
/**
|
|
|
63109 |
* Fixes situations where there's a bad seek
|
|
|
63110 |
*
|
|
|
63111 |
* @return {boolean} whether an action was taken to fix the seek
|
|
|
63112 |
* @private
|
|
|
63113 |
*/
|
|
|
63114 |
|
|
|
63115 |
fixesBadSeeks_() {
|
|
|
63116 |
const seeking = this.tech_.seeking();
|
|
|
63117 |
if (!seeking) {
|
|
|
63118 |
return false;
|
|
|
63119 |
} // TODO: It's possible that these seekable checks should be moved out of this function
|
|
|
63120 |
// and into a function that runs on seekablechange. It's also possible that we only need
|
|
|
63121 |
// afterSeekableWindow as the buffered check at the bottom is good enough to handle before
|
|
|
63122 |
// seekable range.
|
|
|
63123 |
|
|
|
63124 |
const seekable = this.seekable();
|
|
|
63125 |
const currentTime = this.tech_.currentTime();
|
|
|
63126 |
const isAfterSeekableRange = this.afterSeekableWindow_(seekable, currentTime, this.media(), this.allowSeeksWithinUnsafeLiveWindow);
|
|
|
63127 |
let seekTo;
|
|
|
63128 |
if (isAfterSeekableRange) {
|
|
|
63129 |
const seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting)
|
|
|
63130 |
|
|
|
63131 |
seekTo = seekableEnd;
|
|
|
63132 |
}
|
|
|
63133 |
if (this.beforeSeekableWindow_(seekable, currentTime)) {
|
|
|
63134 |
const seekableStart = seekable.start(0); // sync to the beginning of the live window
|
|
|
63135 |
// provide a buffer of .1 seconds to handle rounding/imprecise numbers
|
|
|
63136 |
|
|
|
63137 |
seekTo = seekableStart + (
|
|
|
63138 |
// if the playlist is too short and the seekable range is an exact time (can
|
|
|
63139 |
// happen in live with a 3 segment playlist), then don't use a time delta
|
|
|
63140 |
seekableStart === seekable.end(0) ? 0 : SAFE_TIME_DELTA);
|
|
|
63141 |
}
|
|
|
63142 |
if (typeof seekTo !== 'undefined') {
|
|
|
63143 |
this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` + `seekable range ${printableRange(seekable)}. Seeking to ` + `${seekTo}.`);
|
|
|
63144 |
this.tech_.setCurrentTime(seekTo);
|
|
|
63145 |
return true;
|
|
|
63146 |
}
|
|
|
63147 |
const sourceUpdater = this.playlistController_.sourceUpdater_;
|
|
|
63148 |
const buffered = this.tech_.buffered();
|
|
|
63149 |
const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null;
|
|
|
63150 |
const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null;
|
|
|
63151 |
const media = this.media(); // verify that at least two segment durations or one part duration have been
|
|
|
63152 |
// appended before checking for a gap.
|
|
|
63153 |
|
|
|
63154 |
const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration : (media.targetDuration - TIME_FUDGE_FACTOR) * 2; // verify that at least two segment durations have been
|
|
|
63155 |
// appended before checking for a gap.
|
|
|
63156 |
|
|
|
63157 |
const bufferedToCheck = [audioBuffered, videoBuffered];
|
|
|
63158 |
for (let i = 0; i < bufferedToCheck.length; i++) {
|
|
|
63159 |
// skip null buffered
|
|
|
63160 |
if (!bufferedToCheck[i]) {
|
|
|
63161 |
continue;
|
|
|
63162 |
}
|
|
|
63163 |
const timeAhead = timeAheadOf(bufferedToCheck[i], currentTime); // if we are less than two video/audio segment durations or one part
|
|
|
63164 |
// duration behind we haven't appended enough to call this a bad seek.
|
|
|
63165 |
|
|
|
63166 |
if (timeAhead < minAppendedDuration) {
|
|
|
63167 |
return false;
|
|
|
63168 |
}
|
|
|
63169 |
}
|
|
|
63170 |
const nextRange = findNextRange(buffered, currentTime); // we have appended enough content, but we don't have anything buffered
|
|
|
63171 |
// to seek over the gap
|
|
|
63172 |
|
|
|
63173 |
if (nextRange.length === 0) {
|
|
|
63174 |
return false;
|
|
|
63175 |
}
|
|
|
63176 |
seekTo = nextRange.start(0) + SAFE_TIME_DELTA;
|
|
|
63177 |
this.logger_(`Buffered region starts (${nextRange.start(0)}) ` + ` just beyond seek point (${currentTime}). Seeking to ${seekTo}.`);
|
|
|
63178 |
this.tech_.setCurrentTime(seekTo);
|
|
|
63179 |
return true;
|
|
|
63180 |
}
|
|
|
63181 |
/**
|
|
|
63182 |
* Handler for situations when we determine the player is waiting.
|
|
|
63183 |
*
|
|
|
63184 |
* @private
|
|
|
63185 |
*/
|
|
|
63186 |
|
|
|
63187 |
waiting_() {
|
|
|
63188 |
if (this.techWaiting_()) {
|
|
|
63189 |
return;
|
|
|
63190 |
} // All tech waiting checks failed. Use last resort correction
|
|
|
63191 |
|
|
|
63192 |
const currentTime = this.tech_.currentTime();
|
|
|
63193 |
const buffered = this.tech_.buffered();
|
|
|
63194 |
const currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered
|
|
|
63195 |
// region with no indication that anything is amiss (seen in Firefox). Seeking to
|
|
|
63196 |
// currentTime is usually enough to kickstart the player. This checks that the player
|
|
|
63197 |
// is currently within a buffered region before attempting a corrective seek.
|
|
|
63198 |
// Chrome does not appear to continue `timeupdate` events after a `waiting` event
|
|
|
63199 |
// until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also
|
|
|
63200 |
// make sure there is ~3 seconds of forward buffer before taking any corrective action
|
|
|
63201 |
// to avoid triggering an `unknownwaiting` event when the network is slow.
|
|
|
63202 |
|
|
|
63203 |
if (currentRange.length && currentTime + 3 <= currentRange.end(0)) {
|
|
|
63204 |
this.resetTimeUpdate_();
|
|
|
63205 |
this.tech_.setCurrentTime(currentTime);
|
|
|
63206 |
this.logger_(`Stopped at ${currentTime} while inside a buffered region ` + `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` + 'playback by seeking to the current time.'); // unknown waiting corrections may be useful for monitoring QoS
|
|
|
63207 |
|
|
|
63208 |
this.tech_.trigger({
|
|
|
63209 |
type: 'usage',
|
|
|
63210 |
name: 'vhs-unknown-waiting'
|
|
|
63211 |
});
|
|
|
63212 |
return;
|
|
|
63213 |
}
|
|
|
63214 |
}
|
|
|
63215 |
/**
|
|
|
63216 |
* Handler for situations when the tech fires a `waiting` event
|
|
|
63217 |
*
|
|
|
63218 |
* @return {boolean}
|
|
|
63219 |
* True if an action (or none) was needed to correct the waiting. False if no
|
|
|
63220 |
* checks passed
|
|
|
63221 |
* @private
|
|
|
63222 |
*/
|
|
|
63223 |
|
|
|
63224 |
techWaiting_() {
|
|
|
63225 |
const seekable = this.seekable();
|
|
|
63226 |
const currentTime = this.tech_.currentTime();
|
|
|
63227 |
if (this.tech_.seeking()) {
|
|
|
63228 |
// Tech is seeking or already waiting on another action, no action needed
|
|
|
63229 |
return true;
|
|
|
63230 |
}
|
|
|
63231 |
if (this.beforeSeekableWindow_(seekable, currentTime)) {
|
|
|
63232 |
const livePoint = seekable.end(seekable.length - 1);
|
|
|
63233 |
this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` + `live point (seekable end) ${livePoint}`);
|
|
|
63234 |
this.resetTimeUpdate_();
|
|
|
63235 |
this.tech_.setCurrentTime(livePoint); // live window resyncs may be useful for monitoring QoS
|
|
|
63236 |
|
|
|
63237 |
this.tech_.trigger({
|
|
|
63238 |
type: 'usage',
|
|
|
63239 |
name: 'vhs-live-resync'
|
|
|
63240 |
});
|
|
|
63241 |
return true;
|
|
|
63242 |
}
|
|
|
63243 |
const sourceUpdater = this.tech_.vhs.playlistController_.sourceUpdater_;
|
|
|
63244 |
const buffered = this.tech_.buffered();
|
|
|
63245 |
const videoUnderflow = this.videoUnderflow_({
|
|
|
63246 |
audioBuffered: sourceUpdater.audioBuffered(),
|
|
|
63247 |
videoBuffered: sourceUpdater.videoBuffered(),
|
|
|
63248 |
currentTime
|
|
|
63249 |
});
|
|
|
63250 |
if (videoUnderflow) {
|
|
|
63251 |
// Even though the video underflowed and was stuck in a gap, the audio overplayed
|
|
|
63252 |
// the gap, leading currentTime into a buffered range. Seeking to currentTime
|
|
|
63253 |
// allows the video to catch up to the audio position without losing any audio
|
|
|
63254 |
// (only suffering ~3 seconds of frozen video and a pause in audio playback).
|
|
|
63255 |
this.resetTimeUpdate_();
|
|
|
63256 |
this.tech_.setCurrentTime(currentTime); // video underflow may be useful for monitoring QoS
|
|
|
63257 |
|
|
|
63258 |
this.tech_.trigger({
|
|
|
63259 |
type: 'usage',
|
|
|
63260 |
name: 'vhs-video-underflow'
|
|
|
63261 |
});
|
|
|
63262 |
return true;
|
|
|
63263 |
}
|
|
|
63264 |
const nextRange = findNextRange(buffered, currentTime); // check for gap
|
|
|
63265 |
|
|
|
63266 |
if (nextRange.length > 0) {
|
|
|
63267 |
this.logger_(`Stopped at ${currentTime} and seeking to ${nextRange.start(0)}`);
|
|
|
63268 |
this.resetTimeUpdate_();
|
|
|
63269 |
this.skipTheGap_(currentTime);
|
|
|
63270 |
return true;
|
|
|
63271 |
} // All checks failed. Returning false to indicate failure to correct waiting
|
|
|
63272 |
|
|
|
63273 |
return false;
|
|
|
63274 |
}
|
|
|
63275 |
afterSeekableWindow_(seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) {
|
|
|
63276 |
if (!seekable.length) {
|
|
|
63277 |
// we can't make a solid case if there's no seekable, default to false
|
|
|
63278 |
return false;
|
|
|
63279 |
}
|
|
|
63280 |
let allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA;
|
|
|
63281 |
const isLive = !playlist.endList;
|
|
|
63282 |
const isLLHLS = typeof playlist.partTargetDuration === 'number';
|
|
|
63283 |
if (isLive && (isLLHLS || allowSeeksWithinUnsafeLiveWindow)) {
|
|
|
63284 |
allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3;
|
|
|
63285 |
}
|
|
|
63286 |
if (currentTime > allowedEnd) {
|
|
|
63287 |
return true;
|
|
|
63288 |
}
|
|
|
63289 |
return false;
|
|
|
63290 |
}
|
|
|
63291 |
beforeSeekableWindow_(seekable, currentTime) {
|
|
|
63292 |
if (seekable.length &&
|
|
|
63293 |
// can't fall before 0 and 0 seekable start identifies VOD stream
|
|
|
63294 |
seekable.start(0) > 0 && currentTime < seekable.start(0) - this.liveRangeSafeTimeDelta) {
|
|
|
63295 |
return true;
|
|
|
63296 |
}
|
|
|
63297 |
return false;
|
|
|
63298 |
}
|
|
|
63299 |
videoUnderflow_({
|
|
|
63300 |
videoBuffered,
|
|
|
63301 |
audioBuffered,
|
|
|
63302 |
currentTime
|
|
|
63303 |
}) {
|
|
|
63304 |
// audio only content will not have video underflow :)
|
|
|
63305 |
if (!videoBuffered) {
|
|
|
63306 |
return;
|
|
|
63307 |
}
|
|
|
63308 |
let gap; // find a gap in demuxed content.
|
|
|
63309 |
|
|
|
63310 |
if (videoBuffered.length && audioBuffered.length) {
|
|
|
63311 |
// in Chrome audio will continue to play for ~3s when we run out of video
|
|
|
63312 |
// so we have to check that the video buffer did have some buffer in the
|
|
|
63313 |
// past.
|
|
|
63314 |
const lastVideoRange = findRange(videoBuffered, currentTime - 3);
|
|
|
63315 |
const videoRange = findRange(videoBuffered, currentTime);
|
|
|
63316 |
const audioRange = findRange(audioBuffered, currentTime);
|
|
|
63317 |
if (audioRange.length && !videoRange.length && lastVideoRange.length) {
|
|
|
63318 |
gap = {
|
|
|
63319 |
start: lastVideoRange.end(0),
|
|
|
63320 |
end: audioRange.end(0)
|
|
|
63321 |
};
|
|
|
63322 |
} // find a gap in muxed content.
|
|
|
63323 |
} else {
|
|
|
63324 |
const nextRange = findNextRange(videoBuffered, currentTime); // Even if there is no available next range, there is still a possibility we are
|
|
|
63325 |
// stuck in a gap due to video underflow.
|
|
|
63326 |
|
|
|
63327 |
if (!nextRange.length) {
|
|
|
63328 |
gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime);
|
|
|
63329 |
}
|
|
|
63330 |
}
|
|
|
63331 |
if (gap) {
|
|
|
63332 |
this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` + `Seeking to current time ${currentTime}`);
|
|
|
63333 |
return true;
|
|
|
63334 |
}
|
|
|
63335 |
return false;
|
|
|
63336 |
}
|
|
|
63337 |
/**
|
|
|
63338 |
* Timer callback. If playback still has not proceeded, then we seek
|
|
|
63339 |
* to the start of the next buffered region.
|
|
|
63340 |
*
|
|
|
63341 |
* @private
|
|
|
63342 |
*/
|
|
|
63343 |
|
|
|
63344 |
skipTheGap_(scheduledCurrentTime) {
|
|
|
63345 |
const buffered = this.tech_.buffered();
|
|
|
63346 |
const currentTime = this.tech_.currentTime();
|
|
|
63347 |
const nextRange = findNextRange(buffered, currentTime);
|
|
|
63348 |
this.resetTimeUpdate_();
|
|
|
63349 |
if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) {
|
|
|
63350 |
return;
|
|
|
63351 |
}
|
|
|
63352 |
this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played
|
|
|
63353 |
|
|
|
63354 |
this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR);
|
|
|
63355 |
this.tech_.trigger({
|
|
|
63356 |
type: 'usage',
|
|
|
63357 |
name: 'vhs-gap-skip'
|
|
|
63358 |
});
|
|
|
63359 |
}
|
|
|
63360 |
gapFromVideoUnderflow_(buffered, currentTime) {
|
|
|
63361 |
// At least in Chrome, if there is a gap in the video buffer, the audio will continue
|
|
|
63362 |
// playing for ~3 seconds after the video gap starts. This is done to account for
|
|
|
63363 |
// video buffer underflow/underrun (note that this is not done when there is audio
|
|
|
63364 |
// buffer underflow/underrun -- in that case the video will stop as soon as it
|
|
|
63365 |
// encounters the gap, as audio stalls are more noticeable/jarring to a user than
|
|
|
63366 |
// video stalls). The player's time will reflect the playthrough of audio, so the
|
|
|
63367 |
// time will appear as if we are in a buffered region, even if we are stuck in a
|
|
|
63368 |
// "gap."
|
|
|
63369 |
//
|
|
|
63370 |
// Example:
|
|
|
63371 |
// video buffer: 0 => 10.1, 10.2 => 20
|
|
|
63372 |
// audio buffer: 0 => 20
|
|
|
63373 |
// overall buffer: 0 => 10.1, 10.2 => 20
|
|
|
63374 |
// current time: 13
|
|
|
63375 |
//
|
|
|
63376 |
// Chrome's video froze at 10 seconds, where the video buffer encountered the gap,
|
|
|
63377 |
// however, the audio continued playing until it reached ~3 seconds past the gap
|
|
|
63378 |
// (13 seconds), at which point it stops as well. Since current time is past the
|
|
|
63379 |
// gap, findNextRange will return no ranges.
|
|
|
63380 |
//
|
|
|
63381 |
// To check for this issue, we see if there is a gap that starts somewhere within
|
|
|
63382 |
// a 3 second range (3 seconds +/- 1 second) back from our current time.
|
|
|
63383 |
const gaps = findGaps(buffered);
|
|
|
63384 |
for (let i = 0; i < gaps.length; i++) {
|
|
|
63385 |
const start = gaps.start(i);
|
|
|
63386 |
const end = gaps.end(i); // gap is starts no more than 4 seconds back
|
|
|
63387 |
|
|
|
63388 |
if (currentTime - start < 4 && currentTime - start > 2) {
|
|
|
63389 |
return {
|
|
|
63390 |
start,
|
|
|
63391 |
end
|
|
|
63392 |
};
|
|
|
63393 |
}
|
|
|
63394 |
}
|
|
|
63395 |
return null;
|
|
|
63396 |
}
|
|
|
63397 |
}
|
|
|
63398 |
const defaultOptions = {
|
|
|
63399 |
errorInterval: 30,
|
|
|
63400 |
getSource(next) {
|
|
|
63401 |
const tech = this.tech({
|
|
|
63402 |
IWillNotUseThisInPlugins: true
|
|
|
63403 |
});
|
|
|
63404 |
const sourceObj = tech.currentSource_ || this.currentSource();
|
|
|
63405 |
return next(sourceObj);
|
|
|
63406 |
}
|
|
|
63407 |
};
|
|
|
63408 |
/**
|
|
|
63409 |
* Main entry point for the plugin
|
|
|
63410 |
*
|
|
|
63411 |
* @param {Player} player a reference to a videojs Player instance
|
|
|
63412 |
* @param {Object} [options] an object with plugin options
|
|
|
63413 |
* @private
|
|
|
63414 |
*/
|
|
|
63415 |
|
|
|
63416 |
const initPlugin = function (player, options) {
|
|
|
63417 |
let lastCalled = 0;
|
|
|
63418 |
let seekTo = 0;
|
|
|
63419 |
const localOptions = merge(defaultOptions, options);
|
|
|
63420 |
player.ready(() => {
|
|
|
63421 |
player.trigger({
|
|
|
63422 |
type: 'usage',
|
|
|
63423 |
name: 'vhs-error-reload-initialized'
|
|
|
63424 |
});
|
|
|
63425 |
});
|
|
|
63426 |
/**
|
|
|
63427 |
* Player modifications to perform that must wait until `loadedmetadata`
|
|
|
63428 |
* has been triggered
|
|
|
63429 |
*
|
|
|
63430 |
* @private
|
|
|
63431 |
*/
|
|
|
63432 |
|
|
|
63433 |
const loadedMetadataHandler = function () {
|
|
|
63434 |
if (seekTo) {
|
|
|
63435 |
player.currentTime(seekTo);
|
|
|
63436 |
}
|
|
|
63437 |
};
|
|
|
63438 |
/**
|
|
|
63439 |
* Set the source on the player element, play, and seek if necessary
|
|
|
63440 |
*
|
|
|
63441 |
* @param {Object} sourceObj An object specifying the source url and mime-type to play
|
|
|
63442 |
* @private
|
|
|
63443 |
*/
|
|
|
63444 |
|
|
|
63445 |
const setSource = function (sourceObj) {
|
|
|
63446 |
if (sourceObj === null || sourceObj === undefined) {
|
|
|
63447 |
return;
|
|
|
63448 |
}
|
|
|
63449 |
seekTo = player.duration() !== Infinity && player.currentTime() || 0;
|
|
|
63450 |
player.one('loadedmetadata', loadedMetadataHandler);
|
|
|
63451 |
player.src(sourceObj);
|
|
|
63452 |
player.trigger({
|
|
|
63453 |
type: 'usage',
|
|
|
63454 |
name: 'vhs-error-reload'
|
|
|
63455 |
});
|
|
|
63456 |
player.play();
|
|
|
63457 |
};
|
|
|
63458 |
/**
|
|
|
63459 |
* Attempt to get a source from either the built-in getSource function
|
|
|
63460 |
* or a custom function provided via the options
|
|
|
63461 |
*
|
|
|
63462 |
* @private
|
|
|
63463 |
*/
|
|
|
63464 |
|
|
|
63465 |
const errorHandler = function () {
|
|
|
63466 |
// Do not attempt to reload the source if a source-reload occurred before
|
|
|
63467 |
// 'errorInterval' time has elapsed since the last source-reload
|
|
|
63468 |
if (Date.now() - lastCalled < localOptions.errorInterval * 1000) {
|
|
|
63469 |
player.trigger({
|
|
|
63470 |
type: 'usage',
|
|
|
63471 |
name: 'vhs-error-reload-canceled'
|
|
|
63472 |
});
|
|
|
63473 |
return;
|
|
|
63474 |
}
|
|
|
63475 |
if (!localOptions.getSource || typeof localOptions.getSource !== 'function') {
|
|
|
63476 |
videojs.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!');
|
|
|
63477 |
return;
|
|
|
63478 |
}
|
|
|
63479 |
lastCalled = Date.now();
|
|
|
63480 |
return localOptions.getSource.call(player, setSource);
|
|
|
63481 |
};
|
|
|
63482 |
/**
|
|
|
63483 |
* Unbind any event handlers that were bound by the plugin
|
|
|
63484 |
*
|
|
|
63485 |
* @private
|
|
|
63486 |
*/
|
|
|
63487 |
|
|
|
63488 |
const cleanupEvents = function () {
|
|
|
63489 |
player.off('loadedmetadata', loadedMetadataHandler);
|
|
|
63490 |
player.off('error', errorHandler);
|
|
|
63491 |
player.off('dispose', cleanupEvents);
|
|
|
63492 |
};
|
|
|
63493 |
/**
|
|
|
63494 |
* Cleanup before re-initializing the plugin
|
|
|
63495 |
*
|
|
|
63496 |
* @param {Object} [newOptions] an object with plugin options
|
|
|
63497 |
* @private
|
|
|
63498 |
*/
|
|
|
63499 |
|
|
|
63500 |
const reinitPlugin = function (newOptions) {
|
|
|
63501 |
cleanupEvents();
|
|
|
63502 |
initPlugin(player, newOptions);
|
|
|
63503 |
};
|
|
|
63504 |
player.on('error', errorHandler);
|
|
|
63505 |
player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before
|
|
|
63506 |
// initializing the plugin
|
|
|
63507 |
|
|
|
63508 |
player.reloadSourceOnError = reinitPlugin;
|
|
|
63509 |
};
|
|
|
63510 |
/**
|
|
|
63511 |
* Reload the source when an error is detected as long as there
|
|
|
63512 |
* wasn't an error previously within the last 30 seconds
|
|
|
63513 |
*
|
|
|
63514 |
* @param {Object} [options] an object with plugin options
|
|
|
63515 |
*/
|
|
|
63516 |
|
|
|
63517 |
const reloadSourceOnError = function (options) {
|
|
|
63518 |
initPlugin(this, options);
|
|
|
63519 |
};
|
|
|
63520 |
var version$4 = "3.10.0";
|
|
|
63521 |
var version$3 = "7.0.2";
|
|
|
63522 |
var version$2 = "1.3.0";
|
|
|
63523 |
var version$1 = "7.1.0";
|
|
|
63524 |
var version = "4.0.1";
|
|
|
63525 |
|
|
|
63526 |
/**
|
|
|
63527 |
* @file videojs-http-streaming.js
|
|
|
63528 |
*
|
|
|
63529 |
* The main file for the VHS project.
|
|
|
63530 |
* License: https://github.com/videojs/videojs-http-streaming/blob/main/LICENSE
|
|
|
63531 |
*/
|
|
|
63532 |
const Vhs = {
|
|
|
63533 |
PlaylistLoader,
|
|
|
63534 |
Playlist,
|
|
|
63535 |
utils,
|
|
|
63536 |
STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
|
|
|
63537 |
INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
|
|
|
63538 |
lastBandwidthSelector,
|
|
|
63539 |
movingAverageBandwidthSelector,
|
|
|
63540 |
comparePlaylistBandwidth,
|
|
|
63541 |
comparePlaylistResolution,
|
|
|
63542 |
xhr: xhrFactory()
|
|
|
63543 |
}; // Define getter/setters for config properties
|
|
|
63544 |
|
|
|
63545 |
Object.keys(Config).forEach(prop => {
|
|
|
63546 |
Object.defineProperty(Vhs, prop, {
|
|
|
63547 |
get() {
|
|
|
63548 |
videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
|
|
|
63549 |
return Config[prop];
|
|
|
63550 |
},
|
|
|
63551 |
set(value) {
|
|
|
63552 |
videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
|
|
|
63553 |
if (typeof value !== 'number' || value < 0) {
|
|
|
63554 |
videojs.log.warn(`value of Vhs.${prop} must be greater than or equal to 0`);
|
|
|
63555 |
return;
|
|
|
63556 |
}
|
|
|
63557 |
Config[prop] = value;
|
|
|
63558 |
}
|
|
|
63559 |
});
|
|
|
63560 |
});
|
|
|
63561 |
const LOCAL_STORAGE_KEY = 'videojs-vhs';
|
|
|
63562 |
/**
|
|
|
63563 |
* Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs.
|
|
|
63564 |
*
|
|
|
63565 |
* @param {QualityLevelList} qualityLevels The QualityLevelList to update.
|
|
|
63566 |
* @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
|
|
|
63567 |
* @function handleVhsMediaChange
|
|
|
63568 |
*/
|
|
|
63569 |
|
|
|
63570 |
const handleVhsMediaChange = function (qualityLevels, playlistLoader) {
|
|
|
63571 |
const newPlaylist = playlistLoader.media();
|
|
|
63572 |
let selectedIndex = -1;
|
|
|
63573 |
for (let i = 0; i < qualityLevels.length; i++) {
|
|
|
63574 |
if (qualityLevels[i].id === newPlaylist.id) {
|
|
|
63575 |
selectedIndex = i;
|
|
|
63576 |
break;
|
|
|
63577 |
}
|
|
|
63578 |
}
|
|
|
63579 |
qualityLevels.selectedIndex_ = selectedIndex;
|
|
|
63580 |
qualityLevels.trigger({
|
|
|
63581 |
selectedIndex,
|
|
|
63582 |
type: 'change'
|
|
|
63583 |
});
|
|
|
63584 |
};
|
|
|
63585 |
/**
|
|
|
63586 |
* Adds quality levels to list once playlist metadata is available
|
|
|
63587 |
*
|
|
|
63588 |
* @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to.
|
|
|
63589 |
* @param {Object} vhs Vhs object to listen to for media events.
|
|
|
63590 |
* @function handleVhsLoadedMetadata
|
|
|
63591 |
*/
|
|
|
63592 |
|
|
|
63593 |
const handleVhsLoadedMetadata = function (qualityLevels, vhs) {
|
|
|
63594 |
vhs.representations().forEach(rep => {
|
|
|
63595 |
qualityLevels.addQualityLevel(rep);
|
|
|
63596 |
});
|
|
|
63597 |
handleVhsMediaChange(qualityLevels, vhs.playlists);
|
|
|
63598 |
}; // VHS is a source handler, not a tech. Make sure attempts to use it
|
|
|
63599 |
// as one do not cause exceptions.
|
|
|
63600 |
|
|
|
63601 |
Vhs.canPlaySource = function () {
|
|
|
63602 |
return videojs.log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.');
|
|
|
63603 |
};
|
|
|
63604 |
const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => {
|
|
|
63605 |
if (!keySystemOptions) {
|
|
|
63606 |
return keySystemOptions;
|
|
|
63607 |
}
|
|
|
63608 |
let codecs = {};
|
|
|
63609 |
if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) {
|
|
|
63610 |
codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS));
|
|
|
63611 |
}
|
|
|
63612 |
if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) {
|
|
|
63613 |
codecs.audio = audioPlaylist.attributes.CODECS;
|
|
|
63614 |
}
|
|
|
63615 |
const videoContentType = getMimeForCodec(codecs.video);
|
|
|
63616 |
const audioContentType = getMimeForCodec(codecs.audio); // upsert the content types based on the selected playlist
|
|
|
63617 |
|
|
|
63618 |
const keySystemContentTypes = {};
|
|
|
63619 |
for (const keySystem in keySystemOptions) {
|
|
|
63620 |
keySystemContentTypes[keySystem] = {};
|
|
|
63621 |
if (audioContentType) {
|
|
|
63622 |
keySystemContentTypes[keySystem].audioContentType = audioContentType;
|
|
|
63623 |
}
|
|
|
63624 |
if (videoContentType) {
|
|
|
63625 |
keySystemContentTypes[keySystem].videoContentType = videoContentType;
|
|
|
63626 |
} // Default to using the video playlist's PSSH even though they may be different, as
|
|
|
63627 |
// videojs-contrib-eme will only accept one in the options.
|
|
|
63628 |
//
|
|
|
63629 |
// This shouldn't be an issue for most cases as early intialization will handle all
|
|
|
63630 |
// unique PSSH values, and if they aren't, then encrypted events should have the
|
|
|
63631 |
// specific information needed for the unique license.
|
|
|
63632 |
|
|
|
63633 |
if (mainPlaylist.contentProtection && mainPlaylist.contentProtection[keySystem] && mainPlaylist.contentProtection[keySystem].pssh) {
|
|
|
63634 |
keySystemContentTypes[keySystem].pssh = mainPlaylist.contentProtection[keySystem].pssh;
|
|
|
63635 |
} // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url'
|
|
|
63636 |
// so we need to prevent overwriting the URL entirely
|
|
|
63637 |
|
|
|
63638 |
if (typeof keySystemOptions[keySystem] === 'string') {
|
|
|
63639 |
keySystemContentTypes[keySystem].url = keySystemOptions[keySystem];
|
|
|
63640 |
}
|
|
|
63641 |
}
|
|
|
63642 |
return merge(keySystemOptions, keySystemContentTypes);
|
|
|
63643 |
};
|
|
|
63644 |
/**
|
|
|
63645 |
* @typedef {Object} KeySystems
|
|
|
63646 |
*
|
|
|
63647 |
* keySystems configuration for https://github.com/videojs/videojs-contrib-eme
|
|
|
63648 |
* Note: not all options are listed here.
|
|
|
63649 |
*
|
|
|
63650 |
* @property {Uint8Array} [pssh]
|
|
|
63651 |
* Protection System Specific Header
|
|
|
63652 |
*/
|
|
|
63653 |
|
|
|
63654 |
/**
|
|
|
63655 |
* Goes through all the playlists and collects an array of KeySystems options objects
|
|
|
63656 |
* containing each playlist's keySystems and their pssh values, if available.
|
|
|
63657 |
*
|
|
|
63658 |
* @param {Object[]} playlists
|
|
|
63659 |
* The playlists to look through
|
|
|
63660 |
* @param {string[]} keySystems
|
|
|
63661 |
* The keySystems to collect pssh values for
|
|
|
63662 |
*
|
|
|
63663 |
* @return {KeySystems[]}
|
|
|
63664 |
* An array of KeySystems objects containing available key systems and their
|
|
|
63665 |
* pssh values
|
|
|
63666 |
*/
|
|
|
63667 |
|
|
|
63668 |
const getAllPsshKeySystemsOptions = (playlists, keySystems) => {
|
|
|
63669 |
return playlists.reduce((keySystemsArr, playlist) => {
|
|
|
63670 |
if (!playlist.contentProtection) {
|
|
|
63671 |
return keySystemsArr;
|
|
|
63672 |
}
|
|
|
63673 |
const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => {
|
|
|
63674 |
const keySystemOptions = playlist.contentProtection[keySystem];
|
|
|
63675 |
if (keySystemOptions && keySystemOptions.pssh) {
|
|
|
63676 |
keySystemsObj[keySystem] = {
|
|
|
63677 |
pssh: keySystemOptions.pssh
|
|
|
63678 |
};
|
|
|
63679 |
}
|
|
|
63680 |
return keySystemsObj;
|
|
|
63681 |
}, {});
|
|
|
63682 |
if (Object.keys(keySystemsOptions).length) {
|
|
|
63683 |
keySystemsArr.push(keySystemsOptions);
|
|
|
63684 |
}
|
|
|
63685 |
return keySystemsArr;
|
|
|
63686 |
}, []);
|
|
|
63687 |
};
|
|
|
63688 |
/**
|
|
|
63689 |
* Returns a promise that waits for the
|
|
|
63690 |
* [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session.
|
|
|
63691 |
*
|
|
|
63692 |
* Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11
|
|
|
63693 |
* browsers.
|
|
|
63694 |
*
|
|
|
63695 |
* As per the above ticket, this is particularly important for Chrome, where, if
|
|
|
63696 |
* unencrypted content is appended before encrypted content and the key session has not
|
|
|
63697 |
* been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached
|
|
|
63698 |
* during playback.
|
|
|
63699 |
*
|
|
|
63700 |
* @param {Object} player
|
|
|
63701 |
* The player instance
|
|
|
63702 |
* @param {Object[]} sourceKeySystems
|
|
|
63703 |
* The key systems options from the player source
|
|
|
63704 |
* @param {Object} [audioMedia]
|
|
|
63705 |
* The active audio media playlist (optional)
|
|
|
63706 |
* @param {Object[]} mainPlaylists
|
|
|
63707 |
* The playlists found on the main playlist object
|
|
|
63708 |
*
|
|
|
63709 |
* @return {Object}
|
|
|
63710 |
* Promise that resolves when the key session has been created
|
|
|
63711 |
*/
|
|
|
63712 |
|
|
|
63713 |
const waitForKeySessionCreation = ({
|
|
|
63714 |
player,
|
|
|
63715 |
sourceKeySystems,
|
|
|
63716 |
audioMedia,
|
|
|
63717 |
mainPlaylists
|
|
|
63718 |
}) => {
|
|
|
63719 |
if (!player.eme.initializeMediaKeys) {
|
|
|
63720 |
return Promise.resolve();
|
|
|
63721 |
} // TODO should all audio PSSH values be initialized for DRM?
|
|
|
63722 |
//
|
|
|
63723 |
// All unique video rendition pssh values are initialized for DRM, but here only
|
|
|
63724 |
// the initial audio playlist license is initialized. In theory, an encrypted
|
|
|
63725 |
// event should be fired if the user switches to an alternative audio playlist
|
|
|
63726 |
// where a license is required, but this case hasn't yet been tested. In addition, there
|
|
|
63727 |
// may be many alternate audio playlists unlikely to be used (e.g., multiple different
|
|
|
63728 |
// languages).
|
|
|
63729 |
|
|
|
63730 |
const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists;
|
|
|
63731 |
const keySystemsOptionsArr = getAllPsshKeySystemsOptions(playlists, Object.keys(sourceKeySystems));
|
|
|
63732 |
const initializationFinishedPromises = [];
|
|
|
63733 |
const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The
|
|
|
63734 |
// only place where it should not be deduped is for ms-prefixed APIs, but
|
|
|
63735 |
// the existence of modern EME APIs in addition to
|
|
|
63736 |
// ms-prefixed APIs on Edge should prevent this from being a concern.
|
|
|
63737 |
// initializeMediaKeys also won't use the webkit-prefixed APIs.
|
|
|
63738 |
|
|
|
63739 |
keySystemsOptionsArr.forEach(keySystemsOptions => {
|
|
|
63740 |
keySessionCreatedPromises.push(new Promise((resolve, reject) => {
|
|
|
63741 |
player.tech_.one('keysessioncreated', resolve);
|
|
|
63742 |
}));
|
|
|
63743 |
initializationFinishedPromises.push(new Promise((resolve, reject) => {
|
|
|
63744 |
player.eme.initializeMediaKeys({
|
|
|
63745 |
keySystems: keySystemsOptions
|
|
|
63746 |
}, err => {
|
|
|
63747 |
if (err) {
|
|
|
63748 |
reject(err);
|
|
|
63749 |
return;
|
|
|
63750 |
}
|
|
|
63751 |
resolve();
|
|
|
63752 |
});
|
|
|
63753 |
}));
|
|
|
63754 |
}); // The reasons Promise.race is chosen over Promise.any:
|
|
|
63755 |
//
|
|
|
63756 |
// * Promise.any is only available in Safari 14+.
|
|
|
63757 |
// * None of these promises are expected to reject. If they do reject, it might be
|
|
|
63758 |
// better here for the race to surface the rejection, rather than mask it by using
|
|
|
63759 |
// Promise.any.
|
|
|
63760 |
|
|
|
63761 |
return Promise.race([
|
|
|
63762 |
// If a session was previously created, these will all finish resolving without
|
|
|
63763 |
// creating a new session, otherwise it will take until the end of all license
|
|
|
63764 |
// requests, which is why the key session check is used (to make setup much faster).
|
|
|
63765 |
Promise.all(initializationFinishedPromises),
|
|
|
63766 |
// Once a single session is created, the browser knows DRM will be used.
|
|
|
63767 |
Promise.race(keySessionCreatedPromises)]);
|
|
|
63768 |
};
|
|
|
63769 |
/**
|
|
|
63770 |
* If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and
|
|
|
63771 |
* there are keySystems on the source, sets up source options to prepare the source for
|
|
|
63772 |
* eme.
|
|
|
63773 |
*
|
|
|
63774 |
* @param {Object} player
|
|
|
63775 |
* The player instance
|
|
|
63776 |
* @param {Object[]} sourceKeySystems
|
|
|
63777 |
* The key systems options from the player source
|
|
|
63778 |
* @param {Object} media
|
|
|
63779 |
* The active media playlist
|
|
|
63780 |
* @param {Object} [audioMedia]
|
|
|
63781 |
* The active audio media playlist (optional)
|
|
|
63782 |
*
|
|
|
63783 |
* @return {boolean}
|
|
|
63784 |
* Whether or not options were configured and EME is available
|
|
|
63785 |
*/
|
|
|
63786 |
|
|
|
63787 |
const setupEmeOptions = ({
|
|
|
63788 |
player,
|
|
|
63789 |
sourceKeySystems,
|
|
|
63790 |
media,
|
|
|
63791 |
audioMedia
|
|
|
63792 |
}) => {
|
|
|
63793 |
const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia);
|
|
|
63794 |
if (!sourceOptions) {
|
|
|
63795 |
return false;
|
|
|
63796 |
}
|
|
|
63797 |
player.currentSource().keySystems = sourceOptions; // eme handles the rest of the setup, so if it is missing
|
|
|
63798 |
// do nothing.
|
|
|
63799 |
|
|
|
63800 |
if (sourceOptions && !player.eme) {
|
|
|
63801 |
videojs.log.warn('DRM encrypted source cannot be decrypted without a DRM plugin');
|
|
|
63802 |
return false;
|
|
|
63803 |
}
|
|
|
63804 |
return true;
|
|
|
63805 |
};
|
|
|
63806 |
const getVhsLocalStorage = () => {
|
|
|
63807 |
if (!window.localStorage) {
|
|
|
63808 |
return null;
|
|
|
63809 |
}
|
|
|
63810 |
const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
|
63811 |
if (!storedObject) {
|
|
|
63812 |
return null;
|
|
|
63813 |
}
|
|
|
63814 |
try {
|
|
|
63815 |
return JSON.parse(storedObject);
|
|
|
63816 |
} catch (e) {
|
|
|
63817 |
// someone may have tampered with the value
|
|
|
63818 |
return null;
|
|
|
63819 |
}
|
|
|
63820 |
};
|
|
|
63821 |
const updateVhsLocalStorage = options => {
|
|
|
63822 |
if (!window.localStorage) {
|
|
|
63823 |
return false;
|
|
|
63824 |
}
|
|
|
63825 |
let objectToStore = getVhsLocalStorage();
|
|
|
63826 |
objectToStore = objectToStore ? merge(objectToStore, options) : options;
|
|
|
63827 |
try {
|
|
|
63828 |
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore));
|
|
|
63829 |
} catch (e) {
|
|
|
63830 |
// Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where
|
|
|
63831 |
// storage is set to 0).
|
|
|
63832 |
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions
|
|
|
63833 |
// No need to perform any operation.
|
|
|
63834 |
return false;
|
|
|
63835 |
}
|
|
|
63836 |
return objectToStore;
|
|
|
63837 |
};
|
|
|
63838 |
/**
|
|
|
63839 |
* Parses VHS-supported media types from data URIs. See
|
|
|
63840 |
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
|
|
|
63841 |
* for information on data URIs.
|
|
|
63842 |
*
|
|
|
63843 |
* @param {string} dataUri
|
|
|
63844 |
* The data URI
|
|
|
63845 |
*
|
|
|
63846 |
* @return {string|Object}
|
|
|
63847 |
* The parsed object/string, or the original string if no supported media type
|
|
|
63848 |
* was found
|
|
|
63849 |
*/
|
|
|
63850 |
|
|
|
63851 |
const expandDataUri = dataUri => {
|
|
|
63852 |
if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) {
|
|
|
63853 |
return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1));
|
|
|
63854 |
} // no known case for this data URI, return the string as-is
|
|
|
63855 |
|
|
|
63856 |
return dataUri;
|
|
|
63857 |
};
|
|
|
63858 |
/**
|
|
|
63859 |
* Adds a request hook to an xhr object
|
|
|
63860 |
*
|
|
|
63861 |
* @param {Object} xhr object to add the onRequest hook to
|
|
|
63862 |
* @param {function} callback hook function for an xhr request
|
|
|
63863 |
*/
|
|
|
63864 |
|
|
|
63865 |
const addOnRequestHook = (xhr, callback) => {
|
|
|
63866 |
if (!xhr._requestCallbackSet) {
|
|
|
63867 |
xhr._requestCallbackSet = new Set();
|
|
|
63868 |
}
|
|
|
63869 |
xhr._requestCallbackSet.add(callback);
|
|
|
63870 |
};
|
|
|
63871 |
/**
|
|
|
63872 |
* Adds a response hook to an xhr object
|
|
|
63873 |
*
|
|
|
63874 |
* @param {Object} xhr object to add the onResponse hook to
|
|
|
63875 |
* @param {function} callback hook function for an xhr response
|
|
|
63876 |
*/
|
|
|
63877 |
|
|
|
63878 |
const addOnResponseHook = (xhr, callback) => {
|
|
|
63879 |
if (!xhr._responseCallbackSet) {
|
|
|
63880 |
xhr._responseCallbackSet = new Set();
|
|
|
63881 |
}
|
|
|
63882 |
xhr._responseCallbackSet.add(callback);
|
|
|
63883 |
};
|
|
|
63884 |
/**
|
|
|
63885 |
* Removes a request hook on an xhr object, deletes the onRequest set if empty.
|
|
|
63886 |
*
|
|
|
63887 |
* @param {Object} xhr object to remove the onRequest hook from
|
|
|
63888 |
* @param {function} callback hook function to remove
|
|
|
63889 |
*/
|
|
|
63890 |
|
|
|
63891 |
const removeOnRequestHook = (xhr, callback) => {
|
|
|
63892 |
if (!xhr._requestCallbackSet) {
|
|
|
63893 |
return;
|
|
|
63894 |
}
|
|
|
63895 |
xhr._requestCallbackSet.delete(callback);
|
|
|
63896 |
if (!xhr._requestCallbackSet.size) {
|
|
|
63897 |
delete xhr._requestCallbackSet;
|
|
|
63898 |
}
|
|
|
63899 |
};
|
|
|
63900 |
/**
|
|
|
63901 |
* Removes a response hook on an xhr object, deletes the onResponse set if empty.
|
|
|
63902 |
*
|
|
|
63903 |
* @param {Object} xhr object to remove the onResponse hook from
|
|
|
63904 |
* @param {function} callback hook function to remove
|
|
|
63905 |
*/
|
|
|
63906 |
|
|
|
63907 |
const removeOnResponseHook = (xhr, callback) => {
|
|
|
63908 |
if (!xhr._responseCallbackSet) {
|
|
|
63909 |
return;
|
|
|
63910 |
}
|
|
|
63911 |
xhr._responseCallbackSet.delete(callback);
|
|
|
63912 |
if (!xhr._responseCallbackSet.size) {
|
|
|
63913 |
delete xhr._responseCallbackSet;
|
|
|
63914 |
}
|
|
|
63915 |
};
|
|
|
63916 |
/**
|
|
|
63917 |
* Whether the browser has built-in HLS support.
|
|
|
63918 |
*/
|
|
|
63919 |
|
|
|
63920 |
Vhs.supportsNativeHls = function () {
|
|
|
63921 |
if (!document || !document.createElement) {
|
|
|
63922 |
return false;
|
|
|
63923 |
}
|
|
|
63924 |
const video = document.createElement('video'); // native HLS is definitely not supported if HTML5 video isn't
|
|
|
63925 |
|
|
|
63926 |
if (!videojs.getTech('Html5').isSupported()) {
|
|
|
63927 |
return false;
|
|
|
63928 |
} // HLS manifests can go by many mime-types
|
|
|
63929 |
|
|
|
63930 |
const canPlay = [
|
|
|
63931 |
// Apple santioned
|
|
|
63932 |
'application/vnd.apple.mpegurl',
|
|
|
63933 |
// Apple sanctioned for backwards compatibility
|
|
|
63934 |
'audio/mpegurl',
|
|
|
63935 |
// Very common
|
|
|
63936 |
'audio/x-mpegurl',
|
|
|
63937 |
// Very common
|
|
|
63938 |
'application/x-mpegurl',
|
|
|
63939 |
// Included for completeness
|
|
|
63940 |
'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl'];
|
|
|
63941 |
return canPlay.some(function (canItPlay) {
|
|
|
63942 |
return /maybe|probably/i.test(video.canPlayType(canItPlay));
|
|
|
63943 |
});
|
|
|
63944 |
}();
|
|
|
63945 |
Vhs.supportsNativeDash = function () {
|
|
|
63946 |
if (!document || !document.createElement || !videojs.getTech('Html5').isSupported()) {
|
|
|
63947 |
return false;
|
|
|
63948 |
}
|
|
|
63949 |
return /maybe|probably/i.test(document.createElement('video').canPlayType('application/dash+xml'));
|
|
|
63950 |
}();
|
|
|
63951 |
Vhs.supportsTypeNatively = type => {
|
|
|
63952 |
if (type === 'hls') {
|
|
|
63953 |
return Vhs.supportsNativeHls;
|
|
|
63954 |
}
|
|
|
63955 |
if (type === 'dash') {
|
|
|
63956 |
return Vhs.supportsNativeDash;
|
|
|
63957 |
}
|
|
|
63958 |
return false;
|
|
|
63959 |
};
|
|
|
63960 |
/**
|
|
|
63961 |
* VHS is a source handler, not a tech. Make sure attempts to use it
|
|
|
63962 |
* as one do not cause exceptions.
|
|
|
63963 |
*/
|
|
|
63964 |
|
|
|
63965 |
Vhs.isSupported = function () {
|
|
|
63966 |
return videojs.log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.');
|
|
|
63967 |
};
|
|
|
63968 |
/**
|
|
|
63969 |
* A global function for setting an onRequest hook
|
|
|
63970 |
*
|
|
|
63971 |
* @param {function} callback for request modifiction
|
|
|
63972 |
*/
|
|
|
63973 |
|
|
|
63974 |
Vhs.xhr.onRequest = function (callback) {
|
|
|
63975 |
addOnRequestHook(Vhs.xhr, callback);
|
|
|
63976 |
};
|
|
|
63977 |
/**
|
|
|
63978 |
* A global function for setting an onResponse hook
|
|
|
63979 |
*
|
|
|
63980 |
* @param {callback} callback for response data retrieval
|
|
|
63981 |
*/
|
|
|
63982 |
|
|
|
63983 |
Vhs.xhr.onResponse = function (callback) {
|
|
|
63984 |
addOnResponseHook(Vhs.xhr, callback);
|
|
|
63985 |
};
|
|
|
63986 |
/**
|
|
|
63987 |
* Deletes a global onRequest callback if it exists
|
|
|
63988 |
*
|
|
|
63989 |
* @param {function} callback to delete from the global set
|
|
|
63990 |
*/
|
|
|
63991 |
|
|
|
63992 |
Vhs.xhr.offRequest = function (callback) {
|
|
|
63993 |
removeOnRequestHook(Vhs.xhr, callback);
|
|
|
63994 |
};
|
|
|
63995 |
/**
|
|
|
63996 |
* Deletes a global onResponse callback if it exists
|
|
|
63997 |
*
|
|
|
63998 |
* @param {function} callback to delete from the global set
|
|
|
63999 |
*/
|
|
|
64000 |
|
|
|
64001 |
Vhs.xhr.offResponse = function (callback) {
|
|
|
64002 |
removeOnResponseHook(Vhs.xhr, callback);
|
|
|
64003 |
};
|
|
|
64004 |
const Component = videojs.getComponent('Component');
|
|
|
64005 |
/**
|
|
|
64006 |
* The Vhs Handler object, where we orchestrate all of the parts
|
|
|
64007 |
* of VHS to interact with video.js
|
|
|
64008 |
*
|
|
|
64009 |
* @class VhsHandler
|
|
|
64010 |
* @extends videojs.Component
|
|
|
64011 |
* @param {Object} source the soruce object
|
|
|
64012 |
* @param {Tech} tech the parent tech object
|
|
|
64013 |
* @param {Object} options optional and required options
|
|
|
64014 |
*/
|
|
|
64015 |
|
|
|
64016 |
class VhsHandler extends Component {
|
|
|
64017 |
constructor(source, tech, options) {
|
|
|
64018 |
super(tech, options.vhs); // if a tech level `initialBandwidth` option was passed
|
|
|
64019 |
// use that over the VHS level `bandwidth` option
|
|
|
64020 |
|
|
|
64021 |
if (typeof options.initialBandwidth === 'number') {
|
|
|
64022 |
this.options_.bandwidth = options.initialBandwidth;
|
|
|
64023 |
}
|
|
|
64024 |
this.logger_ = logger('VhsHandler'); // we need access to the player in some cases,
|
|
|
64025 |
// so, get it from Video.js via the `playerId`
|
|
|
64026 |
|
|
|
64027 |
if (tech.options_ && tech.options_.playerId) {
|
|
|
64028 |
const _player = videojs.getPlayer(tech.options_.playerId);
|
|
|
64029 |
this.player_ = _player;
|
|
|
64030 |
}
|
|
|
64031 |
this.tech_ = tech;
|
|
|
64032 |
this.source_ = source;
|
|
|
64033 |
this.stats = {};
|
|
|
64034 |
this.ignoreNextSeekingEvent_ = false;
|
|
|
64035 |
this.setOptions_();
|
|
|
64036 |
if (this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) {
|
|
|
64037 |
tech.overrideNativeAudioTracks(true);
|
|
|
64038 |
tech.overrideNativeVideoTracks(true);
|
|
|
64039 |
} else if (this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
|
|
|
64040 |
// overriding native VHS only works if audio tracks have been emulated
|
|
|
64041 |
// error early if we're misconfigured
|
|
|
64042 |
throw new Error('Overriding native VHS requires emulated tracks. ' + 'See https://git.io/vMpjB');
|
|
|
64043 |
} // listen for fullscreenchange events for this player so that we
|
|
|
64044 |
// can adjust our quality selection quickly
|
|
|
64045 |
|
|
|
64046 |
this.on(document, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], event => {
|
|
|
64047 |
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;
|
|
|
64048 |
if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
|
|
|
64049 |
this.playlistController_.fastQualityChange_();
|
|
|
64050 |
} else {
|
|
|
64051 |
// When leaving fullscreen, since the in page pixel dimensions should be smaller
|
|
|
64052 |
// than full screen, see if there should be a rendition switch down to preserve
|
|
|
64053 |
// bandwidth.
|
|
|
64054 |
this.playlistController_.checkABR_();
|
|
|
64055 |
}
|
|
|
64056 |
});
|
|
|
64057 |
this.on(this.tech_, 'seeking', function () {
|
|
|
64058 |
if (this.ignoreNextSeekingEvent_) {
|
|
|
64059 |
this.ignoreNextSeekingEvent_ = false;
|
|
|
64060 |
return;
|
|
|
64061 |
}
|
|
|
64062 |
this.setCurrentTime(this.tech_.currentTime());
|
|
|
64063 |
});
|
|
|
64064 |
this.on(this.tech_, 'error', function () {
|
|
|
64065 |
// verify that the error was real and we are loaded
|
|
|
64066 |
// enough to have pc loaded.
|
|
|
64067 |
if (this.tech_.error() && this.playlistController_) {
|
|
|
64068 |
this.playlistController_.pauseLoading();
|
|
|
64069 |
}
|
|
|
64070 |
});
|
|
|
64071 |
this.on(this.tech_, 'play', this.play);
|
|
|
64072 |
}
|
|
|
64073 |
/**
|
|
|
64074 |
* Set VHS options based on options from configuration, as well as partial
|
|
|
64075 |
* options to be passed at a later time.
|
|
|
64076 |
*
|
|
|
64077 |
* @param {Object} options A partial chunk of config options
|
|
|
64078 |
*/
|
|
|
64079 |
|
|
|
64080 |
setOptions_(options = {}) {
|
|
|
64081 |
this.options_ = merge(this.options_, options); // defaults
|
|
|
64082 |
|
|
|
64083 |
this.options_.withCredentials = this.options_.withCredentials || false;
|
|
|
64084 |
this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true;
|
|
|
64085 |
this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false;
|
|
|
64086 |
this.options_.useBandwidthFromLocalStorage = typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false;
|
|
|
64087 |
this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
|
|
|
64088 |
this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false;
|
|
|
64089 |
this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
|
|
|
64090 |
this.options_.customTagParsers = this.options_.customTagParsers || [];
|
|
|
64091 |
this.options_.customTagMappers = this.options_.customTagMappers || [];
|
|
|
64092 |
this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;
|
|
|
64093 |
this.options_.llhls = this.options_.llhls === false ? false : true;
|
|
|
64094 |
this.options_.bufferBasedABR = this.options_.bufferBasedABR || false;
|
|
|
64095 |
if (typeof this.options_.playlistExclusionDuration !== 'number') {
|
|
|
64096 |
this.options_.playlistExclusionDuration = 60;
|
|
|
64097 |
}
|
|
|
64098 |
if (typeof this.options_.bandwidth !== 'number') {
|
|
|
64099 |
if (this.options_.useBandwidthFromLocalStorage) {
|
|
|
64100 |
const storedObject = getVhsLocalStorage();
|
|
|
64101 |
if (storedObject && storedObject.bandwidth) {
|
|
|
64102 |
this.options_.bandwidth = storedObject.bandwidth;
|
|
|
64103 |
this.tech_.trigger({
|
|
|
64104 |
type: 'usage',
|
|
|
64105 |
name: 'vhs-bandwidth-from-local-storage'
|
|
|
64106 |
});
|
|
|
64107 |
}
|
|
|
64108 |
if (storedObject && storedObject.throughput) {
|
|
|
64109 |
this.options_.throughput = storedObject.throughput;
|
|
|
64110 |
this.tech_.trigger({
|
|
|
64111 |
type: 'usage',
|
|
|
64112 |
name: 'vhs-throughput-from-local-storage'
|
|
|
64113 |
});
|
|
|
64114 |
}
|
|
|
64115 |
}
|
|
|
64116 |
} // if bandwidth was not set by options or pulled from local storage, start playlist
|
|
|
64117 |
// selection at a reasonable bandwidth
|
|
|
64118 |
|
|
|
64119 |
if (typeof this.options_.bandwidth !== 'number') {
|
|
|
64120 |
this.options_.bandwidth = Config.INITIAL_BANDWIDTH;
|
|
|
64121 |
} // If the bandwidth number is unchanged from the initial setting
|
|
|
64122 |
// then this takes precedence over the enableLowInitialPlaylist option
|
|
|
64123 |
|
|
|
64124 |
this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src
|
|
|
64125 |
|
|
|
64126 |
['withCredentials', 'useDevicePixelRatio', 'limitRenditionByPlayerDimensions', 'bandwidth', 'customTagParsers', 'customTagMappers', 'cacheEncryptionKeys', 'playlistSelector', 'initialPlaylistSelector', 'bufferBasedABR', 'liveRangeSafeTimeDelta', 'llhls', 'useForcedSubtitles', 'useNetworkInformationApi', 'useDtsForTimestampOffset', 'exactManifestTimings', 'leastPixelDiffSelector'].forEach(option => {
|
|
|
64127 |
if (typeof this.source_[option] !== 'undefined') {
|
|
|
64128 |
this.options_[option] = this.source_[option];
|
|
|
64129 |
}
|
|
|
64130 |
});
|
|
|
64131 |
this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions;
|
|
|
64132 |
this.useDevicePixelRatio = this.options_.useDevicePixelRatio;
|
|
|
64133 |
} // alias for public method to set options
|
|
|
64134 |
|
|
|
64135 |
setOptions(options = {}) {
|
|
|
64136 |
this.setOptions_(options);
|
|
|
64137 |
}
|
|
|
64138 |
/**
|
|
|
64139 |
* called when player.src gets called, handle a new source
|
|
|
64140 |
*
|
|
|
64141 |
* @param {Object} src the source object to handle
|
|
|
64142 |
*/
|
|
|
64143 |
|
|
|
64144 |
src(src, type) {
|
|
|
64145 |
// do nothing if the src is falsey
|
|
|
64146 |
if (!src) {
|
|
|
64147 |
return;
|
|
|
64148 |
}
|
|
|
64149 |
this.setOptions_(); // add main playlist controller options
|
|
|
64150 |
|
|
|
64151 |
this.options_.src = expandDataUri(this.source_.src);
|
|
|
64152 |
this.options_.tech = this.tech_;
|
|
|
64153 |
this.options_.externVhs = Vhs;
|
|
|
64154 |
this.options_.sourceType = simpleTypeFromSourceType(type); // Whenever we seek internally, we should update the tech
|
|
|
64155 |
|
|
|
64156 |
this.options_.seekTo = time => {
|
|
|
64157 |
this.tech_.setCurrentTime(time);
|
|
|
64158 |
};
|
|
|
64159 |
this.playlistController_ = new PlaylistController(this.options_);
|
|
|
64160 |
const playbackWatcherOptions = merge({
|
|
|
64161 |
liveRangeSafeTimeDelta: SAFE_TIME_DELTA
|
|
|
64162 |
}, this.options_, {
|
|
|
64163 |
seekable: () => this.seekable(),
|
|
|
64164 |
media: () => this.playlistController_.media(),
|
|
|
64165 |
playlistController: this.playlistController_
|
|
|
64166 |
});
|
|
|
64167 |
this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions);
|
|
|
64168 |
this.playlistController_.on('error', () => {
|
|
|
64169 |
const player = videojs.players[this.tech_.options_.playerId];
|
|
|
64170 |
let error = this.playlistController_.error;
|
|
|
64171 |
if (typeof error === 'object' && !error.code) {
|
|
|
64172 |
error.code = 3;
|
|
|
64173 |
} else if (typeof error === 'string') {
|
|
|
64174 |
error = {
|
|
|
64175 |
message: error,
|
|
|
64176 |
code: 3
|
|
|
64177 |
};
|
|
|
64178 |
}
|
|
|
64179 |
player.error(error);
|
|
|
64180 |
});
|
|
|
64181 |
const defaultSelector = this.options_.bufferBasedABR ? Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR; // `this` in selectPlaylist should be the VhsHandler for backwards
|
|
|
64182 |
// compatibility with < v2
|
|
|
64183 |
|
|
|
64184 |
this.playlistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : defaultSelector.bind(this);
|
|
|
64185 |
this.playlistController_.selectInitialPlaylist = Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2
|
|
|
64186 |
|
|
|
64187 |
this.playlists = this.playlistController_.mainPlaylistLoader_;
|
|
|
64188 |
this.mediaSource = this.playlistController_.mediaSource; // Proxy assignment of some properties to the main playlist
|
|
|
64189 |
// controller. Using a custom property for backwards compatibility
|
|
|
64190 |
// with < v2
|
|
|
64191 |
|
|
|
64192 |
Object.defineProperties(this, {
|
|
|
64193 |
selectPlaylist: {
|
|
|
64194 |
get() {
|
|
|
64195 |
return this.playlistController_.selectPlaylist;
|
|
|
64196 |
},
|
|
|
64197 |
set(selectPlaylist) {
|
|
|
64198 |
this.playlistController_.selectPlaylist = selectPlaylist.bind(this);
|
|
|
64199 |
}
|
|
|
64200 |
},
|
|
|
64201 |
throughput: {
|
|
|
64202 |
get() {
|
|
|
64203 |
return this.playlistController_.mainSegmentLoader_.throughput.rate;
|
|
|
64204 |
},
|
|
|
64205 |
set(throughput) {
|
|
|
64206 |
this.playlistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value
|
|
|
64207 |
// for the cumulative average
|
|
|
64208 |
|
|
|
64209 |
this.playlistController_.mainSegmentLoader_.throughput.count = 1;
|
|
|
64210 |
}
|
|
|
64211 |
},
|
|
|
64212 |
bandwidth: {
|
|
|
64213 |
get() {
|
|
|
64214 |
let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth;
|
|
|
64215 |
const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
|
|
|
64216 |
const tenMbpsAsBitsPerSecond = 10e6;
|
|
|
64217 |
if (this.options_.useNetworkInformationApi && networkInformation) {
|
|
|
64218 |
// downlink returns Mbps
|
|
|
64219 |
// https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink
|
|
|
64220 |
const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player
|
|
|
64221 |
// estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that
|
|
|
64222 |
// high quality streams are not filtered out.
|
|
|
64223 |
|
|
|
64224 |
if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) {
|
|
|
64225 |
playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec);
|
|
|
64226 |
} else {
|
|
|
64227 |
playerBandwidthEst = networkInfoBandwidthEstBitsPerSec;
|
|
|
64228 |
}
|
|
|
64229 |
}
|
|
|
64230 |
return playerBandwidthEst;
|
|
|
64231 |
},
|
|
|
64232 |
set(bandwidth) {
|
|
|
64233 |
this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter
|
|
|
64234 |
// `count` is set to zero that current value of `rate` isn't included
|
|
|
64235 |
// in the cumulative average
|
|
|
64236 |
|
|
|
64237 |
this.playlistController_.mainSegmentLoader_.throughput = {
|
|
|
64238 |
rate: 0,
|
|
|
64239 |
count: 0
|
|
|
64240 |
};
|
|
|
64241 |
}
|
|
|
64242 |
},
|
|
|
64243 |
/**
|
|
|
64244 |
* `systemBandwidth` is a combination of two serial processes bit-rates. The first
|
|
|
64245 |
* is the network bitrate provided by `bandwidth` and the second is the bitrate of
|
|
|
64246 |
* the entire process after that - decryption, transmuxing, and appending - provided
|
|
|
64247 |
* by `throughput`.
|
|
|
64248 |
*
|
|
|
64249 |
* Since the two process are serial, the overall system bandwidth is given by:
|
|
|
64250 |
* sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
|
|
|
64251 |
*/
|
|
|
64252 |
systemBandwidth: {
|
|
|
64253 |
get() {
|
|
|
64254 |
const invBandwidth = 1 / (this.bandwidth || 1);
|
|
|
64255 |
let invThroughput;
|
|
|
64256 |
if (this.throughput > 0) {
|
|
|
64257 |
invThroughput = 1 / this.throughput;
|
|
|
64258 |
} else {
|
|
|
64259 |
invThroughput = 0;
|
|
|
64260 |
}
|
|
|
64261 |
const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
|
|
|
64262 |
return systemBitrate;
|
|
|
64263 |
},
|
|
|
64264 |
set() {
|
|
|
64265 |
videojs.log.error('The "systemBandwidth" property is read-only');
|
|
|
64266 |
}
|
|
|
64267 |
}
|
|
|
64268 |
});
|
|
|
64269 |
if (this.options_.bandwidth) {
|
|
|
64270 |
this.bandwidth = this.options_.bandwidth;
|
|
|
64271 |
}
|
|
|
64272 |
if (this.options_.throughput) {
|
|
|
64273 |
this.throughput = this.options_.throughput;
|
|
|
64274 |
}
|
|
|
64275 |
Object.defineProperties(this.stats, {
|
|
|
64276 |
bandwidth: {
|
|
|
64277 |
get: () => this.bandwidth || 0,
|
|
|
64278 |
enumerable: true
|
|
|
64279 |
},
|
|
|
64280 |
mediaRequests: {
|
|
|
64281 |
get: () => this.playlistController_.mediaRequests_() || 0,
|
|
|
64282 |
enumerable: true
|
|
|
64283 |
},
|
|
|
64284 |
mediaRequestsAborted: {
|
|
|
64285 |
get: () => this.playlistController_.mediaRequestsAborted_() || 0,
|
|
|
64286 |
enumerable: true
|
|
|
64287 |
},
|
|
|
64288 |
mediaRequestsTimedout: {
|
|
|
64289 |
get: () => this.playlistController_.mediaRequestsTimedout_() || 0,
|
|
|
64290 |
enumerable: true
|
|
|
64291 |
},
|
|
|
64292 |
mediaRequestsErrored: {
|
|
|
64293 |
get: () => this.playlistController_.mediaRequestsErrored_() || 0,
|
|
|
64294 |
enumerable: true
|
|
|
64295 |
},
|
|
|
64296 |
mediaTransferDuration: {
|
|
|
64297 |
get: () => this.playlistController_.mediaTransferDuration_() || 0,
|
|
|
64298 |
enumerable: true
|
|
|
64299 |
},
|
|
|
64300 |
mediaBytesTransferred: {
|
|
|
64301 |
get: () => this.playlistController_.mediaBytesTransferred_() || 0,
|
|
|
64302 |
enumerable: true
|
|
|
64303 |
},
|
|
|
64304 |
mediaSecondsLoaded: {
|
|
|
64305 |
get: () => this.playlistController_.mediaSecondsLoaded_() || 0,
|
|
|
64306 |
enumerable: true
|
|
|
64307 |
},
|
|
|
64308 |
mediaAppends: {
|
|
|
64309 |
get: () => this.playlistController_.mediaAppends_() || 0,
|
|
|
64310 |
enumerable: true
|
|
|
64311 |
},
|
|
|
64312 |
mainAppendsToLoadedData: {
|
|
|
64313 |
get: () => this.playlistController_.mainAppendsToLoadedData_() || 0,
|
|
|
64314 |
enumerable: true
|
|
|
64315 |
},
|
|
|
64316 |
audioAppendsToLoadedData: {
|
|
|
64317 |
get: () => this.playlistController_.audioAppendsToLoadedData_() || 0,
|
|
|
64318 |
enumerable: true
|
|
|
64319 |
},
|
|
|
64320 |
appendsToLoadedData: {
|
|
|
64321 |
get: () => this.playlistController_.appendsToLoadedData_() || 0,
|
|
|
64322 |
enumerable: true
|
|
|
64323 |
},
|
|
|
64324 |
timeToLoadedData: {
|
|
|
64325 |
get: () => this.playlistController_.timeToLoadedData_() || 0,
|
|
|
64326 |
enumerable: true
|
|
|
64327 |
},
|
|
|
64328 |
buffered: {
|
|
|
64329 |
get: () => timeRangesToArray(this.tech_.buffered()),
|
|
|
64330 |
enumerable: true
|
|
|
64331 |
},
|
|
|
64332 |
currentTime: {
|
|
|
64333 |
get: () => this.tech_.currentTime(),
|
|
|
64334 |
enumerable: true
|
|
|
64335 |
},
|
|
|
64336 |
currentSource: {
|
|
|
64337 |
get: () => this.tech_.currentSource_,
|
|
|
64338 |
enumerable: true
|
|
|
64339 |
},
|
|
|
64340 |
currentTech: {
|
|
|
64341 |
get: () => this.tech_.name_,
|
|
|
64342 |
enumerable: true
|
|
|
64343 |
},
|
|
|
64344 |
duration: {
|
|
|
64345 |
get: () => this.tech_.duration(),
|
|
|
64346 |
enumerable: true
|
|
|
64347 |
},
|
|
|
64348 |
main: {
|
|
|
64349 |
get: () => this.playlists.main,
|
|
|
64350 |
enumerable: true
|
|
|
64351 |
},
|
|
|
64352 |
playerDimensions: {
|
|
|
64353 |
get: () => this.tech_.currentDimensions(),
|
|
|
64354 |
enumerable: true
|
|
|
64355 |
},
|
|
|
64356 |
seekable: {
|
|
|
64357 |
get: () => timeRangesToArray(this.tech_.seekable()),
|
|
|
64358 |
enumerable: true
|
|
|
64359 |
},
|
|
|
64360 |
timestamp: {
|
|
|
64361 |
get: () => Date.now(),
|
|
|
64362 |
enumerable: true
|
|
|
64363 |
},
|
|
|
64364 |
videoPlaybackQuality: {
|
|
|
64365 |
get: () => this.tech_.getVideoPlaybackQuality(),
|
|
|
64366 |
enumerable: true
|
|
|
64367 |
}
|
|
|
64368 |
});
|
|
|
64369 |
this.tech_.one('canplay', this.playlistController_.setupFirstPlay.bind(this.playlistController_));
|
|
|
64370 |
this.tech_.on('bandwidthupdate', () => {
|
|
|
64371 |
if (this.options_.useBandwidthFromLocalStorage) {
|
|
|
64372 |
updateVhsLocalStorage({
|
|
|
64373 |
bandwidth: this.bandwidth,
|
|
|
64374 |
throughput: Math.round(this.throughput)
|
|
|
64375 |
});
|
|
|
64376 |
}
|
|
|
64377 |
});
|
|
|
64378 |
this.playlistController_.on('selectedinitialmedia', () => {
|
|
|
64379 |
// Add the manual rendition mix-in to VhsHandler
|
|
|
64380 |
renditionSelectionMixin(this);
|
|
|
64381 |
});
|
|
|
64382 |
this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => {
|
|
|
64383 |
this.setupEme_();
|
|
|
64384 |
}); // the bandwidth of the primary segment loader is our best
|
|
|
64385 |
// estimate of overall bandwidth
|
|
|
64386 |
|
|
|
64387 |
this.on(this.playlistController_, 'progress', function () {
|
|
|
64388 |
this.tech_.trigger('progress');
|
|
|
64389 |
}); // In the live case, we need to ignore the very first `seeking` event since
|
|
|
64390 |
// that will be the result of the seek-to-live behavior
|
|
|
64391 |
|
|
|
64392 |
this.on(this.playlistController_, 'firstplay', function () {
|
|
|
64393 |
this.ignoreNextSeekingEvent_ = true;
|
|
|
64394 |
});
|
|
|
64395 |
this.setupQualityLevels_(); // do nothing if the tech has been disposed already
|
|
|
64396 |
// this can occur if someone sets the src in player.ready(), for instance
|
|
|
64397 |
|
|
|
64398 |
if (!this.tech_.el()) {
|
|
|
64399 |
return;
|
|
|
64400 |
}
|
|
|
64401 |
this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource);
|
|
|
64402 |
this.tech_.src(this.mediaSourceUrl_);
|
|
|
64403 |
}
|
|
|
64404 |
createKeySessions_() {
|
|
|
64405 |
const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
|
|
|
64406 |
this.logger_('waiting for EME key session creation');
|
|
|
64407 |
waitForKeySessionCreation({
|
|
|
64408 |
player: this.player_,
|
|
|
64409 |
sourceKeySystems: this.source_.keySystems,
|
|
|
64410 |
audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(),
|
|
|
64411 |
mainPlaylists: this.playlists.main.playlists
|
|
|
64412 |
}).then(() => {
|
|
|
64413 |
this.logger_('created EME key session');
|
|
|
64414 |
this.playlistController_.sourceUpdater_.initializedEme();
|
|
|
64415 |
}).catch(err => {
|
|
|
64416 |
this.logger_('error while creating EME key session', err);
|
|
|
64417 |
this.player_.error({
|
|
|
64418 |
message: 'Failed to initialize media keys for EME',
|
|
|
64419 |
code: 3
|
|
|
64420 |
});
|
|
|
64421 |
});
|
|
|
64422 |
}
|
|
|
64423 |
handleWaitingForKey_() {
|
|
|
64424 |
// If waitingforkey is fired, it's possible that the data that's necessary to retrieve
|
|
|
64425 |
// the key is in the manifest. While this should've happened on initial source load, it
|
|
|
64426 |
// may happen again in live streams where the keys change, and the manifest info
|
|
|
64427 |
// reflects the update.
|
|
|
64428 |
//
|
|
|
64429 |
// Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's
|
|
|
64430 |
// already requested keys for, we don't have to worry about this generating extraneous
|
|
|
64431 |
// requests.
|
|
|
64432 |
this.logger_('waitingforkey fired, attempting to create any new key sessions');
|
|
|
64433 |
this.createKeySessions_();
|
|
|
64434 |
}
|
|
|
64435 |
/**
|
|
|
64436 |
* If necessary and EME is available, sets up EME options and waits for key session
|
|
|
64437 |
* creation.
|
|
|
64438 |
*
|
|
|
64439 |
* This function also updates the source updater so taht it can be used, as for some
|
|
|
64440 |
* browsers, EME must be configured before content is appended (if appending unencrypted
|
|
|
64441 |
* content before encrypted content).
|
|
|
64442 |
*/
|
|
|
64443 |
|
|
|
64444 |
setupEme_() {
|
|
|
64445 |
const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
|
|
|
64446 |
const didSetupEmeOptions = setupEmeOptions({
|
|
|
64447 |
player: this.player_,
|
|
|
64448 |
sourceKeySystems: this.source_.keySystems,
|
|
|
64449 |
media: this.playlists.media(),
|
|
|
64450 |
audioMedia: audioPlaylistLoader && audioPlaylistLoader.media()
|
|
|
64451 |
});
|
|
|
64452 |
this.player_.tech_.on('keystatuschange', e => {
|
|
|
64453 |
this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status);
|
|
|
64454 |
});
|
|
|
64455 |
this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this);
|
|
|
64456 |
this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_);
|
|
|
64457 |
if (!didSetupEmeOptions) {
|
|
|
64458 |
// If EME options were not set up, we've done all we could to initialize EME.
|
|
|
64459 |
this.playlistController_.sourceUpdater_.initializedEme();
|
|
|
64460 |
return;
|
|
|
64461 |
}
|
|
|
64462 |
this.createKeySessions_();
|
|
|
64463 |
}
|
|
|
64464 |
/**
|
|
|
64465 |
* Initializes the quality levels and sets listeners to update them.
|
|
|
64466 |
*
|
|
|
64467 |
* @method setupQualityLevels_
|
|
|
64468 |
* @private
|
|
|
64469 |
*/
|
|
|
64470 |
|
|
|
64471 |
setupQualityLevels_() {
|
|
|
64472 |
const player = videojs.players[this.tech_.options_.playerId]; // if there isn't a player or there isn't a qualityLevels plugin
|
|
|
64473 |
// or qualityLevels_ listeners have already been setup, do nothing.
|
|
|
64474 |
|
|
|
64475 |
if (!player || !player.qualityLevels || this.qualityLevels_) {
|
|
|
64476 |
return;
|
|
|
64477 |
}
|
|
|
64478 |
this.qualityLevels_ = player.qualityLevels();
|
|
|
64479 |
this.playlistController_.on('selectedinitialmedia', () => {
|
|
|
64480 |
handleVhsLoadedMetadata(this.qualityLevels_, this);
|
|
|
64481 |
});
|
|
|
64482 |
this.playlists.on('mediachange', () => {
|
|
|
64483 |
handleVhsMediaChange(this.qualityLevels_, this.playlists);
|
|
|
64484 |
});
|
|
|
64485 |
}
|
|
|
64486 |
/**
|
|
|
64487 |
* return the version
|
|
|
64488 |
*/
|
|
|
64489 |
|
|
|
64490 |
static version() {
|
|
|
64491 |
return {
|
|
|
64492 |
'@videojs/http-streaming': version$4,
|
|
|
64493 |
'mux.js': version$3,
|
|
|
64494 |
'mpd-parser': version$2,
|
|
|
64495 |
'm3u8-parser': version$1,
|
|
|
64496 |
'aes-decrypter': version
|
|
|
64497 |
};
|
|
|
64498 |
}
|
|
|
64499 |
/**
|
|
|
64500 |
* return the version
|
|
|
64501 |
*/
|
|
|
64502 |
|
|
|
64503 |
version() {
|
|
|
64504 |
return this.constructor.version();
|
|
|
64505 |
}
|
|
|
64506 |
canChangeType() {
|
|
|
64507 |
return SourceUpdater.canChangeType();
|
|
|
64508 |
}
|
|
|
64509 |
/**
|
|
|
64510 |
* Begin playing the video.
|
|
|
64511 |
*/
|
|
|
64512 |
|
|
|
64513 |
play() {
|
|
|
64514 |
this.playlistController_.play();
|
|
|
64515 |
}
|
|
|
64516 |
/**
|
|
|
64517 |
* a wrapper around the function in PlaylistController
|
|
|
64518 |
*/
|
|
|
64519 |
|
|
|
64520 |
setCurrentTime(currentTime) {
|
|
|
64521 |
this.playlistController_.setCurrentTime(currentTime);
|
|
|
64522 |
}
|
|
|
64523 |
/**
|
|
|
64524 |
* a wrapper around the function in PlaylistController
|
|
|
64525 |
*/
|
|
|
64526 |
|
|
|
64527 |
duration() {
|
|
|
64528 |
return this.playlistController_.duration();
|
|
|
64529 |
}
|
|
|
64530 |
/**
|
|
|
64531 |
* a wrapper around the function in PlaylistController
|
|
|
64532 |
*/
|
|
|
64533 |
|
|
|
64534 |
seekable() {
|
|
|
64535 |
return this.playlistController_.seekable();
|
|
|
64536 |
}
|
|
|
64537 |
/**
|
|
|
64538 |
* Abort all outstanding work and cleanup.
|
|
|
64539 |
*/
|
|
|
64540 |
|
|
|
64541 |
dispose() {
|
|
|
64542 |
if (this.playbackWatcher_) {
|
|
|
64543 |
this.playbackWatcher_.dispose();
|
|
|
64544 |
}
|
|
|
64545 |
if (this.playlistController_) {
|
|
|
64546 |
this.playlistController_.dispose();
|
|
|
64547 |
}
|
|
|
64548 |
if (this.qualityLevels_) {
|
|
|
64549 |
this.qualityLevels_.dispose();
|
|
|
64550 |
}
|
|
|
64551 |
if (this.tech_ && this.tech_.vhs) {
|
|
|
64552 |
delete this.tech_.vhs;
|
|
|
64553 |
}
|
|
|
64554 |
if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) {
|
|
|
64555 |
window.URL.revokeObjectURL(this.mediaSourceUrl_);
|
|
|
64556 |
this.mediaSourceUrl_ = null;
|
|
|
64557 |
}
|
|
|
64558 |
if (this.tech_) {
|
|
|
64559 |
this.tech_.off('waitingforkey', this.handleWaitingForKey_);
|
|
|
64560 |
}
|
|
|
64561 |
super.dispose();
|
|
|
64562 |
}
|
|
|
64563 |
convertToProgramTime(time, callback) {
|
|
|
64564 |
return getProgramTime({
|
|
|
64565 |
playlist: this.playlistController_.media(),
|
|
|
64566 |
time,
|
|
|
64567 |
callback
|
|
|
64568 |
});
|
|
|
64569 |
} // the player must be playing before calling this
|
|
|
64570 |
|
|
|
64571 |
seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) {
|
|
|
64572 |
return seekToProgramTime({
|
|
|
64573 |
programTime,
|
|
|
64574 |
playlist: this.playlistController_.media(),
|
|
|
64575 |
retryCount,
|
|
|
64576 |
pauseAfterSeek,
|
|
|
64577 |
seekTo: this.options_.seekTo,
|
|
|
64578 |
tech: this.options_.tech,
|
|
|
64579 |
callback
|
|
|
64580 |
});
|
|
|
64581 |
}
|
|
|
64582 |
/**
|
|
|
64583 |
* Adds the onRequest, onResponse, offRequest and offResponse functions
|
|
|
64584 |
* to the VhsHandler xhr Object.
|
|
|
64585 |
*/
|
|
|
64586 |
|
|
|
64587 |
setupXhrHooks_() {
|
|
|
64588 |
/**
|
|
|
64589 |
* A player function for setting an onRequest hook
|
|
|
64590 |
*
|
|
|
64591 |
* @param {function} callback for request modifiction
|
|
|
64592 |
*/
|
|
|
64593 |
this.xhr.onRequest = callback => {
|
|
|
64594 |
addOnRequestHook(this.xhr, callback);
|
|
|
64595 |
};
|
|
|
64596 |
/**
|
|
|
64597 |
* A player function for setting an onResponse hook
|
|
|
64598 |
*
|
|
|
64599 |
* @param {callback} callback for response data retrieval
|
|
|
64600 |
*/
|
|
|
64601 |
|
|
|
64602 |
this.xhr.onResponse = callback => {
|
|
|
64603 |
addOnResponseHook(this.xhr, callback);
|
|
|
64604 |
};
|
|
|
64605 |
/**
|
|
|
64606 |
* Deletes a player onRequest callback if it exists
|
|
|
64607 |
*
|
|
|
64608 |
* @param {function} callback to delete from the player set
|
|
|
64609 |
*/
|
|
|
64610 |
|
|
|
64611 |
this.xhr.offRequest = callback => {
|
|
|
64612 |
removeOnRequestHook(this.xhr, callback);
|
|
|
64613 |
};
|
|
|
64614 |
/**
|
|
|
64615 |
* Deletes a player onResponse callback if it exists
|
|
|
64616 |
*
|
|
|
64617 |
* @param {function} callback to delete from the player set
|
|
|
64618 |
*/
|
|
|
64619 |
|
|
|
64620 |
this.xhr.offResponse = callback => {
|
|
|
64621 |
removeOnResponseHook(this.xhr, callback);
|
|
|
64622 |
}; // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks.
|
|
|
64623 |
// This allows hooks to be set before the source is set to vhs when handleSource is called.
|
|
|
64624 |
|
|
|
64625 |
this.player_.trigger('xhr-hooks-ready');
|
|
|
64626 |
}
|
|
|
64627 |
}
|
|
|
64628 |
/**
|
|
|
64629 |
* The Source Handler object, which informs video.js what additional
|
|
|
64630 |
* MIME types are supported and sets up playback. It is registered
|
|
|
64631 |
* automatically to the appropriate tech based on the capabilities of
|
|
|
64632 |
* the browser it is running in. It is not necessary to use or modify
|
|
|
64633 |
* this object in normal usage.
|
|
|
64634 |
*/
|
|
|
64635 |
|
|
|
64636 |
const VhsSourceHandler = {
|
|
|
64637 |
name: 'videojs-http-streaming',
|
|
|
64638 |
VERSION: version$4,
|
|
|
64639 |
canHandleSource(srcObj, options = {}) {
|
|
|
64640 |
const localOptions = merge(videojs.options, options);
|
|
|
64641 |
return VhsSourceHandler.canPlayType(srcObj.type, localOptions);
|
|
|
64642 |
},
|
|
|
64643 |
handleSource(source, tech, options = {}) {
|
|
|
64644 |
const localOptions = merge(videojs.options, options);
|
|
|
64645 |
tech.vhs = new VhsHandler(source, tech, localOptions);
|
|
|
64646 |
tech.vhs.xhr = xhrFactory();
|
|
|
64647 |
tech.vhs.setupXhrHooks_();
|
|
|
64648 |
tech.vhs.src(source.src, source.type);
|
|
|
64649 |
return tech.vhs;
|
|
|
64650 |
},
|
|
|
64651 |
canPlayType(type, options) {
|
|
|
64652 |
const simpleType = simpleTypeFromSourceType(type);
|
|
|
64653 |
if (!simpleType) {
|
|
|
64654 |
return '';
|
|
|
64655 |
}
|
|
|
64656 |
const overrideNative = VhsSourceHandler.getOverrideNative(options);
|
|
|
64657 |
const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType);
|
|
|
64658 |
const canUseMsePlayback = !supportsTypeNatively || overrideNative;
|
|
|
64659 |
return canUseMsePlayback ? 'maybe' : '';
|
|
|
64660 |
},
|
|
|
64661 |
getOverrideNative(options = {}) {
|
|
|
64662 |
const {
|
|
|
64663 |
vhs = {}
|
|
|
64664 |
} = options;
|
|
|
64665 |
const defaultOverrideNative = !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS);
|
|
|
64666 |
const {
|
|
|
64667 |
overrideNative = defaultOverrideNative
|
|
|
64668 |
} = vhs;
|
|
|
64669 |
return overrideNative;
|
|
|
64670 |
}
|
|
|
64671 |
};
|
|
|
64672 |
/**
|
|
|
64673 |
* Check to see if the native MediaSource object exists and supports
|
|
|
64674 |
* an MP4 container with both H.264 video and AAC-LC audio.
|
|
|
64675 |
*
|
|
|
64676 |
* @return {boolean} if native media sources are supported
|
|
|
64677 |
*/
|
|
|
64678 |
|
|
|
64679 |
const supportsNativeMediaSources = () => {
|
|
|
64680 |
return browserSupportsCodec('avc1.4d400d,mp4a.40.2');
|
|
|
64681 |
}; // register source handlers with the appropriate techs
|
|
|
64682 |
|
|
|
64683 |
if (supportsNativeMediaSources()) {
|
|
|
64684 |
videojs.getTech('Html5').registerSourceHandler(VhsSourceHandler, 0);
|
|
|
64685 |
}
|
|
|
64686 |
videojs.VhsHandler = VhsHandler;
|
|
|
64687 |
videojs.VhsSourceHandler = VhsSourceHandler;
|
|
|
64688 |
videojs.Vhs = Vhs;
|
|
|
64689 |
if (!videojs.use) {
|
|
|
64690 |
videojs.registerComponent('Vhs', Vhs);
|
|
|
64691 |
}
|
|
|
64692 |
videojs.options.vhs = videojs.options.vhs || {};
|
|
|
64693 |
if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) {
|
|
|
64694 |
videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError);
|
|
|
64695 |
}
|
|
|
64696 |
|
|
|
64697 |
return videojs;
|
|
|
64698 |
|
|
|
64699 |
}));
|