vtt.js 41 KB

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