| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 | /* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { i18next } from '../../../react/features/base/i18n';
import {
    JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import {
    getPinnedParticipant,
    pinParticipant
} from '../../../react/features/base/participants';
import { PresenceLabel } from '../../../react/features/presence-status';
import {
    REMOTE_CONTROL_MENU_STATES,
    RemoteVideoMenuTriggerButton
} from '../../../react/features/remote-video-menu';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import SmallVideo from './SmallVideo';
import UIUtils from '../util/UIUtil';
/**
 *
 * @param {*} spanId
 */
function createContainer(spanId) {
    const container = document.createElement('span');
    container.id = spanId;
    container.className = 'videocontainer';
    container.innerHTML = `
        <div class = 'videocontainer__background'></div>
        <div class = 'videocontainer__toptoolbar'></div>
        <div class = 'videocontainer__toolbar'></div>
        <div class = 'videocontainer__hoverOverlay'></div>
        <div class = 'displayNameContainer'></div>
        <div class = 'avatar-container'></div>
        <div class ='presence-label-container'></div>
        <span class = 'remotevideomenu'></span>`;
    const remoteVideosContainer
        = document.getElementById('filmstripRemoteVideosContainer');
    const localVideoContainer
        = document.getElementById('localVideoTileViewContainer');
    remoteVideosContainer.insertBefore(container, localVideoContainer);
    return container;
}
/**
 *
 */
export default class RemoteVideo extends SmallVideo {
    /**
     * Creates new instance of the <tt>RemoteVideo</tt>.
     * @param user {JitsiParticipant} the user for whom remote video instance will
     * be created.
     * @param {VideoLayout} VideoLayout the video layout instance.
     * @constructor
     */
    constructor(user, VideoLayout) {
        super(VideoLayout);
        this.user = user;
        this.id = user.getId();
        this.videoSpanId = `participant_${this.id}`;
        this._audioStreamElement = null;
        this._supportsRemoteControl = false;
        this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
        this.addRemoteVideoContainer();
        this.updateIndicators();
        this.updateDisplayName();
        this.bindHoverHandler();
        this.flipX = false;
        this.isLocal = false;
        this.popupMenuIsHovered = false;
        this._isRemoteControlSessionActive = false;
        /**
         * The flag is set to <tt>true</tt> after the 'onplay' event has been
         * triggered on the current video element. It goes back to <tt>false</tt>
         * when the stream is removed. It is used to determine whether the video
         * playback has ever started.
         * @type {boolean}
         */
        this.wasVideoPlayed = false;
        /**
         * The flag is set to <tt>true</tt> if remote participant's video gets muted
         * during his media connection disruption. This is to prevent black video
         * being render on the thumbnail, because even though once the video has
         * been played the image usually remains on the video element it seems that
         * after longer period of the video element being hidden this image can be
         * lost.
         * @type {boolean}
         */
        this.mutedWhileDisconnected = false;
        // Bind event handlers so they are only bound once for every instance.
        // TODO The event handlers should be turned into actions so changes can be
        // handled through reducers and middleware.
        this._requestRemoteControlPermissions
            = this._requestRemoteControlPermissions.bind(this);
        this._setAudioVolume = this._setAudioVolume.bind(this);
        this._stopRemoteControl = this._stopRemoteControl.bind(this);
        this.container.onclick = this._onContainerClick;
    }
    /**
     *
     */
    addRemoteVideoContainer() {
        this.container = createContainer(this.videoSpanId);
        this.$container = $(this.container);
        this.initializeAvatar();
        this._setThumbnailSize();
        this.initBrowserSpecificProperties();
        this.updateRemoteVideoMenu();
        this.updateStatusBar();
        this.addAudioLevelIndicator();
        this.addPresenceLabel();
        return this.container;
    }
    /**
     * Checks whether current video is considered hovered. Currently it is hovered
     * if the mouse is over the video, or if the connection indicator or the popup
     * menu is shown(hovered).
     * @private
     * NOTE: extends SmallVideo's method
     */
    _isHovered() {
        return super._isHovered() || this.popupMenuIsHovered;
    }
    /**
     * Generates the popup menu content.
     *
     * @returns {Element|*} the constructed element, containing popup menu items
     * @private
     */
    _generatePopupContent() {
        if (interfaceConfig.filmStripOnly) {
            return;
        }
        const remoteVideoMenuContainer
            = this.container.querySelector('.remotevideomenu');
        if (!remoteVideoMenuContainer) {
            return;
        }
        const { controller } = APP.remoteControl;
        let remoteControlState = null;
        let onRemoteControlToggle;
        if (this._supportsRemoteControl
            && ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
                || APP.remoteControl.controller.activeParticipant === this.id)) {
            if (controller.getRequestedParticipant() === this.id) {
                remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
            } else if (controller.isStarted()) {
                onRemoteControlToggle = this._stopRemoteControl;
                remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
            } else {
                onRemoteControlToggle = this._requestRemoteControlPermissions;
                remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
            }
        }
        const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
        // hide volume when in silent mode
        const onVolumeChange
            = APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
        const participantID = this.id;
        const currentLayout = getCurrentLayout(APP.store.getState());
        let remoteMenuPosition;
        if (currentLayout === LAYOUTS.TILE_VIEW) {
            remoteMenuPosition = 'left top';
        } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
            remoteMenuPosition = 'left bottom';
        } else {
            remoteMenuPosition = 'top center';
        }
        ReactDOM.render(
            <Provider store = { APP.store }>
                <I18nextProvider i18n = { i18next }>
                    <AtlasKitThemeProvider mode = 'dark'>
                        <RemoteVideoMenuTriggerButton
                            initialVolumeValue = { initialVolumeValue }
                            isAudioMuted = { this.isAudioMuted }
                            menuPosition = { remoteMenuPosition }
                            onMenuDisplay
                                = {this._onRemoteVideoMenuDisplay.bind(this)}
                            onRemoteControlToggle = { onRemoteControlToggle }
                            onVolumeChange = { onVolumeChange }
                            participantID = { participantID }
                            remoteControlState = { remoteControlState } />
                    </AtlasKitThemeProvider>
                </I18nextProvider>
            </Provider>,
            remoteVideoMenuContainer);
    }
    /**
     *
     */
    _onRemoteVideoMenuDisplay() {
        this.updateRemoteVideoMenu();
    }
    /**
     * Sets the remote control active status for the remote video.
     *
     * @param {boolean} isActive - The new remote control active status.
     * @returns {void}
     */
    setRemoteControlActiveStatus(isActive) {
        this._isRemoteControlSessionActive = isActive;
        this.updateRemoteVideoMenu();
    }
    /**
     * Sets the remote control supported value and initializes or updates the menu
     * depending on the remote control is supported or not.
     * @param {boolean} isSupported
     */
    setRemoteControlSupport(isSupported = false) {
        if (this._supportsRemoteControl === isSupported) {
            return;
        }
        this._supportsRemoteControl = isSupported;
        this.updateRemoteVideoMenu();
    }
    /**
     * Requests permissions for remote control session.
     */
    _requestRemoteControlPermissions() {
        APP.remoteControl.controller.requestPermissions(this.id, this.VideoLayout.getLargeVideoWrapper())
            .then(result => {
                if (result === null) {
                    return;
                }
                this.updateRemoteVideoMenu();
                APP.UI.messageHandler.notify(
                    'dialog.remoteControlTitle',
                    result === false ? 'dialog.remoteControlDeniedMessage' : 'dialog.remoteControlAllowedMessage',
                    { user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
                );
                if (result === true) {
                    // the remote control permissions has been granted
                    // pin the controlled participant
                    const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
                    const pinnedId = pinnedParticipant.id;
                    if (pinnedId !== this.id) {
                        APP.store.dispatch(pinParticipant(this.id));
                    }
                }
            }, error => {
                logger.error(error);
                this.updateRemoteVideoMenu();
                APP.UI.messageHandler.notify(
                    'dialog.remoteControlTitle',
                    'dialog.remoteControlErrorMessage',
                    { user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
                );
            });
        this.updateRemoteVideoMenu();
    }
    /**
     * Stops remote control session.
     */
    _stopRemoteControl() {
        // send message about stopping
        APP.remoteControl.controller.stop();
        this.updateRemoteVideoMenu();
    }
    /**
     * Change the remote participant's volume level.
     *
     * @param {int} newVal - The value to set the slider to.
     */
    _setAudioVolume(newVal) {
        if (this._audioStreamElement) {
            this._audioStreamElement.volume = newVal;
        }
    }
    /**
     * Updates the remote video menu.
     *
     * @param isMuted the new muted state to update to
     */
    updateRemoteVideoMenu(isMuted) {
        if (typeof isMuted !== 'undefined') {
            this.isAudioMuted = isMuted;
        }
        this._generatePopupContent();
    }
    /**
     * @inheritDoc
     * @override
     */
    setVideoMutedView(isMuted) {
        super.setVideoMutedView(isMuted);
        // Update 'mutedWhileDisconnected' flag
        this._figureOutMutedWhileDisconnected();
    }
    /**
     * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
     * account remote participant's network connectivity and video muted status.
     *
     * @private
     */
    _figureOutMutedWhileDisconnected() {
        const isActive = this.isConnectionActive();
        if (!isActive && this.isVideoMuted) {
            this.mutedWhileDisconnected = true;
        } else if (isActive && !this.isVideoMuted) {
            this.mutedWhileDisconnected = false;
        }
    }
    /**
     * Removes the remote stream element corresponding to the given stream and
     * parent container.
     *
     * @param stream the MediaStream
     * @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
     */
    removeRemoteStreamElement(stream) {
        if (!this.container) {
            return false;
        }
        const isVideo = stream.isVideoTrack();
        const elementID = SmallVideo.getStreamElementID(stream);
        const select = $(`#${elementID}`);
        select.remove();
        if (isVideo) {
            this.wasVideoPlayed = false;
        }
        logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
        if (stream === this.videoStream) {
            this.videoStream = null;
        }
        this.updateView();
    }
    /**
     * Checks whether the remote user associated with this <tt>RemoteVideo</tt>
     * has connectivity issues.
     *
     * @return {boolean} <tt>true</tt> if the user's connection is fine or
     * <tt>false</tt> otherwise.
     */
    isConnectionActive() {
        return this.user.getConnectionStatus() === JitsiParticipantConnectionStatus.ACTIVE;
    }
    /**
     * The remote video is considered "playable" once the stream has started
     * according to the {@link #hasVideoStarted} result.
     * It will be allowed to display video also in
     * {@link JitsiParticipantConnectionStatus.INTERRUPTED} if the video was ever
     *  played and was not muted while not in ACTIVE state. This basically means
     * that there is stalled video image cached that could be displayed. It's used
     * to show "grey video image" in user's thumbnail when there are connectivity
     * issues.
     *
     * @inheritdoc
     * @override
     */
    isVideoPlayable() {
        const connectionState = APP.conference.getParticipantConnectionStatus(this.id);
        return super.isVideoPlayable()
            && this.hasVideoStarted()
            && (connectionState === JitsiParticipantConnectionStatus.ACTIVE
                || (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED && !this.mutedWhileDisconnected));
    }
    /**
     * @inheritDoc
     */
    updateView() {
        this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
        this.updateConnectionStatusIndicator();
        // This must be called after 'updateConnectionStatusIndicator' because it
        // affects the display mode by modifying 'mutedWhileDisconnected' flag
        super.updateView();
    }
    /**
     * Updates the UI to reflect user's connectivity status.
     */
    updateConnectionStatusIndicator() {
        const connectionStatus = this.user.getConnectionStatus();
        logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
        // FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering'
        // Update 'mutedWhileDisconnected' flag
        this._figureOutMutedWhileDisconnected();
        this.updateConnectionStatus(connectionStatus);
    }
    /**
     * Removes RemoteVideo from the page.
     */
    remove() {
        super.remove();
        this.removePresenceLabel();
        this.removeRemoteVideoMenu();
    }
    /**
     *
     * @param {*} streamElement
     * @param {*} stream
     */
    waitForPlayback(streamElement, stream) {
        const webRtcStream = stream.getOriginalStream();
        const isVideo = stream.isVideoTrack();
        if (!isVideo || webRtcStream.id === 'mixedmslabel') {
            return;
        }
        streamElement.onplaying = () => {
            this.wasVideoPlayed = true;
            this.VideoLayout.remoteVideoActive(streamElement, this.id);
            streamElement.onplaying = null;
            // Refresh to show the video
            this.updateView();
        };
    }
    /**
     * Checks whether the video stream has started for this RemoteVideo instance.
     *
     * @returns {boolean} true if this RemoteVideo has a video stream for which
     * the playback has been started.
     */
    hasVideoStarted() {
        return this.wasVideoPlayed;
    }
    /**
     *
     * @param {*} stream
     */
    addRemoteStreamElement(stream) {
        if (!this.container) {
            logger.debug('Not attaching remote stream due to no container');
            return;
        }
        const isVideo = stream.isVideoTrack();
        isVideo ? this.videoStream = stream : this.audioStream = stream;
        if (isVideo) {
            this.setVideoType(stream.videoType);
        }
        if (!stream.getOriginalStream()) {
            logger.debug('Remote video stream has no original stream');
            return;
        }
        const streamElement = SmallVideo.createStreamElement(stream);
        // Put new stream element always in front
        UIUtils.prependChild(this.container, streamElement);
        $(streamElement).hide();
        this.waitForPlayback(streamElement, stream);
        stream.attach(streamElement);
        if (!isVideo) {
            this._audioStreamElement = streamElement;
            // If the remote video menu was created before the audio stream was
            // attached we need to update the menu in order to show the volume
            // slider.
            this.updateRemoteVideoMenu();
        }
    }
    /**
     * Triggers re-rendering of the display name using current instance state.
     *
     * @returns {void}
     */
    updateDisplayName() {
        if (!this.container) {
            logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
            return;
        }
        this._renderDisplayName({
            elementID: `${this.videoSpanId}_name`,
            participantID: this.id
        });
    }
    /**
     * Removes remote video menu element from video element identified by
     * given <tt>videoElementId</tt>.
     *
     * @param videoElementId the id of local or remote video element.
     */
    removeRemoteVideoMenu() {
        const menuSpan = this.$container.find('.remotevideomenu');
        if (menuSpan.length) {
            ReactDOM.unmountComponentAtNode(menuSpan.get(0));
            menuSpan.remove();
        }
    }
    /**
     * Mounts the {@code PresenceLabel} for displaying the participant's current
     * presence status.
     *
     * @return {void}
     */
    addPresenceLabel() {
        const presenceLabelContainer = this.container.querySelector('.presence-label-container');
        if (presenceLabelContainer) {
            ReactDOM.render(
                <Provider store = { APP.store }>
                    <I18nextProvider i18n = { i18next }>
                        <PresenceLabel
                            participantID = { this.id }
                            className = 'presence-label' />
                    </I18nextProvider>
                </Provider>,
                presenceLabelContainer);
        }
    }
    /**
     * Unmounts the {@code PresenceLabel} component.
     *
     * @return {void}
     */
    removePresenceLabel() {
        const presenceLabelContainer = this.container.querySelector('.presence-label-container');
        if (presenceLabelContainer) {
            ReactDOM.unmountComponentAtNode(presenceLabelContainer);
        }
    }
}
 |