Source: media/player/ShakaPlayer.js

import shaka from "shaka-player";
import "shaka-player/dist/controls.css";

import { HTML5Video } from "./HTML5Video.js";
import { ThumbnailVideoPlayer } from "../thumbnail/ThumbnailVideoPlayer.js";
import { VectorGraphics } from "./VectorGraphics.js";
import { VideoPlayer } from "./VideoPlayer.js";
import { ShakaPlayerErrorHandler } from "./ShakaPlayerErrorHandler.js";
export { ShakaPlayer }

/**
 * Class to build the AWI version of the Shaka Player.
 * @author ckraemme <ckraemme@awi.de>
 */
class ShakaPlayer extends VideoPlayer {
    /**
     * Build the player.
     * @param {string/HTMLElement} target - HTML element.
     * @param {Object.<string,Array>} options - Dictionary with input configuration for the player. The configuration can be set for the keys media, thumbnail, cover and poster. They contain the respective URLs.
     */
    constructor(target, options) {
        super(target, options);
        this.manifest;
        this.videoElement = document.createElement('video');
        this.videoElement.className = "video-player-video";
        this.videoContainer.appendChild(this.videoElement);
        this.shakaPlayer = new shaka.Player(this.videoElement);
        this.shakaUI = new shaka.ui.Overlay(this.shakaPlayer, this.videoContainer, this.videoElement);
        this.shakaUIControls = this.shakaUI.getControls();
        this.buildShakaPlayer_();
        window.shakaPlayer = this.shakaPlayer;
        window.shakaUI = this.shakaUI;
    }

    /**
     * Configure, initialize and load media into the player. Because the Shaka UI library is used, the player instance is build automatically by the former, that triggers the event 'shaka-ui-loaded' when it is done.
     * @private
     */
    buildShakaPlayer_() {
        this.initEventListeners_();
        this.applyHTML5VideoSettings_();
        this.addExperimentalABRRestriction_();
        this.applyShakaSpecificPlayerConfigurations_();
        this.loadVideoManifestAndSelectThumbnailManifest_();
    }

    /**
     * Initialize events to the Shaka Player that extend its functionality, apply configuration and improve error logging.
     * @private
     */
    initEventListeners_() {
        this.shakaPlayer.addEventListener('error', ShakaPlayerErrorHandler.onErrorEvent);
        this.shakaUIControls.addEventListener('error', ShakaPlayerErrorHandler.onErrorEvent);
        this.initEventsAfterVideoHasLoaded_();
    }

    /**
     * Initialize events that should trigger when video has loaded.
     * @private
     */
    initEventsAfterVideoHasLoaded_() {
        this.videoContainer.querySelector('video').addEventListener('loadeddata', () => {
            this.videoContainer.querySelector('video').classList.add("loaded");
            this.configureUIShakaPlayer_();
            this.extendFunctionalityForAudio_();
            if (this.webvttUrl)
                new ThumbnailVideoPlayer(this.videoContainer, this.webvttUrl, 20, true, '.shaka-seek-bar-container .shaka-seek-bar');
        }, false);
    }

    /**
     * Configure adaptive bitrate restriction in the Shaka Player that are based on the video player size to reduce traffic and loading time. Experimental state.
     * @private
     */
    addExperimentalABRRestriction_() {
        new ResizeObserver(() => {
            if (typeof this.videoContainer !== 'undefined' && this.shakaPlayer.getVariantTracks() !== 'undefined') {
                const realHeight = window.devicePixelRatio * this.videoContainer.clientHeight;
                const manifestHeights = this.shakaPlayer.getVariantTracks().map((x) => {
                    return x.height;
                });
                const streamHeight = Math.min.apply(Math, manifestHeights.filter((x) => {
                    return x >= realHeight;
                }));
                this.shakaPlayer.configure({
                    'abr': {
                        'restrictions': {
                            'maxHeight': streamHeight
                        }
                    }
                });
            }
        }).observe(this.videoElement);
    }

    /**
     * Method for applying configurations to the Shaka Player provided by its developer.
     * <ul>
     * <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.StreamingConfiguration"> Streaming configuration </a> </li>
     * <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.AbrConfiguration"> ABR configuration </a> </li>
     * <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.PlayerConfiguration"> Further player configuration </a> </li>
     * </ul>
     * @private
     */
    applyShakaSpecificPlayerConfigurations_() {

        // Example: this.player.configure("streaming.rebufferingGoal", 4)
        this.shakaPlayer.configure("preferredAudioLanguage", "en");
    }

