diff --git a/lib/vtt.js b/lib/vtt.js index 66ab23f..6b7c56a 100644 --- a/lib/vtt.js +++ b/lib/vtt.js @@ -204,15 +204,18 @@ } } - function parseCue(input, cue, regionList) { + function parseCue(input, cue, regionList, successCb, errCb) { // Remember the original input if we need to throw an error. var oInput = input; // 4.1 WebVTT timestamp function consumeTimeStamp() { var ts = parseTimeStamp(input); if (ts === null) { - throw new ParsingError(ParsingError.Errors.BadTimeStamp, - "Malformed timestamp: " + oInput); + if (errCb) { + errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed timestamp: " + oInput)); + return; + } } // Remove time stamp from input. input = input.replace(/^[^\sa-zA-Z-]+/, ""); @@ -270,15 +273,18 @@ cue.lineAlign = settings.get("lineAlign", "start"); cue.snapToLines = settings.get("snapToLines", true); cue.size = settings.get("size", 100); - cue.align = settings.get("align", "middle"); + cue.align = settings.get("align", "center"); cue.position = settings.get("position", "auto"); cue.positionAlign = settings.get("positionAlign", { start: "start", left: "start", middle: "middle", end: "end", - right: "end" + right: "end", + center: "center" }, cue.align); + + successCb(); } function skipWhitespace() { @@ -287,16 +293,23 @@ // 4.1 WebVTT cue timings. skipWhitespace(); - cue.startTime = consumeTimeStamp(); // (1) collect cue start time + + var timestamp = consumeTimeStamp() + if (timestamp === undefined) return + cue.startTime = timestamp; // (1) collect cue start time skipWhitespace(); - if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" - throw new ParsingError(ParsingError.Errors.BadTimeStamp, + if (input.substr(0, 3) !== "-->") { + // (3) next characters must match "-->" + errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + - oInput); + oInput)); + return; } input = input.substr(3); skipWhitespace(); - cue.endTime = consumeTimeStamp(); // (5) collect cue end time + var timestamp = consumeTimeStamp() + if (timestamp === undefined) return + cue.endTime = timestamp; // (5) collect cue end time // 4.1 WebVTT cue settings list. skipWhitespace(); @@ -1076,12 +1089,13 @@ })(); }; - WebVTT.Parser = function(window, decoder) { + WebVTT.Parser = function(window, decoder, VTTCue) { this.window = window; this.state = "INITIAL"; this.buffer = ""; this.decoder = decoder || new TextDecoder("utf8"); this.regionList = []; + this.VTTCue = VTTCue }; WebVTT.Parser.prototype = { @@ -1183,51 +1197,104 @@ } } - // 3.2 WebVTT metadata header syntax - function parseHeader(input) { - parseOptions(input, function (k, v) { + // draft-pantos-http-live-streaming-20 + // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5 + // 3.5 WebVTT + function parseTimestampMap(input) { + var settings = new Settings(); + + parseOptions(input, function(k, v) { switch (k) { - case "Region": - // 3.3 WebVTT region metadata header syntax - parseRegion(v); - break; + case "MPEGT": + settings.integer(k + 'S', v); + break; + case "LOCA": + settings.set(k + 'L', parseTimeStamp(v)); + break; } - }, /:/); + }, /[^\d]:/, /,/); + + self.ontimestampmap && self.ontimestampmap({ + "MPEGTS": settings.get("MPEGTS"), + "LOCAL": settings.get("LOCAL") + }); + } + + // 3.2 WebVTT metadata header syntax + function parseHeader(input) { + if (input.match(/X-TIMESTAMP-MAP/)) { + // This line contains HLS X-TIMESTAMP-MAP metadata + parseOptions(input, function(k, v) { + switch (k) { + case "X-TIMESTAMP-MAP": + parseTimestampMap(v); + break; + } + }, /=/); + } else { + parseOptions(input, function (k, v) { + switch (k) { + case "Region": + // 3.3 WebVTT region metadata header syntax + parseRegion(v); + break; + } + }, /:/); + } } // 5.1 WebVTT file parsing. - try { - var line; - if (self.state === "INITIAL") { - // We can't start parsing until we have the first line. - if (!/\r\n|\n/.test(self.buffer)) { - return this; - } - line = collectNextLine(); + function fail(e) { - var m = line.match(/^WEBVTT([ \t].*)?$/); - if (!m || !m[0]) { - throw new ParsingError(ParsingError.Errors.BadSignature); - } + self.reportOrThrowError(e); - self.state = "HEADER"; + // If we are currently parsing a cue, report what we have. + if (self.state === "CUETEXT" && self.cue && self.oncue) { + self.oncue(self.cue); } + self.cue = null; + // Enter BADWEBVTT state if header was not parsed correctly otherwise + // another exception occurred so enter BADCUE state. + self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; - var alreadyCollectedLine = false; - while (self.buffer) { - // We can't parse a line until we have the full line. - if (!/\r\n|\n/.test(self.buffer)) { - return this; - } + } - if (!alreadyCollectedLine) { - line = collectNextLine(); - } else { - alreadyCollectedLine = false; - } + var line; + + if (self.state === "INITIAL") { + // We can't start parsing until we have the first line. + if (!/\r\n|\n/.test(self.buffer)) { + return this; + } + + line = collectNextLine(); + + var m = line.match(/^WEBVTT([ \t].*)?$/); + if (!m || !m[0]) { + fail(new ParsingError(ParsingError.Errors.BadSignature)) + return + } - switch (self.state) { + self.state = "HEADER"; + } + + var alreadyCollectedLine = false; + + var lineparse = function() { + // We can't parse a line until we have the full line. + if (!self.buffer || !/\r\n|\n/.test(self.buffer)) { + self.flush() + return + } + + if (!alreadyCollectedLine) { + line = collectNextLine(); + } else { + alreadyCollectedLine = false; + } + + switch (self.state) { case "HEADER": // 13-18 - Allow a header (metadata) under the WEBVTT line. if (/:/.test(line)) { @@ -1236,45 +1303,59 @@ // An empty line terminates the header and starts the body (cues). self.state = "ID"; } - continue; + linebyline(); + break; case "NOTE": // Ignore NOTE blocks. if (!line) { self.state = "ID"; } - continue; + linebyline(); + break; case "ID": // Check for the start of NOTE blocks. if (/^NOTE($|[ \t])/.test(line)) { self.state = "NOTE"; + linebyline(); break; } // 19-29 - Allow any number of line terminators, then initialize new cue values. if (!line) { - continue; + linebyline(); + break; } - self.cue = new self.window.VTTCue(0, 0, ""); + self.cue = new (self.VTTCue || self.window.VTTCue)(0, 0, ""); self.state = "CUE"; // 30-39 - Check if self line contains an optional identifier or timing data. if (line.indexOf("-->") === -1) { self.cue.id = line; - continue; + linebyline(); + break; } - // Process line as start of a cue. - /*falls through*/ + // Process line as start of a cue. + /*falls through*/ case "CUE": // 40 - Collect cue timings and settings. - try { - parseCue(line, self.cue, self.regionList); - } catch (e) { - self.reportOrThrowError(e); + + parseCue(line, self.cue, self.regionList, function successCb() { + + self.state = "CUETEXT"; + + linebyline(); + + }, function errCb(err) { + + self.reportOrThrowError(err); + // In case of an error ignore rest of the cue. self.cue = null; self.state = "BADCUE"; - continue; - } - self.state = "CUETEXT"; - continue; + + linebyline(); + + }); + + break; case "CUETEXT": var hasSubstring = line.indexOf("-->") !== -1; // 34 - If we have an empty line then report the cue. @@ -1286,33 +1367,45 @@ self.oncue && self.oncue(self.cue); self.cue = null; self.state = "ID"; - continue; - } + linebyline(); + break; + } if (self.cue.text) { self.cue.text += "\n"; } self.cue.text += line; - continue; - case "BADCUE": // BADCUE + linebyline(); + break; + case "BADCUE": + // BADCUE // 54-62 - Collect and discard the remaining cue. if (!line) { self.state = "ID"; } - continue; - } + linebyline(); + break; } - } catch (e) { - self.reportOrThrowError(e); + }; - // If we are currently parsing a cue, report what we have. - if (self.state === "CUETEXT" && self.cue && self.oncue) { - self.oncue(self.cue); + var STACK_LIMIT = 1000; + + var count = 0 + + function unwrapStack(fn) { + return function() { + count++ + if (count < STACK_LIMIT) return fn() + setTimeout(function() { + count = 0 + fn() + }) } - self.cue = null; - // Enter BADWEBVTT state if header was not parsed correctly otherwise - // another exception occurred so enter BADCUE state. - self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; } + + var linebyline = unwrapStack(lineparse) + + linebyline(); + return this; }, flush: function () {