Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Parser Async #373

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 167 additions & 74 deletions lib/vtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-]+/, "");
Expand Down Expand Up @@ -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() {
Expand All @@ -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();
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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.
Expand All @@ -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 () {
Expand Down