Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / node_modules / videojs-vtt.js / lib / vtt.js
1 /**
2  * Copyright 2013 vtt.js Contributors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
18 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
19
20 (function(global) {
21
22   var _objCreate = Object.create || (function() {
23     function F() {}
24     return function(o) {
25       if (arguments.length !== 1) {
26         throw new Error('Object.create shim only accepts one parameter.');
27       }
28       F.prototype = o;
29       return new F();
30     };
31   })();
32
33   // Creates a new ParserError object from an errorData object. The errorData
34   // object should have default code and message properties. The default message
35   // property can be overriden by passing in a message parameter.
36   // See ParsingError.Errors below for acceptable errors.
37   function ParsingError(errorData, message) {
38     this.name = "ParsingError";
39     this.code = errorData.code;
40     this.message = message || errorData.message;
41   }
42   ParsingError.prototype = _objCreate(Error.prototype);
43   ParsingError.prototype.constructor = ParsingError;
44
45   // ParsingError metadata for acceptable ParsingErrors.
46   ParsingError.Errors = {
47     BadSignature: {
48       code: 0,
49       message: "Malformed WebVTT signature."
50     },
51     BadTimeStamp: {
52       code: 1,
53       message: "Malformed time stamp."
54     }
55   };
56
57   // Try to parse input as a time stamp.
58   function parseTimeStamp(input) {
59
60     function computeSeconds(h, m, s, f) {
61       return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
62     }
63
64     var m = input.match(/^(\d+):(\d{2})(:\d{2})?\.(\d{3})/);
65     if (!m) {
66       return null;
67     }
68
69     if (m[3]) {
70       // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
71       return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]);
72     } else if (m[1] > 59) {
73       // Timestamp takes the form of [hours]:[minutes].[milliseconds]
74       // First position is hours as it's over 59.
75       return computeSeconds(m[1], m[2], 0,  m[4]);
76     } else {
77       // Timestamp takes the form of [minutes]:[seconds].[milliseconds]
78       return computeSeconds(0, m[1], m[2], m[4]);
79     }
80   }
81
82   // A settings object holds key/value pairs and will ignore anything but the first
83   // assignment to a specific key.
84   function Settings() {
85     this.values = _objCreate(null);
86   }
87
88   Settings.prototype = {
89     // Only accept the first assignment to any key.
90     set: function(k, v) {
91       if (!this.get(k) && v !== "") {
92         this.values[k] = v;
93       }
94     },
95     // Return the value for a key, or a default value.
96     // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
97     // a number of possible default values as properties where 'defaultKey' is
98     // the key of the property that will be chosen; otherwise it's assumed to be
99     // a single value.
100     get: function(k, dflt, defaultKey) {
101       if (defaultKey) {
102         return this.has(k) ? this.values[k] : dflt[defaultKey];
103       }
104       return this.has(k) ? this.values[k] : dflt;
105     },
106     // Check whether we have a value for a key.
107     has: function(k) {
108       return k in this.values;
109     },
110     // Accept a setting if its one of the given alternatives.
111     alt: function(k, v, a) {
112       for (var n = 0; n < a.length; ++n) {
113         if (v === a[n]) {
114           this.set(k, v);
115           break;
116         }
117       }
118     },
119     // Accept a setting if its a valid (signed) integer.
120     integer: function(k, v) {
121       if (/^-?\d+$/.test(v)) { // integer
122         this.set(k, parseInt(v, 10));
123       }
124     },
125     // Accept a setting if its a valid percentage.
126     percent: function(k, v) {
127       var m;
128       if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) {
129         v = parseFloat(v);
130         if (v >= 0 && v <= 100) {
131           this.set(k, v);
132           return true;
133         }
134       }
135       return false;
136     }
137   };
138
139   // Helper function to parse input into groups separated by 'groupDelim', and
140   // interprete each group as a key/value pair separated by 'keyValueDelim'.
141   function parseOptions(input, callback, keyValueDelim, groupDelim) {
142     var groups = groupDelim ? input.split(groupDelim) : [input];
143     for (var i in groups) {
144       if (typeof groups[i] !== "string") {
145         continue;
146       }
147       var kv = groups[i].split(keyValueDelim);
148       if (kv.length !== 2) {
149         continue;
150       }
151       var k = kv[0];
152       var v = kv[1];
153       callback(k, v);
154     }
155   }
156
157   function parseCue(input, cue, regionList) {
158     // Remember the original input if we need to throw an error.
159     var oInput = input;
160     // 4.1 WebVTT timestamp
161     function consumeTimeStamp() {
162       var ts = parseTimeStamp(input);
163       if (ts === null) {
164         throw new ParsingError(ParsingError.Errors.BadTimeStamp,
165                               "Malformed timestamp: " + oInput);
166       }
167       // Remove time stamp from input.
168       input = input.replace(/^[^\sa-zA-Z-]+/, "");
169       return ts;
170     }
171
172     // 4.4.2 WebVTT cue settings
173     function consumeCueSettings(input, cue) {
174       var settings = new Settings();
175
176       parseOptions(input, function (k, v) {
177         switch (k) {
178         case "region":
179           // Find the last region we parsed with the same region id.
180           for (var i = regionList.length - 1; i >= 0; i--) {
181             if (regionList[i].id === v) {
182               settings.set(k, regionList[i].region);
183               break;
184             }
185           }
186           break;
187         case "vertical":
188           settings.alt(k, v, ["rl", "lr"]);
189           break;
190         case "line":
191           var vals = v.split(","),
192               vals0 = vals[0];
193           settings.integer(k, vals0);
194           settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
195           settings.alt(k, vals0, ["auto"]);
196           if (vals.length === 2) {
197             settings.alt("lineAlign", vals[1], ["start", "middle", "end"]);
198           }
199           break;
200         case "position":
201           vals = v.split(",");
202           settings.percent(k, vals[0]);
203           if (vals.length === 2) {
204             settings.alt("positionAlign", vals[1], ["start", "middle", "end"]);
205           }
206           break;
207         case "size":
208           settings.percent(k, v);
209           break;
210         case "align":
211           settings.alt(k, v, ["start", "middle", "end", "left", "right"]);
212           break;
213         }
214       }, /:/, /\s/);
215
216       // Apply default values for any missing fields.
217       cue.region = settings.get("region", null);
218       cue.vertical = settings.get("vertical", "");
219       cue.line = settings.get("line", "auto");
220       cue.lineAlign = settings.get("lineAlign", "start");
221       cue.snapToLines = settings.get("snapToLines", true);
222       cue.size = settings.get("size", 100);
223       cue.align = settings.get("align", "middle");
224       cue.position = settings.get("position", {
225         start: 0,
226         left: 0,
227         middle: 50,
228         end: 100,
229         right: 100
230       }, cue.align);
231       cue.positionAlign = settings.get("positionAlign", {
232         start: "start",
233         left: "start",
234         middle: "middle",
235         end: "end",
236         right: "end"
237       }, cue.align);
238     }
239
240     function skipWhitespace() {
241       input = input.replace(/^\s+/, "");
242     }
243
244     // 4.1 WebVTT cue timings.
245     skipWhitespace();
246     cue.startTime = consumeTimeStamp();   // (1) collect cue start time
247     skipWhitespace();
248     if (input.substr(0, 3) !== "-->") {     // (3) next characters must match "-->"
249       throw new ParsingError(ParsingError.Errors.BadTimeStamp,
250                              "Malformed time stamp (time stamps must be separated by '-->'): " +
251                              oInput);
252     }
253     input = input.substr(3);
254     skipWhitespace();
255     cue.endTime = consumeTimeStamp();     // (5) collect cue end time
256
257     // 4.1 WebVTT cue settings list.
258     skipWhitespace();
259     consumeCueSettings(input, cue);
260   }
261
262   var ESCAPE = {
263     "&amp;": "&",
264     "&lt;": "<",
265     "&gt;": ">",
266     "&lrm;": "\u200e",
267     "&rlm;": "\u200f",
268     "&nbsp;": "\u00a0"
269   };
270
271   var TAG_NAME = {
272     c: "span",
273     i: "i",
274     b: "b",
275     u: "u",
276     ruby: "ruby",
277     rt: "rt",
278     v: "span",
279     lang: "span"
280   };
281
282   var TAG_ANNOTATION = {
283     v: "title",
284     lang: "lang"
285   };
286
287   var NEEDS_PARENT = {
288     rt: "ruby"
289   };
290
291   // Parse content into a document fragment.
292   function parseContent(window, input) {
293     function nextToken() {
294       // Check for end-of-string.
295       if (!input) {
296         return null;
297       }
298
299       // Consume 'n' characters from the input.
300       function consume(result) {
301         input = input.substr(result.length);
302         return result;
303       }
304
305       var m = input.match(/^([^<]*)(<[^>]+>?)?/);
306       // If there is some text before the next tag, return it, otherwise return
307       // the tag.
308       return consume(m[1] ? m[1] : m[2]);
309     }
310
311     // Unescape a string 's'.
312     function unescape1(e) {
313       return ESCAPE[e];
314     }
315     function unescape(s) {
316       while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) {
317         s = s.replace(m[0], unescape1);
318       }
319       return s;
320     }
321
322     function shouldAdd(current, element) {
323       return !NEEDS_PARENT[element.localName] ||
324              NEEDS_PARENT[element.localName] === current.localName;
325     }
326
327     // Create an element for this tag.
328     function createElement(type, annotation) {
329       var tagName = TAG_NAME[type];
330       if (!tagName) {
331         return null;
332       }
333       var element = window.document.createElement(tagName);
334       element.localName = tagName;
335       var name = TAG_ANNOTATION[type];
336       if (name && annotation) {
337         element[name] = annotation.trim();
338       }
339       return element;
340     }
341
342     var rootDiv = window.document.createElement("div"),
343         current = rootDiv,
344         t,
345         tagStack = [];
346
347     while ((t = nextToken()) !== null) {
348       if (t[0] === '<') {
349         if (t[1] === "/") {
350           // If the closing tag matches, move back up to the parent node.
351           if (tagStack.length &&
352               tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
353             tagStack.pop();
354             current = current.parentNode;
355           }
356           // Otherwise just ignore the end tag.
357           continue;
358         }
359         var ts = parseTimeStamp(t.substr(1, t.length - 2));
360         var node;
361         if (ts) {
362           // Timestamps are lead nodes as well.
363           node = window.document.createProcessingInstruction("timestamp", ts);
364           current.appendChild(node);
365           continue;
366         }
367         var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
368         // If we can't parse the tag, skip to the next tag.
369         if (!m) {
370           continue;
371         }
372         // Try to construct an element, and ignore the tag if we couldn't.
373         node = createElement(m[1], m[3]);
374         if (!node) {
375           continue;
376         }
377         // Determine if the tag should be added based on the context of where it
378         // is placed in the cuetext.
379         if (!shouldAdd(current, node)) {
380           continue;
381         }
382         // Set the class list (as a list of classes, separated by space).
383         if (m[2]) {
384           node.className = m[2].substr(1).replace('.', ' ');
385         }
386         // Append the node to the current node, and enter the scope of the new
387         // node.
388         tagStack.push(m[1]);
389         current.appendChild(node);
390         current = node;
391         continue;
392       }
393
394       // Text nodes are leaf nodes.
395       current.appendChild(window.document.createTextNode(unescape(t)));
396     }
397
398     return rootDiv;
399   }
400
401   // This is a list of all the Unicode characters that have a strong
402   // right-to-left category. What this means is that these characters are
403   // written right-to-left for sure. It was generated by pulling all the strong
404   // right-to-left characters out of the Unicode data table. That table can
405   // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
406   var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6],
407    [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d],
408    [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6],
409    [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5],
410    [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815],
411    [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858],
412    [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f],
413    [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c],
414    [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1],
415    [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc],
416    [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808],
417    [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855],
418    [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f],
419    [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13],
420    [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58],
421    [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72],
422    [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f],
423    [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32],
424    [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42],
425    [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f],
426    [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59],
427    [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62],
428    [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77],
429    [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b],
430    [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]];
431
432   function isStrongRTLChar(charCode) {
433     for (var i = 0; i < strongRTLRanges.length; i++) {
434       var currentRange = strongRTLRanges[i];
435       if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
436         return true;
437       }
438     }
439
440     return false;
441   }
442
443   function determineBidi(cueDiv) {
444     var nodeStack = [],
445         text = "",
446         charCode;
447
448     if (!cueDiv || !cueDiv.childNodes) {
449       return "ltr";
450     }
451
452     function pushNodes(nodeStack, node) {
453       for (var i = node.childNodes.length - 1; i >= 0; i--) {
454         nodeStack.push(node.childNodes[i]);
455       }
456     }
457
458     function nextTextNode(nodeStack) {
459       if (!nodeStack || !nodeStack.length) {
460         return null;
461       }
462
463       var node = nodeStack.pop(),
464           text = node.textContent || node.innerText;
465       if (text) {
466         // TODO: This should match all unicode type B characters (paragraph
467         // separator characters). See issue #115.
468         var m = text.match(/^.*(\n|\r)/);
469         if (m) {
470           nodeStack.length = 0;
471           return m[0];
472         }
473         return text;
474       }
475       if (node.tagName === "ruby") {
476         return nextTextNode(nodeStack);
477       }
478       if (node.childNodes) {
479         pushNodes(nodeStack, node);
480         return nextTextNode(nodeStack);
481       }
482     }
483
484     pushNodes(nodeStack, cueDiv);
485     while ((text = nextTextNode(nodeStack))) {
486       for (var i = 0; i < text.length; i++) {
487         charCode = text.charCodeAt(i);
488         if (isStrongRTLChar(charCode)) {
489           return "rtl";
490         }
491       }
492     }
493     return "ltr";
494   }
495
496   function computeLinePos(cue) {
497     if (typeof cue.line === "number" &&
498         (cue.snapToLines || (cue.line >= 0 && cue.line <= 100))) {
499       return cue.line;
500     }
501     if (!cue.track || !cue.track.textTrackList ||
502         !cue.track.textTrackList.mediaElement) {
503       return -1;
504     }
505     var track = cue.track,
506         trackList = track.textTrackList,
507         count = 0;
508     for (var i = 0; i < trackList.length && trackList[i] !== track; i++) {
509       if (trackList[i].mode === "showing") {
510         count++;
511       }
512     }
513     return ++count * -1;
514   }
515
516   function StyleBox() {
517   }
518
519   // Apply styles to a div. If there is no div passed then it defaults to the
520   // div on 'this'.
521   StyleBox.prototype.applyStyles = function(styles, div) {
522     div = div || this.div;
523     for (var prop in styles) {
524       if (styles.hasOwnProperty(prop)) {
525         div.style[prop] = styles[prop];
526       }
527     }
528   };
529
530   StyleBox.prototype.formatStyle = function(val, unit) {
531     return val === 0 ? 0 : val + unit;
532   };
533
534   // Constructs the computed display state of the cue (a div). Places the div
535   // into the overlay which should be a block level element (usually a div).
536   function CueStyleBox(window, cue, styleOptions) {
537     var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent);
538     var color = "rgba(255, 255, 255, 1)";
539     var backgroundColor = "rgba(0, 0, 0, 0.8)";
540
541     if (isIE8) {
542       color = "rgb(255, 255, 255)";
543       backgroundColor = "rgb(0, 0, 0)";
544     }
545
546     StyleBox.call(this);
547     this.cue = cue;
548
549     // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
550     // have inline positioning and will function as the cue background box.
551     this.cueDiv = parseContent(window, cue.text);
552     var styles = {
553       color: color,
554       backgroundColor: backgroundColor,
555       position: "relative",
556       left: 0,
557       right: 0,
558       top: 0,
559       bottom: 0,
560       display: "inline"
561     };
562
563     if (!isIE8) {
564       styles.writingMode = cue.vertical === "" ? "horizontal-tb"
565                                                : cue.vertical === "lr" ? "vertical-lr"
566                                                                        : "vertical-rl";
567       styles.unicodeBidi = "plaintext";
568     }
569     this.applyStyles(styles, this.cueDiv);
570
571     // Create an absolutely positioned div that will be used to position the cue
572     // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
573     // mirrors of them except "middle" which is "center" in CSS.
574     this.div = window.document.createElement("div");
575     styles = {
576       textAlign: cue.align === "middle" ? "center" : cue.align,
577       font: styleOptions.font,
578       whiteSpace: "pre-line",
579       position: "absolute"
580     };
581
582     if (!isIE8) {
583       styles.direction = determineBidi(this.cueDiv);
584       styles.writingMode = cue.vertical === "" ? "horizontal-tb"
585                                                : cue.vertical === "lr" ? "vertical-lr"
586                                                                        : "vertical-rl".
587       stylesunicodeBidi =  "plaintext";
588     }
589
590     this.applyStyles(styles);
591
592     this.div.appendChild(this.cueDiv);
593
594     // Calculate the distance from the reference edge of the viewport to the text
595     // position of the cue box. The reference edge will be resolved later when
596     // the box orientation styles are applied.
597     var textPos = 0;
598     switch (cue.positionAlign) {
599     case "start":
600       textPos = cue.position;
601       break;
602     case "middle":
603       textPos = cue.position - (cue.size / 2);
604       break;
605     case "end":
606       textPos = cue.position - cue.size;
607       break;
608     }
609
610     // Horizontal box orientation; textPos is the distance from the left edge of the
611     // area to the left edge of the box and cue.size is the distance extending to
612     // the right from there.
613     if (cue.vertical === "") {
614       this.applyStyles({
615         left:  this.formatStyle(textPos, "%"),
616         width: this.formatStyle(cue.size, "%")
617       });
618     // Vertical box orientation; textPos is the distance from the top edge of the
619     // area to the top edge of the box and cue.size is the height extending
620     // downwards from there.
621     } else {
622       this.applyStyles({
623         top: this.formatStyle(textPos, "%"),
624         height: this.formatStyle(cue.size, "%")
625       });
626     }
627
628     this.move = function(box) {
629       this.applyStyles({
630         top: this.formatStyle(box.top, "px"),
631         bottom: this.formatStyle(box.bottom, "px"),
632         left: this.formatStyle(box.left, "px"),
633         right: this.formatStyle(box.right, "px"),
634         height: this.formatStyle(box.height, "px"),
635         width: this.formatStyle(box.width, "px")
636       });
637     };
638   }
639   CueStyleBox.prototype = _objCreate(StyleBox.prototype);
640   CueStyleBox.prototype.constructor = CueStyleBox;
641
642   // Represents the co-ordinates of an Element in a way that we can easily
643   // compute things with such as if it overlaps or intersects with another Element.
644   // Can initialize it with either a StyleBox or another BoxPosition.
645   function BoxPosition(obj) {
646     var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent);
647
648     // Either a BoxPosition was passed in and we need to copy it, or a StyleBox
649     // was passed in and we need to copy the results of 'getBoundingClientRect'
650     // as the object returned is readonly. All co-ordinate values are in reference
651     // to the viewport origin (top left).
652     var lh, height, width, top;
653     if (obj.div) {
654       height = obj.div.offsetHeight;
655       width = obj.div.offsetWidth;
656       top = obj.div.offsetTop;
657
658       var rects = (rects = obj.div.childNodes) && (rects = rects[0]) &&
659                   rects.getClientRects && rects.getClientRects();
660       obj = obj.div.getBoundingClientRect();
661       // In certain cases the outter div will be slightly larger then the sum of
662       // the inner div's lines. This could be due to bold text, etc, on some platforms.
663       // In this case we should get the average line height and use that. This will
664       // result in the desired behaviour.
665       lh = rects ? Math.max((rects[0] && rects[0].height) || 0, obj.height / rects.length)
666                  : 0;
667
668     }
669     this.left = obj.left;
670     this.right = obj.right;
671     this.top = obj.top || top;
672     this.height = obj.height || height;
673     this.bottom = obj.bottom || (top + (obj.height || height));
674     this.width = obj.width || width;
675     this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
676
677     if (isIE8 && !this.lineHeight) {
678       this.lineHeight = 13;
679     }
680   }
681
682   // Move the box along a particular axis. Optionally pass in an amount to move
683   // the box. If no amount is passed then the default is the line height of the
684   // box.
685   BoxPosition.prototype.move = function(axis, toMove) {
686     toMove = toMove !== undefined ? toMove : this.lineHeight;
687     switch (axis) {
688     case "+x":
689       this.left += toMove;
690       this.right += toMove;
691       break;
692     case "-x":
693       this.left -= toMove;
694       this.right -= toMove;
695       break;
696     case "+y":
697       this.top += toMove;
698       this.bottom += toMove;
699       break;
700     case "-y":
701       this.top -= toMove;
702       this.bottom -= toMove;
703       break;
704     }
705   };
706
707   // Check if this box overlaps another box, b2.
708   BoxPosition.prototype.overlaps = function(b2) {
709     return this.left < b2.right &&
710            this.right > b2.left &&
711            this.top < b2.bottom &&
712            this.bottom > b2.top;
713   };
714
715   // Check if this box overlaps any other boxes in boxes.
716   BoxPosition.prototype.overlapsAny = function(boxes) {
717     for (var i = 0; i < boxes.length; i++) {
718       if (this.overlaps(boxes[i])) {
719         return true;
720       }
721     }
722     return false;
723   };
724
725   // Check if this box is within another box.
726   BoxPosition.prototype.within = function(container) {
727     return this.top >= container.top &&
728            this.bottom <= container.bottom &&
729            this.left >= container.left &&
730            this.right <= container.right;
731   };
732
733   // Check if this box is entirely within the container or it is overlapping
734   // on the edge opposite of the axis direction passed. For example, if "+x" is
735   // passed and the box is overlapping on the left edge of the container, then
736   // return true.
737   BoxPosition.prototype.overlapsOppositeAxis = function(container, axis) {
738     switch (axis) {
739     case "+x":
740       return this.left < container.left;
741     case "-x":
742       return this.right > container.right;
743     case "+y":
744       return this.top < container.top;
745     case "-y":
746       return this.bottom > container.bottom;
747     }
748   };
749
750   // Find the percentage of the area that this box is overlapping with another
751   // box.
752   BoxPosition.prototype.intersectPercentage = function(b2) {
753     var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
754         y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
755         intersectArea = x * y;
756     return intersectArea / (this.height * this.width);
757   };
758
759   // Convert the positions from this box to CSS compatible positions using
760   // the reference container's positions. This has to be done because this
761   // box's positions are in reference to the viewport origin, whereas, CSS
762   // values are in referecne to their respective edges.
763   BoxPosition.prototype.toCSSCompatValues = function(reference) {
764     return {
765       top: this.top - reference.top,
766       bottom: reference.bottom - this.bottom,
767       left: this.left - reference.left,
768       right: reference.right - this.right,
769       height: this.height,
770       width: this.width
771     };
772   };
773
774   // Get an object that represents the box's position without anything extra.
775   // Can pass a StyleBox, HTMLElement, or another BoxPositon.
776   BoxPosition.getSimpleBoxPosition = function(obj) {
777     var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0;
778     var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0;
779     var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0;
780
781     obj = obj.div ? obj.div.getBoundingClientRect() :
782                   obj.tagName ? obj.getBoundingClientRect() : obj;
783     var ret = {
784       left: obj.left,
785       right: obj.right,
786       top: obj.top || top,
787       height: obj.height || height,
788       bottom: obj.bottom || (top + (obj.height || height)),
789       width: obj.width || width
790     };
791     return ret;
792   };
793
794   // Move a StyleBox to its specified, or next best, position. The containerBox
795   // is the box that contains the StyleBox, such as a div. boxPositions are
796   // a list of other boxes that the styleBox can't overlap with.
797   function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
798
799     // Find the best position for a cue box, b, on the video. The axis parameter
800     // is a list of axis, the order of which, it will move the box along. For example:
801     // Passing ["+x", "-x"] will move the box first along the x axis in the positive
802     // direction. If it doesn't find a good position for it there it will then move
803     // it along the x axis in the negative direction.
804     function findBestPosition(b, axis) {
805       var bestPosition,
806           specifiedPosition = new BoxPosition(b),
807           percentage = 1; // Highest possible so the first thing we get is better.
808
809       for (var i = 0; i < axis.length; i++) {
810         while (b.overlapsOppositeAxis(containerBox, axis[i]) ||
811                (b.within(containerBox) && b.overlapsAny(boxPositions))) {
812           b.move(axis[i]);
813         }
814         // We found a spot where we aren't overlapping anything. This is our
815         // best position.
816         if (b.within(containerBox)) {
817           return b;
818         }
819         var p = b.intersectPercentage(containerBox);
820         // If we're outside the container box less then we were on our last try
821         // then remember this position as the best position.
822         if (percentage > p) {
823           bestPosition = new BoxPosition(b);
824           percentage = p;
825         }
826         // Reset the box position to the specified position.
827         b = new BoxPosition(specifiedPosition);
828       }
829       return bestPosition || specifiedPosition;
830     }
831
832     var boxPosition = new BoxPosition(styleBox),
833         cue = styleBox.cue,
834         linePos = computeLinePos(cue),
835         axis = [];
836
837     // If we have a line number to align the cue to.
838     if (cue.snapToLines) {
839       var size;
840       switch (cue.vertical) {
841       case "":
842         axis = [ "+y", "-y" ];
843         size = "height";
844         break;
845       case "rl":
846         axis = [ "+x", "-x" ];
847         size = "width";
848         break;
849       case "lr":
850         axis = [ "-x", "+x" ];
851         size = "width";
852         break;
853       }
854
855       var step = boxPosition.lineHeight,
856           position = step * Math.round(linePos),
857           maxPosition = containerBox[size] + step,
858           initialAxis = axis[0];
859
860       // If the specified intial position is greater then the max position then
861       // clamp the box to the amount of steps it would take for the box to
862       // reach the max position.
863       if (Math.abs(position) > maxPosition) {
864         position = position < 0 ? -1 : 1;
865         position *= Math.ceil(maxPosition / step) * step;
866       }
867
868       // If computed line position returns negative then line numbers are
869       // relative to the bottom of the video instead of the top. Therefore, we
870       // need to increase our initial position by the length or width of the
871       // video, depending on the writing direction, and reverse our axis directions.
872       if (linePos < 0) {
873         position += cue.vertical === "" ? containerBox.height : containerBox.width;
874         axis = axis.reverse();
875       }
876
877       // Move the box to the specified position. This may not be its best
878       // position.
879       boxPosition.move(initialAxis, position);
880
881     } else {
882       // If we have a percentage line value for the cue.
883       var calculatedPercentage = (boxPosition.lineHeight / containerBox.height) * 100;
884
885       switch (cue.lineAlign) {
886       case "middle":
887         linePos -= (calculatedPercentage / 2);
888         break;
889       case "end":
890         linePos -= calculatedPercentage;
891         break;
892       }
893
894       // Apply initial line position to the cue box.
895       switch (cue.vertical) {
896       case "":
897         styleBox.applyStyles({
898           top: styleBox.formatStyle(linePos, "%")
899         });
900         break;
901       case "rl":
902         styleBox.applyStyles({
903           left: styleBox.formatStyle(linePos, "%")
904         });
905         break;
906       case "lr":
907         styleBox.applyStyles({
908           right: styleBox.formatStyle(linePos, "%")
909         });
910         break;
911       }
912
913       axis = [ "+y", "-x", "+x", "-y" ];
914
915       // Get the box position again after we've applied the specified positioning
916       // to it.
917       boxPosition = new BoxPosition(styleBox);
918     }
919
920     var bestPosition = findBestPosition(boxPosition, axis);
921     styleBox.move(bestPosition.toCSSCompatValues(containerBox));
922   }
923
924   function WebVTT() {
925     // Nothing
926   }
927
928   // Helper to allow strings to be decoded instead of the default binary utf8 data.
929   WebVTT.StringDecoder = function() {
930     return {
931       decode: function(data) {
932         if (!data) {
933           return "";
934         }
935         if (typeof data !== "string") {
936           throw new Error("Error - expected string data.");
937         }
938         return decodeURIComponent(encodeURIComponent(data));
939       }
940     };
941   };
942
943   WebVTT.convertCueToDOMTree = function(window, cuetext) {
944     if (!window || !cuetext) {
945       return null;
946     }
947     return parseContent(window, cuetext);
948   };
949
950   var FONT_SIZE_PERCENT = 0.05;
951   var FONT_STYLE = "sans-serif";
952   var CUE_BACKGROUND_PADDING = "1.5%";
953
954   // Runs the processing model over the cues and regions passed to it.
955   // @param overlay A block level element (usually a div) that the computed cues
956   //                and regions will be placed into.
957   WebVTT.processCues = function(window, cues, overlay) {
958     if (!window || !cues || !overlay) {
959       return null;
960     }
961
962     // Remove all previous children.
963     while (overlay.firstChild) {
964       overlay.removeChild(overlay.firstChild);
965     }
966
967     var paddedOverlay = window.document.createElement("div");
968     paddedOverlay.style.position = "absolute";
969     paddedOverlay.style.left = "0";
970     paddedOverlay.style.right = "0";
971     paddedOverlay.style.top = "0";
972     paddedOverlay.style.bottom = "0";
973     paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
974     overlay.appendChild(paddedOverlay);
975
976     // Determine if we need to compute the display states of the cues. This could
977     // be the case if a cue's state has been changed since the last computation or
978     // if it has not been computed yet.
979     function shouldCompute(cues) {
980       for (var i = 0; i < cues.length; i++) {
981         if (cues[i].hasBeenReset || !cues[i].displayState) {
982           return true;
983         }
984       }
985       return false;
986     }
987
988     // We don't need to recompute the cues' display states. Just reuse them.
989     if (!shouldCompute(cues)) {
990       for (var i = 0; i < cues.length; i++) {
991         paddedOverlay.appendChild(cues[i].displayState);
992       }
993       return;
994     }
995
996     var boxPositions = [],
997         containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
998         fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
999     var styleOptions = {
1000       font: fontSize + "px " + FONT_STYLE
1001     };
1002
1003     (function() {
1004       var styleBox, cue;
1005
1006       for (var i = 0; i < cues.length; i++) {
1007         cue = cues[i];
1008
1009         // Compute the intial position and styles of the cue div.
1010         styleBox = new CueStyleBox(window, cue, styleOptions);
1011         paddedOverlay.appendChild(styleBox.div);
1012
1013         // Move the cue div to it's correct line position.
1014         moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
1015
1016         // Remember the computed div so that we don't have to recompute it later
1017         // if we don't have too.
1018         cue.displayState = styleBox.div;
1019
1020         boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
1021       }
1022     })();
1023   };
1024
1025   WebVTT.Parser = function(window, vttjs, decoder) {
1026     if (!decoder) {
1027       decoder = vttjs;
1028       vttjs = {};
1029     }
1030     if (!vttjs) {
1031       vttjs = {};
1032     }
1033
1034     this.window = window;
1035     this.vttjs = vttjs;
1036     this.state = "INITIAL";
1037     this.buffer = "";
1038     this.decoder = decoder || new TextDecoder("utf8");
1039     this.regionList = [];
1040   };
1041
1042   WebVTT.Parser.prototype = {
1043     // If the error is a ParsingError then report it to the consumer if
1044     // possible. If it's not a ParsingError then throw it like normal.
1045     reportOrThrowError: function(e) {
1046       if (e instanceof ParsingError) {
1047         this.onparsingerror && this.onparsingerror(e);
1048       } else {
1049         throw e;
1050       }
1051     },
1052     parse: function (data) {
1053       var self = this;
1054
1055       // If there is no data then we won't decode it, but will just try to parse
1056       // whatever is in buffer already. This may occur in circumstances, for
1057       // example when flush() is called.
1058       if (data) {
1059         // Try to decode the data that we received.
1060         self.buffer += self.decoder.decode(data, {stream: true});
1061       }
1062
1063       function collectNextLine() {
1064         var buffer = self.buffer;
1065         var pos = 0;
1066         while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
1067           ++pos;
1068         }
1069         var line = buffer.substr(0, pos);
1070         // Advance the buffer early in case we fail below.
1071         if (buffer[pos] === '\r') {
1072           ++pos;
1073         }
1074         if (buffer[pos] === '\n') {
1075           ++pos;
1076         }
1077         self.buffer = buffer.substr(pos);
1078         return line;
1079       }
1080
1081       // 3.4 WebVTT region and WebVTT region settings syntax
1082       function parseRegion(input) {
1083         var settings = new Settings();
1084
1085         parseOptions(input, function (k, v) {
1086           switch (k) {
1087           case "id":
1088             settings.set(k, v);
1089             break;
1090           case "width":
1091             settings.percent(k, v);
1092             break;
1093           case "lines":
1094             settings.integer(k, v);
1095             break;
1096           case "regionanchor":
1097           case "viewportanchor":
1098             var xy = v.split(',');
1099             if (xy.length !== 2) {
1100               break;
1101             }
1102             // We have to make sure both x and y parse, so use a temporary
1103             // settings object here.
1104             var anchor = new Settings();
1105             anchor.percent("x", xy[0]);
1106             anchor.percent("y", xy[1]);
1107             if (!anchor.has("x") || !anchor.has("y")) {
1108               break;
1109             }
1110             settings.set(k + "X", anchor.get("x"));
1111             settings.set(k + "Y", anchor.get("y"));
1112             break;
1113           case "scroll":
1114             settings.alt(k, v, ["up"]);
1115             break;
1116           }
1117         }, /=/, /\s/);
1118
1119         // Create the region, using default values for any values that were not
1120         // specified.
1121         if (settings.has("id")) {
1122           var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)();
1123           region.width = settings.get("width", 100);
1124           region.lines = settings.get("lines", 3);
1125           region.regionAnchorX = settings.get("regionanchorX", 0);
1126           region.regionAnchorY = settings.get("regionanchorY", 100);
1127           region.viewportAnchorX = settings.get("viewportanchorX", 0);
1128           region.viewportAnchorY = settings.get("viewportanchorY", 100);
1129           region.scroll = settings.get("scroll", "");
1130           // Register the region.
1131           self.onregion && self.onregion(region);
1132           // Remember the VTTRegion for later in case we parse any VTTCues that
1133           // reference it.
1134           self.regionList.push({
1135             id: settings.get("id"),
1136             region: region
1137           });
1138         }
1139       }
1140
1141       // draft-pantos-http-live-streaming-20
1142       // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
1143       // 3.5 WebVTT
1144       function parseTimestampMap(input) {
1145         var settings = new Settings();
1146
1147         parseOptions(input, function(k, v) {
1148           switch(k) {
1149           case "MPEGT":
1150             settings.integer(k + 'S', v);
1151             break;
1152           case "LOCA":
1153             settings.set(k + 'L', parseTimeStamp(v));
1154             break;
1155           }
1156         }, /[^\d]:/, /,/);
1157
1158         self.ontimestampmap && self.ontimestampmap({
1159           "MPEGTS": settings.get("MPEGTS"),
1160           "LOCAL": settings.get("LOCAL")
1161         });
1162       }
1163
1164       // 3.2 WebVTT metadata header syntax
1165       function parseHeader(input) {
1166         if (input.match(/X-TIMESTAMP-MAP/)) {
1167           // This line contains HLS X-TIMESTAMP-MAP metadata
1168           parseOptions(input, function(k, v) {
1169             switch(k) {
1170             case "X-TIMESTAMP-MAP":
1171               parseTimestampMap(v);
1172               break;
1173             }
1174           }, /=/);
1175         } else {
1176           parseOptions(input, function (k, v) {
1177             switch (k) {
1178             case "Region":
1179               // 3.3 WebVTT region metadata header syntax
1180               parseRegion(v);
1181               break;
1182             }
1183           }, /:/);
1184         }
1185
1186       }
1187
1188       // 5.1 WebVTT file parsing.
1189       try {
1190         var line;
1191         if (self.state === "INITIAL") {
1192           // We can't start parsing until we have the first line.
1193           if (!/\r\n|\n/.test(self.buffer)) {
1194             return this;
1195           }
1196
1197           line = collectNextLine();
1198
1199           var m = line.match(/^WEBVTT([ \t].*)?$/);
1200           if (!m || !m[0]) {
1201             throw new ParsingError(ParsingError.Errors.BadSignature);
1202           }
1203
1204           self.state = "HEADER";
1205         }
1206
1207         var alreadyCollectedLine = false;
1208         while (self.buffer) {
1209           // We can't parse a line until we have the full line.
1210           if (!/\r\n|\n/.test(self.buffer)) {
1211             return this;
1212           }
1213
1214           if (!alreadyCollectedLine) {
1215             line = collectNextLine();
1216           } else {
1217             alreadyCollectedLine = false;
1218           }
1219
1220           switch (self.state) {
1221           case "HEADER":
1222             // 13-18 - Allow a header (metadata) under the WEBVTT line.
1223             if (/:/.test(line)) {
1224               parseHeader(line);
1225             } else if (!line) {
1226               // An empty line terminates the header and starts the body (cues).
1227               self.state = "ID";
1228             }
1229             continue;
1230           case "NOTE":
1231             // Ignore NOTE blocks.
1232             if (!line) {
1233               self.state = "ID";
1234             }
1235             continue;
1236           case "ID":
1237             // Check for the start of NOTE blocks.
1238             if (/^NOTE($|[ \t])/.test(line)) {
1239               self.state = "NOTE";
1240               break;
1241             }
1242             // 19-29 - Allow any number of line terminators, then initialize new cue values.
1243             if (!line) {
1244               continue;
1245             }
1246             self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, "");
1247             self.state = "CUE";
1248             // 30-39 - Check if self line contains an optional identifier or timing data.
1249             if (line.indexOf("-->") === -1) {
1250               self.cue.id = line;
1251               continue;
1252             }
1253             // Process line as start of a cue.
1254             /*falls through*/
1255           case "CUE":
1256             // 40 - Collect cue timings and settings.
1257             try {
1258               parseCue(line, self.cue, self.regionList);
1259             } catch (e) {
1260               self.reportOrThrowError(e);
1261               // In case of an error ignore rest of the cue.
1262               self.cue = null;
1263               self.state = "BADCUE";
1264               continue;
1265             }
1266             self.state = "CUETEXT";
1267             continue;
1268           case "CUETEXT":
1269             var hasSubstring = line.indexOf("-->") !== -1;
1270             // 34 - If we have an empty line then report the cue.
1271             // 35 - If we have the special substring '-->' then report the cue,
1272             // but do not collect the line as we need to process the current
1273             // one as a new cue.
1274             if (!line || hasSubstring && (alreadyCollectedLine = true)) {
1275               // We are done parsing self cue.
1276               self.oncue && self.oncue(self.cue);
1277               self.cue = null;
1278               self.state = "ID";
1279               continue;
1280             }
1281             if (self.cue.text) {
1282               self.cue.text += "\n";
1283             }
1284             self.cue.text += line;
1285             continue;
1286           case "BADCUE": // BADCUE
1287             // 54-62 - Collect and discard the remaining cue.
1288             if (!line) {
1289               self.state = "ID";
1290             }
1291             continue;
1292           }
1293         }
1294       } catch (e) {
1295         self.reportOrThrowError(e);
1296
1297         // If we are currently parsing a cue, report what we have.
1298         if (self.state === "CUETEXT" && self.cue && self.oncue) {
1299           self.oncue(self.cue);
1300         }
1301         self.cue = null;
1302         // Enter BADWEBVTT state if header was not parsed correctly otherwise
1303         // another exception occurred so enter BADCUE state.
1304         self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
1305       }
1306       return this;
1307     },
1308     flush: function () {
1309       var self = this;
1310       try {
1311         // Finish decoding the stream.
1312         self.buffer += self.decoder.decode();
1313         // Synthesize the end of the current cue or region.
1314         if (self.cue || self.state === "HEADER") {
1315           self.buffer += "\n\n";
1316           self.parse();
1317         }
1318         // If we've flushed, parsed, and we're still on the INITIAL state then
1319         // that means we don't have enough of the stream to parse the first
1320         // line.
1321         if (self.state === "INITIAL") {
1322           throw new ParsingError(ParsingError.Errors.BadSignature);
1323         }
1324       } catch(e) {
1325         self.reportOrThrowError(e);
1326       }
1327       self.onflush && self.onflush();
1328       return this;
1329     }
1330   };
1331
1332   global.WebVTT = WebVTT;
1333
1334 }(this, (this.vttjs || {})));