rx-player
Version:
Canal+ HTML5 Video Player
217 lines (198 loc) • 6.71 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.
*/
import arrayFind from "../../../utils/array_find";
import isNonEmptyString from "../../../utils/is_non_empty_string";
import objectAssign from "../../../utils/object_assign";
import type { ITTParameters } from "./get_parameters";
import getParameters from "./get_parameters";
import type { IStyleObject } from "./get_styling";
import { getStylingAttributes, getStylingFromElement } from "./get_styling";
import resolveStylesInheritance from "./resolve_styles_inheritance";
import {
getParentDivElements,
getBodyNode,
getRegionNodes,
getStyleNodes,
getTextNodes,
} from "./xml_utils";
const STYLE_ATTRIBUTES = [
"align",
"backgroundColor",
"color",
"direction",
"display",
"displayAlign",
"extent",
"fontFamily",
"fontSize",
"fontStyle",
"fontWeight",
"lineHeight",
"opacity",
"origin",
"overflow",
"padding",
"textAlign",
"textDecoration",
"textOutline",
"unicodeBidi",
"visibility",
"wrapOption",
"writingMode",
// Not managed anywhere for now
// "showBackground",
// "zIndex",
];
export interface IParsedTTMLCue {
/** The DOM Element that contains text node */ paragraph: Element;
/** An offset to apply to cues start and end */
timeOffset: number;
/** An array of objects containing TTML styles */
idStyles: IStyleObject[];
/** An array of objects containing region TTML style */
regionStyles: IStyleObject[];
/** An object containing paragraph style */
paragraphStyle: Partial<Record<string, string>>;
/** An object containing TTML parameters */
ttParams: ITTParameters;
shouldTrimWhiteSpace: boolean;
/** TTML body as a DOM Element */
body: Element | null;
}
/**
* Create array of objects which should represent the given TTML text track.
* TODO TTML parsing is still pretty heavy on the CPU.
* Optimizations have been done, principally to avoid using too much XML APIs,
* but we can still do better.
* @param {string} str
* @param {Number} timeOffset
* @returns {Array.<Object>}
*/
export default function parseTTMLString(
str: string,
timeOffset: number,
): IParsedTTMLCue[] {
const cues: IParsedTTMLCue[] = [];
const xml = new DOMParser().parseFromString(str, "text/xml");
if (xml !== null && xml !== undefined) {
const tts = xml.getElementsByTagName("tt");
let tt: Element = tts[0];
if (tt === undefined) {
// EBU-TT sometimes namespaces tt, by "tt:"
// Just catch all namespaces to play it safe
const namespacedTT = xml.getElementsByTagNameNS("*", "tt");
tt = namespacedTT[0];
if (tt === undefined) {
throw new Error("invalid XML");
}
}
const body = getBodyNode(tt);
const styleNodes = getStyleNodes(tt);
const regionNodes = getRegionNodes(tt);
const paragraphNodes = getTextNodes(tt);
const ttParams = getParameters(tt);
// construct idStyles array based on the xml as an optimization
const idStyles: IStyleObject[] = [];
for (let i = 0; i <= styleNodes.length - 1; i++) {
const styleNode = styleNodes[i];
if (styleNode instanceof Element) {
const styleID = styleNode.getAttribute("xml:id");
if (styleID !== null) {
const subStyles = styleNode.getAttribute("style");
const extendsStyles = subStyles === null ? [] : subStyles.split(" ");
idStyles.push({
id: styleID,
style: getStylingFromElement(styleNode),
extendsStyles,
});
}
}
}
resolveStylesInheritance(idStyles);
// construct regionStyles array based on the xml as an optimization
const regionStyles: IStyleObject[] = [];
for (let i = 0; i <= regionNodes.length - 1; i++) {
const regionNode = regionNodes[i];
if (regionNode instanceof Element) {
const regionID = regionNode.getAttribute("xml:id");
if (regionID !== null) {
let regionStyle = getStylingFromElement(regionNode);
const associatedStyleID = regionNode.getAttribute("style");
if (isNonEmptyString(associatedStyleID)) {
const style = arrayFind(idStyles, (x) => x.id === associatedStyleID);
if (style !== undefined) {
regionStyle = objectAssign({}, style.style, regionStyle);
}
}
regionStyles.push({
id: regionID,
style: regionStyle,
// already handled
extendsStyles: [],
});
}
}
}
// Computing the style takes a lot of ressources.
// To avoid too much re-computation, let's compute the body style right
// now and do the rest progressively.
// TODO Compute corresponding CSS style here (as soon as we now the TTML
// style) to speed up the process even more.
const bodyStyle = getStylingAttributes(
STYLE_ATTRIBUTES,
body !== null ? [body] : [],
idStyles,
regionStyles,
);
const bodySpaceAttribute = body !== null ? body.getAttribute("xml:space") : undefined;
const shouldTrimWhiteSpaceOnBody =
bodySpaceAttribute === "default" || ttParams.spaceStyle === "default";
for (let i = 0; i < paragraphNodes.length; i++) {
const paragraph = paragraphNodes[i];
if (paragraph instanceof Element) {
const divs = getParentDivElements(paragraph);
const paragraphStyle = objectAssign(
{},
bodyStyle,
getStylingAttributes(
STYLE_ATTRIBUTES,
[paragraph, ...divs],
idStyles,
regionStyles,
),
);
const paragraphSpaceAttribute = paragraph.getAttribute("xml:space");
const shouldTrimWhiteSpace = isNonEmptyString(paragraphSpaceAttribute)
? paragraphSpaceAttribute === "default"
: shouldTrimWhiteSpaceOnBody;
const cue = {
paragraph,
timeOffset,
idStyles,
regionStyles,
body,
paragraphStyle,
ttParams,
shouldTrimWhiteSpace,
};
if (cue !== null) {
cues.push(cue);
}
}
}
}
return cues;
}