@svta/common-media-library
Version:
A common library for media playback in JavaScript
373 lines • 18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebVttParser = void 0;
const createWebVttCue_js_1 = require("./createWebVttCue.js");
const createWebVttRegion_js_1 = require("./createWebVttRegion.js");
const parseCue_js_1 = require("./parse/parseCue.js");
const parseOptions_js_1 = require("./parse/parseOptions.js");
const parseTimestamp_js_1 = require("./parse/parseTimestamp.js");
const Settings_js_1 = require("./parse/Settings.js");
const WebVttParserState_js_1 = require("./parse/WebVttParserState.js");
const WebVttParsingError_js_1 = require("./WebVttParsingError.js");
const BAD_SIGNATURE = 'Malformed WebVTT signature.';
const createCue = () => typeof VTTCue !== 'undefined' ? new VTTCue(0, 0, '') : (0, createWebVttCue_js_1.createWebVttCue)();
const createRegion = () => typeof VTTRegion !== 'undefined' ? new VTTRegion() : (0, createWebVttRegion_js_1.createWebVttRegion)();
/**
* A WebVTT parser.
*
* @group WebVTT
*
* @beta
*
* @example
* {@includeCode ../../test/webvtt/WebVttParser.test.ts#example}
*
* @see {@link https://www.w3.org/TR/webvtt1/ | WebVTT Specification}
*/
class WebVttParser {
/**
* Create a new WebVTT parser.
*
* @param options - The options to use for the parser.
*/
constructor(options = {}) {
var _a;
this.regionSettings = null;
this.cue = null;
const useDomTypes = (_a = options.useDomTypes) !== null && _a !== void 0 ? _a : true;
this.createCue = options.createCue || useDomTypes ? createCue : createWebVttCue_js_1.createWebVttCue;
this.createRegion = options.createRegion || useDomTypes ? createRegion : createWebVttRegion_js_1.createWebVttRegion;
this.state = WebVttParserState_js_1.WebVttParserState.INITIAL;
this.buffer = '';
this.style = '';
this.regionList = [];
}
/**
* Parse the given data.
*
* @param data - The data to parse.
* @param reuseCue - Whether to reuse the cue.
* @returns The parser.
*/
parse(data, reuseCue = false) {
var _a, _b, _c, _d, _e;
var _f;
// If there is no data then we will just try to parse whatever is in buffer already.
// This may occur in circumstances, for example when flush() is called.
if (data) {
this.buffer += data;
}
const collectNextLine = () => {
const buffer = this.buffer;
let pos = 0;
while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
++pos;
}
const line = buffer.substr(0, pos);
// Advance the buffer early in case we fail below.
if (buffer[pos] === '\r') {
++pos;
}
if (buffer[pos] === '\n') {
++pos;
}
this.buffer = buffer.substr(pos);
return line;
};
// draft-pantos-http-live-streaming-20
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
// 3.5 WebVTT
const parseTimestampMap = (input) => {
var _a;
const settings = new Settings_js_1.Settings();
(0, parseOptions_js_1.parseOptions)(input, (k, v) => {
switch (k) {
case 'MPEGT':
settings.integer(k + 'S', v);
break;
case 'LOCA':
settings.set(k + 'L', (0, parseTimestamp_js_1.parseTimeStamp)(v));
break;
}
}, /[^\d]:/, /,/);
(_a = this.ontimestampmap) === null || _a === void 0 ? void 0 : _a.call(this, {
'MPEGTS': settings.get('MPEGTS'),
'LOCAL': settings.get('LOCAL'),
});
};
// 3.2 WebVtt metadata header syntax
const parseHeader = (input) => {
if (input.match(/X-TIMESTAMP-MAP/)) {
// This line contains HLS X-TIMESTAMP-MAP metadata
(0, parseOptions_js_1.parseOptions)(input, (k, v) => {
switch (k) {
case 'X-TIMESTAMP-MAP':
parseTimestampMap(v);
break;
}
}, /=/);
}
};
// 6.1 WebVTT file parsing.
try {
let line;
if (this.state === WebVttParserState_js_1.WebVttParserState.INITIAL) {
// We can't start parsing until we have the first line.
if (!/\r\n|\n/.test(this.buffer)) {
return this;
}
line = collectNextLine();
// Remove the UTF-8 BOM if it exists.
if (line.charCodeAt(0) === 0xFEFF) {
line = line.slice(1);
}
const m = line.match(/^WEBVTT([ \t].*)?$/);
if (!m || !m[0]) {
throw new WebVttParsingError_js_1.WebVttParsingError(BAD_SIGNATURE);
}
this.state = WebVttParserState_js_1.WebVttParserState.HEADER;
}
let alreadyCollectedLine = false;
var sawCue = reuseCue;
if (!reuseCue) {
this.cue = null;
this.regionSettings = null;
}
while (this.buffer) {
// We can't parse a line until we have the full line.
if (!/\r\n|\n/.test(this.buffer)) {
return this;
}
if (!alreadyCollectedLine) {
line = collectNextLine();
}
else {
alreadyCollectedLine = false;
}
switch (this.state) {
case WebVttParserState_js_1.WebVttParserState.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 blocks section.
this.state = WebVttParserState_js_1.WebVttParserState.BLOCKS;
}
continue;
case WebVttParserState_js_1.WebVttParserState.REGION:
if (!line && this.regionSettings) {
// create the region
const region = this.createRegion();
region.id = this.regionSettings.get('id', '');
region.width = this.regionSettings.get('width', 100);
region.lines = this.regionSettings.get('lines', 3);
region.regionAnchorX = this.regionSettings.get('regionanchorX', 0);
region.regionAnchorY = this.regionSettings.get('regionanchorY', 100);
region.viewportAnchorX = this.regionSettings.get('viewportanchorX', 0);
region.viewportAnchorY = this.regionSettings.get('viewportanchorY', 100);
region.scroll = this.regionSettings.get('scroll', '');
// Register the region.
(_a = this.onregion) === null || _a === void 0 ? void 0 : _a.call(this, region);
// Remember the VTTRegion for later in case we parse any VTTCues that reference it.
this.regionList.push(region);
// An empty line terminates the REGION block
this.regionSettings = null;
this.state = WebVttParserState_js_1.WebVttParserState.BLOCKS;
break;
}
// if it's a new region block, create a new VTTRegion
if (this.regionSettings === null) {
this.regionSettings = new Settings_js_1.Settings();
}
const regionSettings = this.regionSettings;
// parse region options and set it as appropriate on the region
(0, parseOptions_js_1.parseOptions)(line, (k, v) => {
switch (k) {
case 'id':
regionSettings.set(k, v);
break;
case 'width':
regionSettings.percent(k, v);
break;
case 'lines':
regionSettings.integer(k, v);
break;
case 'regionanchor':
case 'viewportanchor':
const xy = v.split(',');
if (xy.length !== 2) {
break;
}
// We have to make sure both x and y parse, so use a temporary
// settings object here.
const anchor = new Settings_js_1.Settings();
anchor.percent('x', xy[0]);
anchor.percent('y', xy[1]);
if (!anchor.has('x') || !anchor.has('y')) {
break;
}
regionSettings.set(k + 'X', anchor.get('x'));
regionSettings.set(k + 'Y', anchor.get('y'));
break;
case 'scroll':
regionSettings.alt(k, v, ['up']);
break;
}
}, /:/, /\s/);
continue;
case WebVttParserState_js_1.WebVttParserState.STYLE:
if (!line) {
(_b = this.onstyle) === null || _b === void 0 ? void 0 : _b.call(this, this.style);
this.style = '';
this.state = WebVttParserState_js_1.WebVttParserState.BLOCKS;
break;
}
this.style += line + '\n';
continue;
case WebVttParserState_js_1.WebVttParserState.NOTE:
// Ignore NOTE blocks.
if (!line) {
this.state = WebVttParserState_js_1.WebVttParserState.ID;
}
continue;
case WebVttParserState_js_1.WebVttParserState.BLOCKS:
if (!line) {
continue;
}
// Check for the start of a NOTE blocks
if (/^NOTE($[ \t])/.test(line)) {
this.state = WebVttParserState_js_1.WebVttParserState.NOTE;
break;
}
// Check for the start of a REGION blocks
if (/^REGION/.test(line) && !sawCue) {
this.state = WebVttParserState_js_1.WebVttParserState.REGION;
break;
}
// Check for the start of a STYLE blocks
if (/^STYLE/.test(line) && !sawCue) {
this.state = WebVttParserState_js_1.WebVttParserState.STYLE;
break;
}
this.state = WebVttParserState_js_1.WebVttParserState.ID;
// Process line as an ID.
/* falls through */
case WebVttParserState_js_1.WebVttParserState.ID:
// Check for the start of NOTE blocks.
if (/^NOTE($|[ \t])/.test(line)) {
this.state = WebVttParserState_js_1.WebVttParserState.NOTE;
break;
}
// 19-29 - Allow any number of line terminators, then initialize new cue values.
if (!line) {
continue;
}
sawCue = true;
this.cue = this.createCue();
(_c = (_f = this.cue).text) !== null && _c !== void 0 ? _c : (_f.text = '');
this.state = WebVttParserState_js_1.WebVttParserState.CUE;
// 30-39 - Check if this line contains an optional identifier or timing data.
if (line.indexOf('-->') === -1) {
this.cue.id = line;
continue;
}
// Process line as start of a cue.
/*falls through*/
case WebVttParserState_js_1.WebVttParserState.CUE:
// 40 - Collect cue timings and settings.
try {
(0, parseCue_js_1.parseCue)(line, this.cue, this.regionList);
}
catch (e) {
this.reportOrThrowError(e);
// In case of an error ignore rest of the cue.
this.cue = null;
this.state = WebVttParserState_js_1.WebVttParserState.BAD_CUE;
continue;
}
this.state = WebVttParserState_js_1.WebVttParserState.CUE_TEXT;
continue;
case WebVttParserState_js_1.WebVttParserState.CUE_TEXT:
const 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 this cue.
(_d = this.oncue) === null || _d === void 0 ? void 0 : _d.call(this, this.cue);
this.cue = null;
this.state = WebVttParserState_js_1.WebVttParserState.ID;
continue;
}
if ((_e = this.cue) === null || _e === void 0 ? void 0 : _e.text) {
this.cue.text += '\n';
}
this.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n');
continue;
case WebVttParserState_js_1.WebVttParserState.BAD_CUE: // BADCUE
// 54-62 - Collect and discard the remaining cue.
if (!line) {
this.state = WebVttParserState_js_1.WebVttParserState.ID;
}
continue;
}
}
}
catch (e) {
this.reportOrThrowError(e);
// If we are currently parsing a cue, report what we have.
if (this.state === WebVttParserState_js_1.WebVttParserState.CUE_TEXT && this.cue && this.oncue) {
this.oncue(this.cue);
}
this.cue = null;
this.regionSettings = null;
// Enter BADWEBVTT state if header was not parsed correctly otherwise
// another exception occurred so enter BADCUE state.
this.state = this.state === WebVttParserState_js_1.WebVttParserState.INITIAL ? WebVttParserState_js_1.WebVttParserState.BAD_WEBVTT : WebVttParserState_js_1.WebVttParserState.BAD_CUE;
}
return this;
}
/**
* Flush the parser.
*
* @returns The parser.
*/
flush() {
var _a;
try {
// Finish parsing the stream.
this.buffer += '';
// Synthesize the end of the current cue or region.
if (this.cue || this.state === WebVttParserState_js_1.WebVttParserState.HEADER) {
this.buffer += '\n\n';
this.parse(undefined, true);
}
// If we've flushed, parsed, and we're still on the INITIAL state then
// that means we don't have enough of the stream to parse the first
// line.
if (this.state === WebVttParserState_js_1.WebVttParserState.INITIAL) {
throw new WebVttParsingError_js_1.WebVttParsingError(BAD_SIGNATURE);
}
}
catch (e) {
this.reportOrThrowError(e);
}
(_a = this.onflush) === null || _a === void 0 ? void 0 : _a.call(this);
return this;
}
// If the error is a ParsingError then report it to the consumer if
// possible. If it's not a ParsingError then throw it like normal.
reportOrThrowError(error) {
var _a;
if (error instanceof WebVttParsingError_js_1.WebVttParsingError) {
(_a = this.onparsingerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
}
else {
throw error;
}
}
}
exports.WebVttParser = WebVttParser;
//# sourceMappingURL=WebVttParser.js.map