    /**
     * Selects the webVTT thumbnail manifest based on the video manifest file that can be loaded.
     * @private
     */
    loadVideoManifestAndSelectThumbnailManifest_() {
        this.loadMedia_(this.shakaPlayer, this.mediaUrlList).then((manifest) => {
            this.manifest = manifest;
            let index = this.mediaUrlList.indexOf(manifest);
        }).catch(error => {
            console.error("Error", error.message);
        });
    }

    /**
     * Try to load a media file from a list of media paths as long as there are items in the list.
     * @param {Object} player - Class instance of the player.
     * @param {Array} files - Files with all media files that the player tries to load in ascending (array position) order.
     * @return {string|Promise} When successful, return the path of the media file, else return Promise.reject(reason).
     * @private
     */
    loadMedia_(player, files) {
        function load(path) {
            return player.load(path).then(() => {
                //console.info('Manifest loaded: ' + path);
                return true;
            })
                .catch(error => {
                    console.info('Manifest can not be loaded: ' + path);
                    ShakaPlayerErrorHandler.onError(error);
                    return false;
                });
        }

        files = files.filter(n => n !== null && n !== undefined);
        return load(files[0]).then(success => {
            if (success) {
                return files[0];
            } else {
                const tmp = files.slice(1, files.length);
                if (tmp == 0) {
                    this.setErrorImage_(player);
                    return Promise.reject(new Error('No supported manifest found! Playback cannot be started!'));
                }
                else
                    return this.loadMedia_(player, tmp);
            }
        });
    }

    /**
     * Include user notification as poster in case of video error.
     * @param {Object} player - Class instance of the player.
     * @private
     */
    setErrorImage_(player) {
        const backgroundColor = '#F2F2F2';
        player.getMediaElement().poster = VectorGraphics.errorImage('Video', backgroundColor);
        player.getMediaElement().style.backgroundColor = backgroundColor;
        player.getMediaElement().style.transition = undefined;
    }

    /**
     * Configure the user interface with respect to the media content.
     * <ul>
     * <li> <a href="https://shaka-player-demo.appspot.com/docs/api/tutorial-ui-customization.html"> Overflow menu and control panel configuration </a> </li>
     * <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.UIConfiguration"> UI configuration </a> </li>
     * </ul>
     * @private
     */
    configureUIShakaPlayer_() {
        let overflowMenu, controlPanel;

        if (this.shakaPlayer.isAudioOnly()) {
            overflowMenu = ["loop", "playback_rate"];
            controlPanel = ["play_pause", "time_and_duration", "spacer", "mute", "volume", "overflow_menu"];
        }
        else {
            overflowMenu = ["loop", "playback_rate", "language", "captions", "quality"];
            controlPanel = ["play_pause", "time_and_duration", "spacer", "mute", "volume", "fullscreen", "overflow_menu"];
        }

        if (!HTML5Video.hasAudio(this.videoContainer)) {
            overflowMenu = overflowMenu.filter(v => !['language'].includes(v));
            controlPanel = controlPanel.filter(v => !["mute", "volume"].includes(v));
        }

        if (['m3u8', 'mpd'].includes(this.manifest.split('.').pop().toLowerCase())) {
            if (this.shakaPlayer.isLive())
                overflowMenu = overflowMenu.filter(v => !['loop', 'playback_rate'].includes(v));
            if (!this.shakaPlayer.getAudioLanguages() || this.shakaPlayer.getAudioLanguages().length <= 1)
                overflowMenu = overflowMenu.filter(v => !['language'].includes(v));
        } else {
            overflowMenu = overflowMenu.filter(v => !['language', 'quality'].includes(v));
        }

        const keyControlViaHTML5VideoElement = true;
        let uiConfigurationOptions = {
            'controlPanelElements': controlPanel,
            'overflowMenuButtons': overflowMenu,
            'enableKeyboardPlaybackControls': !keyControlViaHTML5VideoElement,
            'playbackRates': [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
            'showUnbufferedStart': true
        };
        this.shakaUI.configure(uiConfigurationOptions);
    }

    /**
     * Extend the functionality of the shaka player by providing a method that prevents the control bar from hiding while playing audio.
     * @private
     */
    extendFunctionalityForAudio_() {

        if (this.shakaPlayer.isAudioOnly()) {
            const setUIAlwaysVisible = true;
            const targetNode = this.videoContainer.querySelector('.shaka-controls-container');
            const config = { attributes: true, childList: false, subtree: true };

            const callback = (mutationList, observer) => {
                for (const mutation of mutationList) {
                    if (mutation.type === 'attributes' && mutation.attributeName == 'shown') {
                        if (mutation.target.getAttribute(mutation.attributeName) == undefined)
                            mutation.target.setAttribute("shown", setUIAlwaysVisible);
                    }
                }
            };
            const observer = new MutationObserver(callback);
            observer.observe(targetNode, config);
        }
    }
}