rx-player
Version:
Canal+ HTML5 Video Player
205 lines (181 loc) • 5.65 kB
text/typescript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* /!\ This file is feature-switchable.
* It always should be imported through the `features` object.
*/
import type { ICompatVTTCue } from "../../../compat/browser_compatibility_types";
import makeVTTCue from "../../../compat/make_vtt_cue";
import bufferSourceToUint8 from "../../../utils/buffer_source_to_uint8";
import isNonEmptyString from "../../../utils/is_non_empty_string";
import isNullOrUndefined from "../../../utils/is_null_or_undefined";
import { utf8ToStr } from "../../../utils/string_parsing";
const HTML_ENTITIES = /&#([0-9]+);/g;
const BR = /<br>/gi;
const STYLE = /<style[^>]*>([\s\S]*?)<\/style[^>]*>/i;
const PARAG = /\s*<p (?:class=([^>]+))?>(.*)/i;
const START = /<sync[^>]+?start="?([0-9]*)"?[^0-9]/i;
interface ISubs {
start: number;
end?: number;
text: string;
}
/**
* Creates an array of VTTCue/TextTrackCue from a given array of cue objects.
* @param {Array.<Object>} cuesArray - Objects containing the start, end and
* text.
* @returns {Array.<VTTCue>}
*/
function createCuesFromArray(cuesArray: ISubs[]): Array<TextTrackCue | ICompatVTTCue> {
const nativeCues: Array<TextTrackCue | ICompatVTTCue> = [];
for (let i = 0; i < cuesArray.length; i++) {
const { start, end, text } = cuesArray[i];
if (isNonEmptyString(text) && !isNullOrUndefined(end)) {
const cue = makeVTTCue(start, end, text);
if (cue !== null) {
nativeCues.push(cue);
}
}
}
return nativeCues;
}
/**
* Returns classnames for every languages.
* @param {string} str
* @returns {Object}
*/
function getClassNameByLang(str: string): Partial<Record<string, string>> {
const ruleRe = /\.(\S+)\s*{([^}]*)}/gi;
const langs: { [lang: string]: string } = {};
let m = ruleRe.exec(str);
while (Array.isArray(m)) {
const name = m[1];
const lang = getCSSProperty(m[2], "lang");
if (!isNullOrUndefined(name) && !isNullOrUndefined(lang)) {
langs[lang] = name;
}
m = ruleRe.exec(str);
}
return langs;
}
/**
* @param {string} str - entire CSS rule
* @param {string} name - name of the property
* @returns {string|null} - value of the property. Null if not found.
*/
function getCSSProperty(str: string, name: string): string | null {
const matches = new RegExp("\\s*" + name + ":\\s*(\\S+);", "i").exec(str);
return Array.isArray(matches) ? matches[1] : null;
}
/**
* Decode HMTL formatting into a string.
* @param {string} text
* @returns {string}
*/
function decodeEntities(text: string): string {
return text
.replace(BR, "\n")
.replace(HTML_ENTITIES, (_, $1) => String.fromCharCode(Number($1)));
}
/**
* Because sami is not really html... we have to use
* some kind of regular expressions to parse it...
* the cthulhu way :)
* The specification being quite clunky, this parser
* may not work for every sami input.
*
* @param {string|BufferSource} input
* @param {Object} context
* @param {Number} timeOffset
* @returns {Array.<VTTCue|TextTrackCue>}
*/
function parseSami(
input: string | BufferSource,
{
language: lang,
}: {
language: string | undefined;
},
timeOffset: number,
): Array<TextTrackCue | ICompatVTTCue> {
let smi: string;
if (typeof input !== "string") {
// Assume UTF-8
// TODO: detection?
smi = utf8ToStr(bufferSourceToUint8(input));
} else {
smi = input;
}
const syncOpen = /<sync[ >]/gi;
const syncClose = /<sync[ >]|<\/body>/gi;
const subs: ISubs[] = [];
const styleMatches = STYLE.exec(smi);
const css = styleMatches !== null ? styleMatches[1] : "";
let up;
let to;
// FIXME Is that wanted?
// previously written as let to = SyncClose.exec(smi); but never used
syncClose.exec(smi);
const langs = getClassNameByLang(css);
let klass: string | undefined;
if (isNonEmptyString(lang)) {
klass = langs[lang];
if (klass === undefined) {
throw new Error(`sami: could not find lang ${lang} in CSS`);
}
}
while (true) {
up = syncOpen.exec(smi);
to = syncClose.exec(smi);
if (up === null && to === null) {
break;
}
if (up === null || to === null || up.index >= to.index) {
throw new Error("parse error");
}
const str = smi.slice(up.index, to.index);
const tim = START.exec(str);
if (tim === null) {
throw new Error("parse error (sync time attribute)");
}
const start = +tim[1];
if (isNaN(start)) {
throw new Error("parse error (sync time attribute NaN)");
}
appendToSubs(str.split("\n"), start / 1000);
}
return createCuesFromArray(subs);
function appendToSubs(lines: string[], start: number) {
let i = lines.length;
let m;
while (--i >= 0) {
m = PARAG.exec(lines[i]);
if (m === null) {
continue;
}
const [, kl, txt] = m;
if (klass !== kl) {
continue;
}
if (txt === " ") {
subs[subs.length - 1].end = start;
} else {
subs.push({ text: decodeEntities(txt), start: start + timeOffset });
}
}
}
}
export default parseSami;