streaming_text_TextController.js

/**
 * The copyright in this software is being made available under the BSD License,
 * included below. This software may be subject to other third party and contributor
 * rights, including patent rights, and no such rights are granted under this license.
 *
 * Copyright (c) 2013, Dash Industry Forum.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation and/or
 *  other materials provided with the distribution.
 *  * Neither the name of Dash Industry Forum nor the names of its
 *  contributors may be used to endorse or promote products derived from this software
 *  without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */
import Constants from '../constants/Constants';
import FactoryMaker from '../../core/FactoryMaker';
import TextSourceBuffer from './TextSourceBuffer';
import TextTracks from './TextTracks';
import VTTParser from '../utils/VTTParser';
import VttCustomRenderingParser from '../utils/VttCustomRenderingParser';
import TTMLParser from '../utils/TTMLParser';
import Debug from '../../core/Debug';
import EventBus from '../../core/EventBus';
import Events from '../../core/events/Events';
import MediaPlayerEvents from '../../streaming/MediaPlayerEvents';
import {checkParameterType} from '../utils/SupervisorTools';
import DVBFonts from './DVBFonts';

function TextController(config) {

    let context = this.context;

    const adapter = config.adapter;
    const errHandler = config.errHandler;
    const manifestModel = config.manifestModel;
    const mediaController = config.mediaController;
    const baseURLController = config.baseURLController;
    const videoModel = config.videoModel;
    const settings = config.settings;

    let instance,
        streamData,
        textSourceBuffers,
        textTracks,
        vttParser,
        vttCustomRenderingParser,
        ttmlParser,
        eventBus,
        allTracksAreDisabled,
        forceTextStreaming,
        textTracksAdded,
        disableTextBeforeTextTracksAdded,
        dvbFonts,
        logger;

    function setup() {
        forceTextStreaming = false;
        textTracksAdded = false;
        disableTextBeforeTextTracksAdded = false;

        vttParser = VTTParser(context).getInstance();
        vttCustomRenderingParser = VttCustomRenderingParser(context).getInstance();
        ttmlParser = TTMLParser(context).getInstance();
        eventBus = EventBus(context).getInstance();
        logger = Debug(context).getInstance().getLogger(instance);

        resetInitialSettings();
    }

    function initialize() {
        dvbFonts = DVBFonts(context).create({
            adapter,
            baseURLController,
        });
        eventBus.on(Events.TEXT_TRACKS_QUEUE_INITIALIZED, _onTextTracksAdded, instance);
        eventBus.on(Events.DVB_FONT_DOWNLOAD_FAILED, _onFontDownloadFailure, instance);
        eventBus.on(Events.DVB_FONT_DOWNLOAD_COMPLETE, _onFontDownloadSuccess, instance);
        if (settings.get().streaming.text.webvtt.customRenderingEnabled) {
            eventBus.on(Events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance);
            eventBus.on(Events.PLAYBACK_SEEKING, _onPlaybackSeeking, instance);
        }
    }

    function initializeForStream(streamInfo) {
        const streamId = streamInfo.id;
        const tracks = TextTracks(context).create({
            videoModel,
            settings,
            streamInfo
        });
        tracks.initialize();
        textTracks[streamId] = tracks;

        const textSourceBuffer = TextSourceBuffer(context).create({
            errHandler,
            adapter,
            dvbFonts,
            manifestModel,
            mediaController,
            videoModel,
            textTracks: tracks,
            vttParser,
            vttCustomRenderingParser,
            ttmlParser,
            streamInfo,
            settings
        });
        textSourceBuffer.initialize();
        textSourceBuffers[streamId] = textSourceBuffer;

        streamData[streamId] = {};
        streamData[streamId].lastEnabledIndex = -1;
    }

    /**
     * All media infos have been added. Start creating the track objects
     * @param {object} streamInfo
     */
    function createTracks(streamInfo) {
        const streamId = streamInfo.id;

        if (!textTracks[streamId]) {
            return;
        }
        textTracks[streamId].createTracks();
    }

    /**
     * Adds the new mediaInfo objects to the textSourceBuffer.
     * @param {object} streamInfo
     * @param {array} mInfos
     * @param {string|null} mimeType
     * @param {object} fragmentModel
     */
    function addMediaInfosToBuffer(streamInfo, type, mInfos, fragmentModel = null) {
        const streamId = streamInfo.id;

        if (!textSourceBuffers[streamId]) {
            return;
        }
        textSourceBuffers[streamId].addMediaInfos(type, mInfos, fragmentModel);
    }

    function getTextSourceBuffer(streamInfo) {
        const streamId = streamInfo.id;

        if (textSourceBuffers && textSourceBuffers[streamId]) {
            return textSourceBuffers[streamId];
        }
    }

    function getAllTracksAreDisabled() {
        return allTracksAreDisabled;
    }

    function addEmbeddedTrack(streamInfo, mediaInfo) {
        const streamId = streamInfo.id;

        if (!textSourceBuffers[streamId]) {
            return;
        }

        textSourceBuffers[streamId].addEmbeddedTrack(mediaInfo);
    }

    /**
     * Event that is triggered if a font download of a font described in an essential property descriptor
     * tag fails.
     * @param {FontInfo} font - font information
     * @private
     */
    function _onFontDownloadFailure(font) {
        logger.error(`Could not download ${font.isEssential ? 'an essential' : 'a'} font - fontFamily: ${font.fontFamily}, url: ${font.url}`);
        if (font.isEssential) {
            let idx = textTracks[font.streamId].getTrackIdxForId(font.trackId);
            textTracks[font.streamId].setModeForTrackIdx(idx, Constants.TEXT_DISABLED);
        }
    };

    /**
     * Set a font with an essential property
     * @private
     */
    function _onFontDownloadSuccess(font) {
        logger.debug(`Successfully downloaded ${font.isEssential ? 'an essential' : 'a'} font - fontFamily: ${font.fontFamily}, url: ${font.url}`);
        if (font.isEssential) {
            let idx = textTracks[font.streamId].getTrackIdxForId(font.trackId);
            if (idx === textTracks[font.streamId].getCurrentTrackIdx()) {
                textTracks[font.streamId].setModeForTrackIdx(idx, Constants.TEXT_SHOWING);
            } else {
                textTracks[font.streamId].setModeForTrackIdx(idx, Constants.TEXT_HIDDEN);
            }
        }
    }

    function _onTextTracksAdded(e) {
        let tracks = e.tracks;
        let index = e.index;
        const streamId = e.streamId;

        const textDefaultEnabled = settings.get().streaming.text.defaultEnabled;

        if ((textDefaultEnabled === false && !isTextEnabled()) || disableTextBeforeTextTracksAdded) {
            // disable text at startup if explicitly configured with setTextDefaultEnabled(false) or if there is no defaultSettings (configuration or from domStorage)
            setTextTrack(streamId, -1);
        } else {
            const currentTrack = mediaController.getCurrentTrackFor(Constants.TEXT, streamId);
            if (currentTrack) {
                const defaultSettings = {
                    lang: currentTrack.lang,
                    role: currentTrack.roles[0],
                    index: currentTrack.index,
                    codec: currentTrack.codec,
                    accessibility: currentTrack.accessibility[0]
                };
                tracks.some((item, idx) => {
                    // matchSettings is compatible with setTextDefaultLanguage and setInitialSettings
                    if (mediaController.matchSettings(defaultSettings, item)) {
                        setTextTrack(streamId, idx);
                        index = idx;
                        return true;
                    }
                });
            }
            allTracksAreDisabled = false;
        }

        streamData[streamId].lastEnabledIndex = index;

        eventBus.trigger(MediaPlayerEvents.TEXT_TRACKS_ADDED, {
            enabled: isTextEnabled(),
            index: index,
            tracks: tracks,
            streamId
        });

        textTracksAdded = true;

        dvbFonts.addFontsFromTracks(tracks, streamId);

        // Initially disable any tracks with essential property font downloads
        dvbFonts.getFonts().forEach(font => {
            if (font.isEssential) {
                let idx = textTracks[font.streamId].getTrackIdxForId(font.trackId);
                textTracks[font.streamId].setModeForTrackIdx(idx, Constants.TEXT_DISABLED);
            }
        });

        dvbFonts.downloadFonts();
    }

    function _onPlaybackTimeUpdated(e) {
        try {
            const streamId = e.streamId;

            if (!textTracks[streamId] || isNaN(e.time)) {
                return;
            }
            textTracks[streamId].manualCueProcessing(e.time);
        } catch (err) {
        }
    }

    function _onPlaybackSeeking(e) {
        try {
            const streamId = e.streamId;

            if (!textTracks[streamId]) {
                return;
            }
            textTracks[streamId].disableManualTracks();
        } catch (e) {

        }
    }

    function enableText(streamId, enable) {
        checkParameterType(enable, 'boolean');
        if (isTextEnabled() !== enable) {
            // change track selection
            if (enable) {
                // apply last enabled track
                setTextTrack(streamId, streamData[streamId].lastEnabledIndex);
            }

            if (!enable) {
                // keep last index and disable text track
                streamData[streamId].lastEnabledIndex = getCurrentTrackIdx(streamId);
                if (!textTracksAdded) {
                    disableTextBeforeTextTracksAdded = true;
                } else {
                    setTextTrack(streamId, -1);
                }
            }
        }

        return true
    }

    function isTextEnabled() {
        let enabled = true;
        if (allTracksAreDisabled && !forceTextStreaming) {
            enabled = false;
        }
        return enabled;
    }

    // when set to true ScheduleController will allow schedule of chunks even if tracks are all disabled. Allowing streaming to hidden track for external players to work with.
    function enableForcedTextStreaming(enable) {
        checkParameterType(enable, 'boolean');
        forceTextStreaming = enable;
        return true
    }

    function setTextTrack(streamId, idx) {
        // For external time text file, the only action needed to change a track is marking the track mode to showing.
        // Fragmented text tracks need the additional step of calling TextController.setTextTrack();
        allTracksAreDisabled = idx === -1;

        if (allTracksAreDisabled && mediaController) {
            mediaController.saveTextSettingsDisabled();
        }

        let oldTrackIdx = getCurrentTrackIdx(streamId);

        // No change, no action required
        if (oldTrackIdx === idx || !textTracks[streamId]) {
            return;
        }

        textTracks[streamId].disableManualTracks();

        let currentTrackInfo = textTracks[streamId].getCurrentTrackInfo();
        let currentNativeTrackInfo = (currentTrackInfo) ? videoModel.getTextTrack(currentTrackInfo.kind, currentTrackInfo.id, currentTrackInfo.lang, currentTrackInfo.isTTML, currentTrackInfo.isEmbedded) : null;

        // Don't change disabled tracks - dvb font download for essential property failed or not complete
        if (currentNativeTrackInfo && (currentNativeTrackInfo.mode !== Constants.TEXT_DISABLED)) {
            textTracks[streamId].setModeForTrackIdx(oldTrackIdx, Constants.TEXT_HIDDEN);
        }

        textTracks[streamId].setCurrentTrackIdx(idx);

        currentTrackInfo = textTracks[streamId].getCurrentTrackInfo();

        const dispatchForManualRendering = settings.get().streaming.text.dispatchForManualRendering;

        if (currentTrackInfo && !dispatchForManualRendering && (currentTrackInfo.mode !== Constants.TEXT_DISABLED)) {
            textTracks[streamId].setModeForTrackIdx(idx, Constants.TEXT_SHOWING);
        }

        if (currentTrackInfo && currentTrackInfo.isFragmented && !currentTrackInfo.isEmbedded) {
            _setFragmentedTextTrack(streamId, currentTrackInfo, oldTrackIdx);
        } else if (currentTrackInfo && !currentTrackInfo.isFragmented) {
            _setNonFragmentedTextTrack(streamId, currentTrackInfo);
        }

        mediaController.setTrack(currentTrackInfo);
    }

    function _setFragmentedTextTrack(streamId, currentTrackInfo, oldTrackIdx) {

        if (!textSourceBuffers[streamId]) {
            return;
        }

        let config = textSourceBuffers[streamId].getConfig();
        let fragmentedTracks = config.fragmentedTracks;

        for (let i = 0; i < fragmentedTracks.length; i++) {
            let mediaInfo = fragmentedTracks[i];
            if (currentTrackInfo.lang === mediaInfo.lang &&
                (mediaInfo.id ? currentTrackInfo.id === mediaInfo.id : currentTrackInfo.index === mediaInfo.index)) {
                let currentFragTrack = mediaController.getCurrentTrackFor(Constants.TEXT, streamId);
                if (mediaInfo.id ? currentFragTrack.id !== mediaInfo.id : currentFragTrack.index !== mediaInfo.index) {
                    textTracks[streamId].deleteCuesFromTrackIdx(oldTrackIdx);
                    textSourceBuffers[streamId].setCurrentFragmentedTrackIdx(i);
                } else if (oldTrackIdx === -1) {
                    // in fragmented use case, if the user selects the older track (the one selected before disabled text track)
                    // no CURRENT_TRACK_CHANGED event will be triggered because the mediaInfo in the StreamProcessor is equal to the one we are selecting
                    // For that reason we reactivate the StreamProcessor and the ScheduleController
                    eventBus.trigger(Events.SET_FRAGMENTED_TEXT_AFTER_DISABLED, {}, {
                        streamId,
                        mediaType: Constants.TEXT
                    });
                }
            }
        }
    }

    function _setNonFragmentedTextTrack(streamId, currentTrackInfo) {
        eventBus.trigger(Events.SET_NON_FRAGMENTED_TEXT, {
            currentTrackInfo
        }, {
            streamId,
            mediaType: Constants.TEXT
        });
    }

    function getCurrentTrackIdx(streamId) {
        return textTracks[streamId].getCurrentTrackIdx();
    }

    function deactivateStream(streamInfo) {
        if (!streamInfo) {
            return;
        }
        const streamId = streamInfo.id;

        if (textSourceBuffers[streamId]) {
            textSourceBuffers[streamId].resetMediaInfos();
        }

        if (textTracks[streamId]) {
            textTracks[streamId].deleteAllTextTracks();
        }
    }

    function resetInitialSettings() {
        textSourceBuffers = {};
        textTracks = {};
        streamData = {};
        allTracksAreDisabled = true;
        textTracksAdded = false;
        disableTextBeforeTextTracksAdded = false;
    }

    function reset() {
        dvbFonts.reset();
        resetInitialSettings();
        eventBus.off(Events.TEXT_TRACKS_QUEUE_INITIALIZED, _onTextTracksAdded, instance);
        eventBus.off(Events.DVB_FONT_DOWNLOAD_FAILED, _onFontDownloadFailure, instance);
        eventBus.off(Events.DVB_FONT_DOWNLOAD_COMPLETE, _onFontDownloadSuccess, instance);
        if (settings.get().streaming.text.webvtt.customRenderingEnabled) {
            eventBus.off(Events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance);
            eventBus.off(Events.PLAYBACK_SEEKING, _onPlaybackSeeking, instance)
        }

        Object.keys(textSourceBuffers).forEach((key) => {
            textSourceBuffers[key].resetEmbedded();
            textSourceBuffers[key].reset();
        });
    }

    instance = {
        deactivateStream,
        initialize,
        initializeForStream,
        createTracks,
        getTextSourceBuffer,
        getAllTracksAreDisabled,
        addEmbeddedTrack,
        enableText,
        isTextEnabled,
        setTextTrack,
        getCurrentTrackIdx,
        enableForcedTextStreaming,
        addMediaInfosToBuffer,
        reset
    };
    setup();
    return instance;
}

TextController.__dashjs_factory_name = 'TextController';
export default FactoryMaker.getClassFactory(TextController);