From e950ce911972ea0f86da2f3a398002db069852c7 Mon Sep 17 00:00:00 2001 From: Jaruba Firanelli Date: Sun, 17 Jun 2018 18:39:18 +0300 Subject: [PATCH 1/4] Make Parser Async --- lib/vtt.js | 211 ++++++++++++++++++++++++++--------------------------- 1 file changed, 104 insertions(+), 107 deletions(-) diff --git a/lib/vtt.js b/lib/vtt.js index 66ab23f..6757f49 100644 --- a/lib/vtt.js +++ b/lib/vtt.js @@ -204,15 +204,16 @@ } } - 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); + errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed timestamp: " + oInput)); + return; } // Remove time stamp from input. input = input.replace(/^[^\sa-zA-Z-]+/, ""); @@ -279,6 +280,8 @@ end: "end", right: "end" }, cue.align); + + successCb(); } function skipWhitespace() { @@ -290,9 +293,12 @@ cue.startTime = consumeTimeStamp(); // (1) collect cue start time skipWhitespace(); if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" - throw new ParsingError(ParsingError.Errors.BadTimeStamp, + + errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + - oInput); + oInput)); + return; + } input = input.substr(3); skipWhitespace(); @@ -1195,31 +1201,12 @@ }, /:/); } - // 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(); - - var m = line.match(/^WEBVTT([ \t].*)?$/); - if (!m || !m[0]) { - throw new ParsingError(ParsingError.Errors.BadSignature); - } + var VTTCue = (self.vttjs.VTTCue || self.window.VTTCue); - self.state = "HEADER"; - } + // 5.1 WebVTT file parsing. + var linebyline = function() { - 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 (self.buffer && /\r\n|\n/.test(self.buffer)) { if (!alreadyCollectedLine) { line = collectNextLine(); @@ -1228,91 +1215,101 @@ } switch (self.state) { - case "HEADER": - // 13-18 - Allow a header (metadata) under the WEBVTT line. - if (/:/.test(line)) { - parseHeader(line); - } else if (!line) { - // An empty line terminates the header and starts the body (cues). - self.state = "ID"; - } - continue; - case "NOTE": - // Ignore NOTE blocks. - if (!line) { - self.state = "ID"; - } - continue; - case "ID": - // Check for the start of NOTE blocks. - if (/^NOTE($|[ \t])/.test(line)) { - self.state = "NOTE"; + case "HEADER": + // 13-18 - Allow a header (metadata) under the WEBVTT line. + if (/:/.test(line)) { + parseHeader(line); + } else if (!line) { + // An empty line terminates the header and starts the body (cues). + self.state = "ID"; + } + linebyline(); break; - } - // 19-29 - Allow any number of line terminators, then initialize new cue values. - if (!line) { - continue; - } - self.cue = new 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; - } + case "NOTE": + // Ignore NOTE blocks. + if (!line) { + self.state = "ID"; + } + 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) { + linebyline(); + break; + } + self.cue = new 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; + linebyline(); + break; + } // 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); - // In case of an error ignore rest of the cue. - self.cue = null; - self.state = "BADCUE"; - continue; - } - self.state = "CUETEXT"; - continue; - case "CUETEXT": - var hasSubstring = line.indexOf("-->") !== -1; - // 34 - If we have an empty line then report the cue. - // 35 - If we have the special substring '-->' then report the cue, - // but do not collect the line as we need to process the current - // one as a new cue. - if (!line || hasSubstring && (alreadyCollectedLine = true)) { - // We are done parsing self cue. - self.oncue && self.oncue(self.cue); - self.cue = null; - self.state = "ID"; - continue; - } - if (self.cue.text) { - self.cue.text += "\n"; - } - self.cue.text += line; - continue; - case "BADCUE": // BADCUE - // 54-62 - Collect and discard the remaining cue. - if (!line) { - self.state = "ID"; - } - continue; + case "CUE": + // 40 - Collect cue timings and settings. + + 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"; + + linebyline(); + + }); + + break; + case "CUETEXT": + var hasSubstring = line.indexOf("-->") !== -1; + // 34 - If we have an empty line then report the cue. + // 35 - If we have the special substring '-->' then report the cue, + // but do not collect the line as we need to process the current + // one as a new cue. + if (!line || hasSubstring && (alreadyCollectedLine = true)) { + // We are done parsing self cue. + self.oncue && self.oncue(self.cue); + self.cue = null; + self.state = "ID"; + linebyline(); + break; + } + if (self.cue.text) { + self.cue.text += "\n"; + } + self.cue.text += line; + linebyline(); + break; + case "BADCUE": + // BADCUE + // 54-62 - Collect and discard the remaining cue. + if (!line) { + self.state = "ID"; + } + linebyline(); + break; } } - } catch (e) { - self.reportOrThrowError(e); + }; + + linebyline(); - // 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"; - } return this; }, flush: function () { From 77a83c1c11af4e0a424c6b2de05b2477d58e8df0 Mon Sep 17 00:00:00 2001 From: Jaruba Firanelli Date: Mon, 18 Jun 2018 05:24:47 +0300 Subject: [PATCH 2/4] Don't check for `self.vttjs.VTTCue` This was specific to VideoJS's `vtt.js` --- lib/vtt.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/vtt.js b/lib/vtt.js index 6757f49..5b8fe3d 100644 --- a/lib/vtt.js +++ b/lib/vtt.js @@ -1201,8 +1201,6 @@ }, /:/); } - var VTTCue = (self.vttjs.VTTCue || self.window.VTTCue); - // 5.1 WebVTT file parsing. var linebyline = function() { @@ -1244,7 +1242,7 @@ linebyline(); break; } - self.cue = new VTTCue(0, 0, ""); + self.cue = new 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) { From e4f5f5603730866bacb174a93f51b734c9f29e6a Mon Sep 17 00:00:00 2001 From: Alexandru Branza Date: Tue, 24 Aug 2021 20:02:38 +0300 Subject: [PATCH 3/4] Update vtt.js --- lib/vtt.js | 297 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 197 insertions(+), 100 deletions(-) diff --git a/lib/vtt.js b/lib/vtt.js index 5b8fe3d..0f2a64f 100644 --- a/lib/vtt.js +++ b/lib/vtt.js @@ -211,9 +211,11 @@ function consumeTimeStamp() { var ts = parseTimeStamp(input); if (ts === null) { - errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, - "Malformed timestamp: " + oInput)); - return; + if (errCb) { + errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed timestamp: " + oInput)); + return; + } } // Remove time stamp from input. input = input.replace(/^[^\sa-zA-Z-]+/, ""); @@ -271,7 +273,7 @@ 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", @@ -290,19 +292,23 @@ // 4.1 WebVTT cue timings. skipWhitespace(); - cue.startTime = consumeTimeStamp(); // (1) collect cue start time - skipWhitespace(); - if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" + 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 "-->" errCb(new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + 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(); @@ -1082,12 +1088,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 = { @@ -1189,123 +1196,213 @@ } } - // 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. - var linebyline = function() { - if (self.buffer && /\r\n|\n/.test(self.buffer)) { + function fail(e) { - if (!alreadyCollectedLine) { - line = collectNextLine(); - } else { - alreadyCollectedLine = false; - } + 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); + } + 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"; - switch (self.state) { - case "HEADER": - // 13-18 - Allow a header (metadata) under the WEBVTT line. - if (/:/.test(line)) { - parseHeader(line); - } else if (!line) { - // An empty line terminates the header and starts the body (cues). - self.state = "ID"; - } + } + + 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 + } + + 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)) { + parseHeader(line); + } else if (!line) { + // An empty line terminates the header and starts the body (cues). + self.state = "ID"; + } + linebyline(); + break; + case "NOTE": + // Ignore NOTE blocks. + if (!line) { + self.state = "ID"; + } + linebyline(); + break; + case "ID": + // Check for the start of NOTE blocks. + if (/^NOTE($|[ \t])/.test(line)) { + self.state = "NOTE"; linebyline(); break; - case "NOTE": - // Ignore NOTE blocks. - if (!line) { - self.state = "ID"; - } + } + // 19-29 - Allow any number of line terminators, then initialize new cue values. + if (!line) { 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) { - linebyline(); - break; - } - self.cue = new 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; - linebyline(); - break; - } - // Process line as start of a cue. - /*falls through*/ - case "CUE": - // 40 - Collect cue timings and settings. + } + 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; + linebyline(); + break; + } + // Process line as start of a cue. + /*falls through*/ + case "CUE": + // 40 - Collect cue timings and settings. - parseCue(line, self.cue, self.regionList, function successCb() { + parseCue(line, self.cue, self.regionList, function successCb() { - self.state = "CUETEXT"; + self.state = "CUETEXT"; - linebyline(); + linebyline(); - }, function errCb(err) { + }, function errCb(err) { - self.reportOrThrowError(err); + self.reportOrThrowError(err); - // In case of an error ignore rest of the cue. - self.cue = null; - self.state = "BADCUE"; + // In case of an error ignore rest of the cue. + self.cue = null; + self.state = "BADCUE"; - linebyline(); + linebyline(); - }); + }); - break; - case "CUETEXT": - var hasSubstring = line.indexOf("-->") !== -1; - // 34 - If we have an empty line then report the cue. - // 35 - If we have the special substring '-->' then report the cue, - // but do not collect the line as we need to process the current - // one as a new cue. - if (!line || hasSubstring && (alreadyCollectedLine = true)) { - // We are done parsing self cue. - self.oncue && self.oncue(self.cue); - self.cue = null; - self.state = "ID"; - linebyline(); - break; - } - if (self.cue.text) { - self.cue.text += "\n"; - } - self.cue.text += line; - linebyline(); - break; - case "BADCUE": - // BADCUE - // 54-62 - Collect and discard the remaining cue. - if (!line) { - self.state = "ID"; - } + break; + case "CUETEXT": + var hasSubstring = line.indexOf("-->") !== -1; + // 34 - If we have an empty line then report the cue. + // 35 - If we have the special substring '-->' then report the cue, + // but do not collect the line as we need to process the current + // one as a new cue. + if (!line || hasSubstring && (alreadyCollectedLine = true)) { + // We are done parsing self cue. + self.oncue && self.oncue(self.cue); + self.cue = null; + self.state = "ID"; linebyline(); break; } + if (self.cue.text) { + self.cue.text += "\n"; + } + self.cue.text += line; + linebyline(); + break; + case "BADCUE": + // BADCUE + // 54-62 - Collect and discard the remaining cue. + if (!line) { + self.state = "ID"; + } + linebyline(); + break; } }; + var STACK_LIMIT = 1000; + + var count = 0 + + function unwrapStack(fn) { + return function() { + count++ + if (count < STACK_LIMIT) return fn() + setTimeout(function() { + count = 0 + fn() + }) + } + } + + var linebyline = unwrapStack(lineparse) + linebyline(); return this; From 84d33d157848407d790d78423dacc41a096294f0 Mon Sep 17 00:00:00 2001 From: Alexandru Branza Date: Fri, 1 Mar 2024 07:39:17 +0200 Subject: [PATCH 4/4] Fix Warning About Invalid `cue.positionAlign` Value --- lib/vtt.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/vtt.js b/lib/vtt.js index 0f2a64f..6b7c56a 100644 --- a/lib/vtt.js +++ b/lib/vtt.js @@ -280,7 +280,8 @@ left: "start", middle: "middle", end: "end", - right: "end" + right: "end", + center: "center" }, cue.align); successCb();