* Source: https://github.com/mozilla/vtt.js/blob/master/dist/vtt.js
*/
import VTTCue from './vttcue';
class StringDecoder {
decode(data: string | any, options?: Object): string | never {
if (!data) {
return '';
}
if (typeof data !== 'string') {
throw new Error('Error - expected string data.');
}
return decodeURIComponent(encodeURIComponent(data));
}
}
export function parseTimeStamp(input: string) {
function computeSeconds(h, m, s, f) {
return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + parseFloat(f || 0);
}
const m = input.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);
if (!m) {
return null;
}
if (parseFloat(m[2]) > 59) {
return computeSeconds(m[2], m[3], 0, m[4]);
}
return computeSeconds(m[1], m[2], m[3], m[4]);
}
class Settings {
private readonly values: { [key: string]: any } = Object.create(null);
set(k: string, v: any) {
if (!this.get(k) && v !== '') {
this.values[k] = v;
}
}
get(k: string, dflt?: any, defaultKey?: string): any {
if (defaultKey) {
return this.has(k) ? this.values[k] : dflt[defaultKey];
}
return this.has(k) ? this.values[k] : dflt;
}
has(k: string): boolean {
return k in this.values;
}
alt(k: string, v: any, a: any[]) {
for (let n = 0; n < a.length; ++n) {
if (v === a[n]) {
this.set(k, v);
break;
}
}
}
integer(k: string, v: any) {
if (/^-?\d+$/.test(v)) {
this.set(k, parseInt(v, 10));
}
}
percent(k: string, v: any): boolean {
if (/^([\d]{1,3})(\.[\d]*)?%$/.test(v)) {
const percent = parseFloat(v);
if (percent >= 0 && percent <= 100) {
this.set(k, percent);
return true;
}
}
return false;
}
}
function parseOptions(
input: string,
callback: (k: string, v: any) => void,
keyValueDelim: RegExp,
groupDelim?: RegExp,
) {
const groups = groupDelim ? input.split(groupDelim) : [input];
for (const i in groups) {
if (typeof groups[i] !== 'string') {
continue;
}
const kv = groups[i].split(keyValueDelim);
if (kv.length !== 2) {
continue;
}
const k = kv[0];
const v = kv[1];
callback(k, v);
}
}
const defaults = new VTTCue(0, 0, '');
const center = (defaults.align as string) === 'middle' ? 'middle' : 'center';
function parseCue(input: string, cue: VTTCue, regionList: Region[]) {
const oInput = input;
function consumeTimeStamp(): number | never {
const ts = parseTimeStamp(input);
if (ts === null) {
throw new Error('Malformed timestamp: ' + oInput);
}
input = input.replace(/^[^\sa-zA-Z-]+/, '');
return ts;
}
function consumeCueSettings(input: string, cue: VTTCue) {
const settings = new Settings();
parseOptions(
input,
function (k, v) {
let vals;
switch (k) {
case 'region':
for (let i = regionList.length - 1; i >= 0; i--) {
if (regionList[i].id === v) {
settings.set(k, regionList[i].region);
break;
}
}
break;
case 'vertical':
settings.alt(k, v, ['rl', 'lr']);
break;
case 'line':
vals = v.split(',');
settings.integer(k, vals[0]);
if (settings.percent(k, vals[0])) {
settings.set('snapToLines', false);
}
settings.alt(k, vals[0], ['auto']);
if (vals.length === 2) {
settings.alt('lineAlign', vals[1], ['start', center, 'end']);
}
break;
case 'position':
vals = v.split(',');
settings.percent(k, vals[0]);
if (vals.length === 2) {
settings.alt('positionAlign', vals[1], [
'start',
center,
'end',
'line-left',
'line-right',
'auto',
]);
}
break;
case 'size':
settings.percent(k, v);
break;
case 'align':
settings.alt(k, v, ['start', center, 'end', 'left', 'right']);
break;
}
},
/:/,
/\s/,
);
cue.region = settings.get('region', null);
cue.vertical = settings.get('vertical', '');
let line = settings.get('line', 'auto');
if (line === 'auto' && defaults.line === -1) {
line = -1;
}
cue.line = line;
cue.lineAlign = settings.get('lineAlign', 'start');
cue.snapToLines = settings.get('snapToLines', true);
cue.size = settings.get('size', 100);
cue.align = settings.get('align', center);
let position = settings.get('position', 'auto');
if (position === 'auto' && defaults.position === 50) {
position =
cue.align === 'start' || cue.align === 'left'
? 0
: cue.align === 'end' || cue.align === 'right'
? 100
: 50;
}
cue.position = position;
}
function skipWhitespace() {
input = input.replace(/^\s+/, '');
}
skipWhitespace();
cue.startTime = consumeTimeStamp();
skipWhitespace();
if (input.slice(0, 3) !== '-->') {
throw new Error(
"Malformed time stamp (time stamps must be separated by '-->'): " +
oInput,
);
}
input = input.slice(3);
skipWhitespace();
cue.endTime = consumeTimeStamp();
skipWhitespace();
consumeCueSettings(input, cue);
}
export function fixLineBreaks(input: string): string {
return input.replace(/<br(?: \/)?>/gi, '\n');
}
type Region = {
id: string;
region: any;
};
export class VTTParser {
private state:
| 'INITIAL'
| 'HEADER'
| 'ID'
| 'CUE'
| 'CUETEXT'
| 'NOTE'
| 'BADWEBVTT'
| 'BADCUE' = 'INITIAL';
private buffer: string = '';
private decoder: StringDecoder = new StringDecoder();
private regionList: Region[] = [];
private cue: VTTCue | null = null;
public oncue?: (cue: VTTCue) => void;
public onparsingerror?: (error: Error) => void;
public onflush?: () => void;
parse(data?: string): VTTParser {
const _this = this;
if (data) {
_this.buffer += _this.decoder.decode(data, { stream: true });
}
function collectNextLine(): string {
let buffer: string = _this.buffer;
let pos = 0;
buffer = fixLineBreaks(buffer);
while (
pos < buffer.length &&
buffer[pos] !== '\r' &&
buffer[pos] !== '\n'
) {
++pos;
}
const line: string = buffer.slice(0, pos);
if (buffer[pos] === '\r') {
++pos;
}
if (buffer[pos] === '\n') {
++pos;
}
_this.buffer = buffer.slice(pos);
return line;
}
function parseHeader(input) {
parseOptions(
input,
function (k, v) {
},
/:/,
);
}
try {
let line: string = '';
if (_this.state === 'INITIAL') {
if (!/\r\n|\n/.test(_this.buffer)) {
return this;
}
line = collectNextLine();
const m = line.match(/^()?WEBVTT([ \t].*)?$/);
if (!m?.[0]) {
throw new Error('Malformed WebVTT signature.');
}
_this.state = 'HEADER';
}
let alreadyCollectedLine = false;
while (_this.buffer) {
if (!/\r\n|\n/.test(_this.buffer)) {
return this;
}
if (!alreadyCollectedLine) {
line = collectNextLine();
} else {
alreadyCollectedLine = false;
}
switch (_this.state) {
case 'HEADER':
if (/:/.test(line)) {
parseHeader(line);
} else if (!line) {
_this.state = 'ID';
}
continue;
case 'NOTE':
if (!line) {
_this.state = 'ID';
}
continue;
case 'ID':
if (/^NOTE($|[ \t])/.test(line)) {
_this.state = 'NOTE';
break;
}
if (!line) {
continue;
}
_this.cue = new VTTCue(0, 0, '');
_this.state = 'CUE';
if (line.indexOf('-->') === -1) {
_this.cue.id = line;
continue;
}
case 'CUE':
if (!_this.cue) {
_this.state = 'BADCUE';
continue;
}
try {
parseCue(line, _this.cue, _this.regionList);
} catch (e) {
_this.cue = null;
_this.state = 'BADCUE';
continue;
}
_this.state = 'CUETEXT';
continue;
case 'CUETEXT':
{
const hasSubstring = line.indexOf('-->') !== -1;
if (!line || (hasSubstring && (alreadyCollectedLine = true))) {
if (_this.oncue && _this.cue) {
_this.oncue(_this.cue);
}
_this.cue = null;
_this.state = 'ID';
continue;
}
if (_this.cue === null) {
continue;
}
if (_this.cue.text) {
_this.cue.text += '\n';
}
_this.cue.text += line;
}
continue;
case 'BADCUE':
if (!line) {
_this.state = 'ID';
}
}
}
} catch (e) {
if (_this.state === 'CUETEXT' && _this.cue && _this.oncue) {
_this.oncue(_this.cue);
}
_this.cue = null;
_this.state = _this.state === 'INITIAL' ? 'BADWEBVTT' : 'BADCUE';
}
return this;
}
flush(): VTTParser {
const _this = this;
try {
if (_this.cue || _this.state === 'HEADER') {
_this.buffer += '\n\n';
_this.parse();
}
if (_this.state === 'INITIAL' || _this.state === 'BADWEBVTT') {
throw new Error('Malformed WebVTT signature.');
}
} catch (e) {
if (_this.onparsingerror) {
_this.onparsingerror(e);
}
}
if (_this.onflush) {
_this.onflush();
}
return this;
}
}