Skip to content

Source: src/text/SubtitleRendererIsd.js

import DomSmith from '../../lib/dom/DomSmith.js';

/**
 * SubtitleRendererIsd is a plugin renderer for subtitles in TTML format.
 * It is registered with the Subtitles component and creates DOM output from `isd` (Intermediate Synchronic Document) subtitle cues.
 * The renderer currently supports _very_ basic structure parsing (`div`, `p`, `span`, `br`) and ignores formatting and styling instructions.
 * This component is intended for rendering embedded TTML subtitles provided by streaming engines like DASH.js.
 * @exports module:src/text/SubtitleRendererIsd
 * @author  Frank Kudermann - alphanull
 * @version 1.0.0
 * @license MIT
 */
export default class SubtitleRendererIsd {

    #dom;

    #subtitleTtml;

    /**
     * Creates a new TTML subtitle renderer and registers it with the parent Subtitles component.
     * @param {module:src/core/Player}    player  Main player instance.
     * @param {module:src/text/Subtitles} parent  The parent Subtitles component.
     */
    constructor(player, parent) {

        parent.registerRenderer(this);

        this.#dom = new DomSmith({
            _ref: 'root',
            className: 'vip-subtitles-container vip-subtitles-isd'
        }, parent.getElement());

    }

    /**
     * Determines whether this renderer is suitable for rendering the given cue.
     * It only renders cues that contain an `isd` object and no `text`.
     * @param   {Object}  cue  The subtitle cue object.
     * @returns {boolean}      True if this renderer can handle the cue, false otherwise.
     */
    canRender(cue) { // eslint-disable-line class-methods-use-this

        return !cue.text && cue.isd;

    }

    /**
     * Renders a TTML subtitle cue to the DOM using DomSmith.
     * The rendered node is stored in the `currentCues` map, so it can be removed later.
     * @param {Object} cue          The subtitle cue to render.
     * @param {Map}    currentCues  Map of active cues to DOM elements.
     */
    render(cue, currentCues) {

        this.#subtitleTtml = new DomSmith({
            _ref: 'subtitleTtml',
            className: 'vip-subtitle-item',
            _nodes: this.#renderIsd(cue.isd.contents)
        }, this.#dom.root);

        currentCues.set(cue, { ele: this.#subtitleTtml.subtitleTtml, renderer: this });

    }

    /**
     * Recursively converts ISD (Intermediate Synchronic Document) structure into a DOMSmith node tree.
     * Currently only supports `div`, `p`, `span`, and `br` elements with optional text content.
     * @param   {Array} contents  The TTML ISD contents array.
     * @returns {Array}           Array of DomSmith node descriptors.
     */
    #renderIsd(contents) {

        let result = [];

        contents.forEach(content => {

            let newNode;

            if (['div', 'p', 'span', 'br'].includes(content.kind)) {
                newNode = { _tag: content.kind };
                if (content.kind === 'p') newNode.className = 'vip-subtitle-text';
                if (content.text) newNode._nodes = [content.text];
            }

            if (newNode) result.push(newNode);

            if (content.contents && content.contents.length) {
                if (newNode) newNode._nodes = this.#renderIsd(content.contents);
                else result = this.#renderIsd(content.contents);
            }

        });

        return result;

    }

    /**
     * Clears the current subtitle output.
     */
    clear() {

        this.#dom.root.innerHTML = '';

    }

    /**
     * Destroys the renderer and cleans up DOM and references.
     */
    destroy() {

        this.clear();
        this.#dom.destroy();
        this.#subtitleTtml?.destroy();

    }
}