UNPKG

@svta/common-media-library

Version:
373 lines 18 kB
"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