ref(Thumbnail): Create React component.
This commit is contained in:
parent
d8dd644f38
commit
51e381a0b1
|
@ -2014,7 +2014,6 @@ export default {
|
|||
formattedDisplayName
|
||||
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
|
||||
});
|
||||
APP.UI.changeDisplayName(id, formattedDisplayName);
|
||||
}
|
||||
);
|
||||
room.on(
|
||||
|
@ -2053,10 +2052,7 @@ export default {
|
|||
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
|
||||
|
||||
room.on(JitsiConferenceEvents.KICKED, participant => {
|
||||
APP.UI.hideStats();
|
||||
APP.store.dispatch(kickedOut(room, participant));
|
||||
|
||||
// FIXME close
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
|
||||
|
@ -2389,11 +2385,6 @@ export default {
|
|||
APP.keyboardshortcut.init();
|
||||
|
||||
APP.store.dispatch(conferenceJoined(room));
|
||||
|
||||
const displayName
|
||||
= APP.store.getState()['features/base/settings'].displayName;
|
||||
|
||||
APP.UI.changeDisplayName('localVideoContainer', displayName);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -2871,10 +2862,6 @@ export default {
|
|||
APP.store.dispatch(updateSettings({
|
||||
displayName: formattedNickname
|
||||
}));
|
||||
|
||||
if (room) {
|
||||
APP.UI.changeDisplayName(id, formattedNickname);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,6 @@ import EventEmitter from 'events';
|
|||
import Logger from 'jitsi-meet-logger';
|
||||
|
||||
import { isMobileBrowser } from '../../react/features/base/environment/utils';
|
||||
import { getLocalParticipant } from '../../react/features/base/participants';
|
||||
import { toggleChat } from '../../react/features/chat';
|
||||
import { setDocumentUrl } from '../../react/features/etherpad';
|
||||
import { setFilmstripVisible } from '../../react/features/filmstrip';
|
||||
|
@ -91,29 +90,11 @@ UI.notifyReservationError = function(code, msg) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change nickname for the user.
|
||||
* @param {string} id user id
|
||||
* @param {string} displayName new nickname
|
||||
*/
|
||||
UI.changeDisplayName = function(id, displayName) {
|
||||
VideoLayout.onDisplayNameChanged(id, displayName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize conference UI.
|
||||
*/
|
||||
UI.initConference = function() {
|
||||
const { getState } = APP.store;
|
||||
const { id, name } = getLocalParticipant(getState);
|
||||
|
||||
UI.showToolbar();
|
||||
|
||||
const displayName = config.displayJids ? id : name;
|
||||
|
||||
if (displayName) {
|
||||
UI.changeDisplayName('localVideoContainer', displayName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -238,19 +219,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
|
|||
* @param {JitsiParticipant} user
|
||||
*/
|
||||
UI.addUser = function(user) {
|
||||
const id = user.getId();
|
||||
const displayName = user.getDisplayName();
|
||||
const status = user.getStatus();
|
||||
|
||||
if (status) {
|
||||
// FIXME: move updateUserStatus in participantPresenceChanged action
|
||||
UI.updateUserStatus(user, status);
|
||||
}
|
||||
|
||||
// set initial display name
|
||||
if (displayName) {
|
||||
UI.changeDisplayName(id, displayName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -442,14 +416,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
|
|||
*/
|
||||
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
|
||||
|
||||
/**
|
||||
* Hide connection quality statistics from UI.
|
||||
*/
|
||||
UI.hideStats = function() {
|
||||
VideoLayout.hideStats();
|
||||
};
|
||||
|
||||
|
||||
UI.notifyTokenAuthFailed = function() {
|
||||
messageHandler.showError({
|
||||
descriptionKey: 'dialog.tokenAuthFailed',
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
/* global $ */
|
||||
/* global $, APP */
|
||||
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { Component } 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 Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
|
||||
import SmallVideo from '../videolayout/SmallVideo';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -13,28 +18,21 @@ export default class SharedVideoThumb extends SmallVideo {
|
|||
/**
|
||||
*
|
||||
* @param {*} participant
|
||||
* @param {*} videoType
|
||||
* @param {*} VideoLayout
|
||||
*/
|
||||
constructor(participant, videoType, VideoLayout) {
|
||||
super(VideoLayout);
|
||||
constructor(participant) {
|
||||
super();
|
||||
this.id = participant.id;
|
||||
this.isLocal = false;
|
||||
this.url = participant.id;
|
||||
this.videoSpanId = 'sharedVideoContainer';
|
||||
this.container = this.createContainer(this.videoSpanId);
|
||||
this.$container = $(this.container);
|
||||
this.renderThumbnail();
|
||||
this._setThumbnailSize();
|
||||
this.bindHoverHandler();
|
||||
this.updateDisplayName();
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
initializeAvatar() {} // eslint-disable-line no-empty-function
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} spanId
|
||||
|
@ -45,18 +43,6 @@ export default class SharedVideoThumb extends SmallVideo {
|
|||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
|
||||
// add the avatar
|
||||
const avatar = document.createElement('img');
|
||||
|
||||
avatar.className = 'sharedVideoAvatar';
|
||||
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
|
||||
container.appendChild(avatar);
|
||||
|
||||
const displayNameContainer = document.createElement('div');
|
||||
|
||||
displayNameContainer.className = 'displayNameContainer';
|
||||
container.appendChild(displayNameContainer);
|
||||
|
||||
const remoteVideosContainer
|
||||
= document.getElementById('filmstripRemoteVideosContainer');
|
||||
const localVideoContainer
|
||||
|
@ -68,21 +54,14 @@ export default class SharedVideoThumb extends SmallVideo {
|
|||
}
|
||||
|
||||
/**
|
||||
* Triggers re-rendering of the display name using current instance state.
|
||||
*
|
||||
* @returns {void}
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
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
|
||||
});
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ const Filmstrip = {
|
|||
*/
|
||||
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
|
||||
const thumbs = this._getThumbs(!forceUpdate);
|
||||
const avatarSize = height / 2;
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
thumbs.localThumb.css({
|
||||
|
@ -58,11 +57,6 @@ const Filmstrip = {
|
|||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
|
||||
$('.avatar-container').css({
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -77,7 +71,6 @@ const Filmstrip = {
|
|||
|
||||
if (thumbs.localThumb) {
|
||||
const { height, width } = local;
|
||||
const avatarSize = height / 2;
|
||||
|
||||
thumbs.localThumb.css({
|
||||
height: `${height}px`,
|
||||
|
@ -85,15 +78,10 @@ const Filmstrip = {
|
|||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
$('#localVideoContainer > .avatar-container').css({
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
const { height, width } = remote;
|
||||
const avatarSize = height / 2;
|
||||
|
||||
thumbs.remoteThumbs.css({
|
||||
height: `${height}px`,
|
||||
|
@ -101,10 +89,6 @@ const Filmstrip = {
|
|||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -126,10 +110,6 @@ const Filmstrip = {
|
|||
'min-width': '',
|
||||
'min-height': ''
|
||||
});
|
||||
$('#localVideoContainer > .avatar-container').css({
|
||||
height: '50%',
|
||||
width: `${heightToWidthPercent / 2}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
|
@ -142,10 +122,6 @@ const Filmstrip = {
|
|||
'min-width': '',
|
||||
'min-height': ''
|
||||
});
|
||||
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
|
||||
height: '50%',
|
||||
width: `${heightToWidthPercent / 2}%`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
/* global $, config, interfaceConfig, APP */
|
||||
/* global $, config, APP */
|
||||
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { Component } 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 { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { VideoTrack } from '../../../react/features/base/media';
|
||||
import { updateSettings } from '../../../react/features/base/settings';
|
||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
||||
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import SmallVideo from './SmallVideo';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class LocalVideo extends SmallVideo {
|
||||
/**
|
||||
*
|
||||
* @param {*} VideoLayout
|
||||
* @param {*} emitter
|
||||
* @param {*} streamEndedCallback
|
||||
*/
|
||||
constructor(VideoLayout, emitter, streamEndedCallback) {
|
||||
super(VideoLayout);
|
||||
constructor(emitter, streamEndedCallback) {
|
||||
super();
|
||||
this.videoSpanId = 'localVideoContainer';
|
||||
this.streamEndedCallback = streamEndedCallback;
|
||||
this.container = this.createContainer();
|
||||
|
@ -37,6 +36,7 @@ export default class LocalVideo extends SmallVideo {
|
|||
this.isLocal = true;
|
||||
this._setThumbnailSize();
|
||||
this.updateDOMLocation();
|
||||
this.renderThumbnail();
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
|
@ -44,7 +44,6 @@ export default class LocalVideo extends SmallVideo {
|
|||
this._buildContextMenu();
|
||||
}
|
||||
this.emitter = emitter;
|
||||
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get() {
|
||||
|
@ -53,18 +52,6 @@ export default class LocalVideo extends SmallVideo {
|
|||
});
|
||||
this.initBrowserSpecificProperties();
|
||||
|
||||
// Set default display name.
|
||||
this.updateDisplayName();
|
||||
|
||||
// Initialize the avatar display with an avatar url selected from the redux
|
||||
// state. Redux stores the local user with a hardcoded participant id of
|
||||
// 'local' if no id has been assigned yet.
|
||||
this.initializeAvatar();
|
||||
|
||||
this.addAudioLevelIndicator();
|
||||
this.updateIndicators();
|
||||
this.updateStatusBar();
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
|
@ -77,38 +64,19 @@ export default class LocalVideo extends SmallVideo {
|
|||
containerSpan.classList.add('videocontainer');
|
||||
containerSpan.id = this.videoSpanId;
|
||||
|
||||
containerSpan.innerHTML = `
|
||||
<div class = 'videocontainer__background'></div>
|
||||
<span id = 'localVideoWrapper'></span>
|
||||
<div class = 'videocontainer__toolbar'></div>
|
||||
<div class = 'videocontainer__toptoolbar'></div>
|
||||
<div class = 'videocontainer__hoverOverlay'></div>
|
||||
<div class = 'displayNameContainer'></div>
|
||||
<div class = 'avatar-container'></div>`;
|
||||
|
||||
return containerSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers re-rendering of the display name using current instance state.
|
||||
*
|
||||
* @returns {void}
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
updateDisplayName() {
|
||||
if (!this.container) {
|
||||
logger.warn(
|
||||
`Unable to set displayName - ${this.videoSpanId
|
||||
} does not exist`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderDisplayName({
|
||||
allowEditing: !config.disableProfile,
|
||||
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
||||
elementID: 'localDisplayName',
|
||||
participantID: this.id
|
||||
});
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,9 +84,7 @@ export default class LocalVideo extends SmallVideo {
|
|||
* @param {*} stream
|
||||
*/
|
||||
changeVideo(stream) {
|
||||
this.videoStream = stream;
|
||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||
this._updateVideoElement();
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const isVideo = stream.videoType != 'desktop';
|
||||
|
@ -128,17 +94,6 @@ export default class LocalVideo extends SmallVideo {
|
|||
this.setFlipX(isVideo ? settings.localFlipX : false);
|
||||
|
||||
const endedHandler = () => {
|
||||
const localVideoContainer
|
||||
= document.getElementById('localVideoWrapper');
|
||||
|
||||
// Only remove if there is no video and not a transition state.
|
||||
// Previous non-react logic created a new video element with each track
|
||||
// removal whereas react reuses the video component so it could be the
|
||||
// stream ended but a new one is being used.
|
||||
if (localVideoContainer && this.videoStream.isEnded()) {
|
||||
ReactDOM.unmountComponentAtNode(localVideoContainer);
|
||||
}
|
||||
|
||||
this._notifyOfStreamEnded();
|
||||
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
};
|
||||
|
@ -254,35 +209,5 @@ export default class LocalVideo extends SmallVideo {
|
|||
: document.getElementById('filmstripLocalVideoThumbnail');
|
||||
|
||||
appendTarget && appendTarget.appendChild(this.container);
|
||||
this._updateVideoElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the React Element for displaying video in {@code LocalVideo}.
|
||||
*
|
||||
*/
|
||||
_updateVideoElement() {
|
||||
const localVideoContainer = document.getElementById('localVideoWrapper');
|
||||
const videoTrack
|
||||
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<VideoTrack
|
||||
id = 'localVideo_container'
|
||||
videoTrack = { videoTrack } />
|
||||
</Provider>,
|
||||
localVideoContainer
|
||||
);
|
||||
|
||||
// Ensure the video gets play() called on it. This may be necessary in the
|
||||
// case where the local video container was moved and re-attached, in which
|
||||
// case video does not autoplay. Also, set the playsinline attribute on the
|
||||
// video element so that local video doesn't open in full screen by default
|
||||
// in Safari browser on iOS.
|
||||
const video = this.container.querySelector('video');
|
||||
|
||||
video && video.setAttribute('playsinline', 'true');
|
||||
video && !config.testing?.noAutoPlayVideo && video.play();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global $, APP, interfaceConfig */
|
||||
/* global $, APP, config */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
import { getParticipantById } from '../../../react/features/base/participants';
|
||||
import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
|
||||
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
|
||||
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
|
||||
|
@ -44,16 +45,6 @@ function createContainer(spanId) {
|
|||
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
|
||||
|
@ -72,21 +63,16 @@ 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);
|
||||
constructor(user) {
|
||||
super();
|
||||
|
||||
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;
|
||||
|
@ -100,11 +86,6 @@ export default class RemoteVideo extends SmallVideo {
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -114,76 +95,23 @@ export default class RemoteVideo extends SmallVideo {
|
|||
addRemoteVideoContainer() {
|
||||
this.container = createContainer(this.videoSpanId);
|
||||
this.$container = $(this.container);
|
||||
this.initializeAvatar();
|
||||
this.renderThumbnail();
|
||||
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
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
_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;
|
||||
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { initialVolumeValue }
|
||||
onMenuDisplay
|
||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { this.id } />
|
||||
</AtlasKitThemeProvider>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
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();
|
||||
</Provider>, this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -199,7 +127,7 @@ export default class RemoteVideo extends SmallVideo {
|
|||
}
|
||||
|
||||
const isVideo = stream.isVideoTrack();
|
||||
const elementID = SmallVideo.getStreamElementID(stream);
|
||||
const elementID = `remoteVideo_${stream.getId()}`;
|
||||
const select = $(`#${elementID}`);
|
||||
|
||||
select.remove();
|
||||
|
@ -207,11 +135,7 @@ export default class RemoteVideo extends SmallVideo {
|
|||
this._canPlayEventReceived = false;
|
||||
}
|
||||
|
||||
logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
|
||||
|
||||
if (stream === this.videoStream) {
|
||||
this.videoStream = null;
|
||||
}
|
||||
logger.info(`Video removed ${this.id}`, select);
|
||||
|
||||
this.updateView();
|
||||
}
|
||||
|
@ -223,14 +147,7 @@ export default class RemoteVideo extends SmallVideo {
|
|||
* @override
|
||||
*/
|
||||
isVideoPlayable() {
|
||||
const participant = getParticipantById(APP.store.getState(), this.id);
|
||||
const { connectionStatus } = participant || {};
|
||||
|
||||
return (
|
||||
super.isVideoPlayable()
|
||||
&& this._canPlayEventReceived
|
||||
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
|
||||
);
|
||||
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -245,9 +162,8 @@ export default class RemoteVideo extends SmallVideo {
|
|||
* Removes RemoteVideo from the page.
|
||||
*/
|
||||
remove() {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
super.remove();
|
||||
this.removePresenceLabel();
|
||||
this.removeRemoteVideoMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -295,19 +211,16 @@ export default class RemoteVideo extends SmallVideo {
|
|||
|
||||
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);
|
||||
let streamElement = document.createElement('video');
|
||||
|
||||
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
|
||||
streamElement.id = `remoteVideo_${stream.getId()}`;
|
||||
|
||||
// Put new stream element always in front
|
||||
streamElement = UIUtils.prependChild(this.container, streamElement);
|
||||
|
@ -315,14 +228,7 @@ export default class RemoteVideo extends SmallVideo {
|
|||
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())) {
|
||||
if (isVideo && isTestModeEnabled(APP.store.getState())) {
|
||||
|
||||
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
|
||||
|
||||
|
@ -331,72 +237,4 @@ export default class RemoteVideo extends SmallVideo {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global $, APP, config, interfaceConfig */
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
|
@ -21,6 +21,7 @@ import {
|
|||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
import {
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
isLocalTrackMuted,
|
||||
isRemoteTrackMuted
|
||||
|
@ -29,6 +30,7 @@ import { ConnectionIndicator } from '../../../react/features/connection-indicato
|
|||
import { DisplayName } from '../../../react/features/display-name';
|
||||
import {
|
||||
DominantSpeakerIndicator,
|
||||
isVideoPlayable,
|
||||
RaisedHandIndicator,
|
||||
StatusIndicators
|
||||
} from '../../../react/features/filmstrip';
|
||||
|
@ -89,37 +91,10 @@ export default class SmallVideo {
|
|||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
constructor(VideoLayout) {
|
||||
this.videoStream = null;
|
||||
this.audioStream = null;
|
||||
this.VideoLayout = VideoLayout;
|
||||
constructor() {
|
||||
this.videoIsHovered = false;
|
||||
this.videoType = undefined;
|
||||
|
||||
/**
|
||||
* Whether or not the connection indicator should be displayed.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
|
||||
|
||||
/**
|
||||
* Whether or not the dominant speaker indicator should be displayed.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._showDominantSpeaker = false;
|
||||
|
||||
/**
|
||||
* Whether or not the raised hand indicator should be displayed.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._showRaisedHand = false;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this.updateView = this.updateView.bind(this);
|
||||
|
||||
|
@ -145,33 +120,6 @@ export default class SmallVideo {
|
|||
return this.$container.is(':visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio or video element for a particular MediaStream.
|
||||
*/
|
||||
static createStreamElement(stream) {
|
||||
const isVideo = stream.isVideoTrack();
|
||||
const element = isVideo ? document.createElement('video') : document.createElement('audio');
|
||||
|
||||
if (isVideo) {
|
||||
element.setAttribute('muted', 'true');
|
||||
element.setAttribute('playsInline', 'true'); /* for Safari on iOS to work */
|
||||
} else if (config.startSilent) {
|
||||
element.muted = true;
|
||||
}
|
||||
|
||||
element.autoplay = !config.testing?.noAutoPlayVideo;
|
||||
element.id = SmallVideo.getStreamElementID(stream);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element id for a particular MediaStream.
|
||||
*/
|
||||
static getStreamElementID(stream) {
|
||||
return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
|
||||
*/
|
||||
|
@ -180,103 +128,22 @@ export default class SmallVideo {
|
|||
this.$container.hover(
|
||||
() => {
|
||||
this.videoIsHovered = true;
|
||||
this.renderThumbnail(true);
|
||||
this.updateView();
|
||||
this.updateIndicators();
|
||||
},
|
||||
() => {
|
||||
this.videoIsHovered = false;
|
||||
this.renderThumbnail(false);
|
||||
this.updateView();
|
||||
this.updateIndicators();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmounts the ConnectionIndicator component.
|
||||
|
||||
* @returns {void}
|
||||
*/
|
||||
removeConnectionIndicator() {
|
||||
this._showConnectionIndicator = false;
|
||||
this.updateIndicators();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or updates the ReactElement for displaying status indicators about
|
||||
* audio mute, video mute, and moderator status.
|
||||
*
|
||||
* @returns {void}
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
updateStatusBar() {
|
||||
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
|
||||
|
||||
if (!statusBarContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<StatusIndicators
|
||||
participantID = { this.id } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
statusBarContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the element indicating the audio level of the participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
addAudioLevelIndicator() {
|
||||
let audioLevelContainer = this._getAudioLevelContainer();
|
||||
|
||||
if (audioLevelContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
audioLevelContainer = document.createElement('span');
|
||||
audioLevelContainer.className = 'audioindicator-container';
|
||||
this.container.appendChild(audioLevelContainer);
|
||||
this.updateAudioLevelIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the element indicating the audio level of the participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
removeAudioLevelIndicator() {
|
||||
const audioLevelContainer = this._getAudioLevelContainer();
|
||||
|
||||
if (audioLevelContainer) {
|
||||
ReactDOM.unmountComponentAtNode(audioLevelContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the audio level for this small video.
|
||||
*
|
||||
* @param lvl the new audio level to set
|
||||
* @returns {void}
|
||||
*/
|
||||
updateAudioLevelIndicator(lvl = 0) {
|
||||
const audioLevelContainer = this._getAudioLevelContainer();
|
||||
|
||||
if (audioLevelContainer) {
|
||||
ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the component's DOM for the element that should be the parent to the
|
||||
* AudioLevelIndicator.
|
||||
*
|
||||
* @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
|
||||
*/
|
||||
_getAudioLevelContainer() {
|
||||
return this.container.querySelector('.audioindicator-container');
|
||||
renderThumbnail() {
|
||||
// Should be implemented by in subclasses.
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,62 +160,6 @@ export default class SmallVideo {
|
|||
return $($(this.container).find('video')[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the HTML image element which displays user's avatar.
|
||||
*
|
||||
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
|
||||
* element which displays the user's avatar.
|
||||
*/
|
||||
$avatar() {
|
||||
return this.$container.find('.avatar-container');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name element, which appears on the video thumbnail.
|
||||
*
|
||||
* @return {jQuery} a jQuery selector pointing to the display name element of
|
||||
* the video thumbnail
|
||||
*/
|
||||
$displayName() {
|
||||
return this.$container.find('.displayNameContainer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates the participant's display name that is shown over the
|
||||
* video preview.
|
||||
*
|
||||
* @param {Object} props - The React {@code Component} props to pass into the
|
||||
* {@code DisplayName} component.
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderDisplayName(props) {
|
||||
const displayNameContainer = this.container.querySelector('.displayNameContainer');
|
||||
|
||||
if (displayNameContainer) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<DisplayName { ...props } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
displayNameContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the component responsible for showing the participant's display name,
|
||||
* if its container is present.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
removeDisplayName() {
|
||||
const displayNameContainer = this.container.querySelector('.displayNameContainer');
|
||||
|
||||
if (displayNameContainer) {
|
||||
ReactDOM.unmountComponentAtNode(displayNameContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / disables the css responsible for focusing/pinning a video
|
||||
* thumbnail.
|
||||
|
@ -392,18 +203,7 @@ export default class SmallVideo {
|
|||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isVideoPlayable() {
|
||||
const state = APP.store.getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const participant = this.id ? getParticipantById(state, this.id) : getLocalParticipant(state);
|
||||
let isVideoMuted = true;
|
||||
|
||||
if (participant?.local) {
|
||||
isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
|
||||
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
|
||||
isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, this.id);
|
||||
}
|
||||
|
||||
return this.videoStream && !isVideoMuted && !APP.conference.isAudioOnly();
|
||||
return isVideoPlayable(APP.store.getState(), this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -436,13 +236,15 @@ export default class SmallVideo {
|
|||
let isScreenSharing = false;
|
||||
let connectionStatus;
|
||||
const state = APP.store.getState();
|
||||
const participant = getParticipantById(state, this.id);
|
||||
const id = this.id;
|
||||
const participant = getParticipantById(state, id);
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const videoTrack
|
||||
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
|
||||
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
|
||||
const tracks = state['features/base/tracks'];
|
||||
const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, this.id);
|
||||
|
||||
isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop';
|
||||
isScreenSharing = typeof track !== 'undefined' && videoTrack?.videoType === 'desktop';
|
||||
connectionStatus = participant.connectionStatus;
|
||||
}
|
||||
|
||||
|
@ -455,9 +257,9 @@ export default class SmallVideo {
|
|||
hasVideo: Boolean(this.selectVideoElement().length),
|
||||
connectionStatus,
|
||||
canPlayEventReceived: this._canPlayEventReceived,
|
||||
videoStream: Boolean(this.videoStream),
|
||||
videoStream: Boolean(videoTrack),
|
||||
isScreenSharing,
|
||||
videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
|
||||
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -527,43 +329,6 @@ export default class SmallVideo {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the react component displaying the avatar with the passed in avatar
|
||||
* url.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
initializeAvatar() {
|
||||
const thumbnail = this.$avatar().get(0);
|
||||
|
||||
if (thumbnail) {
|
||||
// Maybe add a special case for local participant, as on init of
|
||||
// LocalVideo.js the id is set to "local" but will get updated later.
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<AvatarDisplay
|
||||
className = 'userAvatar'
|
||||
participantId = { this.id } />
|
||||
</Provider>,
|
||||
thumbnail
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmounts any attached react components (particular the avatar image) from
|
||||
* the avatar container.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
removeAvatar() {
|
||||
const thumbnail = this.$avatar().get(0);
|
||||
|
||||
if (thumbnail) {
|
||||
ReactDOM.unmountComponentAtNode(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the dominant speaker indicator.
|
||||
* @param show whether to show or hide.
|
||||
|
@ -580,30 +345,8 @@ export default class SmallVideo {
|
|||
|
||||
return;
|
||||
}
|
||||
if (this._showDominantSpeaker === show) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showDominantSpeaker = show;
|
||||
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
|
||||
this.updateIndicators();
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the raised hand indicator.
|
||||
* @param show whether to show or hide.
|
||||
*/
|
||||
showRaisedHandIndicator(show) {
|
||||
if (!this.container) {
|
||||
logger.warn(`Unable to raised hand indication - ${
|
||||
this.videoSpanId} does not exist`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._showRaisedHand = show;
|
||||
this.updateIndicators();
|
||||
this.$container.toggleClass('active-speaker', show);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -634,19 +377,7 @@ export default class SmallVideo {
|
|||
*/
|
||||
remove() {
|
||||
logger.log('Remove thumbnail', this.id);
|
||||
this.removeAudioLevelIndicator();
|
||||
|
||||
const toolbarContainer
|
||||
= this.container.querySelector('.videocontainer__toolbar');
|
||||
|
||||
if (toolbarContainer) {
|
||||
ReactDOM.unmountComponentAtNode(toolbarContainer);
|
||||
}
|
||||
|
||||
this.removeConnectionIndicator();
|
||||
this.removeDisplayName();
|
||||
this.removeAvatar();
|
||||
this._unmountIndicators();
|
||||
this._unmountThumbnail();
|
||||
|
||||
// Remove whole container
|
||||
if (this.container.parentNode) {
|
||||
|
@ -661,76 +392,9 @@ export default class SmallVideo {
|
|||
* @returns {void}
|
||||
*/
|
||||
rerender() {
|
||||
this.updateIndicators();
|
||||
this.updateStatusBar();
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the React element responsible for showing connection status, dominant
|
||||
* speaker, and raised hand icons. Uses instance variables to get the necessary
|
||||
* state to display. Will create the React element if not already created.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
updateIndicators() {
|
||||
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
|
||||
|
||||
if (!indicatorToolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
|
||||
const iconSize = NORMAL;
|
||||
const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
|
||||
const state = APP.store.getState();
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
const participantCount = getParticipantCount(state);
|
||||
let statsPopoverPosition, tooltipPosition;
|
||||
|
||||
if (currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
statsPopoverPosition = 'right top';
|
||||
tooltipPosition = 'right';
|
||||
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
statsPopoverPosition = this.statsPopoverLocation;
|
||||
tooltipPosition = 'left';
|
||||
} else {
|
||||
statsPopoverPosition = this.statsPopoverLocation;
|
||||
tooltipPosition = 'top';
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<div>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
{ this._showConnectionIndicator
|
||||
? <ConnectionIndicator
|
||||
alwaysVisible = { showConnectionIndicator }
|
||||
iconSize = { iconSize }
|
||||
isLocalVideo = { this.isLocal }
|
||||
enableStatsDisplay = { true }
|
||||
participantId = { this.id }
|
||||
statsPopoverPosition = { statsPopoverPosition } />
|
||||
: null }
|
||||
<RaisedHandIndicator
|
||||
iconSize = { iconSize }
|
||||
participantId = { this.id }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
{ this._showDominantSpeaker && participantCount > 2
|
||||
? <DominantSpeakerIndicator
|
||||
iconSize = { iconSize }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
: null }
|
||||
</AtlasKitThemeProvider>
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
indicatorToolbar
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the thumbnail is clicked and potentially trigger
|
||||
* pinning of the participant.
|
||||
|
@ -788,18 +452,10 @@ export default class SmallVideo {
|
|||
}
|
||||
|
||||
/**
|
||||
* Removes the React element responsible for showing connection status, dominant
|
||||
* speaker, and raised hand icons.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
* Unmounts the thumbnail.
|
||||
*/
|
||||
_unmountIndicators() {
|
||||
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
|
||||
|
||||
if (indicatorToolbar) {
|
||||
ReactDOM.unmountComponentAtNode(indicatorToolbar);
|
||||
}
|
||||
_unmountThumbnail() {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -813,10 +469,6 @@ export default class SmallVideo {
|
|||
switch (layout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
this.$container.css('padding-top', `${heightToWidthPercent}%`);
|
||||
this.$avatar().css({
|
||||
height: '50%',
|
||||
width: `${heightToWidthPercent / 2}%`
|
||||
});
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
|
@ -826,7 +478,6 @@ export default class SmallVideo {
|
|||
|
||||
if (typeof size !== 'undefined') {
|
||||
const { height, width } = size;
|
||||
const avatarSize = height / 2;
|
||||
|
||||
this.$container.css({
|
||||
height: `${height}px`,
|
||||
|
@ -834,10 +485,6 @@ export default class SmallVideo {
|
|||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
this.$avatar().css({
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -847,7 +494,6 @@ export default class SmallVideo {
|
|||
|
||||
if (typeof thumbnailSize !== 'undefined') {
|
||||
const { height, width } = thumbnailSize;
|
||||
const avatarSize = height / 2;
|
||||
|
||||
this.$container.css({
|
||||
height: `${height}px`,
|
||||
|
@ -855,10 +501,6 @@ export default class SmallVideo {
|
|||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
this.$avatar().css({
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@ const VideoLayout = {
|
|||
eventEmitter = emitter;
|
||||
|
||||
localVideoThumbnail = new LocalVideo(
|
||||
VideoLayout,
|
||||
emitter,
|
||||
this._updateLargeVideoIfDisplayed.bind(this));
|
||||
|
||||
|
@ -116,12 +115,6 @@ const VideoLayout = {
|
|||
* @param lvl the new audio level to update to
|
||||
*/
|
||||
setAudioLevel(id, lvl) {
|
||||
const smallVideo = this.getSmallVideo(id);
|
||||
|
||||
if (smallVideo) {
|
||||
smallVideo.updateAudioLevelIndicator(lvl);
|
||||
}
|
||||
|
||||
if (largeVideo && id === largeVideo.id) {
|
||||
largeVideo.updateLargeVideoAudioLevel(lvl);
|
||||
}
|
||||
|
@ -137,19 +130,6 @@ const VideoLayout = {
|
|||
this._updateLargeVideoIfDisplayed(localId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get's the localID of the conference and set it to the local video
|
||||
* (small one). This needs to be called as early as possible, when muc is
|
||||
* actually joined. Otherwise events can come with information like email
|
||||
* and setting them assume the id is already set.
|
||||
*/
|
||||
mucJoined() {
|
||||
// FIXME: replace this call with a generic update call once SmallVideo
|
||||
// only contains a ReactElement. Then remove this call once the
|
||||
// Filmstrip is fully in React.
|
||||
localVideoThumbnail.updateIndicators();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides local video.
|
||||
* @param {boolean} true to make the local video visible, false - otherwise
|
||||
|
@ -172,11 +152,8 @@ const VideoLayout = {
|
|||
|
||||
remoteVideo.addRemoteStreamElement(stream);
|
||||
|
||||
// Make sure track's muted state is reflected
|
||||
if (stream.getType() !== 'audio') {
|
||||
this.onVideoMute(id);
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
this.onVideoMute(id);
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
onRemoteStreamRemoved(stream) {
|
||||
|
@ -184,13 +161,12 @@ const VideoLayout = {
|
|||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
// Remote stream may be removed after participant left the conference.
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.removeRemoteStreamElement(stream);
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
|
||||
this.updateMutedForNoTracks(id, stream.getType());
|
||||
this.updateVideoMutedForNoTracks(id);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -199,19 +175,12 @@ const VideoLayout = {
|
|||
*
|
||||
* If participant has no tracks will make the UI display muted status.
|
||||
* @param {string} participantId
|
||||
* @param {string} mediaType 'audio' or 'video'
|
||||
*/
|
||||
updateMutedForNoTracks(participantId, mediaType) {
|
||||
updateVideoMutedForNoTracks(participantId) {
|
||||
const participant = APP.conference.getParticipantById(participantId);
|
||||
|
||||
if (participant && !participant.getTracksByMediaType(mediaType).length) {
|
||||
if (mediaType === 'audio') {
|
||||
APP.UI.setAudioMuted(participantId, true);
|
||||
} else if (mediaType === 'video') {
|
||||
APP.UI.setVideoMuted(participantId);
|
||||
} else {
|
||||
logger.error(`Unsupported media type: ${mediaType}`);
|
||||
}
|
||||
if (participant && !participant.getTracksByMediaType('video').length) {
|
||||
APP.UI.setVideoMuted(participantId);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -279,10 +248,7 @@ const VideoLayout = {
|
|||
if (!participant || participant.local) {
|
||||
return;
|
||||
} else if (participant.isFakeParticipant) {
|
||||
const sharedVideoThumb = new SharedVideoThumb(
|
||||
participant,
|
||||
SHARED_VIDEO_CONTAINER_TYPE,
|
||||
VideoLayout);
|
||||
const sharedVideoThumb = new SharedVideoThumb(participant);
|
||||
|
||||
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
|
||||
|
||||
|
@ -291,12 +257,10 @@ const VideoLayout = {
|
|||
|
||||
const id = participant.id;
|
||||
const jitsiParticipant = APP.conference.getParticipantById(id);
|
||||
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
|
||||
const remoteVideo = new RemoteVideo(jitsiParticipant);
|
||||
|
||||
this.addRemoteVideoContainer(id, remoteVideo);
|
||||
|
||||
this.updateMutedForNoTracks(id, 'audio');
|
||||
this.updateMutedForNoTracks(id, 'video');
|
||||
this.updateVideoMutedForNoTracks(id);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -331,22 +295,6 @@ const VideoLayout = {
|
|||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display name changed.
|
||||
*/
|
||||
onDisplayNameChanged(id) {
|
||||
if (id === 'localVideoContainer'
|
||||
|| APP.conference.isLocalId(id)) {
|
||||
localVideoThumbnail.updateDisplayName();
|
||||
} else {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateDisplayName();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On dominant speaker changed event.
|
||||
*
|
||||
|
@ -413,20 +361,6 @@ const VideoLayout = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides all the indicators
|
||||
*/
|
||||
hideStats() {
|
||||
for (const video in remoteVideos) { // eslint-disable-line guard-for-in
|
||||
const remoteVideo = remoteVideos[video];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.removeConnectionIndicator();
|
||||
}
|
||||
}
|
||||
localVideoThumbnail.removeConnectionIndicator();
|
||||
},
|
||||
|
||||
removeParticipantContainer(id) {
|
||||
// Unlock large video
|
||||
if (this.getPinnedId() === id) {
|
||||
|
@ -477,15 +411,6 @@ const VideoLayout = {
|
|||
},
|
||||
|
||||
changeUserAvatar(id, avatarUrl) {
|
||||
const smallVideo = VideoLayout.getSmallVideo(id);
|
||||
|
||||
if (smallVideo) {
|
||||
smallVideo.initializeAvatar();
|
||||
} else {
|
||||
logger.warn(
|
||||
`Missed avatar update - no small video yet for ${id}`
|
||||
);
|
||||
}
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(avatarUrl);
|
||||
}
|
||||
|
|
|
@ -128,9 +128,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
|
|||
titleKey: 'dialog.sessTerminated'
|
||||
}));
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.UI.hideStats();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioTrack}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The value of the id attribute of the audio element.
|
||||
*/
|
||||
id: string,
|
||||
|
||||
|
||||
/**
|
||||
* The audio track.
|
||||
*/
|
||||
audioTrack: ?Object,
|
||||
|
||||
/**
|
||||
* Used to determine the value of the autoplay attribute of the underlying
|
||||
* audio element.
|
||||
*/
|
||||
autoPlay: boolean,
|
||||
|
||||
/**
|
||||
* Represents muted property of the underlying audio element.
|
||||
*/
|
||||
muted: ?Boolean,
|
||||
|
||||
/**
|
||||
* Represents volume property of the underlying audio element.
|
||||
*/
|
||||
volume: ?number,
|
||||
|
||||
/**
|
||||
* A function that will be executed when the reference to the underlying audio element changes.
|
||||
*/
|
||||
onAudioElementReferenceChanged: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
|
||||
*/
|
||||
export default class AudioTrack extends Component<Props> {
|
||||
/**
|
||||
* Reference to the HTML audio element, stored until the file is ready.
|
||||
*/
|
||||
_ref: ?HTMLAudioElement;
|
||||
|
||||
/**
|
||||
* Default values for {@code AudioTrack} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
autoPlay: true,
|
||||
id: ''
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates new <code>Audio</code> element instance with given props.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._setRef = this._setRef.bind(this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attaches the audio track to the audio element and plays it.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._attachTrack(this.props.audioTrack);
|
||||
|
||||
if (this._ref) {
|
||||
const { autoPlay, muted, volume } = this.props;
|
||||
|
||||
if (autoPlay) {
|
||||
// Ensure the audio gets play() called on it. This may be necessary in the
|
||||
// case where the local video container was moved and re-attached, in which
|
||||
// case the audio may not autoplay.
|
||||
this._ref.play();
|
||||
}
|
||||
|
||||
if (typeof volume === 'number') {
|
||||
this._ref.volume = volume;
|
||||
}
|
||||
|
||||
if (typeof muted === 'boolean') {
|
||||
this._ref.muted = muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any existing associations between the current audio track and the
|
||||
* component's audio element.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._detachTrack(this.props.audioTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* This component's updating is blackboxed from React to prevent re-rendering of the audio
|
||||
* element, as we set all the properties manually.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {boolean} - False is always returned to blackbox this component
|
||||
* from React.
|
||||
*/
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
const currentJitsiTrack = this.props.audioTrack && this.props.audioTrack.jitsiTrack;
|
||||
const nextJitsiTrack = nextProps.audioTrack && nextProps.audioTrack.jitsiTrack;
|
||||
|
||||
if (currentJitsiTrack !== nextJitsiTrack) {
|
||||
this._detachTrack(this.props.audioTrack);
|
||||
this._attachTrack(nextProps.audioTrack);
|
||||
}
|
||||
|
||||
if (this._ref) {
|
||||
const currentVolume = this._ref.volume;
|
||||
const nextVolume = nextProps.volume;
|
||||
|
||||
if (typeof nextVolume === 'number' && currentVolume !== nextVolume) {
|
||||
this._ref.volume = nextVolume;
|
||||
}
|
||||
|
||||
const currentMuted = this._ref.muted;
|
||||
const nextMuted = nextProps.muted;
|
||||
|
||||
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
|
||||
this._ref.muted = nextMuted;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { autoPlay, id } = this.props;
|
||||
|
||||
return (
|
||||
<audio
|
||||
autoPlay = { autoPlay }
|
||||
id = { id }
|
||||
ref = { this._setRef } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls into the passed in track to associate the track with the component's audio element.
|
||||
*
|
||||
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_attachTrack(track) {
|
||||
if (!track || !track.jitsiTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
track.jitsiTrack.attach(this._ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the association to the component's audio element from the passed
|
||||
* in redux representation of jitsi audio track.
|
||||
*
|
||||
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_detachTrack(track) {
|
||||
if (this._ref && track && track.jitsiTrack) {
|
||||
track.jitsiTrack.detach(this._ref);
|
||||
}
|
||||
}
|
||||
|
||||
_setRef: (?HTMLAudioElement) => void;
|
||||
|
||||
/**
|
||||
* Sets the reference to the HTML audio element.
|
||||
*
|
||||
* @param {HTMLAudioElement} audioElement - The HTML audio element instance.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setRef(audioElement: ?HTMLAudioElement) {
|
||||
this._ref = audioElement;
|
||||
const { onAudioElementReferenceChanged } = this.props;
|
||||
|
||||
if (this._ref && onAudioElementReferenceChanged) {
|
||||
onAudioElementReferenceChanged({ volume: this._ref.volume });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -99,6 +99,13 @@ class Video extends Component<Props> {
|
|||
}
|
||||
|
||||
this._attachTrack(this.props.videoTrack);
|
||||
|
||||
if (this._videoElement && this.props.autoPlay) {
|
||||
// Ensure the video gets play() called on it. This may be necessary in the
|
||||
// case where the local video container was moved and re-attached, in which
|
||||
// case video does not autoplay.
|
||||
this._videoElement.play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,639 @@
|
|||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||
import AudioTrack from '../../../base/media/components/web/AudioTrack';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||
import { ConnectionIndicator } from '../../../connection-indicator';
|
||||
import { DisplayName } from '../../../display-name';
|
||||
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
|
||||
import { PresenceLabel } from '../../../presence-status';
|
||||
import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link Thumbnail}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current audio level value for the Thumbnail.
|
||||
*/
|
||||
audioLevel: number,
|
||||
|
||||
/**
|
||||
* The current volume setting for the Thumbnail.
|
||||
*/
|
||||
volume: ?number
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Thumbnail}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The audio track related to the participant.
|
||||
*/
|
||||
_audioTrack: ?Object,
|
||||
|
||||
/**
|
||||
* Disable/enable the auto hide functionality for the connection indicator.
|
||||
*/
|
||||
_connectionIndicatorAutoHideEnabled: boolean,
|
||||
|
||||
/**
|
||||
* Disable/enable the connection indicator.
|
||||
*/
|
||||
_connectionIndicatorDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The current layout of the filmstrip.
|
||||
*/
|
||||
_currentLayout: string,
|
||||
|
||||
/**
|
||||
* The default display name for the local participant.
|
||||
*/
|
||||
_defaultLocalDisplayName: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the profile functionality is disabled.
|
||||
*/
|
||||
_disableProfile: boolean,
|
||||
|
||||
/**
|
||||
* The height of the Thumbnail.
|
||||
*/
|
||||
_height: number,
|
||||
|
||||
/**
|
||||
* The aspect ratio of the Thumbnail in percents.
|
||||
*/
|
||||
_heightToWidthPercent: number,
|
||||
|
||||
/**
|
||||
* Disable/enable the dominant speaker indicator.
|
||||
*/
|
||||
_isDominantSpeakerDisabled: boolean,
|
||||
|
||||
/**
|
||||
* The size of the icon of indicators.
|
||||
*/
|
||||
_indicatorIconSize: number,
|
||||
|
||||
/**
|
||||
* An object with information about the participant related to the thumbnaul.
|
||||
*/
|
||||
_participant: Object,
|
||||
|
||||
/**
|
||||
* The number of participants in the call.
|
||||
*/
|
||||
_participantCount: number,
|
||||
|
||||
/**
|
||||
* Indicates whether the "start silent" mode is enabled.
|
||||
*/
|
||||
_startSilent: Boolean,
|
||||
|
||||
/**
|
||||
* The video track that will be displayed in the thumbnail.
|
||||
*/
|
||||
_videoTrack: ?Object,
|
||||
|
||||
/**
|
||||
* The width of the thumbnail.
|
||||
*/
|
||||
_width: number,
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Indicates whether the thumbnail is hovered or not.
|
||||
*/
|
||||
isHovered: ?boolean,
|
||||
|
||||
/**
|
||||
* The ID of the participant related to the thumbnail.
|
||||
*/
|
||||
participantID: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a thumbnail.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Thumbnail extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Initializes a new Thumbnail instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioLevel: 0,
|
||||
volume: undefined
|
||||
};
|
||||
|
||||
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
||||
this._onVolumeChange = this._onVolumeChange.bind(this);
|
||||
this._onAudioElementReferenceChanged = this._onAudioElementReferenceChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates after the initial render.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._listenForAudioUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening for audio level updates on the old track and starts
|
||||
* listening instead on the new track.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps._audioTrack !== this.props._audioTrack) {
|
||||
this._stopListeningForAudioUpdates(prevProps._audioTrack);
|
||||
this._listenForAudioUpdates();
|
||||
this._updateAudioLevel(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from audio level updates.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._stopListeningForAudioUpdates(this.props._audioTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening for audio level updates from the library.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_listenForAudioUpdates() {
|
||||
const { _audioTrack } = this.props;
|
||||
|
||||
if (_audioTrack) {
|
||||
const { jitsiTrack } = _audioTrack;
|
||||
|
||||
jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to further updates from the passed track.
|
||||
*
|
||||
* @param {Object} audioTrack - The track.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_stopListeningForAudioUpdates(audioTrack) {
|
||||
if (audioTrack) {
|
||||
const { jitsiTrack } = audioTrack;
|
||||
|
||||
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
|
||||
}
|
||||
}
|
||||
|
||||
_updateAudioLevel: (number) => void;
|
||||
|
||||
/**
|
||||
* Updates the internal state of the last know audio level. The level should
|
||||
* be between 0 and 1, as the level will be used as a percentage out of 1.
|
||||
*
|
||||
* @param {number} audioLevel - The new audio level for the track.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAudioLevel(audioLevel) {
|
||||
this.setState({
|
||||
audioLevel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with the styles for thumbnail.
|
||||
*
|
||||
* @returns {Object} - The styles for the thumbnail.
|
||||
*/
|
||||
_getStyles(): Object {
|
||||
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
|
||||
let styles;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const avatarSize = _height / 2;
|
||||
|
||||
styles = {
|
||||
avatarContainer: {
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
styles = {
|
||||
avatarContainer: {
|
||||
height: '50%',
|
||||
width: `${_heightToWidthPercent / 2}%`
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a fake participant (youtube video) thumbnail.
|
||||
*
|
||||
* @param {string} id - The id of the participant.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFakeParticipant() {
|
||||
const { _participant } = this.props;
|
||||
const { id } = _participant;
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
className = 'sharedVideoAvatar'
|
||||
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
|
||||
<div className = 'displayNameContainer'>
|
||||
<DisplayName
|
||||
elementID = 'sharedVideoContainer_name'
|
||||
participantID = { id } />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the top toolbar of the thumbnail.
|
||||
*
|
||||
* @returns {Component}
|
||||
*/
|
||||
_renderTopToolbar() {
|
||||
const {
|
||||
_connectionIndicatorAutoHideEnabled,
|
||||
_connectionIndicatorDisabled,
|
||||
_currentLayout,
|
||||
_isDominantSpeakerDisabled,
|
||||
_indicatorIconSize: iconSize,
|
||||
_participant,
|
||||
_participantCount,
|
||||
isHovered
|
||||
} = this.props;
|
||||
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
||||
const { id, local = false, dominantSpeaker = false } = _participant;
|
||||
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
|
||||
let statsPopoverPosition, tooltipPosition;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
statsPopoverPosition = 'right top';
|
||||
tooltipPosition = 'right';
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
statsPopoverPosition = 'left top';
|
||||
tooltipPosition = 'left';
|
||||
break;
|
||||
default:
|
||||
statsPopoverPosition = 'top center';
|
||||
tooltipPosition = 'top';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
{ _connectionIndicatorDisabled
|
||||
? null
|
||||
: <ConnectionIndicator
|
||||
alwaysVisible = { showConnectionIndicator }
|
||||
enableStatsDisplay = { true }
|
||||
iconSize = { iconSize }
|
||||
isLocalVideo = { local }
|
||||
participantId = { id }
|
||||
statsPopoverPosition = { statsPopoverPosition } />
|
||||
}
|
||||
<RaisedHandIndicator
|
||||
iconSize = { iconSize }
|
||||
participantId = { id }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
{ showDominantSpeaker && _participantCount > 2
|
||||
? <DominantSpeakerIndicator
|
||||
iconSize = { iconSize }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
: null }
|
||||
</AtlasKitThemeProvider>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the avatar.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAvatar() {
|
||||
const { _participant } = this.props;
|
||||
const { id } = _participant;
|
||||
const styles = this._getStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'avatar-container'
|
||||
style = { styles.avatarContainer }>
|
||||
<Avatar
|
||||
className = 'userAvatar'
|
||||
participantId = { id } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the local participant's thumbnail.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderLocalParticipant() {
|
||||
const {
|
||||
_defaultLocalDisplayName,
|
||||
_disableProfile,
|
||||
_participant,
|
||||
_videoTrack
|
||||
} = this.props;
|
||||
const { id } = _participant || {};
|
||||
const { audioLevel = 0 } = this.state;
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className = 'videocontainer__background' />
|
||||
<span id = 'localVideoWrapper'>
|
||||
<VideoTrack
|
||||
id = 'localVideo_container'
|
||||
videoTrack = { _videoTrack } />
|
||||
</span>
|
||||
<div className = 'videocontainer__toolbar'>
|
||||
<StatusIndicators participantID = { id } />
|
||||
</div>
|
||||
<div className = 'videocontainer__toptoolbar'>
|
||||
{ this._renderTopToolbar() }
|
||||
</div>
|
||||
<div className = 'videocontainer__hoverOverlay' />
|
||||
<div className = 'displayNameContainer'>
|
||||
<DisplayName
|
||||
allowEditing = { !_disableProfile }
|
||||
displayNameSuffix = { _defaultLocalDisplayName }
|
||||
elementID = 'localDisplayName'
|
||||
participantID = { id } />
|
||||
</div>
|
||||
{ this._renderAvatar() }
|
||||
<span className = 'audioindicator-container'>
|
||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders a remote participant's 'thumbnail.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderRemoteParticipant() {
|
||||
const {
|
||||
_audioTrack,
|
||||
_participant,
|
||||
_startSilent
|
||||
} = this.props;
|
||||
const { id } = _participant;
|
||||
const { audioLevel = 0, volume = 1 } = this.state;
|
||||
|
||||
// hide volume when in silent mode
|
||||
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
|
||||
const { jitsiTrack } = _audioTrack ?? {};
|
||||
const audioTrackId = jitsiTrack && jitsiTrack.getId();
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
_audioTrack
|
||||
? <AudioTrack
|
||||
audioTrack = { _audioTrack }
|
||||
id = { `remoteAudio_${audioTrackId}` }
|
||||
muted = { _startSilent }
|
||||
onAudioElementReferenceChanged = { this._onAudioElementReferenceChanged }
|
||||
volume = { this.state.volume } />
|
||||
: null
|
||||
|
||||
}
|
||||
<div className = 'videocontainer__background' />
|
||||
<div className = 'videocontainer__toptoolbar'>
|
||||
{ this._renderTopToolbar() }
|
||||
</div>
|
||||
<div className = 'videocontainer__toolbar'>
|
||||
<StatusIndicators participantID = { id } />
|
||||
</div>
|
||||
<div className = 'videocontainer__hoverOverlay' />
|
||||
<div className = 'displayNameContainer'>
|
||||
<DisplayName
|
||||
elementID = { `participant_${id}_name` }
|
||||
participantID = { id } />
|
||||
</div>
|
||||
{ this._renderAvatar() }
|
||||
<div className = 'presence-label-container'>
|
||||
<PresenceLabel
|
||||
className = 'presence-label'
|
||||
participantID = { id } />
|
||||
</div>
|
||||
<span className = 'remotevideomenu'>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { volume }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { id } />
|
||||
</AtlasKitThemeProvider>
|
||||
</span>
|
||||
<span className = 'audioindicator-container'>
|
||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
_onAudioElementReferenceChanged: Object => void;
|
||||
|
||||
/**
|
||||
* Handles audio element references changes by receiving some properties from the audio element.
|
||||
*
|
||||
* @param {Obejct} audioElementProps - Properties of the audio element.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAudioElementReferenceChanged({ volume }) {
|
||||
if (this.state.volume !== volume) {
|
||||
this.setState({ volume });
|
||||
}
|
||||
}
|
||||
|
||||
_onVolumeChange: number => void;
|
||||
|
||||
/**
|
||||
* Handles volume changes.
|
||||
*
|
||||
* @param {number} value - The new value for the volume.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onVolumeChange(value) {
|
||||
this.setState({ volume: value });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _participant } = this.props;
|
||||
|
||||
if (!_participant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isFakeParticipant, local = false } = _participant;
|
||||
|
||||
if (local) {
|
||||
return this._renderLocalParticipant();
|
||||
}
|
||||
|
||||
if (isFakeParticipant) {
|
||||
return this._renderFakeParticipant();
|
||||
}
|
||||
|
||||
return this._renderRemoteParticipant();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
||||
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
||||
const isLocal = participant?.local ?? true;
|
||||
const _videoTrack = isLocal
|
||||
? getLocalVideoTrack(state['features/base/tracks'])
|
||||
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
|
||||
const _audioTrack = isLocal
|
||||
? getLocalAudioTrack(state['features/base/tracks'])
|
||||
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
let size = {};
|
||||
const { startSilent, disableProfile = false } = state['features/base/config'];
|
||||
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
|
||||
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const {
|
||||
horizontalViewDimensions = {
|
||||
local: {},
|
||||
remote: {}
|
||||
}
|
||||
} = state['features/filmstrip'];
|
||||
const { local, remote } = horizontalViewDimensions;
|
||||
const { width, height } = isLocal ? local : remote;
|
||||
|
||||
size = {
|
||||
_width: width,
|
||||
_height: height
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
size = {
|
||||
_heightToWidthPercent: isLocal
|
||||
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
|
||||
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
|
||||
};
|
||||
break;
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
|
||||
|
||||
size = {
|
||||
_width: width,
|
||||
_height: height
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
_audioTrack,
|
||||
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
||||
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
_currentLayout,
|
||||
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
||||
_disableProfile: disableProfile,
|
||||
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
||||
_indicatorIconSize: NORMAL,
|
||||
_participant: participant,
|
||||
_participantCount: getParticipantCount(state),
|
||||
_startSilent: Boolean(startSilent),
|
||||
_videoTrack,
|
||||
...size
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Thumbnail);
|
|
@ -7,3 +7,4 @@ export { default as ModeratorIndicator } from './ModeratorIndicator';
|
|||
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
|
||||
export { default as StatusIndicators } from './StatusIndicators';
|
||||
export { default as VideoMutedIndicator } from './VideoMutedIndicator';
|
||||
export { default as Thumbnail } from './Thumbnail';
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
// @flow
|
||||
|
||||
import { JitsiParticipantConnectionStatus } from '../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../base/media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCountWithFake,
|
||||
getPinnedParticipant
|
||||
} from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import {
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
isLocalTrackMuted,
|
||||
isRemoteTrackMuted
|
||||
} from '../base/tracks';
|
||||
|
||||
import { TILE_ASPECT_RATIO } from './constants';
|
||||
|
||||
|
@ -63,6 +73,39 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|
|||
|| state['features/base/config'].disable1On1Mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a playable video stream available for the user associated with the passed ID.
|
||||
*
|
||||
* @param {Object | Function} stateful - The Object or Function that can be
|
||||
* resolved to a Redux state object with the toState function.
|
||||
* @param {string} id - The id of the participant.
|
||||
* @returns {boolean} <tt>true</tt> if there is a playable video stream available
|
||||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
export function isVideoPlayable(stateful: Object | Function, id: String) {
|
||||
const state = toState(stateful);
|
||||
const tracks = state['features/base/tracks'];
|
||||
const participant = id ? getParticipantById(state, id) : getLocalParticipant(state);
|
||||
let isVideoMuted = true;
|
||||
const isLocal = participant?.local ?? true;
|
||||
const { connectionStatus } = participant || {};
|
||||
const videoTrack
|
||||
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
const isAudioOnly = Boolean(state['features/base/audio-only'].enabled);
|
||||
let isPlayable = false;
|
||||
|
||||
if (participant?.local) {
|
||||
isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
|
||||
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly;
|
||||
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
|
||||
isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
|
||||
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE;
|
||||
}
|
||||
|
||||
return isPlayable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the size for thumbnails when in horizontal view layout.
|
||||
*
|
||||
|
|
|
@ -77,11 +77,6 @@ type Props = {
|
|||
*/
|
||||
initialVolumeValue: number,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the popover has been displayed.
|
||||
*/
|
||||
onMenuDisplay: Function,
|
||||
|
||||
/**
|
||||
* Callback to invoke when changing the level of the participant's
|
||||
* audio element.
|
||||
|
@ -111,19 +106,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
*/
|
||||
_rootElement = null;
|
||||
|
||||
/**
|
||||
* Initializes a new {#@code RemoteVideoMenuTriggerButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onShowRemoteMenu = this._onShowRemoteMenu.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -140,7 +122,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
return (
|
||||
<Popover
|
||||
content = { content }
|
||||
onPopoverOpen = { this._onShowRemoteMenu }
|
||||
position = { this.props._menuPosition }>
|
||||
<span
|
||||
className = 'popover-trigger remote-video-menu-trigger'>
|
||||
|
@ -153,18 +134,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
_onShowRemoteMenu: () => void;
|
||||
|
||||
/**
|
||||
* Opens the {@code RemoteVideoMenu}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShowRemoteMenu() {
|
||||
this.props.onMenuDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with
|
||||
* the remote participant.
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
|
||||
import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../base/conference';
|
||||
import { CONFERENCE_WILL_LEAVE } from '../base/conference';
|
||||
import { MEDIA_TYPE } from '../base/media/index.js';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
PARTICIPANT_JOINED,
|
||||
|
@ -33,10 +34,6 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED:
|
||||
VideoLayout.mucJoined();
|
||||
break;
|
||||
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
VideoLayout.reset();
|
||||
break;
|
||||
|
@ -77,13 +74,13 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
|
||||
case TRACK_ADDED:
|
||||
if (!action.track.local) {
|
||||
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
||||
}
|
||||
|
||||
break;
|
||||
case TRACK_REMOVED:
|
||||
if (!action.track.local) {
|
||||
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue