jiti-meet/modules/UI/videolayout/RemoteVideo.js

626 lines
19 KiB
JavaScript
Raw Normal View History

/* 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,
2018-11-30 22:13:39 +00:00
getCurrentLayout
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
2015-12-14 12:26:50 +00:00
import SmallVideo from './SmallVideo';
import UIUtils from '../util/UIUtil';
2015-12-14 12:26:50 +00:00
/**
* 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.
* @param {EventEmitter} emitter the event emitter which will be used by
* the new instance to emit events.
* @constructor
*/
function RemoteVideo(user, VideoLayout, emitter) {
this.user = user;
this.id = user.getId();
2015-12-14 12:26:50 +00:00
this.emitter = emitter;
this.videoSpanId = `participant_${this.id}`;
SmallVideo.call(this, VideoLayout);
this._audioStreamElement = null;
this._supportsRemoteControl = false;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
? 'left bottom' : 'top center';
2015-06-23 08:00:46 +00:00
this.addRemoteVideoContainer();
this.updateIndicators();
this.updateDisplayName();
this.bindHoverHandler();
2015-06-23 08:00:46 +00:00
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);
2018-11-30 22:13:39 +00:00
this.container.onclick = this._onContainerClick;
2015-06-23 08:00:46 +00:00
}
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
RemoteVideo.prototype.constructor = RemoteVideo;
RemoteVideo.prototype.addRemoteVideoContainer = function() {
this.container = RemoteVideo.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu();
2016-09-15 02:20:54 +00:00
this.VideoLayout.resizeThumbnails(true);
2016-09-28 21:31:40 +00:00
this.addAudioLevelIndicator();
2015-06-23 08:00:46 +00:00
this.addPresenceLabel();
2015-06-23 08:00:46 +00:00
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
*/
RemoteVideo.prototype._isHovered = function() {
const isHovered = SmallVideo.prototype._isHovered.call(this)
|| this.popupMenuIsHovered;
return isHovered;
};
/**
* Generates the popup menu content.
*
* @returns {Element|*} the constructed element, containing popup menu items
* @private
*/
RemoteVideo.prototype._generatePopupContent = function() {
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 { isModerator } = APP.conference;
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 }
isModerator = { isModerator }
menuPosition = { remoteMenuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
onVolumeChange = { onVolumeChange }
participantID = { participantID }
remoteControlState = { remoteControlState } />
</AtlasKitThemeProvider>
</I18nextProvider>
</Provider>,
remoteVideoMenuContainer);
2016-10-27 13:09:27 +00:00
};
RemoteVideo.prototype._onRemoteVideoMenuDisplay = function() {
this.updateRemoteVideoMenu();
};
/**
* Sets the remote control active status for the remote video.
*
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
RemoteVideo.prototype.setRemoteControlActiveStatus = function(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
*/
RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) {
if (this._supportsRemoteControl === isSupported) {
return;
}
this._supportsRemoteControl = isSupported;
this.updateRemoteVideoMenu();
};
/**
* Requests permissions for remote control session.
*/
RemoteVideo.prototype._requestRemoteControlPermissions = function() {
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.
*/
RemoteVideo.prototype._stopRemoteControl = function() {
// 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.
*/
RemoteVideo.prototype._setAudioVolume = function(newVal) {
if (this._audioStreamElement) {
this._audioStreamElement.volume = newVal;
}
};
/**
* Updates the remote video menu.
*
* @param isMuted the new muted state to update to
*/
RemoteVideo.prototype.updateRemoteVideoMenu = function(isMuted) {
if (typeof isMuted !== 'undefined') {
this.isAudioMuted = isMuted;
}
this._generatePopupContent();
};
/**
* @inheritDoc
* @override
*/
RemoteVideo.prototype.setVideoMutedView = function(isMuted) {
SmallVideo.prototype.setVideoMutedView.call(this, isMuted);
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected();
2016-09-28 16:29:47 +00:00
};
/**
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
* account remote participant's network connectivity and video muted status.
*
* @private
*/
RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() {
const isActive = this.isConnectionActive();
if (!isActive && this.isVideoMuted) {
this.mutedWhileDisconnected = true;
} else if (isActive && !this.isVideoMuted) {
this.mutedWhileDisconnected = false;
}
2016-09-28 16:29:47 +00:00
};
2015-06-23 08:00:46 +00:00
/**
* Removes the remote stream element corresponding to the given stream and
* parent container.
*
* @param stream the MediaStream
2015-06-23 08:00:46 +00:00
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
*/
RemoteVideo.prototype.removeRemoteStreamElement = function(stream) {
if (!this.container) {
2015-06-23 08:00:46 +00:00
return false;
}
2015-06-23 08:00:46 +00:00
const isVideo = stream.isVideoTrack();
const elementID = SmallVideo.getStreamElementID(stream);
const select = $(`#${elementID}`);
2015-06-23 08:00:46 +00:00
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();
2015-06-23 08:00:46 +00:00
};
/**
* 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.
*/
RemoteVideo.prototype.isConnectionActive = function() {
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
*/
RemoteVideo.prototype.isVideoPlayable = function() {
const connectionState
= APP.conference.getParticipantConnectionStatus(this.id);
return SmallVideo.prototype.isVideoPlayable.call(this)
&& this.hasVideoStarted()
&& (connectionState === JitsiParticipantConnectionStatus.ACTIVE
|| (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED
&& !this.mutedWhileDisconnected));
};
/**
* @inheritDoc
*/
RemoteVideo.prototype.updateView = function() {
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
SmallVideo.prototype.updateView.call(this);
};
/**
* Updates the UI to reflect user's connectivity status.
*/
RemoteVideo.prototype.updateConnectionStatusIndicator = function() {
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);
const isInterrupted
= connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED;
// Toggle thumbnail video problem filter
this.selectVideoElement().toggleClass(
'videoThumbnailProblemFilter', isInterrupted);
this.$avatar().toggleClass(
'videoThumbnailProblemFilter', isInterrupted);
2015-06-23 08:00:46 +00:00
};
/**
* Removes RemoteVideo from the page.
*/
RemoteVideo.prototype.remove = function() {
SmallVideo.prototype.remove.call(this);
this.removePresenceLabel();
this.removeRemoteVideoMenu();
};
RemoteVideo.prototype.waitForPlayback = function(streamElement, stream) {
const webRtcStream = stream.getOriginalStream();
const isVideo = stream.isVideoTrack();
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
return;
}
const self = this;
// Triggers when video playback starts
const onPlayingHandler = function() {
self.wasVideoPlayed = true;
self.VideoLayout.remoteVideoActive(streamElement, self.id);
streamElement.onplaying = null;
// Refresh to show the video
self.updateView();
};
streamElement.onplaying = onPlayingHandler;
};
/**
* 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.
*/
RemoteVideo.prototype.hasVideoStarted = function() {
return this.wasVideoPlayed;
};
RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
2015-12-14 12:26:50 +00:00
if (!this.container) {
2015-06-23 08:00:46 +00:00
return;
2015-12-14 12:26:50 +00:00
}
2015-06-23 08:00:46 +00:00
const isVideo = stream.isVideoTrack();
isVideo ? this.videoStream = stream : this.audioStream = stream;
2015-06-23 08:00:46 +00:00
if (isVideo) {
this.setVideoType(stream.videoType);
}
if (!stream.getOriginalStream()) {
return;
}
const streamElement = SmallVideo.createStreamElement(stream);
// Put new stream element always in front
UIUtils.prependChild(this.container, streamElement);
2016-02-02 21:50:02 +00:00
$(streamElement).hide();
2016-02-10 15:26:16 +00:00
// If the container is currently visible
// we attach the stream to the element.
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
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();
}
2016-10-31 16:12:28 +00:00
};
2015-06-23 08:00:46 +00:00
/**
* Triggers re-rendering of the display name using current instance state.
2016-10-20 19:28:10 +00:00
*
* @returns {void}
2015-06-23 08:00:46 +00:00
*/
RemoteVideo.prototype.updateDisplayName = function() {
2015-06-23 08:00:46 +00:00
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
2015-06-23 08:00:46 +00:00
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
};
2015-06-23 08:00:46 +00:00
/**
* Removes remote video menu element from video element identified by
* given <tt>videoElementId</tt>.
*
* @param videoElementId the id of local or remote video element.
*/
RemoteVideo.prototype.removeRemoteVideoMenu = function() {
const menuSpan = this.$container.find('.remotevideomenu');
2015-06-23 08:00:46 +00:00
if (menuSpan.length) {
ReactDOM.unmountComponentAtNode(menuSpan.get(0));
2015-06-23 08:00:46 +00:00
menuSpan.remove();
}
};
/**
* Mounts the {@code PresenceLabel} for displaying the participant's current
* presence status.
*
* @return {void}
*/
RemoteVideo.prototype.addPresenceLabel = function() {
const presenceLabelContainer
= this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
2018-06-26 22:56:22 +00:00
<PresenceLabel
participantID = { this.id }
2018-07-09 22:18:09 +00:00
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
}
};
/**
* Unmounts the {@code PresenceLabel} component.
*
* @return {void}
*/
RemoteVideo.prototype.removePresenceLabel = function() {
const presenceLabelContainer
= this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
};
RemoteVideo.createContainer = function(spanId) {
const container = document.createElement('span');
2015-06-23 08:00:46 +00:00
container.id = spanId;
container.className = 'videocontainer';
2016-09-15 02:20:54 +00:00
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;
2015-06-23 08:00:46 +00:00
};
2015-12-14 12:26:50 +00:00
export default RemoteVideo;