UNPKG

rx-player

Version:
217 lines (198 loc) 6.71 kB
/** * 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; }