ref(Thumbnail): Create React component.
This commit is contained in:
parent
d8dd644f38
commit
51e381a0b1
|
@ -2014,7 +2014,6 @@ export default {
|
||||||
formattedDisplayName
|
formattedDisplayName
|
||||||
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
|
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
|
||||||
});
|
});
|
||||||
APP.UI.changeDisplayName(id, formattedDisplayName);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
room.on(
|
room.on(
|
||||||
|
@ -2053,10 +2052,7 @@ export default {
|
||||||
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
|
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
|
||||||
|
|
||||||
room.on(JitsiConferenceEvents.KICKED, participant => {
|
room.on(JitsiConferenceEvents.KICKED, participant => {
|
||||||
APP.UI.hideStats();
|
|
||||||
APP.store.dispatch(kickedOut(room, participant));
|
APP.store.dispatch(kickedOut(room, participant));
|
||||||
|
|
||||||
// FIXME close
|
|
||||||
});
|
});
|
||||||
|
|
||||||
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
|
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
|
||||||
|
@ -2389,11 +2385,6 @@ export default {
|
||||||
APP.keyboardshortcut.init();
|
APP.keyboardshortcut.init();
|
||||||
|
|
||||||
APP.store.dispatch(conferenceJoined(room));
|
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({
|
APP.store.dispatch(updateSettings({
|
||||||
displayName: formattedNickname
|
displayName: formattedNickname
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (room) {
|
|
||||||
APP.UI.changeDisplayName(id, formattedNickname);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,7 +7,6 @@ import EventEmitter from 'events';
|
||||||
import Logger from 'jitsi-meet-logger';
|
import Logger from 'jitsi-meet-logger';
|
||||||
|
|
||||||
import { isMobileBrowser } from '../../react/features/base/environment/utils';
|
import { isMobileBrowser } from '../../react/features/base/environment/utils';
|
||||||
import { getLocalParticipant } from '../../react/features/base/participants';
|
|
||||||
import { toggleChat } from '../../react/features/chat';
|
import { toggleChat } from '../../react/features/chat';
|
||||||
import { setDocumentUrl } from '../../react/features/etherpad';
|
import { setDocumentUrl } from '../../react/features/etherpad';
|
||||||
import { setFilmstripVisible } from '../../react/features/filmstrip';
|
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.
|
* Initialize conference UI.
|
||||||
*/
|
*/
|
||||||
UI.initConference = function() {
|
UI.initConference = function() {
|
||||||
const { getState } = APP.store;
|
|
||||||
const { id, name } = getLocalParticipant(getState);
|
|
||||||
|
|
||||||
UI.showToolbar();
|
UI.showToolbar();
|
||||||
|
|
||||||
const displayName = config.displayJids ? id : name;
|
|
||||||
|
|
||||||
if (displayName) {
|
|
||||||
UI.changeDisplayName('localVideoContainer', displayName);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -238,19 +219,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
|
||||||
* @param {JitsiParticipant} user
|
* @param {JitsiParticipant} user
|
||||||
*/
|
*/
|
||||||
UI.addUser = function(user) {
|
UI.addUser = function(user) {
|
||||||
const id = user.getId();
|
|
||||||
const displayName = user.getDisplayName();
|
|
||||||
const status = user.getStatus();
|
const status = user.getStatus();
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
// FIXME: move updateUserStatus in participantPresenceChanged action
|
// FIXME: move updateUserStatus in participantPresenceChanged action
|
||||||
UI.updateUserStatus(user, status);
|
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);
|
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide connection quality statistics from UI.
|
|
||||||
*/
|
|
||||||
UI.hideStats = function() {
|
|
||||||
VideoLayout.hideStats();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UI.notifyTokenAuthFailed = function() {
|
UI.notifyTokenAuthFailed = function() {
|
||||||
messageHandler.showError({
|
messageHandler.showError({
|
||||||
descriptionKey: 'dialog.tokenAuthFailed',
|
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';
|
import SmallVideo from '../videolayout/SmallVideo';
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
const logger = Logger.getLogger(__filename);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -13,28 +18,21 @@ export default class SharedVideoThumb extends SmallVideo {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} participant
|
* @param {*} participant
|
||||||
* @param {*} videoType
|
|
||||||
* @param {*} VideoLayout
|
|
||||||
*/
|
*/
|
||||||
constructor(participant, videoType, VideoLayout) {
|
constructor(participant) {
|
||||||
super(VideoLayout);
|
super();
|
||||||
this.id = participant.id;
|
this.id = participant.id;
|
||||||
this.isLocal = false;
|
this.isLocal = false;
|
||||||
this.url = participant.id;
|
this.url = participant.id;
|
||||||
this.videoSpanId = 'sharedVideoContainer';
|
this.videoSpanId = 'sharedVideoContainer';
|
||||||
this.container = this.createContainer(this.videoSpanId);
|
this.container = this.createContainer(this.videoSpanId);
|
||||||
this.$container = $(this.container);
|
this.$container = $(this.container);
|
||||||
|
this.renderThumbnail();
|
||||||
this._setThumbnailSize();
|
this._setThumbnailSize();
|
||||||
this.bindHoverHandler();
|
this.bindHoverHandler();
|
||||||
this.updateDisplayName();
|
|
||||||
this.container.onclick = this._onContainerClick;
|
this.container.onclick = this._onContainerClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
initializeAvatar() {} // eslint-disable-line no-empty-function
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} spanId
|
* @param {*} spanId
|
||||||
|
@ -45,18 +43,6 @@ export default class SharedVideoThumb extends SmallVideo {
|
||||||
container.id = spanId;
|
container.id = spanId;
|
||||||
container.className = 'videocontainer';
|
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
|
const remoteVideosContainer
|
||||||
= document.getElementById('filmstripRemoteVideosContainer');
|
= document.getElementById('filmstripRemoteVideosContainer');
|
||||||
const localVideoContainer
|
const localVideoContainer
|
||||||
|
@ -68,21 +54,14 @@ export default class SharedVideoThumb extends SmallVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers re-rendering of the display name using current instance state.
|
* Renders the thumbnail.
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
updateDisplayName() {
|
renderThumbnail(isHovered = false) {
|
||||||
if (!this.container) {
|
ReactDOM.render(
|
||||||
logger.warn(`Unable to set displayName - ${this.videoSpanId
|
<Provider store = { APP.store }>
|
||||||
} does not exist`);
|
<I18nextProvider i18n = { i18next }>
|
||||||
|
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||||
return;
|
</I18nextProvider>
|
||||||
}
|
</Provider>, this.container);
|
||||||
|
|
||||||
this._renderDisplayName({
|
|
||||||
elementID: `${this.videoSpanId}_name`,
|
|
||||||
participantID: this.id
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ const Filmstrip = {
|
||||||
*/
|
*/
|
||||||
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
|
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
|
||||||
const thumbs = this._getThumbs(!forceUpdate);
|
const thumbs = this._getThumbs(!forceUpdate);
|
||||||
const avatarSize = height / 2;
|
|
||||||
|
|
||||||
if (thumbs.localThumb) {
|
if (thumbs.localThumb) {
|
||||||
thumbs.localThumb.css({
|
thumbs.localThumb.css({
|
||||||
|
@ -58,11 +57,6 @@ const Filmstrip = {
|
||||||
width: `${width}px`
|
width: `${width}px`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.avatar-container').css({
|
|
||||||
height: `${avatarSize}px`,
|
|
||||||
width: `${avatarSize}px`
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,7 +71,6 @@ const Filmstrip = {
|
||||||
|
|
||||||
if (thumbs.localThumb) {
|
if (thumbs.localThumb) {
|
||||||
const { height, width } = local;
|
const { height, width } = local;
|
||||||
const avatarSize = height / 2;
|
|
||||||
|
|
||||||
thumbs.localThumb.css({
|
thumbs.localThumb.css({
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
@ -85,15 +78,10 @@ const Filmstrip = {
|
||||||
'min-width': `${width}px`,
|
'min-width': `${width}px`,
|
||||||
width: `${width}px`
|
width: `${width}px`
|
||||||
});
|
});
|
||||||
$('#localVideoContainer > .avatar-container').css({
|
|
||||||
height: `${avatarSize}px`,
|
|
||||||
width: `${avatarSize}px`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbs.remoteThumbs) {
|
if (thumbs.remoteThumbs) {
|
||||||
const { height, width } = remote;
|
const { height, width } = remote;
|
||||||
const avatarSize = height / 2;
|
|
||||||
|
|
||||||
thumbs.remoteThumbs.css({
|
thumbs.remoteThumbs.css({
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
@ -101,10 +89,6 @@ const Filmstrip = {
|
||||||
'min-width': `${width}px`,
|
'min-width': `${width}px`,
|
||||||
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-width': '',
|
||||||
'min-height': ''
|
'min-height': ''
|
||||||
});
|
});
|
||||||
$('#localVideoContainer > .avatar-container').css({
|
|
||||||
height: '50%',
|
|
||||||
width: `${heightToWidthPercent / 2}%`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbs.remoteThumbs) {
|
if (thumbs.remoteThumbs) {
|
||||||
|
@ -142,10 +122,6 @@ const Filmstrip = {
|
||||||
'min-width': '',
|
'min-width': '',
|
||||||
'min-height': ''
|
'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 */
|
/* eslint-disable no-unused-vars */
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { i18next } from '../../../react/features/base/i18n';
|
||||||
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
||||||
import { VideoTrack } from '../../../react/features/base/media';
|
import { VideoTrack } from '../../../react/features/base/media';
|
||||||
import { updateSettings } from '../../../react/features/base/settings';
|
import { updateSettings } from '../../../react/features/base/settings';
|
||||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
||||||
|
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
|
||||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
|
|
||||||
import SmallVideo from './SmallVideo';
|
import SmallVideo from './SmallVideo';
|
||||||
|
|
||||||
const logger = Logger.getLogger(__filename);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default class LocalVideo extends SmallVideo {
|
export default class LocalVideo extends SmallVideo {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} VideoLayout
|
|
||||||
* @param {*} emitter
|
* @param {*} emitter
|
||||||
* @param {*} streamEndedCallback
|
* @param {*} streamEndedCallback
|
||||||
*/
|
*/
|
||||||
constructor(VideoLayout, emitter, streamEndedCallback) {
|
constructor(emitter, streamEndedCallback) {
|
||||||
super(VideoLayout);
|
super();
|
||||||
this.videoSpanId = 'localVideoContainer';
|
this.videoSpanId = 'localVideoContainer';
|
||||||
this.streamEndedCallback = streamEndedCallback;
|
this.streamEndedCallback = streamEndedCallback;
|
||||||
this.container = this.createContainer();
|
this.container = this.createContainer();
|
||||||
|
@ -37,6 +36,7 @@ export default class LocalVideo extends SmallVideo {
|
||||||
this.isLocal = true;
|
this.isLocal = true;
|
||||||
this._setThumbnailSize();
|
this._setThumbnailSize();
|
||||||
this.updateDOMLocation();
|
this.updateDOMLocation();
|
||||||
|
this.renderThumbnail();
|
||||||
|
|
||||||
this.localVideoId = null;
|
this.localVideoId = null;
|
||||||
this.bindHoverHandler();
|
this.bindHoverHandler();
|
||||||
|
@ -44,7 +44,6 @@ export default class LocalVideo extends SmallVideo {
|
||||||
this._buildContextMenu();
|
this._buildContextMenu();
|
||||||
}
|
}
|
||||||
this.emitter = emitter;
|
this.emitter = emitter;
|
||||||
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
|
|
||||||
|
|
||||||
Object.defineProperty(this, 'id', {
|
Object.defineProperty(this, 'id', {
|
||||||
get() {
|
get() {
|
||||||
|
@ -53,18 +52,6 @@ export default class LocalVideo extends SmallVideo {
|
||||||
});
|
});
|
||||||
this.initBrowserSpecificProperties();
|
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;
|
this.container.onclick = this._onContainerClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,38 +64,19 @@ export default class LocalVideo extends SmallVideo {
|
||||||
containerSpan.classList.add('videocontainer');
|
containerSpan.classList.add('videocontainer');
|
||||||
containerSpan.id = this.videoSpanId;
|
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;
|
return containerSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers re-rendering of the display name using current instance state.
|
* Renders the thumbnail.
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
updateDisplayName() {
|
renderThumbnail(isHovered = false) {
|
||||||
if (!this.container) {
|
ReactDOM.render(
|
||||||
logger.warn(
|
<Provider store = { APP.store }>
|
||||||
`Unable to set displayName - ${this.videoSpanId
|
<I18nextProvider i18n = { i18next }>
|
||||||
} does not exist`);
|
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||||
|
</I18nextProvider>
|
||||||
return;
|
</Provider>, this.container);
|
||||||
}
|
|
||||||
|
|
||||||
this._renderDisplayName({
|
|
||||||
allowEditing: !config.disableProfile,
|
|
||||||
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
|
||||||
elementID: 'localDisplayName',
|
|
||||||
participantID: this.id
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,9 +84,7 @@ export default class LocalVideo extends SmallVideo {
|
||||||
* @param {*} stream
|
* @param {*} stream
|
||||||
*/
|
*/
|
||||||
changeVideo(stream) {
|
changeVideo(stream) {
|
||||||
this.videoStream = stream;
|
|
||||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||||
this._updateVideoElement();
|
|
||||||
|
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line eqeqeq
|
||||||
const isVideo = stream.videoType != 'desktop';
|
const isVideo = stream.videoType != 'desktop';
|
||||||
|
@ -128,17 +94,6 @@ export default class LocalVideo extends SmallVideo {
|
||||||
this.setFlipX(isVideo ? settings.localFlipX : false);
|
this.setFlipX(isVideo ? settings.localFlipX : false);
|
||||||
|
|
||||||
const endedHandler = () => {
|
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();
|
this._notifyOfStreamEnded();
|
||||||
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||||
};
|
};
|
||||||
|
@ -254,35 +209,5 @@ export default class LocalVideo extends SmallVideo {
|
||||||
: document.getElementById('filmstripLocalVideoThumbnail');
|
: document.getElementById('filmstripLocalVideoThumbnail');
|
||||||
|
|
||||||
appendTarget && appendTarget.appendChild(this.container);
|
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 */
|
/* eslint-disable no-unused-vars */
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||||
|
@ -15,6 +15,7 @@ import {
|
||||||
import { getParticipantById } from '../../../react/features/base/participants';
|
import { getParticipantById } from '../../../react/features/base/participants';
|
||||||
import { isTestModeEnabled } from '../../../react/features/base/testing';
|
import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||||
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
|
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
|
||||||
|
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
|
||||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||||
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
|
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
|
||||||
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
|
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
|
||||||
|
@ -44,16 +45,6 @@ function createContainer(spanId) {
|
||||||
container.id = spanId;
|
container.id = spanId;
|
||||||
container.className = 'videocontainer';
|
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
|
const remoteVideosContainer
|
||||||
= document.getElementById('filmstripRemoteVideosContainer');
|
= document.getElementById('filmstripRemoteVideosContainer');
|
||||||
const localVideoContainer
|
const localVideoContainer
|
||||||
|
@ -72,21 +63,16 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
* Creates new instance of the <tt>RemoteVideo</tt>.
|
* Creates new instance of the <tt>RemoteVideo</tt>.
|
||||||
* @param user {JitsiParticipant} the user for whom remote video instance will
|
* @param user {JitsiParticipant} the user for whom remote video instance will
|
||||||
* be created.
|
* be created.
|
||||||
* @param {VideoLayout} VideoLayout the video layout instance.
|
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor(user, VideoLayout) {
|
constructor(user) {
|
||||||
super(VideoLayout);
|
super();
|
||||||
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.id = user.getId();
|
this.id = user.getId();
|
||||||
this.videoSpanId = `participant_${this.id}`;
|
this.videoSpanId = `participant_${this.id}`;
|
||||||
|
|
||||||
this._audioStreamElement = null;
|
|
||||||
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
|
|
||||||
this.addRemoteVideoContainer();
|
this.addRemoteVideoContainer();
|
||||||
this.updateIndicators();
|
|
||||||
this.updateDisplayName();
|
|
||||||
this.bindHoverHandler();
|
this.bindHoverHandler();
|
||||||
this.flipX = false;
|
this.flipX = false;
|
||||||
this.isLocal = false;
|
this.isLocal = false;
|
||||||
|
@ -100,11 +86,6 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
*/
|
*/
|
||||||
this._canPlayEventReceived = false;
|
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;
|
this.container.onclick = this._onContainerClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,76 +95,23 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
addRemoteVideoContainer() {
|
addRemoteVideoContainer() {
|
||||||
this.container = createContainer(this.videoSpanId);
|
this.container = createContainer(this.videoSpanId);
|
||||||
this.$container = $(this.container);
|
this.$container = $(this.container);
|
||||||
this.initializeAvatar();
|
this.renderThumbnail();
|
||||||
this._setThumbnailSize();
|
this._setThumbnailSize();
|
||||||
this.initBrowserSpecificProperties();
|
this.initBrowserSpecificProperties();
|
||||||
this.updateRemoteVideoMenu();
|
|
||||||
this.updateStatusBar();
|
|
||||||
this.addAudioLevelIndicator();
|
|
||||||
this.addPresenceLabel();
|
|
||||||
|
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the popup menu content.
|
* Renders the thumbnail.
|
||||||
*
|
|
||||||
* @returns {Element|*} the constructed element, containing popup menu items
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_generatePopupContent() {
|
renderThumbnail(isHovered = false) {
|
||||||
const remoteVideoMenuContainer
|
|
||||||
= this.container.querySelector('.remotevideomenu');
|
|
||||||
|
|
||||||
if (!remoteVideoMenuContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
|
|
||||||
|
|
||||||
// hide volume when in silent mode
|
|
||||||
const onVolumeChange
|
|
||||||
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store = { APP.store }>
|
<Provider store = { APP.store }>
|
||||||
<I18nextProvider i18n = { i18next }>
|
<I18nextProvider i18n = { i18next }>
|
||||||
<AtlasKitThemeProvider mode = 'dark'>
|
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||||
<RemoteVideoMenuTriggerButton
|
|
||||||
initialVolumeValue = { initialVolumeValue }
|
|
||||||
onMenuDisplay
|
|
||||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
|
||||||
onVolumeChange = { onVolumeChange }
|
|
||||||
participantID = { this.id } />
|
|
||||||
</AtlasKitThemeProvider>
|
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>,
|
</Provider>, this.container);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,7 +127,7 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isVideo = stream.isVideoTrack();
|
const isVideo = stream.isVideoTrack();
|
||||||
const elementID = SmallVideo.getStreamElementID(stream);
|
const elementID = `remoteVideo_${stream.getId()}`;
|
||||||
const select = $(`#${elementID}`);
|
const select = $(`#${elementID}`);
|
||||||
|
|
||||||
select.remove();
|
select.remove();
|
||||||
|
@ -207,11 +135,7 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
this._canPlayEventReceived = false;
|
this._canPlayEventReceived = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
|
logger.info(`Video removed ${this.id}`, select);
|
||||||
|
|
||||||
if (stream === this.videoStream) {
|
|
||||||
this.videoStream = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateView();
|
this.updateView();
|
||||||
}
|
}
|
||||||
|
@ -223,14 +147,7 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
isVideoPlayable() {
|
isVideoPlayable() {
|
||||||
const participant = getParticipantById(APP.store.getState(), this.id);
|
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
|
||||||
const { connectionStatus } = participant || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
super.isVideoPlayable()
|
|
||||||
&& this._canPlayEventReceived
|
|
||||||
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -245,9 +162,8 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
* Removes RemoteVideo from the page.
|
* Removes RemoteVideo from the page.
|
||||||
*/
|
*/
|
||||||
remove() {
|
remove() {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
super.remove();
|
super.remove();
|
||||||
this.removePresenceLabel();
|
|
||||||
this.removeRemoteVideoMenu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -295,19 +211,16 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
|
|
||||||
const isVideo = stream.isVideoTrack();
|
const isVideo = stream.isVideoTrack();
|
||||||
|
|
||||||
if (isVideo) {
|
|
||||||
this.videoStream = stream;
|
|
||||||
} else {
|
|
||||||
this.audioStream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stream.getOriginalStream()) {
|
if (!stream.getOriginalStream()) {
|
||||||
logger.debug('Remote video stream has no original stream');
|
logger.debug('Remote video stream has no original stream');
|
||||||
|
|
||||||
return;
|
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
|
// Put new stream element always in front
|
||||||
streamElement = UIUtils.prependChild(this.container, streamElement);
|
streamElement = UIUtils.prependChild(this.container, streamElement);
|
||||||
|
@ -315,14 +228,7 @@ export default class RemoteVideo extends SmallVideo {
|
||||||
this.waitForPlayback(streamElement, stream);
|
this.waitForPlayback(streamElement, stream);
|
||||||
stream.attach(streamElement);
|
stream.attach(streamElement);
|
||||||
|
|
||||||
if (!isVideo) {
|
if (isVideo && isTestModeEnabled(APP.store.getState())) {
|
||||||
this._audioStreamElement = streamElement;
|
|
||||||
|
|
||||||
// If the remote video menu was created before the audio stream was
|
|
||||||
// attached we need to update the menu in order to show the volume
|
|
||||||
// slider.
|
|
||||||
this.updateRemoteVideoMenu();
|
|
||||||
} else if (isTestModeEnabled(APP.store.getState())) {
|
|
||||||
|
|
||||||
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
|
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 */
|
/* eslint-disable no-unused-vars */
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||||
|
@ -21,6 +21,7 @@ import {
|
||||||
pinParticipant
|
pinParticipant
|
||||||
} from '../../../react/features/base/participants';
|
} from '../../../react/features/base/participants';
|
||||||
import {
|
import {
|
||||||
|
getLocalVideoTrack,
|
||||||
getTrackByMediaTypeAndParticipant,
|
getTrackByMediaTypeAndParticipant,
|
||||||
isLocalTrackMuted,
|
isLocalTrackMuted,
|
||||||
isRemoteTrackMuted
|
isRemoteTrackMuted
|
||||||
|
@ -29,6 +30,7 @@ import { ConnectionIndicator } from '../../../react/features/connection-indicato
|
||||||
import { DisplayName } from '../../../react/features/display-name';
|
import { DisplayName } from '../../../react/features/display-name';
|
||||||
import {
|
import {
|
||||||
DominantSpeakerIndicator,
|
DominantSpeakerIndicator,
|
||||||
|
isVideoPlayable,
|
||||||
RaisedHandIndicator,
|
RaisedHandIndicator,
|
||||||
StatusIndicators
|
StatusIndicators
|
||||||
} from '../../../react/features/filmstrip';
|
} from '../../../react/features/filmstrip';
|
||||||
|
@ -89,37 +91,10 @@ export default class SmallVideo {
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
*/
|
*/
|
||||||
constructor(VideoLayout) {
|
constructor() {
|
||||||
this.videoStream = null;
|
|
||||||
this.audioStream = null;
|
|
||||||
this.VideoLayout = VideoLayout;
|
|
||||||
this.videoIsHovered = false;
|
this.videoIsHovered = false;
|
||||||
this.videoType = undefined;
|
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.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
this.updateView = this.updateView.bind(this);
|
this.updateView = this.updateView.bind(this);
|
||||||
|
|
||||||
|
@ -145,33 +120,6 @@ export default class SmallVideo {
|
||||||
return this.$container.is(':visible');
|
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.
|
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
|
||||||
*/
|
*/
|
||||||
|
@ -180,103 +128,22 @@ export default class SmallVideo {
|
||||||
this.$container.hover(
|
this.$container.hover(
|
||||||
() => {
|
() => {
|
||||||
this.videoIsHovered = true;
|
this.videoIsHovered = true;
|
||||||
|
this.renderThumbnail(true);
|
||||||
this.updateView();
|
this.updateView();
|
||||||
this.updateIndicators();
|
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.videoIsHovered = false;
|
this.videoIsHovered = false;
|
||||||
|
this.renderThumbnail(false);
|
||||||
this.updateView();
|
this.updateView();
|
||||||
this.updateIndicators();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unmounts the ConnectionIndicator component.
|
* Renders the thumbnail.
|
||||||
|
|
||||||
* @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}
|
|
||||||
*/
|
*/
|
||||||
updateStatusBar() {
|
renderThumbnail() {
|
||||||
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
|
// Should be implemented by in subclasses.
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -293,62 +160,6 @@ export default class SmallVideo {
|
||||||
return $($(this.container).find('video')[0]);
|
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
|
* Enables / disables the css responsible for focusing/pinning a video
|
||||||
* thumbnail.
|
* thumbnail.
|
||||||
|
@ -392,18 +203,7 @@ export default class SmallVideo {
|
||||||
* or <tt>false</tt> otherwise.
|
* or <tt>false</tt> otherwise.
|
||||||
*/
|
*/
|
||||||
isVideoPlayable() {
|
isVideoPlayable() {
|
||||||
const state = APP.store.getState();
|
return isVideoPlayable(APP.store.getState(), this.id);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -436,13 +236,15 @@ export default class SmallVideo {
|
||||||
let isScreenSharing = false;
|
let isScreenSharing = false;
|
||||||
let connectionStatus;
|
let connectionStatus;
|
||||||
const state = APP.store.getState();
|
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) {
|
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
|
||||||
const tracks = state['features/base/tracks'];
|
isScreenSharing = typeof track !== 'undefined' && videoTrack?.videoType === 'desktop';
|
||||||
const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, this.id);
|
|
||||||
|
|
||||||
isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop';
|
|
||||||
connectionStatus = participant.connectionStatus;
|
connectionStatus = participant.connectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,9 +257,9 @@ export default class SmallVideo {
|
||||||
hasVideo: Boolean(this.selectVideoElement().length),
|
hasVideo: Boolean(this.selectVideoElement().length),
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
canPlayEventReceived: this._canPlayEventReceived,
|
canPlayEventReceived: this._canPlayEventReceived,
|
||||||
videoStream: Boolean(this.videoStream),
|
videoStream: Boolean(videoTrack),
|
||||||
isScreenSharing,
|
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.
|
* Shows or hides the dominant speaker indicator.
|
||||||
* @param show whether to show or hide.
|
* @param show whether to show or hide.
|
||||||
|
@ -580,30 +345,8 @@ export default class SmallVideo {
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._showDominantSpeaker === show) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._showDominantSpeaker = show;
|
this.$container.toggleClass('active-speaker', 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -634,19 +377,7 @@ export default class SmallVideo {
|
||||||
*/
|
*/
|
||||||
remove() {
|
remove() {
|
||||||
logger.log('Remove thumbnail', this.id);
|
logger.log('Remove thumbnail', this.id);
|
||||||
this.removeAudioLevelIndicator();
|
this._unmountThumbnail();
|
||||||
|
|
||||||
const toolbarContainer
|
|
||||||
= this.container.querySelector('.videocontainer__toolbar');
|
|
||||||
|
|
||||||
if (toolbarContainer) {
|
|
||||||
ReactDOM.unmountComponentAtNode(toolbarContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeConnectionIndicator();
|
|
||||||
this.removeDisplayName();
|
|
||||||
this.removeAvatar();
|
|
||||||
this._unmountIndicators();
|
|
||||||
|
|
||||||
// Remove whole container
|
// Remove whole container
|
||||||
if (this.container.parentNode) {
|
if (this.container.parentNode) {
|
||||||
|
@ -661,76 +392,9 @@ export default class SmallVideo {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
rerender() {
|
rerender() {
|
||||||
this.updateIndicators();
|
|
||||||
this.updateStatusBar();
|
|
||||||
this.updateView();
|
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
|
* Callback invoked when the thumbnail is clicked and potentially trigger
|
||||||
* pinning of the participant.
|
* pinning of the participant.
|
||||||
|
@ -788,18 +452,10 @@ export default class SmallVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the React element responsible for showing connection status, dominant
|
* Unmounts the thumbnail.
|
||||||
* speaker, and raised hand icons.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
_unmountIndicators() {
|
_unmountThumbnail() {
|
||||||
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
|
|
||||||
if (indicatorToolbar) {
|
|
||||||
ReactDOM.unmountComponentAtNode(indicatorToolbar);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -813,10 +469,6 @@ export default class SmallVideo {
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||||
this.$container.css('padding-top', `${heightToWidthPercent}%`);
|
this.$container.css('padding-top', `${heightToWidthPercent}%`);
|
||||||
this.$avatar().css({
|
|
||||||
height: '50%',
|
|
||||||
width: `${heightToWidthPercent / 2}%`
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||||
|
@ -826,7 +478,6 @@ export default class SmallVideo {
|
||||||
|
|
||||||
if (typeof size !== 'undefined') {
|
if (typeof size !== 'undefined') {
|
||||||
const { height, width } = size;
|
const { height, width } = size;
|
||||||
const avatarSize = height / 2;
|
|
||||||
|
|
||||||
this.$container.css({
|
this.$container.css({
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
@ -834,10 +485,6 @@ export default class SmallVideo {
|
||||||
'min-width': `${width}px`,
|
'min-width': `${width}px`,
|
||||||
width: `${width}px`
|
width: `${width}px`
|
||||||
});
|
});
|
||||||
this.$avatar().css({
|
|
||||||
height: `${avatarSize}px`,
|
|
||||||
width: `${avatarSize}px`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -847,7 +494,6 @@ export default class SmallVideo {
|
||||||
|
|
||||||
if (typeof thumbnailSize !== 'undefined') {
|
if (typeof thumbnailSize !== 'undefined') {
|
||||||
const { height, width } = thumbnailSize;
|
const { height, width } = thumbnailSize;
|
||||||
const avatarSize = height / 2;
|
|
||||||
|
|
||||||
this.$container.css({
|
this.$container.css({
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
@ -855,10 +501,6 @@ export default class SmallVideo {
|
||||||
'min-width': `${width}px`,
|
'min-width': `${width}px`,
|
||||||
width: `${width}px`
|
width: `${width}px`
|
||||||
});
|
});
|
||||||
this.$avatar().css({
|
|
||||||
height: `${avatarSize}px`,
|
|
||||||
width: `${avatarSize}px`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,6 @@ const VideoLayout = {
|
||||||
eventEmitter = emitter;
|
eventEmitter = emitter;
|
||||||
|
|
||||||
localVideoThumbnail = new LocalVideo(
|
localVideoThumbnail = new LocalVideo(
|
||||||
VideoLayout,
|
|
||||||
emitter,
|
emitter,
|
||||||
this._updateLargeVideoIfDisplayed.bind(this));
|
this._updateLargeVideoIfDisplayed.bind(this));
|
||||||
|
|
||||||
|
@ -116,12 +115,6 @@ const VideoLayout = {
|
||||||
* @param lvl the new audio level to update to
|
* @param lvl the new audio level to update to
|
||||||
*/
|
*/
|
||||||
setAudioLevel(id, lvl) {
|
setAudioLevel(id, lvl) {
|
||||||
const smallVideo = this.getSmallVideo(id);
|
|
||||||
|
|
||||||
if (smallVideo) {
|
|
||||||
smallVideo.updateAudioLevelIndicator(lvl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (largeVideo && id === largeVideo.id) {
|
if (largeVideo && id === largeVideo.id) {
|
||||||
largeVideo.updateLargeVideoAudioLevel(lvl);
|
largeVideo.updateLargeVideoAudioLevel(lvl);
|
||||||
}
|
}
|
||||||
|
@ -137,19 +130,6 @@ const VideoLayout = {
|
||||||
this._updateLargeVideoIfDisplayed(localId);
|
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.
|
* Shows/hides local video.
|
||||||
* @param {boolean} true to make the local video visible, false - otherwise
|
* @param {boolean} true to make the local video visible, false - otherwise
|
||||||
|
@ -172,11 +152,8 @@ const VideoLayout = {
|
||||||
|
|
||||||
remoteVideo.addRemoteStreamElement(stream);
|
remoteVideo.addRemoteStreamElement(stream);
|
||||||
|
|
||||||
// Make sure track's muted state is reflected
|
this.onVideoMute(id);
|
||||||
if (stream.getType() !== 'audio') {
|
remoteVideo.updateView();
|
||||||
this.onVideoMute(id);
|
|
||||||
remoteVideo.updateView();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRemoteStreamRemoved(stream) {
|
onRemoteStreamRemoved(stream) {
|
||||||
|
@ -184,13 +161,12 @@ const VideoLayout = {
|
||||||
const remoteVideo = remoteVideos[id];
|
const remoteVideo = remoteVideos[id];
|
||||||
|
|
||||||
// Remote stream may be removed after participant left the conference.
|
// Remote stream may be removed after participant left the conference.
|
||||||
|
|
||||||
if (remoteVideo) {
|
if (remoteVideo) {
|
||||||
remoteVideo.removeRemoteStreamElement(stream);
|
remoteVideo.removeRemoteStreamElement(stream);
|
||||||
remoteVideo.updateView();
|
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.
|
* If participant has no tracks will make the UI display muted status.
|
||||||
* @param {string} participantId
|
* @param {string} participantId
|
||||||
* @param {string} mediaType 'audio' or 'video'
|
|
||||||
*/
|
*/
|
||||||
updateMutedForNoTracks(participantId, mediaType) {
|
updateVideoMutedForNoTracks(participantId) {
|
||||||
const participant = APP.conference.getParticipantById(participantId);
|
const participant = APP.conference.getParticipantById(participantId);
|
||||||
|
|
||||||
if (participant && !participant.getTracksByMediaType(mediaType).length) {
|
if (participant && !participant.getTracksByMediaType('video').length) {
|
||||||
if (mediaType === 'audio') {
|
APP.UI.setVideoMuted(participantId);
|
||||||
APP.UI.setAudioMuted(participantId, true);
|
|
||||||
} else if (mediaType === 'video') {
|
|
||||||
APP.UI.setVideoMuted(participantId);
|
|
||||||
} else {
|
|
||||||
logger.error(`Unsupported media type: ${mediaType}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -279,10 +248,7 @@ const VideoLayout = {
|
||||||
if (!participant || participant.local) {
|
if (!participant || participant.local) {
|
||||||
return;
|
return;
|
||||||
} else if (participant.isFakeParticipant) {
|
} else if (participant.isFakeParticipant) {
|
||||||
const sharedVideoThumb = new SharedVideoThumb(
|
const sharedVideoThumb = new SharedVideoThumb(participant);
|
||||||
participant,
|
|
||||||
SHARED_VIDEO_CONTAINER_TYPE,
|
|
||||||
VideoLayout);
|
|
||||||
|
|
||||||
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
|
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
|
||||||
|
|
||||||
|
@ -291,12 +257,10 @@ const VideoLayout = {
|
||||||
|
|
||||||
const id = participant.id;
|
const id = participant.id;
|
||||||
const jitsiParticipant = APP.conference.getParticipantById(id);
|
const jitsiParticipant = APP.conference.getParticipantById(id);
|
||||||
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
|
const remoteVideo = new RemoteVideo(jitsiParticipant);
|
||||||
|
|
||||||
this.addRemoteVideoContainer(id, remoteVideo);
|
this.addRemoteVideoContainer(id, remoteVideo);
|
||||||
|
this.updateVideoMutedForNoTracks(id);
|
||||||
this.updateMutedForNoTracks(id, 'audio');
|
|
||||||
this.updateMutedForNoTracks(id, 'video');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -331,22 +295,6 @@ const VideoLayout = {
|
||||||
this._updateLargeVideoIfDisplayed(id, true);
|
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.
|
* 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) {
|
removeParticipantContainer(id) {
|
||||||
// Unlock large video
|
// Unlock large video
|
||||||
if (this.getPinnedId() === id) {
|
if (this.getPinnedId() === id) {
|
||||||
|
@ -477,15 +411,6 @@ const VideoLayout = {
|
||||||
},
|
},
|
||||||
|
|
||||||
changeUserAvatar(id, avatarUrl) {
|
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)) {
|
if (this.isCurrentlyOnLarge(id)) {
|
||||||
largeVideo.updateAvatar(avatarUrl);
|
largeVideo.updateAvatar(avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,9 +128,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||||
titleKey: 'dialog.sessTerminated'
|
titleKey: 'dialog.sessTerminated'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (typeof APP !== 'undefined') {
|
|
||||||
APP.UI.hideStats();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
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);
|
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 RaisedHandIndicator } from './RaisedHandIndicator';
|
||||||
export { default as StatusIndicators } from './StatusIndicators';
|
export { default as StatusIndicators } from './StatusIndicators';
|
||||||
export { default as VideoMutedIndicator } from './VideoMutedIndicator';
|
export { default as VideoMutedIndicator } from './VideoMutedIndicator';
|
||||||
|
export { default as Thumbnail } from './Thumbnail';
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { JitsiParticipantConnectionStatus } from '../base/lib-jitsi-meet';
|
||||||
|
import { MEDIA_TYPE } from '../base/media';
|
||||||
import {
|
import {
|
||||||
|
getLocalParticipant,
|
||||||
|
getParticipantById,
|
||||||
getParticipantCountWithFake,
|
getParticipantCountWithFake,
|
||||||
getPinnedParticipant
|
getPinnedParticipant
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
import { toState } from '../base/redux';
|
import { toState } from '../base/redux';
|
||||||
|
import {
|
||||||
|
getLocalVideoTrack,
|
||||||
|
getTrackByMediaTypeAndParticipant,
|
||||||
|
isLocalTrackMuted,
|
||||||
|
isRemoteTrackMuted
|
||||||
|
} from '../base/tracks';
|
||||||
|
|
||||||
import { TILE_ASPECT_RATIO } from './constants';
|
import { TILE_ASPECT_RATIO } from './constants';
|
||||||
|
|
||||||
|
@ -63,6 +73,39 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|
||||||
|| state['features/base/config'].disable1On1Mode);
|
|| 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.
|
* Calculates the size for thumbnails when in horizontal view layout.
|
||||||
*
|
*
|
||||||
|
|
|
@ -77,11 +77,6 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
initialVolumeValue: number,
|
initialVolumeValue: number,
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback to invoke when the popover has been displayed.
|
|
||||||
*/
|
|
||||||
onMenuDisplay: Function,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback to invoke when changing the level of the participant's
|
* Callback to invoke when changing the level of the participant's
|
||||||
* audio element.
|
* audio element.
|
||||||
|
@ -111,19 +106,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
_rootElement = null;
|
_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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -140,7 +122,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content = { content }
|
content = { content }
|
||||||
onPopoverOpen = { this._onShowRemoteMenu }
|
|
||||||
position = { this.props._menuPosition }>
|
position = { this.props._menuPosition }>
|
||||||
<span
|
<span
|
||||||
className = 'popover-trigger remote-video-menu-trigger'>
|
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
|
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with
|
||||||
* the remote participant.
|
* the remote participant.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
|
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 {
|
import {
|
||||||
DOMINANT_SPEAKER_CHANGED,
|
DOMINANT_SPEAKER_CHANGED,
|
||||||
PARTICIPANT_JOINED,
|
PARTICIPANT_JOINED,
|
||||||
|
@ -33,10 +34,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CONFERENCE_JOINED:
|
|
||||||
VideoLayout.mucJoined();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CONFERENCE_WILL_LEAVE:
|
case CONFERENCE_WILL_LEAVE:
|
||||||
VideoLayout.reset();
|
VideoLayout.reset();
|
||||||
break;
|
break;
|
||||||
|
@ -77,13 +74,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TRACK_ADDED:
|
case TRACK_ADDED:
|
||||||
if (!action.track.local) {
|
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case TRACK_REMOVED:
|
case TRACK_REMOVED:
|
||||||
if (!action.track.local) {
|
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||||
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
|
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue