/* global $, APP, interfaceConfig */ /* eslint-disable no-unused-vars */ import { AtlasKitThemeProvider } from '@atlaskit/theme'; import Logger from 'jitsi-meet-logger'; import React from 'react'; import ReactDOM from 'react-dom'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { i18next } from '../../../react/features/base/i18n'; import { JitsiParticipantConnectionStatus } from '../../../react/features/base/lib-jitsi-meet'; import { getParticipantById } from '../../../react/features/base/participants'; import { isTestModeEnabled } from '../../../react/features/base/testing'; import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks'; import { PresenceLabel } from '../../../react/features/presence-status'; import { stopController, requestRemoteControl } from '../../../react/features/remote-control'; import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu'; /* eslint-enable no-unused-vars */ import UIUtils from '../util/UIUtil'; import SmallVideo from './SmallVideo'; const logger = Logger.getLogger(__filename); /** * List of container events that we are going to process, will be added as listener to the * container for every event in the list. The latest event will be stored in redux. */ const containerEvents = [ 'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting' ]; /** * * @param {*} spanId */ function createContainer(spanId) { const container = document.createElement('span'); container.id = spanId; container.className = 'videocontainer'; container.innerHTML = `
`; 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 RemoteVideo. * @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.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center'; this.addRemoteVideoContainer(); this.updateIndicators(); this.updateDisplayName(); this.bindHoverHandler(); this.flipX = false; this.isLocal = false; /** * The flag is set to true after the 'canplay' event has been * triggered on the current video element. It goes back to false * when the stream is removed. It is used to determine whether the video * playback has ever started. * @type {boolean} */ this._canPlayEventReceived = 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._setAudioVolume = this._setAudioVolume.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; } /** * Generates the popup menu content. * * @returns {Element|*} the constructed element, containing popup menu items * @private */ _generatePopupContent() { const remoteVideoMenuContainer = this.container.querySelector('.remotevideomenu'); if (!remoteVideoMenuContainer) { return; } 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; ReactDOM.render( , remoteVideoMenuContainer); } /** * */ _onRemoteVideoMenuDisplay() { 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. */ updateRemoteVideoMenu() { this._generatePopupContent(); } /** * Removes the remote stream element corresponding to the given stream and * parent container. * * @param stream the MediaStream * @param isVideo true if given stream 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._canPlayEventReceived = false; } logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select); if (stream === this.videoStream) { this.videoStream = null; } this.updateView(); } /** * The remote video is considered "playable" once the can play event has been received. * * @inheritdoc * @override */ isVideoPlayable() { const participant = getParticipantById(APP.store.getState(), this.id); const { connectionStatus } = participant || {}; return ( super.isVideoPlayable() && this._canPlayEventReceived && connectionStatus === JitsiParticipantConnectionStatus.ACTIVE ); } /** * @inheritDoc */ updateView() { this.$container.toggleClass('audio-only', APP.conference.isAudioOnly()); super.updateView(); } /** * Removes RemoteVideo from the page. */ remove() { super.remove(); this.removePresenceLabel(); this.removeRemoteVideoMenu(); } /** * * @param {*} streamElement * @param {*} stream */ waitForPlayback(streamElement, stream) { $(streamElement).hide(); const webRtcStream = stream.getOriginalStream(); const isVideo = stream.isVideoTrack(); if (!isVideo || webRtcStream.id === 'mixedmslabel') { return; } const listener = () => { this._canPlayEventReceived = true; logger.info(`${this.id} video is now active`, streamElement); if (streamElement) { $(streamElement).show(); } streamElement.removeEventListener('canplay', listener); // Refresh to show the video this.updateView(); }; streamElement.addEventListener('canplay', listener); } /** * * @param {*} stream */ addRemoteStreamElement(stream) { if (!this.container) { logger.debug('Not attaching remote stream due to no container'); return; } const isVideo = stream.isVideoTrack(); if (isVideo) { this.videoStream = stream; } else { this.audioStream = stream; } if (!stream.getOriginalStream()) { logger.debug('Remote video stream has no original stream'); return; } let streamElement = SmallVideo.createStreamElement(stream); // Put new stream element always in front streamElement = UIUtils.prependChild(this.container, streamElement); 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(); } else if (isTestModeEnabled(APP.store.getState())) { const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name)); containerEvents.forEach(event => { streamElement.addEventListener(event, cb.bind(this, event)); }); } } /** * 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 videoElementId. * * @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( , presenceLabelContainer); } } /** * Unmounts the {@code PresenceLabel} component. * * @return {void} */ removePresenceLabel() { const presenceLabelContainer = this.container.querySelector('.presence-label-container'); if (presenceLabelContainer) { ReactDOM.unmountComponentAtNode(presenceLabelContainer); } } }