2018-06-27 09:43:13 +00:00
|
|
|
/* global $, APP */
|
2017-07-31 23:33:22 +00:00
|
|
|
/* eslint-disable no-unused-vars */
|
2021-11-10 10:11:29 +00:00
|
|
|
import Logger from '@jitsi/logger';
|
2017-07-31 23:33:22 +00:00
|
|
|
import React from 'react';
|
|
|
|
import ReactDOM from 'react-dom';
|
2018-05-16 15:03:10 +00:00
|
|
|
import { I18nextProvider } from 'react-i18next';
|
2017-07-31 23:33:22 +00:00
|
|
|
import { Provider } from 'react-redux';
|
|
|
|
|
2020-12-18 20:42:39 +00:00
|
|
|
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
|
2019-06-26 14:08:23 +00:00
|
|
|
import { Avatar } from '../../../react/features/base/avatar';
|
2021-10-22 13:23:52 +00:00
|
|
|
import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json';
|
2022-02-23 13:30:10 +00:00
|
|
|
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
|
2018-05-16 15:03:10 +00:00
|
|
|
import { i18next } from '../../../react/features/base/i18n';
|
2020-12-18 20:42:39 +00:00
|
|
|
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
2021-11-26 15:39:34 +00:00
|
|
|
import {
|
|
|
|
getParticipantById,
|
|
|
|
getParticipantDisplayName
|
|
|
|
} from '../../../react/features/base/participants';
|
2020-12-18 20:42:39 +00:00
|
|
|
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
2020-03-11 09:45:42 +00:00
|
|
|
import { CHAT_SIZE } from '../../../react/features/chat';
|
2022-02-23 13:30:10 +00:00
|
|
|
import {
|
|
|
|
isParticipantConnectionStatusActive,
|
|
|
|
isParticipantConnectionStatusInactive,
|
|
|
|
isParticipantConnectionStatusInterrupted,
|
|
|
|
isTrackStreamingStatusActive,
|
|
|
|
isTrackStreamingStatusInactive,
|
|
|
|
isTrackStreamingStatusInterrupted
|
|
|
|
} from '../../../react/features/connection-indicator/functions';
|
2022-03-04 13:10:57 +00:00
|
|
|
import { FILMSTRIP_BREAKPOINT, isFilmstripResizable, getVerticalViewMaxWidth } from '../../../react/features/filmstrip';
|
2017-08-09 19:40:03 +00:00
|
|
|
import {
|
|
|
|
updateKnownLargeVideoResolution
|
2020-09-21 23:54:45 +00:00
|
|
|
} from '../../../react/features/large-video/actions';
|
2021-04-21 13:48:05 +00:00
|
|
|
import { getParticipantsPaneOpen } from '../../../react/features/participants-pane/functions';
|
2020-05-20 10:57:03 +00:00
|
|
|
import { PresenceLabel } from '../../../react/features/presence-status';
|
2020-12-18 20:42:39 +00:00
|
|
|
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
2020-05-20 10:57:03 +00:00
|
|
|
/* eslint-enable no-unused-vars */
|
|
|
|
import { createDeferred } from '../../util/helpers';
|
|
|
|
import AudioLevels from '../audio_levels/AudioLevels';
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
import { VideoContainer, VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
2016-09-28 21:31:40 +00:00
|
|
|
|
2020-05-20 10:57:03 +00:00
|
|
|
const logger = Logger.getLogger(__filename);
|
2016-09-28 21:31:40 +00:00
|
|
|
|
2017-04-05 15:14:26 +00:00
|
|
|
const DESKTOP_CONTAINER_TYPE = 'desktop';
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Manager for all Large containers.
|
|
|
|
*/
|
2015-12-25 16:55:45 +00:00
|
|
|
export default class LargeVideoManager {
|
2017-05-25 13:48:06 +00:00
|
|
|
/**
|
|
|
|
* Checks whether given container is a {@link VIDEO_CONTAINER_TYPE}.
|
|
|
|
* FIXME currently this is a workaround for the problem where video type is
|
|
|
|
* mixed up with container type.
|
|
|
|
* @param {string} containerType
|
|
|
|
* @return {boolean}
|
|
|
|
*/
|
|
|
|
static isVideoContainer(containerType) {
|
|
|
|
return containerType === VIDEO_CONTAINER_TYPE
|
|
|
|
|| containerType === DESKTOP_CONTAINER_TYPE;
|
|
|
|
}
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
2021-01-21 20:46:47 +00:00
|
|
|
constructor() {
|
2016-09-24 15:11:57 +00:00
|
|
|
/**
|
|
|
|
* The map of <tt>LargeContainer</tt>s where the key is the video
|
|
|
|
* container type.
|
|
|
|
* @type {Object.<string, LargeContainer>}
|
|
|
|
*/
|
2015-12-25 16:55:45 +00:00
|
|
|
this.containers = {};
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2016-03-18 20:00:55 +00:00
|
|
|
this.state = VIDEO_CONTAINER_TYPE;
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2017-02-08 19:57:55 +00:00
|
|
|
// FIXME: We are passing resizeContainer as parameter which is calling
|
|
|
|
// Container.resize. Probably there's better way to implement this.
|
2021-01-21 20:46:47 +00:00
|
|
|
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
|
2016-03-18 20:00:55 +00:00
|
|
|
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
2016-03-03 23:38:40 +00:00
|
|
|
|
2016-11-11 17:55:18 +00:00
|
|
|
// use the same video container to handle desktop tracks
|
2017-04-05 15:14:26 +00:00
|
|
|
this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
|
2015-08-19 08:55:35 +00:00
|
|
|
|
2020-09-23 16:04:35 +00:00
|
|
|
/**
|
|
|
|
* The preferred width passed as an argument to {@link updateContainerSize}.
|
|
|
|
*
|
|
|
|
* @type {number|undefined}
|
|
|
|
*/
|
|
|
|
this.preferredWidth = undefined;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The preferred height passed as an argument to {@link updateContainerSize}.
|
|
|
|
*
|
|
|
|
* @type {number|undefined}
|
|
|
|
*/
|
|
|
|
this.preferredHeight = undefined;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The calculated width that will be used for the large video.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2015-12-25 16:55:45 +00:00
|
|
|
this.width = 0;
|
2020-09-23 16:04:35 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The calculated height that will be used for the large video.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
2015-12-25 16:55:45 +00:00
|
|
|
this.height = 0;
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2017-05-03 16:47:59 +00:00
|
|
|
/**
|
|
|
|
* Cache the aspect ratio of the video displayed to detect changes to
|
|
|
|
* the aspect ratio on video resize events.
|
|
|
|
*
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
this._videoAspectRatio = 0;
|
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
this.$container = $('#largeVideoContainer');
|
2015-12-14 12:26:50 +00:00
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
this.$container.css({
|
|
|
|
display: 'inline-block'
|
|
|
|
});
|
2015-07-22 08:18:54 +00:00
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
this.$container.hover(
|
|
|
|
e => this.onHoverIn(e),
|
|
|
|
e => this.onHoverOut(e)
|
|
|
|
);
|
2017-05-11 00:19:35 +00:00
|
|
|
|
2017-05-16 20:48:14 +00:00
|
|
|
// Bind event handler so it is only bound once for every instance.
|
2017-05-03 16:47:59 +00:00
|
|
|
this._onVideoResolutionUpdate
|
|
|
|
= this._onVideoResolutionUpdate.bind(this);
|
2017-05-16 20:48:14 +00:00
|
|
|
|
2017-05-03 16:47:59 +00:00
|
|
|
this.videoContainer.addResizeListener(this._onVideoResolutionUpdate);
|
2019-02-07 03:19:02 +00:00
|
|
|
|
|
|
|
this._dominantSpeakerAvatarContainer
|
|
|
|
= document.getElementById('dominantSpeakerAvatarContainer');
|
2017-05-11 00:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-27 09:43:13 +00:00
|
|
|
* Removes any listeners registered on child components, including
|
|
|
|
* React Components.
|
2017-05-11 00:19:35 +00:00
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
destroy() {
|
2017-05-16 20:48:14 +00:00
|
|
|
this.videoContainer.removeResizeListener(
|
2017-05-03 16:47:59 +00:00
|
|
|
this._onVideoResolutionUpdate);
|
2017-07-31 23:33:22 +00:00
|
|
|
|
|
|
|
this.removePresenceLabel();
|
2019-01-01 21:19:34 +00:00
|
|
|
|
2019-02-07 03:19:02 +00:00
|
|
|
ReactDOM.unmountComponentAtNode(this._dominantSpeakerAvatarContainer);
|
|
|
|
|
2019-01-01 21:19:34 +00:00
|
|
|
this.$container.css({ display: 'none' });
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
onHoverIn(e) {
|
2015-12-25 16:55:45 +00:00
|
|
|
if (!this.state) {
|
2015-07-29 19:39:09 +00:00
|
|
|
return;
|
2015-12-14 12:26:50 +00:00
|
|
|
}
|
2017-07-10 09:02:22 +00:00
|
|
|
const container = this.getCurrentContainer();
|
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
container.onHoverIn(e);
|
|
|
|
}
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
onHoverOut(e) {
|
2015-12-25 16:55:45 +00:00
|
|
|
if (!this.state) {
|
2015-07-29 19:39:09 +00:00
|
|
|
return;
|
2015-12-14 12:26:50 +00:00
|
|
|
}
|
2017-07-10 09:02:22 +00:00
|
|
|
const container = this.getCurrentContainer();
|
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
container.onHoverOut(e);
|
|
|
|
}
|
2015-07-29 19:39:09 +00:00
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
get id() {
|
2017-07-10 09:02:22 +00:00
|
|
|
const container = this.getCurrentContainer();
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2019-04-10 15:16:02 +00:00
|
|
|
// If a user switch for large video is in progress then provide what
|
|
|
|
// will be the end result of the update.
|
|
|
|
if (this.updateInProcess
|
|
|
|
&& this.newStreamData
|
|
|
|
&& this.newStreamData.id !== container.id) {
|
|
|
|
return this.newStreamData.id;
|
|
|
|
}
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2016-03-15 21:03:38 +00:00
|
|
|
return container.id;
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|
2015-07-29 19:39:09 +00:00
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
scheduleLargeVideoUpdate() {
|
2016-01-22 15:08:58 +00:00
|
|
|
if (this.updateInProcess || !this.newStreamData) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.updateInProcess = true;
|
2015-07-29 19:39:09 +00:00
|
|
|
|
2016-04-26 20:42:18 +00:00
|
|
|
// Include hide()/fadeOut only if we're switching between users
|
2017-10-12 23:02:29 +00:00
|
|
|
// eslint-disable-next-line eqeqeq
|
2017-07-10 09:02:22 +00:00
|
|
|
const container = this.getCurrentContainer();
|
2019-04-10 15:16:02 +00:00
|
|
|
const isUserSwitch = this.newStreamData.id !== container.id;
|
2016-10-13 18:25:47 +00:00
|
|
|
const preUpdate = isUserSwitch ? container.hide() : Promise.resolve();
|
2016-05-07 01:50:37 +00:00
|
|
|
|
2016-04-26 20:42:18 +00:00
|
|
|
preUpdate.then(() => {
|
2016-10-13 18:25:47 +00:00
|
|
|
const { id, stream, videoType, resolve } = this.newStreamData;
|
2017-05-25 13:48:06 +00:00
|
|
|
|
|
|
|
// FIXME this does not really make sense, because the videoType
|
|
|
|
// (camera or desktop) is a completely different thing than
|
|
|
|
// the video container type (Etherpad, SharedVideo, VideoContainer).
|
2020-12-18 20:42:39 +00:00
|
|
|
const isVideoContainer = LargeVideoManager.isVideoContainer(videoType);
|
2017-04-05 15:14:26 +00:00
|
|
|
|
2016-01-22 15:08:58 +00:00
|
|
|
this.newStreamData = null;
|
|
|
|
|
2020-04-03 13:41:30 +00:00
|
|
|
logger.info(`hover in ${id}`);
|
2016-03-15 21:03:38 +00:00
|
|
|
this.state = videoType;
|
2017-10-12 23:02:29 +00:00
|
|
|
// eslint-disable-next-line no-shadow
|
2017-07-10 09:02:22 +00:00
|
|
|
const container = this.getCurrentContainer();
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2017-06-15 15:01:32 +00:00
|
|
|
container.setStream(id, stream, videoType);
|
2016-01-21 17:38:54 +00:00
|
|
|
|
|
|
|
// change the avatar url on large
|
2019-06-26 14:08:23 +00:00
|
|
|
this.updateAvatar();
|
2016-01-22 15:08:58 +00:00
|
|
|
|
2017-05-25 13:48:06 +00:00
|
|
|
const isVideoMuted = !stream || stream.isMuted();
|
2020-12-18 20:42:39 +00:00
|
|
|
const state = APP.store.getState();
|
|
|
|
const participant = getParticipantById(state, id);
|
2020-10-28 20:06:59 +00:00
|
|
|
const connectionStatus = participant?.connectionStatus;
|
2022-02-23 13:30:10 +00:00
|
|
|
|
|
|
|
let isVideoRenderable;
|
|
|
|
|
|
|
|
if (getSourceNameSignalingFeatureFlag(state)) {
|
|
|
|
const videoTrack = getTrackByMediaTypeAndParticipant(
|
|
|
|
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
|
|
|
|
|
|
|
isVideoRenderable = !isVideoMuted
|
|
|
|
&& (APP.conference.isLocalId(id) || isTrackStreamingStatusActive(videoTrack));
|
|
|
|
} else {
|
|
|
|
isVideoRenderable = !isVideoMuted
|
|
|
|
&& (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant));
|
|
|
|
}
|
|
|
|
|
2020-12-18 20:42:39 +00:00
|
|
|
const isAudioOnly = APP.conference.isAudioOnly();
|
2017-10-12 23:02:29 +00:00
|
|
|
const showAvatar
|
2017-05-25 13:48:06 +00:00
|
|
|
= isVideoContainer
|
2020-12-18 20:42:39 +00:00
|
|
|
&& ((isAudioOnly && videoType !== VIDEO_TYPE.DESKTOP) || !isVideoRenderable);
|
2017-04-05 15:14:26 +00:00
|
|
|
|
2016-02-12 14:47:42 +00:00
|
|
|
let promise;
|
|
|
|
|
2016-01-22 15:08:58 +00:00
|
|
|
// do not show stream if video is muted
|
2016-02-12 14:47:42 +00:00
|
|
|
// but we still should show watermark
|
2016-09-24 18:24:18 +00:00
|
|
|
if (showAvatar) {
|
2016-02-17 12:37:06 +00:00
|
|
|
this.showWatermark(true);
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
// If the intention of this switch is to show the avatar
|
|
|
|
// we need to make sure that the video is hidden
|
2016-09-24 15:18:17 +00:00
|
|
|
promise = container.hide();
|
2020-12-18 20:42:39 +00:00
|
|
|
|
|
|
|
if ((!shouldDisplayTileView(state) || participant?.pinned) // In theory the tile view may not be
|
|
|
|
// enabled yet when we auto pin the participant.
|
|
|
|
|
|
|
|
&& participant && !participant.local && !participant.isFakeParticipant) {
|
|
|
|
// remote participant only
|
|
|
|
const track = getTrackByMediaTypeAndParticipant(
|
|
|
|
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
|
|
|
const isScreenSharing = track?.videoType === 'desktop';
|
|
|
|
|
|
|
|
if (isScreenSharing) {
|
|
|
|
// send the event
|
|
|
|
sendAnalytics(createScreenSharingIssueEvent({
|
|
|
|
source: 'large-video',
|
|
|
|
connectionStatus,
|
|
|
|
isVideoMuted,
|
|
|
|
isAudioOnly,
|
|
|
|
isVideoContainer,
|
|
|
|
videoType
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-12 14:47:42 +00:00
|
|
|
} else {
|
2016-03-15 21:03:38 +00:00
|
|
|
promise = container.show();
|
2016-02-12 14:47:42 +00:00
|
|
|
}
|
2016-01-21 17:38:54 +00:00
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
// show the avatar on large if needed
|
|
|
|
container.showAvatar(showAvatar);
|
|
|
|
|
2016-10-13 18:25:47 +00:00
|
|
|
// Clean up audio level after previous speaker.
|
2016-10-13 13:43:21 +00:00
|
|
|
if (showAvatar) {
|
|
|
|
this.updateLargeVideoAudioLevel(0);
|
2016-09-24 18:24:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-23 13:30:10 +00:00
|
|
|
let messageKey;
|
|
|
|
|
|
|
|
if (getSourceNameSignalingFeatureFlag(state)) {
|
|
|
|
const videoTrack = getTrackByMediaTypeAndParticipant(
|
|
|
|
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
|
|
|
|
|
|
|
|
messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null;
|
|
|
|
} else {
|
|
|
|
messageKey = isParticipantConnectionStatusInactive(participant) ? 'connection.LOW_BANDWIDTH' : null;
|
|
|
|
}
|
2017-05-25 13:48:06 +00:00
|
|
|
|
2020-02-04 07:25:13 +00:00
|
|
|
// Do not show connection status message in the audio only mode,
|
|
|
|
// because it's based on the video playback status.
|
|
|
|
const overrideAndHide = APP.conference.isAudioOnly();
|
2017-03-21 17:14:13 +00:00
|
|
|
|
2016-10-13 18:25:47 +00:00
|
|
|
this.updateParticipantConnStatusIndication(
|
|
|
|
id,
|
2017-06-05 19:37:13 +00:00
|
|
|
!overrideAndHide && messageKey);
|
2016-09-24 18:24:18 +00:00
|
|
|
|
2017-07-31 23:33:22 +00:00
|
|
|
// Change the participant id the presence label is listening to.
|
|
|
|
this.updatePresenceLabel(id);
|
|
|
|
|
|
|
|
this.videoContainer.positionRemoteStatusMessages();
|
|
|
|
|
2016-01-22 15:08:58 +00:00
|
|
|
// resolve updateLargeVideo promise after everything is done
|
|
|
|
promise.then(resolve);
|
|
|
|
|
|
|
|
return promise;
|
|
|
|
}).then(() => {
|
2016-03-03 23:38:40 +00:00
|
|
|
// after everything is done check again if there are any pending
|
|
|
|
// new streams.
|
2016-01-22 15:08:58 +00:00
|
|
|
this.updateInProcess = false;
|
|
|
|
this.scheduleLargeVideoUpdate();
|
2015-12-25 16:55:45 +00:00
|
|
|
});
|
|
|
|
}
|
2015-07-29 19:39:09 +00:00
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
/**
|
|
|
|
* Shows/hides notification about participant's connectivity issues to be
|
|
|
|
* shown on the large video area.
|
|
|
|
*
|
|
|
|
* @param {string} id the id of remote participant(MUC nickname)
|
2017-06-05 19:37:13 +00:00
|
|
|
* @param {string|null} messageKey the i18n key of the message which will be
|
|
|
|
* displayed on the large video or <tt>null</tt> to hide it.
|
2016-09-24 18:24:18 +00:00
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
2020-02-05 22:43:44 +00:00
|
|
|
updateParticipantConnStatusIndication(id, messageKey) {
|
2021-11-26 15:39:34 +00:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
if (messageKey) {
|
2016-09-24 18:24:18 +00:00
|
|
|
// Get user's display name
|
2017-10-12 23:02:29 +00:00
|
|
|
const displayName
|
2021-11-26 15:39:34 +00:00
|
|
|
= getParticipantDisplayName(state, id);
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
this._setRemoteConnectionMessage(
|
2017-03-21 17:14:13 +00:00
|
|
|
messageKey,
|
2017-10-12 23:02:29 +00:00
|
|
|
{ displayName });
|
2016-09-24 18:24:18 +00:00
|
|
|
|
|
|
|
// Show it now only if the VideoContainer is on top
|
|
|
|
this.showRemoteConnectionMessage(
|
2017-05-25 13:48:06 +00:00
|
|
|
LargeVideoManager.isVideoContainer(this.state));
|
2017-10-12 23:02:29 +00:00
|
|
|
} else {
|
|
|
|
// Hide the message
|
|
|
|
this.showRemoteConnectionMessage(false);
|
2016-09-24 18:24:18 +00:00
|
|
|
}
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
}
|
|
|
|
|
2016-01-25 21:35:40 +00:00
|
|
|
/**
|
|
|
|
* Update large video.
|
|
|
|
* Switches to large video even if previously other container was visible.
|
2016-01-29 19:31:58 +00:00
|
|
|
* @param userID the userID of the participant associated with the stream
|
2016-01-25 21:35:40 +00:00
|
|
|
* @param {JitsiTrack?} stream new stream
|
|
|
|
* @param {string?} videoType new video type
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
updateLargeVideo(userID, stream, videoType) {
|
2016-01-22 15:08:58 +00:00
|
|
|
if (this.newStreamData) {
|
|
|
|
this.newStreamData.reject();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.newStreamData = createDeferred();
|
2016-01-29 19:31:58 +00:00
|
|
|
this.newStreamData.id = userID;
|
2016-01-22 15:08:58 +00:00
|
|
|
this.newStreamData.stream = stream;
|
|
|
|
this.newStreamData.videoType = videoType;
|
|
|
|
|
|
|
|
this.scheduleLargeVideoUpdate();
|
|
|
|
|
|
|
|
return this.newStreamData.promise;
|
|
|
|
}
|
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
2016-09-08 18:16:23 +00:00
|
|
|
* Update container size.
|
2016-01-15 14:59:35 +00:00
|
|
|
*/
|
2020-09-16 23:16:32 +00:00
|
|
|
updateContainerSize(width, height) {
|
2020-09-23 16:04:35 +00:00
|
|
|
if (typeof width === 'number') {
|
|
|
|
this.preferredWidth = width;
|
|
|
|
}
|
|
|
|
if (typeof height === 'number') {
|
|
|
|
this.preferredHeight = height;
|
|
|
|
}
|
|
|
|
|
|
|
|
let widthToUse = this.preferredWidth || window.innerWidth;
|
2021-04-21 13:48:05 +00:00
|
|
|
const state = APP.store.getState();
|
|
|
|
const { isOpen } = state['features/chat'];
|
2022-02-24 12:20:37 +00:00
|
|
|
const { width: filmstripWidth, visible } = state['features/filmstrip'];
|
2021-04-21 13:48:05 +00:00
|
|
|
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
|
2022-02-24 12:20:37 +00:00
|
|
|
const resizableFilmstrip = isFilmstripResizable(state);
|
2021-04-21 13:48:05 +00:00
|
|
|
|
|
|
|
if (isParticipantsPaneOpen) {
|
|
|
|
widthToUse -= theme.participantsPaneWidth;
|
|
|
|
}
|
2020-03-11 09:45:42 +00:00
|
|
|
|
2021-01-14 16:12:08 +00:00
|
|
|
if (isOpen && window.innerWidth > 580) {
|
2020-10-14 15:09:29 +00:00
|
|
|
/**
|
|
|
|
* If chat state is open, we re-compute the container width
|
|
|
|
* by subtracting the default width of the chat.
|
|
|
|
*/
|
2020-03-11 09:45:42 +00:00
|
|
|
widthToUse -= CHAT_SIZE;
|
|
|
|
}
|
|
|
|
|
2022-02-24 12:20:37 +00:00
|
|
|
if (resizableFilmstrip && visible && filmstripWidth.current >= FILMSTRIP_BREAKPOINT) {
|
2022-03-04 13:10:57 +00:00
|
|
|
widthToUse -= getVerticalViewMaxWidth(state);
|
2022-02-24 12:20:37 +00:00
|
|
|
}
|
|
|
|
|
2020-03-11 09:45:42 +00:00
|
|
|
this.width = widthToUse;
|
2020-09-23 16:04:35 +00:00
|
|
|
this.height = this.preferredHeight || window.innerHeight;
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|
2015-07-29 19:39:09 +00:00
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Resize Large container of specified type.
|
|
|
|
* @param {string} type type of container which should be resized.
|
|
|
|
* @param {boolean} [animate=false] if resize process should be animated.
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
resizeContainer(type, animate = false) {
|
|
|
|
const container = this.getContainer(type);
|
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
container.resize(this.width, this.height, animate);
|
|
|
|
}
|
2015-07-29 19:39:09 +00:00
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Resize all Large containers.
|
|
|
|
* @param {boolean} animate if resize process should be animated.
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
resize(animate) {
|
2015-12-25 16:55:45 +00:00
|
|
|
// resize all containers
|
2016-01-19 23:10:44 +00:00
|
|
|
Object.keys(this.containers)
|
|
|
|
.forEach(type => this.resizeContainer(type, animate));
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|
2015-08-03 15:58:22 +00:00
|
|
|
|
2015-12-25 16:55:45 +00:00
|
|
|
/**
|
2016-01-12 00:14:01 +00:00
|
|
|
* Updates the src of the dominant speaker avatar
|
2015-12-25 16:55:45 +00:00
|
|
|
*/
|
2019-06-26 14:08:23 +00:00
|
|
|
updateAvatar() {
|
|
|
|
ReactDOM.render(
|
|
|
|
<Provider store = { APP.store }>
|
2019-02-07 03:19:02 +00:00
|
|
|
<Avatar
|
|
|
|
id = "dominantSpeakerAvatar"
|
2019-06-26 14:08:23 +00:00
|
|
|
participantId = { this.id }
|
|
|
|
size = { 200 } />
|
|
|
|
</Provider>,
|
|
|
|
this._dominantSpeakerAvatarContainer
|
|
|
|
);
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|
|
|
|
|
2016-09-28 21:31:40 +00:00
|
|
|
/**
|
|
|
|
* Updates the audio level indicator of the large video.
|
|
|
|
*
|
|
|
|
* @param lvl the new audio level to set
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
updateLargeVideoAudioLevel(lvl) {
|
|
|
|
AudioLevels.updateLargeVideoAudioLevel('dominantSpeaker', lvl);
|
2016-09-28 21:31:40 +00:00
|
|
|
}
|
|
|
|
|
2017-07-31 23:33:22 +00:00
|
|
|
/**
|
|
|
|
* Displays a message of the passed in participant id's presence status. The
|
|
|
|
* message will not display if the remote connection message is displayed.
|
|
|
|
*
|
|
|
|
* @param {string} id - The participant ID whose associated user's presence
|
|
|
|
* status should be displayed.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
updatePresenceLabel(id) {
|
|
|
|
const isConnectionMessageVisible
|
|
|
|
= $('#remoteConnectionMessage').is(':visible');
|
|
|
|
|
|
|
|
if (isConnectionMessageVisible) {
|
|
|
|
this.removePresenceLabel();
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2017-07-31 23:33:22 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const presenceLabelContainer = $('#remotePresenceMessage');
|
|
|
|
|
|
|
|
if (presenceLabelContainer.length) {
|
|
|
|
ReactDOM.render(
|
|
|
|
<Provider store = { APP.store }>
|
2018-05-16 15:03:10 +00:00
|
|
|
<I18nextProvider i18n = { i18next }>
|
2018-06-26 22:56:22 +00:00
|
|
|
<PresenceLabel
|
|
|
|
participantID = { id }
|
2018-07-09 22:18:09 +00:00
|
|
|
className = 'presence-label' />
|
2018-05-16 15:03:10 +00:00
|
|
|
</I18nextProvider>
|
2017-07-31 23:33:22 +00:00
|
|
|
</Provider>,
|
|
|
|
presenceLabelContainer.get(0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the messages about the displayed participant's presence status.
|
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
removePresenceLabel() {
|
|
|
|
const presenceLabelContainer = $('#remotePresenceMessage');
|
|
|
|
|
|
|
|
if (presenceLabelContainer.length) {
|
|
|
|
ReactDOM.unmountComponentAtNode(presenceLabelContainer.get(0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-17 12:37:06 +00:00
|
|
|
/**
|
|
|
|
* Show or hide watermark.
|
|
|
|
* @param {boolean} show
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
showWatermark(show) {
|
2016-02-17 12:37:06 +00:00
|
|
|
$('.watermark').css('visibility', show ? 'visible' : 'hidden');
|
|
|
|
}
|
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
/**
|
|
|
|
* Shows hides the "avatar" message which is to be displayed either in
|
|
|
|
* the middle of the screen or below the avatar image.
|
|
|
|
*
|
2017-06-05 19:43:44 +00:00
|
|
|
* @param {boolean|undefined} [show=undefined] <tt>true</tt> to show
|
|
|
|
* the avatar message or <tt>false</tt> to hide it. If not provided then
|
|
|
|
* the connection status of the user currently on the large video will be
|
|
|
|
* obtained form "APP.conference" and the message will be displayed if
|
|
|
|
* the user's connection is either interrupted or inactive.
|
2016-09-24 18:24:18 +00:00
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
showRemoteConnectionMessage(show) {
|
2016-09-24 18:24:18 +00:00
|
|
|
if (typeof show !== 'boolean') {
|
2020-10-28 20:06:59 +00:00
|
|
|
const participant = getParticipantById(APP.store.getState(), this.id);
|
2022-02-23 13:30:10 +00:00
|
|
|
const state = APP.store.getState();
|
|
|
|
|
|
|
|
if (getSourceNameSignalingFeatureFlag(state)) {
|
|
|
|
const videoTrack = getTrackByMediaTypeAndParticipant(
|
|
|
|
state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id);
|
2017-05-25 13:48:06 +00:00
|
|
|
|
2022-02-23 13:30:10 +00:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
show = !APP.conference.isLocalId(this.id)
|
|
|
|
&& (isTrackStreamingStatusInterrupted(videoTrack)
|
|
|
|
|| isTrackStreamingStatusInactive(videoTrack));
|
|
|
|
} else {
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
show = !APP.conference.isLocalId(this.id)
|
|
|
|
&& (isParticipantConnectionStatusInterrupted(participant)
|
|
|
|
|| isParticipantConnectionStatusInactive(participant));
|
|
|
|
}
|
2016-09-24 18:24:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (show) {
|
2017-10-12 23:02:29 +00:00
|
|
|
$('#remoteConnectionMessage').css({ display: 'block' });
|
2016-09-24 18:24:18 +00:00
|
|
|
} else {
|
|
|
|
$('#remoteConnectionMessage').hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the text which describes that the remote user is having
|
|
|
|
* connectivity issues.
|
|
|
|
*
|
|
|
|
* @param {string} msgKey the translation key which will be used to get
|
|
|
|
* the message text.
|
|
|
|
* @param {object} msgOptions translation options object.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
_setRemoteConnectionMessage(msgKey, msgOptions) {
|
2016-09-24 18:24:18 +00:00
|
|
|
if (msgKey) {
|
|
|
|
$('#remoteConnectionMessage')
|
2017-10-12 23:02:29 +00:00
|
|
|
.attr('data-i18n', msgKey)
|
|
|
|
.attr('data-i18n-options', JSON.stringify(msgOptions));
|
2016-11-02 16:28:44 +00:00
|
|
|
APP.translation.translateElement(
|
|
|
|
$('#remoteConnectionMessage'), msgOptions);
|
2016-09-24 18:24:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Add container of specified type.
|
|
|
|
* @param {string} type container type
|
|
|
|
* @param {LargeContainer} container container to add.
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
addContainer(type, container) {
|
2015-12-25 16:55:45 +00:00
|
|
|
if (this.containers[type]) {
|
|
|
|
throw new Error(`container of type ${type} already exist`);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.containers[type] = container;
|
|
|
|
this.resizeContainer(type);
|
|
|
|
}
|
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Get Large container of specified type.
|
|
|
|
* @param {string} type container type.
|
|
|
|
* @returns {LargeContainer}
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
getContainer(type) {
|
|
|
|
const container = this.containers[type];
|
2015-12-25 16:55:45 +00:00
|
|
|
|
|
|
|
if (!container) {
|
|
|
|
throw new Error(`container of type ${type} doesn't exist`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return container;
|
|
|
|
}
|
|
|
|
|
2017-07-10 09:02:22 +00:00
|
|
|
/**
|
|
|
|
* Returns {@link LargeContainer} for the current {@link state}
|
|
|
|
*
|
|
|
|
* @return {LargeContainer}
|
|
|
|
*
|
|
|
|
* @throws an <tt>Error</tt> if there is no container for the current
|
|
|
|
* {@link state}.
|
|
|
|
*/
|
|
|
|
getCurrentContainer() {
|
|
|
|
return this.getContainer(this.state);
|
|
|
|
}
|
|
|
|
|
2017-07-10 10:06:48 +00:00
|
|
|
/**
|
|
|
|
* Returns type of the current {@link LargeContainer}
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
getCurrentContainerType() {
|
|
|
|
return this.state;
|
|
|
|
}
|
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Remove Large container of specified type.
|
|
|
|
* @param {string} type container type.
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
removeContainer(type) {
|
2015-12-25 16:55:45 +00:00
|
|
|
if (!this.containers[type]) {
|
|
|
|
throw new Error(`container of type ${type} doesn't exist`);
|
|
|
|
}
|
|
|
|
|
|
|
|
delete this.containers[type];
|
|
|
|
}
|
|
|
|
|
2016-01-15 14:59:35 +00:00
|
|
|
/**
|
|
|
|
* Show Large container of specified type.
|
|
|
|
* Does nothing if such container is already visible.
|
|
|
|
* @param {string} type container type.
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
2017-10-12 23:02:29 +00:00
|
|
|
showContainer(type) {
|
2015-12-25 16:55:45 +00:00
|
|
|
if (this.state === type) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
const oldContainer = this.containers[this.state];
|
|
|
|
|
2016-09-22 17:37:14 +00:00
|
|
|
// FIXME when video is being replaced with other content we need to hide
|
|
|
|
// companion icons/messages. It would be best if the container would
|
|
|
|
// be taking care of it by itself, but that is a bigger refactoring
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2017-05-25 13:48:06 +00:00
|
|
|
if (LargeVideoManager.isVideoContainer(this.state)) {
|
2016-02-17 12:37:06 +00:00
|
|
|
this.showWatermark(false);
|
2016-09-24 18:24:18 +00:00
|
|
|
this.showRemoteConnectionMessage(false);
|
2016-02-17 12:37:06 +00:00
|
|
|
}
|
2016-01-22 15:08:58 +00:00
|
|
|
oldContainer.hide();
|
2015-12-25 16:55:45 +00:00
|
|
|
|
|
|
|
this.state = type;
|
2017-10-12 23:02:29 +00:00
|
|
|
const container = this.getContainer(type);
|
2015-12-25 16:55:45 +00:00
|
|
|
|
2016-02-17 12:37:06 +00:00
|
|
|
return container.show().then(() => {
|
2017-05-25 13:48:06 +00:00
|
|
|
if (LargeVideoManager.isVideoContainer(type)) {
|
2016-09-22 17:37:14 +00:00
|
|
|
// FIXME when video appears on top of other content we need to
|
|
|
|
// show companion icons/messages. It would be best if
|
|
|
|
// the container would be taking care of it by itself, but that
|
|
|
|
// is a bigger refactoring
|
2016-02-17 12:37:06 +00:00
|
|
|
this.showWatermark(true);
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2016-09-24 18:24:18 +00:00
|
|
|
// "avatar" and "video connection" can not be displayed both
|
|
|
|
// at the same time, but the latter is of higher priority and it
|
|
|
|
// will hide the avatar one if will be displayed.
|
2017-03-21 17:14:13 +00:00
|
|
|
this.showRemoteConnectionMessage(/* fetch the current state */);
|
2016-02-17 12:37:06 +00:00
|
|
|
}
|
|
|
|
});
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|
2016-05-07 01:50:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Changes the flipX state of the local video.
|
|
|
|
* @param val {boolean} true if flipped.
|
|
|
|
*/
|
|
|
|
onLocalFlipXChange(val) {
|
|
|
|
this.videoContainer.setLocalFlipX(val);
|
|
|
|
}
|
2017-05-11 00:19:35 +00:00
|
|
|
|
|
|
|
/**
|
2019-11-14 13:23:45 +00:00
|
|
|
* Dispatches an action to update the known resolution state of the large video and adjusts container sizes when the
|
|
|
|
* resolution changes.
|
2017-05-11 00:19:35 +00:00
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2017-05-03 16:47:59 +00:00
|
|
|
_onVideoResolutionUpdate() {
|
2017-05-11 00:19:35 +00:00
|
|
|
const { height, width } = this.videoContainer.getStreamSize();
|
2017-08-09 19:40:03 +00:00
|
|
|
const { resolution } = APP.store.getState()['features/large-video'];
|
2017-05-11 00:19:35 +00:00
|
|
|
|
2017-08-09 19:40:03 +00:00
|
|
|
if (height !== resolution) {
|
|
|
|
APP.store.dispatch(updateKnownLargeVideoResolution(height));
|
|
|
|
}
|
|
|
|
|
2019-11-14 13:23:45 +00:00
|
|
|
const currentAspectRatio = height === 0 ? 0 : width / height;
|
2017-05-03 16:47:59 +00:00
|
|
|
|
|
|
|
if (this._videoAspectRatio !== currentAspectRatio) {
|
|
|
|
this._videoAspectRatio = currentAspectRatio;
|
|
|
|
this.resize();
|
|
|
|
}
|
2017-05-11 00:19:35 +00:00
|
|
|
}
|
2015-12-25 16:55:45 +00:00
|
|
|
}
|