Source: player/Media.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** Manage the video or audio elements embedded in a presentation.
 *
 * This module is part of the Sozi player embedded in each presentation.
 *
 * @module
 */

/** The SVG namespace URI.
 *
 * @readonly
 * @type {string} */
const svgNs = "http://www.w3.org/2000/svg";

/** The Sozi namespace URI.
 *
 * @readonly
 * @type {string} */
const soziNs = "http://sozi.baierouge.fr";

/** The XHTML namespace URI.
 *
 * @readonly
 * @type {string} */
const xhtmlNs = "http://www.w3.org/1999/xhtml";

/** The current Sozi player.
 *
 * @type {module:player/Player.Player} */
let player;

/** A default event handler that prevents the propagation of an event.
 *
 * For instance, this function prevents a click event inside a video element
 * from also triggering a transition in the current presentation.
 *
 * @param {Event} evt - The DOM event to stop.
 *
 * @listens click
 * @listens mousedown
 * @listens mouseup
 * @listens mousemove
 * @listens contextmenu
 */
function defaultEventHandler(evt) {
    evt.stopPropagation();
}

/** A dictionary of video and audio elements to start in each frame.
 *
 * @type {object} */
const mediaToStartByFrameId = {};

/** A dictionary of video and audio elements to stop in each frame.
 *
 * @type {object} */
const mediaToStopByFrameId = {};

/** Start or stop media on frame change.
 *
 * @listens module:player/Player.frameChange
 */
function onFrameChange() {
    const frameId = player.currentFrame.frameId;
    if (frameId in mediaToStartByFrameId) {
        for (let m of mediaToStartByFrameId[frameId]) {
            m.play();
        }
    }
    if (frameId in mediaToStopByFrameId) {
        for (let m of mediaToStopByFrameId[frameId]) {
            m.pause();
        }
    }
}

/** Initialize the video and audio element management.
 *
 * This function transforms custom XML `video` and `audio` into their
 * HTML counterparts.
 *
 * It extracts the start/stop frame information for each media element,
 * and registers a {@linkcode module:player/Player.frameChange|frameChange} event handler
 * to start and stop media in the appropriate frames.
 *
 * @param {module:player/Player.Player} p - The current Sozi player.
 */
export function init(p) {
    player = p;

    player.on("frameChange", onFrameChange);

    // Find namespace prefix for Sozi.
    // Inlining SVG inside HTML does not allow to use
    // namespace-aware DOM methods.
    const svgRoot = player.presentation.document.root;
    const svgAttributes = svgRoot.attributes;
    let soziPrefix;
    for (let attrIndex = 0; attrIndex < svgAttributes.length; attrIndex ++) {
        if (svgAttributes[attrIndex].value === soziNs) {
            soziPrefix = svgAttributes[attrIndex].name.slice(6);
            break;
        }
    }

    if (!soziPrefix) {
        return;
    }

    // Get custom video and audio elements
    const videoSources = Array.from(svgRoot.getElementsByTagName(soziPrefix + ":video"));
    const audioSources = Array.from(svgRoot.getElementsByTagName(soziPrefix + ":audio"));

    // Replace them with HTML5 audio and video elements
    const mediaList = [];
    for (let source of videoSources.concat(audioSources)) {
        const rect = source.parentNode;
        const tagName = source.localName.slice(soziPrefix.length + 1);

        // Create HTML media source element
        const htmlSource = document.createElementNS(xhtmlNs, "source");
        htmlSource.setAttribute("type", source.getAttribute(soziPrefix + ":type"));
        htmlSource.setAttribute("src",  source.getAttribute(soziPrefix + ":src"));

        let j;
        for (j = 0; j < mediaList.length; j += 1) {
            if (mediaList[j].rect === rect) {
                break;
            }
        }

        if (j === mediaList.length) {
            rect.setAttribute("visibility", "hidden");

            const width  = rect.getAttribute("width");
            const height = rect.getAttribute("height");

            // Create HTML media element
            const htmlMedia = document.createElementNS(xhtmlNs, tagName);
            if (source.getAttribute(soziPrefix + ":controls") === "true") {
                htmlMedia.setAttribute("controls", "controls");
                htmlMedia.setAttribute("style", `width:${width}px;height:${height}px;`);
            }
            if (tagName === "video") {
                htmlMedia.setAttribute("width", width);
                htmlMedia.setAttribute("height", height);
            }
            htmlMedia.addEventListener("click", defaultEventHandler, false);
            htmlMedia.addEventListener("mousedown", defaultEventHandler, false);
            htmlMedia.addEventListener("mouseup", defaultEventHandler, false);
            htmlMedia.addEventListener("mousemove", defaultEventHandler, false);
            htmlMedia.addEventListener("contextmenu", defaultEventHandler, false);

            // Create HTML root element
            const html = document.createElementNS(xhtmlNs, "html");
            html.appendChild(htmlMedia);

            // Create SVG foreign object
            const foreignObject = document.createElementNS(svgNs, "foreignObject");
            foreignObject.setAttribute("x", rect.getAttribute("x"));
            foreignObject.setAttribute("y", rect.getAttribute("y"));
            foreignObject.setAttribute("width", width);
            foreignObject.setAttribute("height", height);
            foreignObject.appendChild(html);

            rect.parentNode.insertBefore(foreignObject, rect.nextSibling);

            if (source.hasAttribute(soziPrefix + ":start-frame")) {
                const startFrameId = source.getAttribute(soziPrefix + ":start-frame");
                const stopFrameId = source.getAttribute(soziPrefix + ":stop-frame");
                if (!(startFrameId in mediaToStartByFrameId)) {
                    mediaToStartByFrameId[startFrameId] = [];
                }
                if (!(stopFrameId in mediaToStopByFrameId)) {
                    mediaToStopByFrameId[stopFrameId] = [];
                }
                mediaToStartByFrameId[startFrameId].push(htmlMedia);
                mediaToStopByFrameId[stopFrameId].push(htmlMedia);
            }

            if (source.getAttribute(soziPrefix + ":loop") === "true") {
                htmlMedia.setAttribute("loop", "true");
            }

            mediaList.push({
                rect: source.parentNode,
                htmlMedia
            });
        }

        // Append HTML source element to current HTML media element
        mediaList[j].htmlMedia.appendChild(htmlSource);
    }
}

/** Disable video and audio support in the current presentation.
 *
 * This function disables the {@linkcode module:player/Player.frameChange|frameChange} event handler
 * and pauses all playing videos.
 */
export function disable() {
    player.off("frameChange", onFrameChange);

    const frameId = player.currentFrame.frameId;
    if (frameId in mediaToStartByFrameId) {
        for (let m of mediaToStartByFrameId[frameId]) {
            m.pause();
        }
    }
}