ref(Filmstrip): Use Thumbnail component.
This commit is contained in:
parent
e937e99284
commit
f50872285d
1
app.js
1
app.js
|
@ -1,7 +1,6 @@
|
||||||
/* application specific logic */
|
/* application specific logic */
|
||||||
|
|
||||||
import 'jquery';
|
import 'jquery';
|
||||||
import 'jquery-contextmenu';
|
|
||||||
import 'jQuery-Impromptu';
|
import 'jQuery-Impromptu';
|
||||||
|
|
||||||
import 'olm';
|
import 'olm';
|
||||||
|
|
|
@ -1399,9 +1399,6 @@ export default {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.localVideo = newTrack;
|
this.localVideo = newTrack;
|
||||||
this._setSharingScreen(newTrack);
|
this._setSharingScreen(newTrack);
|
||||||
if (newTrack) {
|
|
||||||
APP.UI.addLocalVideoStream(newTrack);
|
|
||||||
}
|
|
||||||
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
||||||
})
|
})
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
|
@ -2408,7 +2405,11 @@ export default {
|
||||||
// There is no guarantee another event will trigger the update
|
// There is no guarantee another event will trigger the update
|
||||||
// immediately and in all situations, for example because a remote
|
// immediately and in all situations, for example because a remote
|
||||||
// participant is having connection trouble so no status changes.
|
// participant is having connection trouble so no status changes.
|
||||||
APP.UI.updateAllVideos();
|
const displayedUserId = APP.UI.getLargeVideoID();
|
||||||
|
|
||||||
|
if (displayedUserId) {
|
||||||
|
APP.UI.updateLargeVideo(displayedUserId, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
APP.UI.addListener(
|
APP.UI.addListener(
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
**/
|
**/
|
||||||
|
|
||||||
.popupmenu {
|
.popupmenu {
|
||||||
min-width: 75px;
|
min-width: 150px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -109,6 +109,6 @@ ul.popupmenu {
|
||||||
margin: -16px -24px;
|
margin: -16px -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
||||||
display:block !important;
|
display:block !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -400,7 +400,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.local-video-menu-trigger,
|
||||||
.remote-video-menu-trigger,
|
.remote-video-menu-trigger,
|
||||||
|
.localvideomenu,
|
||||||
.remotevideomenu
|
.remotevideomenu
|
||||||
{
|
{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -418,6 +420,7 @@
|
||||||
cursor: hand;
|
cursor: hand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.local-video-menu-trigger,
|
||||||
.remote-video-menu-trigger {
|
.remote-video-menu-trigger {
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
0 0 3px $videoThumbnailSelected;
|
0 0 3px $videoThumbnailSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remotevideomenu > .icon-menu {
|
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||||
0 0 3px $videoThumbnailHovered;
|
0 0 3px $videoThumbnailHovered;
|
||||||
|
|
||||||
.remotevideomenu > .icon-menu {
|
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
* specifically the various status icons.
|
* specifically the various status icons.
|
||||||
*/
|
*/
|
||||||
.remotevideomenu,
|
.remotevideomenu,
|
||||||
|
.localvideomenu,
|
||||||
.videocontainer__toptoolbar {
|
.videocontainer__toptoolbar {
|
||||||
z-index: auto;
|
z-index: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
* and tooltips from getting a new location context due to translate3d.
|
* and tooltips from getting a new location context due to translate3d.
|
||||||
*/
|
*/
|
||||||
.connection-indicator,
|
.connection-indicator,
|
||||||
|
.local-video-menu-trigger,
|
||||||
.remote-video-menu-trigger,
|
.remote-video-menu-trigger,
|
||||||
.indicator-icon-container {
|
.indicator-icon-container {
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
|
@ -68,7 +69,9 @@
|
||||||
* Move the remote video menu trigger to the bottom left of the video
|
* Move the remote video menu trigger to the bottom left of the video
|
||||||
* thumbnail.
|
* thumbnail.
|
||||||
*/
|
*/
|
||||||
|
.localvideomenu,
|
||||||
.remotevideomenu,
|
.remotevideomenu,
|
||||||
|
.local-video-menu-trigger,
|
||||||
.remote-video-menu-trigger {
|
.remote-video-menu-trigger {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -76,6 +79,7 @@
|
||||||
right: auto;
|
right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.local-video-menu-trigger,
|
||||||
.remote-video-menu-trigger {
|
.remote-video-menu-trigger {
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
margin-left: $remoteVideoMenuIconMargin;
|
margin-left: $remoteVideoMenuIconMargin;
|
||||||
|
|
|
@ -33,8 +33,8 @@ import {
|
||||||
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
|
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
|
||||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||||
import { getActiveSession } from '../../react/features/recording/functions';
|
import { getActiveSession } from '../../react/features/recording/functions';
|
||||||
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
|
|
||||||
import { toggleTileView } from '../../react/features/video-layout';
|
import { toggleTileView } from '../../react/features/video-layout';
|
||||||
|
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||||
import { setVideoQuality } from '../../react/features/video-quality';
|
import { setVideoQuality } from '../../react/features/video-quality';
|
||||||
import { getJitsiMeetTransport } from '../transport';
|
import { getJitsiMeetTransport } from '../transport';
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,6 @@ UI.start = function() {
|
||||||
// Set the defaults for prompt dialogs.
|
// Set the defaults for prompt dialogs.
|
||||||
$.prompt.setDefaults({ persistent: false });
|
$.prompt.setDefaults({ persistent: false });
|
||||||
|
|
||||||
VideoLayout.init(eventEmitter);
|
|
||||||
VideoLayout.initLargeVideo();
|
VideoLayout.initLargeVideo();
|
||||||
|
|
||||||
// Do not animate the video area on UI start (second argument passed into
|
// Do not animate the video area on UI start (second argument passed into
|
||||||
|
@ -135,7 +134,6 @@ UI.start = function() {
|
||||||
if (config.iAmRecorder) {
|
if (config.iAmRecorder) {
|
||||||
// in case of iAmSipGateway keep local video visible
|
// in case of iAmSipGateway keep local video visible
|
||||||
if (!config.iAmSipGateway) {
|
if (!config.iAmSipGateway) {
|
||||||
VideoLayout.setLocalVideoVisible(false);
|
|
||||||
APP.store.dispatch(setNotificationsEnabled(false));
|
APP.store.dispatch(setNotificationsEnabled(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,14 +177,6 @@ UI.unbindEvents = () => {
|
||||||
$(window).off('resize');
|
$(window).off('resize');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Show local video stream on UI.
|
|
||||||
* @param {JitsiTrack} track stream to show
|
|
||||||
*/
|
|
||||||
UI.addLocalVideoStream = track => {
|
|
||||||
VideoLayout.changeLocalVideo(track);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup and show Etherpad.
|
* Setup and show Etherpad.
|
||||||
* @param {string} name etherpad id
|
* @param {string} name etherpad id
|
||||||
|
@ -227,14 +217,6 @@ UI.addUser = function(user) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Update videotype for specified user.
|
|
||||||
* @param {string} id user id
|
|
||||||
* @param {string} newVideoType new videotype
|
|
||||||
*/
|
|
||||||
UI.onPeerVideoTypeChanged
|
|
||||||
= (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the user status.
|
* Updates the user status.
|
||||||
*
|
*
|
||||||
|
@ -289,19 +271,14 @@ UI.setAudioMuted = function(id) {
|
||||||
* Sets muted video state for participant
|
* Sets muted video state for participant
|
||||||
*/
|
*/
|
||||||
UI.setVideoMuted = function(id) {
|
UI.setVideoMuted = function(id) {
|
||||||
VideoLayout.onVideoMute(id);
|
VideoLayout._updateLargeVideoIfDisplayed(id, true);
|
||||||
|
|
||||||
if (APP.conference.isLocalId(id)) {
|
if (APP.conference.isLocalId(id)) {
|
||||||
APP.conference.updateVideoIconEnabled();
|
APP.conference.updateVideoIconEnabled();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
UI.updateLargeVideo = (id, forceUpdate) => VideoLayout.updateLargeVideo(id, forceUpdate);
|
||||||
* Triggers an update of remote video and large video displays so they may pick
|
|
||||||
* up any state changes that have occurred elsewhere.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
UI.updateAllVideos = () => VideoLayout.updateAllVideos();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a listener that would be notified on the given type of event.
|
* Adds a listener that would be notified on the given type of event.
|
||||||
|
@ -340,8 +317,6 @@ UI.removeListener = function(type, listener) {
|
||||||
*/
|
*/
|
||||||
UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
|
UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
|
||||||
|
|
||||||
UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
|
|
||||||
|
|
||||||
// Used by torture.
|
// Used by torture.
|
||||||
UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
||||||
|
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
/* global $, APP */
|
|
||||||
|
|
||||||
/* 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';
|
|
||||||
import SmallVideo from '../videolayout/SmallVideo';
|
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class SharedVideoThumb extends SmallVideo {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} participant
|
|
||||||
*/
|
|
||||||
constructor(participant) {
|
|
||||||
super();
|
|
||||||
this.id = participant.id;
|
|
||||||
this.isLocal = false;
|
|
||||||
this.url = participant.id;
|
|
||||||
this.videoSpanId = 'sharedVideoContainer';
|
|
||||||
this.container = this.createContainer(this.videoSpanId);
|
|
||||||
this.$container = $(this.container);
|
|
||||||
this.renderThumbnail();
|
|
||||||
this._setThumbnailSize();
|
|
||||||
this.bindHoverHandler();
|
|
||||||
this.container.onclick = this._onContainerClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} spanId
|
|
||||||
*/
|
|
||||||
createContainer(spanId) {
|
|
||||||
const container = document.createElement('span');
|
|
||||||
|
|
||||||
container.id = spanId;
|
|
||||||
container.className = 'videocontainer';
|
|
||||||
|
|
||||||
const remoteVideosContainer
|
|
||||||
= document.getElementById('filmstripRemoteVideosContainer');
|
|
||||||
const localVideoContainer
|
|
||||||
= document.getElementById('localVideoTileViewContainer');
|
|
||||||
|
|
||||||
remoteVideosContainer.insertBefore(container, localVideoContainer);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the thumbnail.
|
|
||||||
*/
|
|
||||||
renderThumbnail(isHovered = false) {
|
|
||||||
ReactDOM.render(
|
|
||||||
<Provider store = { APP.store }>
|
|
||||||
<I18nextProvider i18n = { i18next }>
|
|
||||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
|
||||||
</I18nextProvider>
|
|
||||||
</Provider>, this.container);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,129 +25,6 @@ const Filmstrip = {
|
||||||
*/
|
*/
|
||||||
getVerticalFilmstripWidth() {
|
getVerticalFilmstripWidth() {
|
||||||
return isFilmstripVisible(APP.store) ? getVerticalFilmstripVisibleAreaWidth() : 0;
|
return isFilmstripVisible(APP.store) ? getVerticalFilmstripVisibleAreaWidth() : 0;
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes thumbnails for tile view.
|
|
||||||
*
|
|
||||||
* @param {number} width - The new width of the thumbnails.
|
|
||||||
* @param {number} height - The new height of the thumbnails.
|
|
||||||
* @param {boolean} forceUpdate
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
|
|
||||||
const thumbs = this._getThumbs(!forceUpdate);
|
|
||||||
|
|
||||||
if (thumbs.localThumb) {
|
|
||||||
thumbs.localThumb.css({
|
|
||||||
'padding-top': '',
|
|
||||||
height: `${height}px`,
|
|
||||||
'min-height': `${height}px`,
|
|
||||||
'min-width': `${width}px`,
|
|
||||||
width: `${width}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbs.remoteThumbs) {
|
|
||||||
thumbs.remoteThumbs.css({
|
|
||||||
'padding-top': '',
|
|
||||||
height: `${height}px`,
|
|
||||||
'min-height': `${height}px`,
|
|
||||||
'min-width': `${width}px`,
|
|
||||||
width: `${width}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes thumbnails for horizontal view.
|
|
||||||
*
|
|
||||||
* @param {Object} dimensions - The new dimensions of the thumbnails.
|
|
||||||
* @param {boolean} forceUpdate
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
resizeThumbnailsForHorizontalView({ local = {}, remote = {} }, forceUpdate = false) {
|
|
||||||
const thumbs = this._getThumbs(!forceUpdate);
|
|
||||||
|
|
||||||
if (thumbs.localThumb) {
|
|
||||||
const { height, width } = local;
|
|
||||||
|
|
||||||
thumbs.localThumb.css({
|
|
||||||
height: `${height}px`,
|
|
||||||
'min-height': `${height}px`,
|
|
||||||
'min-width': `${width}px`,
|
|
||||||
width: `${width}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbs.remoteThumbs) {
|
|
||||||
const { height, width } = remote;
|
|
||||||
|
|
||||||
thumbs.remoteThumbs.css({
|
|
||||||
height: `${height}px`,
|
|
||||||
'min-height': `${height}px`,
|
|
||||||
'min-width': `${width}px`,
|
|
||||||
width: `${width}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes thumbnails for vertical view.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
resizeThumbnailsForVerticalView() {
|
|
||||||
const thumbs = this._getThumbs(true);
|
|
||||||
|
|
||||||
if (thumbs.localThumb) {
|
|
||||||
const heightToWidthPercent = 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
|
|
||||||
|
|
||||||
thumbs.localThumb.css({
|
|
||||||
'padding-top': `${heightToWidthPercent}%`,
|
|
||||||
width: '',
|
|
||||||
height: '',
|
|
||||||
'min-width': '',
|
|
||||||
'min-height': ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbs.remoteThumbs) {
|
|
||||||
const heightToWidthPercent = 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO;
|
|
||||||
|
|
||||||
thumbs.remoteThumbs.css({
|
|
||||||
'padding-top': `${heightToWidthPercent}%`,
|
|
||||||
width: '',
|
|
||||||
height: '',
|
|
||||||
'min-width': '',
|
|
||||||
'min-height': ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns thumbnails of the filmstrip
|
|
||||||
* @param onlyVisible
|
|
||||||
* @returns {object} thumbnails
|
|
||||||
*/
|
|
||||||
_getThumbs(onlyVisible = false) {
|
|
||||||
let selector = 'span';
|
|
||||||
|
|
||||||
if (onlyVisible) {
|
|
||||||
selector += ':visible';
|
|
||||||
}
|
|
||||||
|
|
||||||
const localThumb = $('#localVideoContainer');
|
|
||||||
const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector);
|
|
||||||
|
|
||||||
// Exclude the local video container if it has been hidden.
|
|
||||||
if (localThumb.hasClass('hidden')) {
|
|
||||||
return { remoteThumbs };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { remoteThumbs,
|
|
||||||
localThumb };
|
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import {
|
||||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||||
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 { createDeferred } from '../../util/helpers';
|
import { createDeferred } from '../../util/helpers';
|
||||||
import AudioLevels from '../audio_levels/AudioLevels';
|
import AudioLevels from '../audio_levels/AudioLevels';
|
||||||
|
|
||||||
|
@ -51,21 +50,19 @@ export default class LargeVideoManager {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor(emitter) {
|
constructor() {
|
||||||
/**
|
/**
|
||||||
* The map of <tt>LargeContainer</tt>s where the key is the video
|
* The map of <tt>LargeContainer</tt>s where the key is the video
|
||||||
* container type.
|
* container type.
|
||||||
* @type {Object.<string, LargeContainer>}
|
* @type {Object.<string, LargeContainer>}
|
||||||
*/
|
*/
|
||||||
this.containers = {};
|
this.containers = {};
|
||||||
this.eventEmitter = emitter;
|
|
||||||
|
|
||||||
this.state = VIDEO_CONTAINER_TYPE;
|
this.state = VIDEO_CONTAINER_TYPE;
|
||||||
|
|
||||||
// FIXME: We are passing resizeContainer as parameter which is calling
|
// FIXME: We are passing resizeContainer as parameter which is calling
|
||||||
// Container.resize. Probably there's better way to implement this.
|
// Container.resize. Probably there's better way to implement this.
|
||||||
this.videoContainer = new VideoContainer(
|
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
|
||||||
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
|
|
||||||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||||
|
|
||||||
// use the same video container to handle desktop tracks
|
// use the same video container to handle desktop tracks
|
||||||
|
@ -300,7 +297,6 @@ export default class LargeVideoManager {
|
||||||
// after everything is done check again if there are any pending
|
// after everything is done check again if there are any pending
|
||||||
// new streams.
|
// new streams.
|
||||||
this.updateInProcess = false;
|
this.updateInProcess = false;
|
||||||
this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
|
|
||||||
this.scheduleLargeVideoUpdate();
|
this.scheduleLargeVideoUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,213 +0,0 @@
|
||||||
/* global $, config, APP */
|
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
import { i18next } from '../../../react/features/base/i18n';
|
|
||||||
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
|
||||||
import { VideoTrack } from '../../../react/features/base/media';
|
|
||||||
import { updateSettings } from '../../../react/features/base/settings';
|
|
||||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
|
||||||
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
|
|
||||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
|
||||||
|
|
||||||
import SmallVideo from './SmallVideo';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class LocalVideo extends SmallVideo {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} emitter
|
|
||||||
* @param {*} streamEndedCallback
|
|
||||||
*/
|
|
||||||
constructor(emitter, streamEndedCallback) {
|
|
||||||
super();
|
|
||||||
this.videoSpanId = 'localVideoContainer';
|
|
||||||
this.streamEndedCallback = streamEndedCallback;
|
|
||||||
this.container = this.createContainer();
|
|
||||||
this.$container = $(this.container);
|
|
||||||
this.isLocal = true;
|
|
||||||
this._setThumbnailSize();
|
|
||||||
this.updateDOMLocation();
|
|
||||||
this.renderThumbnail();
|
|
||||||
|
|
||||||
this.localVideoId = null;
|
|
||||||
this.bindHoverHandler();
|
|
||||||
if (!config.disableLocalVideoFlip) {
|
|
||||||
this._buildContextMenu();
|
|
||||||
}
|
|
||||||
this.emitter = emitter;
|
|
||||||
|
|
||||||
Object.defineProperty(this, 'id', {
|
|
||||||
get() {
|
|
||||||
return APP.conference.getMyUserId();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.initBrowserSpecificProperties();
|
|
||||||
|
|
||||||
this.container.onclick = this._onContainerClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
createContainer() {
|
|
||||||
const containerSpan = document.createElement('span');
|
|
||||||
|
|
||||||
containerSpan.classList.add('videocontainer');
|
|
||||||
containerSpan.id = this.videoSpanId;
|
|
||||||
|
|
||||||
return containerSpan;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the thumbnail.
|
|
||||||
*/
|
|
||||||
renderThumbnail(isHovered = false) {
|
|
||||||
ReactDOM.render(
|
|
||||||
<Provider store = { APP.store }>
|
|
||||||
<I18nextProvider i18n = { i18next }>
|
|
||||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
|
||||||
</I18nextProvider>
|
|
||||||
</Provider>, this.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} stream
|
|
||||||
*/
|
|
||||||
changeVideo(stream) {
|
|
||||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
|
||||||
|
|
||||||
// eslint-disable-next-line eqeqeq
|
|
||||||
const isVideo = stream.videoType != 'desktop';
|
|
||||||
const settings = APP.store.getState()['features/base/settings'];
|
|
||||||
|
|
||||||
this._enableDisableContextMenu(isVideo);
|
|
||||||
this.setFlipX(isVideo ? settings.localFlipX : false);
|
|
||||||
|
|
||||||
const endedHandler = () => {
|
|
||||||
this._notifyOfStreamEnded();
|
|
||||||
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify any subscribers of the local video stream ending.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_notifyOfStreamEnded() {
|
|
||||||
if (this.streamEndedCallback) {
|
|
||||||
this.streamEndedCallback(this.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows or hides the local video container.
|
|
||||||
* @param {boolean} true to make the local video container visible, false
|
|
||||||
* otherwise
|
|
||||||
*/
|
|
||||||
setVisible(visible) {
|
|
||||||
// We toggle the hidden class as an indication to other interested parties
|
|
||||||
// that this container has been hidden on purpose.
|
|
||||||
this.$container.toggleClass('hidden');
|
|
||||||
|
|
||||||
// We still show/hide it as we need to overwrite the style property if we
|
|
||||||
// want our action to take effect. Toggling the display property through
|
|
||||||
// the above css class didn't succeed in overwriting the style.
|
|
||||||
if (visible) {
|
|
||||||
this.$container.show();
|
|
||||||
} else {
|
|
||||||
this.$container.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the flipX state of the video.
|
|
||||||
* @param val {boolean} true for flipped otherwise false;
|
|
||||||
*/
|
|
||||||
setFlipX(val) {
|
|
||||||
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
|
|
||||||
if (!this.localVideoId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (val) {
|
|
||||||
this.selectVideoElement().addClass('flipVideoX');
|
|
||||||
} else {
|
|
||||||
this.selectVideoElement().removeClass('flipVideoX');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the context menu for the local video.
|
|
||||||
*/
|
|
||||||
_buildContextMenu() {
|
|
||||||
$.contextMenu({
|
|
||||||
selector: `#${this.videoSpanId}`,
|
|
||||||
zIndex: 10000,
|
|
||||||
items: {
|
|
||||||
flip: {
|
|
||||||
name: 'Flip',
|
|
||||||
callback: () => {
|
|
||||||
const { store } = APP;
|
|
||||||
const val = !store.getState()['features/base/settings']
|
|
||||||
.localFlipX;
|
|
||||||
|
|
||||||
this.setFlipX(val);
|
|
||||||
store.dispatch(updateSettings({
|
|
||||||
localFlipX: val
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
show(options) {
|
|
||||||
options.items.flip.name
|
|
||||||
= APP.translation.generateTranslationHTML(
|
|
||||||
'videothumbnail.flip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables or disables the context menu for the local video.
|
|
||||||
* @param enable {boolean} true for enable, false for disable
|
|
||||||
*/
|
|
||||||
_enableDisableContextMenu(enable) {
|
|
||||||
if (this.$container.contextMenu) {
|
|
||||||
this.$container.contextMenu(enable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Places the {@code LocalVideo} in the DOM based on the current video layout.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
updateDOMLocation() {
|
|
||||||
if (!this.container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.container.parentElement) {
|
|
||||||
this.container.parentElement.removeChild(this.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
const appendTarget = shouldDisplayTileView(APP.store.getState())
|
|
||||||
? document.getElementById('localVideoTileViewContainer')
|
|
||||||
: document.getElementById('filmstripLocalVideoThumbnail');
|
|
||||||
|
|
||||||
appendTarget && appendTarget.appendChild(this.container);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
/* global $, APP, config */
|
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
|
||||||
import Logger from 'jitsi-meet-logger';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
import { i18next } from '../../../react/features/base/i18n';
|
|
||||||
import {
|
|
||||||
JitsiParticipantConnectionStatus
|
|
||||||
} from '../../../react/features/base/lib-jitsi-meet';
|
|
||||||
import { getParticipantById } from '../../../react/features/base/participants';
|
|
||||||
import { isTestModeEnabled } from '../../../react/features/base/testing';
|
|
||||||
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
|
|
||||||
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
|
|
||||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
|
||||||
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
|
|
||||||
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
|
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
import UIUtils from '../util/UIUtil';
|
|
||||||
|
|
||||||
import SmallVideo from './SmallVideo';
|
|
||||||
|
|
||||||
const logger = Logger.getLogger(__filename);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of container events that we are going to process, will be added as listener to the
|
|
||||||
* container for every event in the list. The latest event will be stored in redux.
|
|
||||||
*/
|
|
||||||
const containerEvents = [
|
|
||||||
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
|
|
||||||
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} spanId
|
|
||||||
*/
|
|
||||||
function createContainer(spanId) {
|
|
||||||
const container = document.createElement('span');
|
|
||||||
|
|
||||||
container.id = spanId;
|
|
||||||
container.className = 'videocontainer';
|
|
||||||
|
|
||||||
const remoteVideosContainer
|
|
||||||
= document.getElementById('filmstripRemoteVideosContainer');
|
|
||||||
const localVideoContainer
|
|
||||||
= document.getElementById('localVideoTileViewContainer');
|
|
||||||
|
|
||||||
remoteVideosContainer.insertBefore(container, localVideoContainer);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class RemoteVideo extends SmallVideo {
|
|
||||||
/**
|
|
||||||
* Creates new instance of the <tt>RemoteVideo</tt>.
|
|
||||||
* @param user {JitsiParticipant} the user for whom remote video instance will
|
|
||||||
* be created.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
constructor(user) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.user = user;
|
|
||||||
this.id = user.getId();
|
|
||||||
this.videoSpanId = `participant_${this.id}`;
|
|
||||||
|
|
||||||
this.addRemoteVideoContainer();
|
|
||||||
this.bindHoverHandler();
|
|
||||||
this.flipX = false;
|
|
||||||
this.isLocal = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The flag is set to <tt>true</tt> after the 'canplay' event has been
|
|
||||||
* triggered on the current video element. It goes back to <tt>false</tt>
|
|
||||||
* when the stream is removed. It is used to determine whether the video
|
|
||||||
* playback has ever started.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this._canPlayEventReceived = false;
|
|
||||||
|
|
||||||
this.container.onclick = this._onContainerClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
addRemoteVideoContainer() {
|
|
||||||
this.container = createContainer(this.videoSpanId);
|
|
||||||
this.$container = $(this.container);
|
|
||||||
this.renderThumbnail();
|
|
||||||
this._setThumbnailSize();
|
|
||||||
this.initBrowserSpecificProperties();
|
|
||||||
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the thumbnail.
|
|
||||||
*/
|
|
||||||
renderThumbnail(isHovered = false) {
|
|
||||||
ReactDOM.render(
|
|
||||||
<Provider store = { APP.store }>
|
|
||||||
<I18nextProvider i18n = { i18next }>
|
|
||||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
|
||||||
</I18nextProvider>
|
|
||||||
</Provider>, this.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the remote stream element corresponding to the given stream and
|
|
||||||
* parent container.
|
|
||||||
*
|
|
||||||
* @param stream the MediaStream
|
|
||||||
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
|
|
||||||
*/
|
|
||||||
removeRemoteStreamElement(stream) {
|
|
||||||
if (!this.container) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isVideo = stream.isVideoTrack();
|
|
||||||
const elementID = `remoteVideo_${stream.getId()}`;
|
|
||||||
const select = $(`#${elementID}`);
|
|
||||||
|
|
||||||
select.remove();
|
|
||||||
if (isVideo) {
|
|
||||||
this._canPlayEventReceived = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Video removed ${this.id}`, select);
|
|
||||||
|
|
||||||
this.updateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The remote video is considered "playable" once the can play event has been received.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
isVideoPlayable() {
|
|
||||||
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
updateView() {
|
|
||||||
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
|
|
||||||
super.updateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes RemoteVideo from the page.
|
|
||||||
*/
|
|
||||||
remove() {
|
|
||||||
ReactDOM.unmountComponentAtNode(this.container);
|
|
||||||
super.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} streamElement
|
|
||||||
* @param {*} stream
|
|
||||||
*/
|
|
||||||
waitForPlayback(streamElement, stream) {
|
|
||||||
$(streamElement).hide();
|
|
||||||
|
|
||||||
const webRtcStream = stream.getOriginalStream();
|
|
||||||
const isVideo = stream.isVideoTrack();
|
|
||||||
|
|
||||||
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listener = () => {
|
|
||||||
this._canPlayEventReceived = true;
|
|
||||||
|
|
||||||
logger.info(`${this.id} video is now active`, streamElement);
|
|
||||||
if (streamElement) {
|
|
||||||
$(streamElement).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
streamElement.removeEventListener('canplay', listener);
|
|
||||||
|
|
||||||
// Refresh to show the video
|
|
||||||
this.updateView();
|
|
||||||
};
|
|
||||||
|
|
||||||
streamElement.addEventListener('canplay', listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} stream
|
|
||||||
*/
|
|
||||||
addRemoteStreamElement(stream) {
|
|
||||||
if (!this.container) {
|
|
||||||
logger.debug('Not attaching remote stream due to no container');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isVideo = stream.isVideoTrack();
|
|
||||||
|
|
||||||
if (!stream.getOriginalStream()) {
|
|
||||||
logger.debug('Remote video stream has no original stream');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let streamElement = document.createElement('video');
|
|
||||||
|
|
||||||
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
|
|
||||||
streamElement.id = `remoteVideo_${stream.getId()}`;
|
|
||||||
streamElement.mute = true;
|
|
||||||
streamElement.playsInline = true;
|
|
||||||
|
|
||||||
// Put new stream element always in front
|
|
||||||
streamElement = UIUtils.prependChild(this.container, streamElement);
|
|
||||||
|
|
||||||
this.waitForPlayback(streamElement, stream);
|
|
||||||
stream.attach(streamElement);
|
|
||||||
|
|
||||||
if (isVideo && isTestModeEnabled(APP.store.getState())) {
|
|
||||||
|
|
||||||
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
|
|
||||||
|
|
||||||
containerEvents.forEach(event => {
|
|
||||||
streamElement.addEventListener(event, cb.bind(this, event));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,509 +0,0 @@
|
||||||
/* global $, APP, interfaceConfig */
|
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
|
||||||
import Logger from 'jitsi-meet-logger';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
|
|
||||||
import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
|
|
||||||
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
|
|
||||||
import { i18next } from '../../../react/features/base/i18n';
|
|
||||||
import { MEDIA_TYPE } from '../../../react/features/base/media';
|
|
||||||
import {
|
|
||||||
getLocalParticipant,
|
|
||||||
getParticipantById,
|
|
||||||
getParticipantCount,
|
|
||||||
getPinnedParticipant,
|
|
||||||
pinParticipant
|
|
||||||
} from '../../../react/features/base/participants';
|
|
||||||
import {
|
|
||||||
getLocalVideoTrack,
|
|
||||||
getTrackByMediaTypeAndParticipant,
|
|
||||||
isLocalTrackMuted,
|
|
||||||
isRemoteTrackMuted
|
|
||||||
} from '../../../react/features/base/tracks';
|
|
||||||
import { ConnectionIndicator } from '../../../react/features/connection-indicator';
|
|
||||||
import { DisplayName } from '../../../react/features/display-name';
|
|
||||||
import {
|
|
||||||
DominantSpeakerIndicator,
|
|
||||||
RaisedHandIndicator,
|
|
||||||
StatusIndicators,
|
|
||||||
isVideoPlayable
|
|
||||||
} from '../../../react/features/filmstrip';
|
|
||||||
import {
|
|
||||||
LAYOUTS,
|
|
||||||
getCurrentLayout,
|
|
||||||
setTileView,
|
|
||||||
shouldDisplayTileView
|
|
||||||
} from '../../../react/features/video-layout';
|
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
|
|
||||||
const logger = Logger.getLogger(__filename);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode constant used when video is being displayed on the small video.
|
|
||||||
* @type {number}
|
|
||||||
* @constant
|
|
||||||
*/
|
|
||||||
const DISPLAY_VIDEO = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode constant used when the user's avatar is being displayed on
|
|
||||||
* the small video.
|
|
||||||
* @type {number}
|
|
||||||
* @constant
|
|
||||||
*/
|
|
||||||
const DISPLAY_AVATAR = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode constant used when neither video nor avatar is being displayed
|
|
||||||
* on the small video. And we just show the display name.
|
|
||||||
* @type {number}
|
|
||||||
* @constant
|
|
||||||
*/
|
|
||||||
const DISPLAY_BLACKNESS_WITH_NAME = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode constant used when video is displayed and display name
|
|
||||||
* at the same time.
|
|
||||||
* @type {number}
|
|
||||||
* @constant
|
|
||||||
*/
|
|
||||||
const DISPLAY_VIDEO_WITH_NAME = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode constant used when neither video nor avatar is being displayed
|
|
||||||
* on the small video. And we just show the display name.
|
|
||||||
* @type {number}
|
|
||||||
* @constant
|
|
||||||
*/
|
|
||||||
const DISPLAY_AVATAR_WITH_NAME = 4;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class SmallVideo {
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
this.videoIsHovered = false;
|
|
||||||
this.videoType = undefined;
|
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
|
||||||
this.updateView = this.updateView.bind(this);
|
|
||||||
|
|
||||||
this._onContainerClick = this._onContainerClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the identifier of this small video.
|
|
||||||
*
|
|
||||||
* @returns the identifier of this small video
|
|
||||||
*/
|
|
||||||
getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if this small video is currently visible.
|
|
||||||
*
|
|
||||||
* @return <tt>true</tt> if this small video isn't currently visible and
|
|
||||||
* <tt>false</tt> - otherwise.
|
|
||||||
*/
|
|
||||||
isVisible() {
|
|
||||||
return this.$container.is(':visible');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
|
|
||||||
*/
|
|
||||||
bindHoverHandler() {
|
|
||||||
// Add hover handler
|
|
||||||
this.$container.hover(
|
|
||||||
() => {
|
|
||||||
this.videoIsHovered = true;
|
|
||||||
this.renderThumbnail(true);
|
|
||||||
this.updateView();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.videoIsHovered = false;
|
|
||||||
this.renderThumbnail(false);
|
|
||||||
this.updateView();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the thumbnail.
|
|
||||||
*/
|
|
||||||
renderThumbnail() {
|
|
||||||
// Should be implemented by in subclasses.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an especially interesting function. A naive reader might think that
|
|
||||||
* it returns this SmallVideo's "video" element. But it is much more exciting.
|
|
||||||
* It first finds this video's parent element using jquery, then uses a utility
|
|
||||||
* from lib-jitsi-meet to extract the video element from it (with two more
|
|
||||||
* jquery calls), and finally uses jquery again to encapsulate the video element
|
|
||||||
* in an array. This last step allows (some might prefer "forces") users of
|
|
||||||
* this function to access the video element via the 0th element of the returned
|
|
||||||
* array (after checking its length of course!).
|
|
||||||
*/
|
|
||||||
selectVideoElement() {
|
|
||||||
return $($(this.container).find('video')[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables / disables the css responsible for focusing/pinning a video
|
|
||||||
* thumbnail.
|
|
||||||
*
|
|
||||||
* @param isFocused indicates if the thumbnail should be focused/pinned or not
|
|
||||||
*/
|
|
||||||
focus(isFocused) {
|
|
||||||
const focusedCssClass = 'videoContainerFocused';
|
|
||||||
const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
|
|
||||||
|
|
||||||
if (!isFocused && isFocusClassEnabled) {
|
|
||||||
this.$container.removeClass(focusedCssClass);
|
|
||||||
} else if (isFocused && !isFocusClassEnabled) {
|
|
||||||
this.$container.addClass(focusedCssClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
hasVideo() {
|
|
||||||
return this.selectVideoElement().length !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
|
|
||||||
* being displayed on the "large video".
|
|
||||||
*
|
|
||||||
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
|
|
||||||
* or <tt>false</tt> otherwise.
|
|
||||||
*/
|
|
||||||
isCurrentlyOnLargeVideo() {
|
|
||||||
return APP.store.getState()['features/large-video']?.participantId === this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether there is a playable video stream available for the user
|
|
||||||
* associated with this <tt>SmallVideo</tt>.
|
|
||||||
*
|
|
||||||
* @return {boolean} <tt>true</tt> if there is a playable video stream available
|
|
||||||
* or <tt>false</tt> otherwise.
|
|
||||||
*/
|
|
||||||
isVideoPlayable() {
|
|
||||||
return isVideoPlayable(APP.store.getState(), this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines what should be display on the thumbnail.
|
|
||||||
*
|
|
||||||
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
|
|
||||||
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
|
|
||||||
*/
|
|
||||||
selectDisplayMode(input) {
|
|
||||||
if (!input.tileViewActive && input.isScreenSharing) {
|
|
||||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
|
||||||
} else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
|
|
||||||
// Display name is always and only displayed when user is on the stage
|
|
||||||
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
|
||||||
} else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
|
|
||||||
// check hovering and change state to video with name
|
|
||||||
return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hovering and change state to avatar with name
|
|
||||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes information that determine the display mode.
|
|
||||||
*
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
computeDisplayModeInput() {
|
|
||||||
let isScreenSharing = false;
|
|
||||||
let connectionStatus;
|
|
||||||
const state = APP.store.getState();
|
|
||||||
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) {
|
|
||||||
isScreenSharing = videoTrack?.videoType === 'desktop';
|
|
||||||
connectionStatus = participant.connectionStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
|
|
||||||
isHovered: this._isHovered(),
|
|
||||||
isAudioOnly: APP.conference.isAudioOnly(),
|
|
||||||
tileViewActive: shouldDisplayTileView(state),
|
|
||||||
isVideoPlayable: this.isVideoPlayable(),
|
|
||||||
hasVideo: Boolean(this.selectVideoElement().length),
|
|
||||||
connectionStatus,
|
|
||||||
canPlayEventReceived: this._canPlayEventReceived,
|
|
||||||
videoStream: Boolean(videoTrack),
|
|
||||||
isScreenSharing,
|
|
||||||
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether current video is considered hovered. Currently it is hovered
|
|
||||||
* if the mouse is over the video, or if the connection
|
|
||||||
* indicator is shown(hovered).
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_isHovered() {
|
|
||||||
return this.videoIsHovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the css classes of the thumbnail based on the current state.
|
|
||||||
*/
|
|
||||||
updateView() {
|
|
||||||
this.$container.removeClass((index, classNames) =>
|
|
||||||
classNames.split(' ').filter(name => name.startsWith('display-')));
|
|
||||||
|
|
||||||
const oldDisplayMode = this.displayMode;
|
|
||||||
let displayModeString = '';
|
|
||||||
|
|
||||||
const displayModeInput = this.computeDisplayModeInput();
|
|
||||||
|
|
||||||
// Determine whether video, avatar or blackness should be displayed
|
|
||||||
this.displayMode = this.selectDisplayMode(displayModeInput);
|
|
||||||
|
|
||||||
switch (this.displayMode) {
|
|
||||||
case DISPLAY_AVATAR_WITH_NAME:
|
|
||||||
displayModeString = 'avatar-with-name';
|
|
||||||
this.$container.addClass('display-avatar-with-name');
|
|
||||||
break;
|
|
||||||
case DISPLAY_BLACKNESS_WITH_NAME:
|
|
||||||
displayModeString = 'blackness-with-name';
|
|
||||||
this.$container.addClass('display-name-on-black');
|
|
||||||
break;
|
|
||||||
case DISPLAY_VIDEO:
|
|
||||||
displayModeString = 'video';
|
|
||||||
this.$container.addClass('display-video');
|
|
||||||
break;
|
|
||||||
case DISPLAY_VIDEO_WITH_NAME:
|
|
||||||
displayModeString = 'video-with-name';
|
|
||||||
this.$container.addClass('display-name-on-video');
|
|
||||||
break;
|
|
||||||
case DISPLAY_AVATAR:
|
|
||||||
default:
|
|
||||||
displayModeString = 'avatar';
|
|
||||||
this.$container.addClass('display-avatar-only');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.displayMode !== oldDisplayMode) {
|
|
||||||
logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.displayMode !== DISPLAY_VIDEO
|
|
||||||
&& this.displayMode !== DISPLAY_VIDEO_WITH_NAME
|
|
||||||
&& displayModeInput.tileViewActive
|
|
||||||
&& displayModeInput.isScreenSharing
|
|
||||||
&& !displayModeInput.isAudioOnly) {
|
|
||||||
// send the event
|
|
||||||
sendAnalytics(createScreenSharingIssueEvent({
|
|
||||||
source: 'thumbnail',
|
|
||||||
...displayModeInput
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows or hides the dominant speaker indicator.
|
|
||||||
* @param show whether to show or hide.
|
|
||||||
*/
|
|
||||||
showDominantSpeakerIndicator(show) {
|
|
||||||
// Don't create and show dominant speaker indicator if
|
|
||||||
// DISABLE_DOMINANT_SPEAKER_INDICATOR is true
|
|
||||||
if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.container) {
|
|
||||||
logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$container.toggleClass('active-speaker', show);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes any browser specific properties. Currently sets the overflow
|
|
||||||
* property for Qt browsers on Windows to hidden, thus fixing the following
|
|
||||||
* problem:
|
|
||||||
* Some browsers don't have full support of the object-fit property for the
|
|
||||||
* video element and when we set video object-fit to "cover" the video
|
|
||||||
* actually overflows the boundaries of its container, so it's important
|
|
||||||
* to indicate that the "overflow" should be hidden.
|
|
||||||
*
|
|
||||||
* Setting this property for all browsers will result in broken audio levels,
|
|
||||||
* which makes this a temporary solution, before reworking audio levels.
|
|
||||||
*/
|
|
||||||
initBrowserSpecificProperties() {
|
|
||||||
const userAgent = window.navigator.userAgent;
|
|
||||||
|
|
||||||
if (userAgent.indexOf('QtWebEngine') > -1
|
|
||||||
&& (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
|
|
||||||
this.$container.css('overflow', 'hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up components on {@code SmallVideo} and removes itself from the DOM.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
remove() {
|
|
||||||
logger.log('Remove thumbnail', this.id);
|
|
||||||
this._unmountThumbnail();
|
|
||||||
|
|
||||||
// Remove whole container
|
|
||||||
if (this.container.parentNode) {
|
|
||||||
this.container.parentNode.removeChild(this.container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function for re-rendering multiple react components of the small
|
|
||||||
* video.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
rerender() {
|
|
||||||
this.updateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when the thumbnail is clicked and potentially trigger
|
|
||||||
* pinning of the participant.
|
|
||||||
*
|
|
||||||
* @param {MouseEvent} event - The click event to intercept.
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_onContainerClick(event) {
|
|
||||||
const triggerPin = this._shouldTriggerPin(event);
|
|
||||||
|
|
||||||
if (event.stopPropagation && triggerPin) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
if (triggerPin) {
|
|
||||||
this.togglePin();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether or not a click event is targeted at certain elements which
|
|
||||||
* should not trigger a pin.
|
|
||||||
*
|
|
||||||
* @param {MouseEvent} event - The click event to intercept.
|
|
||||||
* @private
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
_shouldTriggerPin(event) {
|
|
||||||
// TODO Checking the classes is a workround to allow events to bubble into
|
|
||||||
// the DisplayName component if it was clicked. React's synthetic events
|
|
||||||
// will fire after jQuery handlers execute, so stop propagation at this
|
|
||||||
// point will prevent DisplayName from getting click events. This workaround
|
|
||||||
// should be removable once LocalVideo is a React Component because then
|
|
||||||
// the components share the same eventing system.
|
|
||||||
const $source = $(event.target || event.srcElement);
|
|
||||||
|
|
||||||
return $source.parents('.displayNameContainer').length === 0
|
|
||||||
&& $source.parents('.popover').length === 0
|
|
||||||
&& !event.target.classList.contains('popover');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pins the participant displayed by this thumbnail or unpins if already pinned.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
togglePin() {
|
|
||||||
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
|
|
||||||
const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
|
|
||||||
|
|
||||||
APP.store.dispatch(pinParticipant(participantIdToPin));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unmounts the thumbnail.
|
|
||||||
*/
|
|
||||||
_unmountThumbnail() {
|
|
||||||
ReactDOM.unmountComponentAtNode(this.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the size of the thumbnail.
|
|
||||||
*/
|
|
||||||
_setThumbnailSize() {
|
|
||||||
const layout = getCurrentLayout(APP.store.getState());
|
|
||||||
const heightToWidthPercent = 100
|
|
||||||
/ (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
|
|
||||||
|
|
||||||
switch (layout) {
|
|
||||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
|
||||||
this.$container.css('padding-top', `${heightToWidthPercent}%`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
|
||||||
const state = APP.store.getState();
|
|
||||||
const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
|
|
||||||
const size = this.isLocal ? local : remote;
|
|
||||||
|
|
||||||
if (typeof size !== 'undefined') {
|
|
||||||
const { height, width } = size;
|
|
||||||
|
|
||||||
this.$container.css({
|
|
||||||
height: `${height}px`,
|
|
||||||
'min-height': `${height}px`,
|
|
||||||
'min-width': `${width}px`,
|
|
||||||
width: `${width}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case LAYOUTS.TILE_VIEW: {
|
|
||||||
const state = APP.store.getState();
|
|
||||||
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
|
|
||||||
|
|
||||||
if (typeof thumbnailSize !== 'undefined') {
|
|
||||||
const { height, width } = thumbnailSize;
|
|
||||||
|
|
||||||
this.$container.css({
|
|
||||||
height: `${height}px`,
|
|
||||||
'min-height': `${height}px`,
|
|
||||||
'min-width': `${width}px`,
|
|
||||||
width: `${width}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||||
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
|
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
|
||||||
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
|
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
|
||||||
import UIUtil from '../util/UIUtil';
|
import UIUtil from '../util/UIUtil';
|
||||||
|
|
||||||
import Filmstrip from './Filmstrip';
|
import Filmstrip from './Filmstrip';
|
||||||
|
@ -187,16 +186,13 @@ export class VideoContainer extends LargeContainer {
|
||||||
* Creates new VideoContainer instance.
|
* Creates new VideoContainer instance.
|
||||||
* @param resizeContainer {Function} function that takes care of the size
|
* @param resizeContainer {Function} function that takes care of the size
|
||||||
* of the video container.
|
* of the video container.
|
||||||
* @param emitter {EventEmitter} the event emitter that will be used by
|
|
||||||
* this instance.
|
|
||||||
*/
|
*/
|
||||||
constructor(resizeContainer, emitter) {
|
constructor(resizeContainer) {
|
||||||
super();
|
super();
|
||||||
this.stream = null;
|
this.stream = null;
|
||||||
this.userId = null;
|
this.userId = null;
|
||||||
this.videoType = null;
|
this.videoType = null;
|
||||||
this.localFlipX = true;
|
this.localFlipX = true;
|
||||||
this.emitter = emitter;
|
|
||||||
this.resizeContainer = resizeContainer;
|
this.resizeContainer = resizeContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -492,7 +488,7 @@ export class VideoContainer extends LargeContainer {
|
||||||
|
|
||||||
stream.attach(this.$video[0]);
|
stream.attach(this.$video[0]);
|
||||||
|
|
||||||
const flipX = stream.isLocal() && this.localFlipX;
|
const flipX = stream.isLocal() && this.localFlipX && !this.isScreenSharing();
|
||||||
|
|
||||||
this.$video.css({
|
this.$video.css({
|
||||||
transform: flipX ? 'scaleX(-1)' : 'none'
|
transform: flipX ? 'scaleX(-1)' : 'none'
|
||||||
|
@ -534,7 +530,6 @@ export class VideoContainer extends LargeContainer {
|
||||||
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
|
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
|
||||||
this.avatarDisplayed = show;
|
this.avatarDisplayed = show;
|
||||||
|
|
||||||
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
|
|
||||||
APP.API.notifyLargeVideoVisibilityChanged(show);
|
APP.API.notifyLargeVideoVisibilityChanged(show);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,88 +4,29 @@ import Logger from 'jitsi-meet-logger';
|
||||||
|
|
||||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant as getLocalParticipantFromStore,
|
|
||||||
getPinnedParticipant,
|
getPinnedParticipant,
|
||||||
getParticipantById,
|
getParticipantById
|
||||||
pinParticipant
|
|
||||||
} from '../../../react/features/base/participants';
|
} from '../../../react/features/base/participants';
|
||||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
|
||||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
|
||||||
|
|
||||||
import LargeVideoManager from './LargeVideoManager';
|
import LargeVideoManager from './LargeVideoManager';
|
||||||
import LocalVideo from './LocalVideo';
|
|
||||||
import RemoteVideo from './RemoteVideo';
|
|
||||||
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
||||||
|
|
||||||
const logger = Logger.getLogger(__filename);
|
const logger = Logger.getLogger(__filename);
|
||||||
|
|
||||||
const remoteVideos = {};
|
|
||||||
let localVideoThumbnail = null;
|
|
||||||
|
|
||||||
let eventEmitter = null;
|
|
||||||
|
|
||||||
let largeVideo;
|
let largeVideo;
|
||||||
|
|
||||||
/**
|
|
||||||
* flipX state of the localVideo
|
|
||||||
*/
|
|
||||||
let localFlipX = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for local flip X changed event.
|
|
||||||
* @param {Object} val
|
|
||||||
*/
|
|
||||||
function onLocalFlipXChanged(val) {
|
|
||||||
localFlipX = val;
|
|
||||||
if (largeVideo) {
|
|
||||||
largeVideo.onLocalFlipXChange(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of all thumbnails in the filmstrip.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
function getAllThumbnails() {
|
|
||||||
return [
|
|
||||||
...localVideoThumbnail ? [ localVideoThumbnail ] : [],
|
|
||||||
...Object.values(remoteVideos)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private helper to get the redux representation of the local participant.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function getLocalParticipant() {
|
|
||||||
return getLocalParticipantFromStore(APP.store.getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
const VideoLayout = {
|
const VideoLayout = {
|
||||||
init(emitter) {
|
|
||||||
eventEmitter = emitter;
|
|
||||||
|
|
||||||
localVideoThumbnail = new LocalVideo(
|
|
||||||
emitter,
|
|
||||||
this._updateLargeVideoIfDisplayed.bind(this));
|
|
||||||
|
|
||||||
this.registerListeners();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registering listeners for UI events in Video layout component.
|
* Handler for local flip X changed event.
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
*/
|
||||||
registerListeners() {
|
onLocalFlipXChanged() {
|
||||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
if (largeVideo) {
|
||||||
onLocalFlipXChanged);
|
const { store } = APP;
|
||||||
|
const { localFlipX } = store.getState()['features/base/settings'];
|
||||||
|
|
||||||
|
largeVideo.onLocalFlipXChange(localFlipX);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -95,14 +36,17 @@ const VideoLayout = {
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
this._resetLargeVideo();
|
this._resetLargeVideo();
|
||||||
this._resetFilmstrip();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initLargeVideo() {
|
initLargeVideo() {
|
||||||
this._resetLargeVideo();
|
this._resetLargeVideo();
|
||||||
|
|
||||||
largeVideo = new LargeVideoManager(eventEmitter);
|
largeVideo = new LargeVideoManager();
|
||||||
if (localFlipX) {
|
|
||||||
|
const { store } = APP;
|
||||||
|
const { localFlipX } = store.getState()['features/base/settings'];
|
||||||
|
|
||||||
|
if (typeof localFlipX === 'boolean') {
|
||||||
largeVideo.onLocalFlipXChange(localFlipX);
|
largeVideo.onLocalFlipXChange(localFlipX);
|
||||||
}
|
}
|
||||||
largeVideo.updateContainerSize();
|
largeVideo.updateContainerSize();
|
||||||
|
@ -120,55 +64,6 @@ const VideoLayout = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
changeLocalVideo(stream) {
|
|
||||||
const localId = getLocalParticipant().id;
|
|
||||||
|
|
||||||
this.onVideoTypeChanged(localId, stream.videoType);
|
|
||||||
|
|
||||||
localVideoThumbnail.changeVideo(stream);
|
|
||||||
|
|
||||||
this._updateLargeVideoIfDisplayed(localId);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows/hides local video.
|
|
||||||
* @param {boolean} true to make the local video visible, false - otherwise
|
|
||||||
*/
|
|
||||||
setLocalVideoVisible(visible) {
|
|
||||||
localVideoThumbnail.setVisible(visible);
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemoteStreamAdded(stream) {
|
|
||||||
const id = stream.getParticipantId();
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
logger.debug(`Received a new ${stream.getType()} stream for ${id}`);
|
|
||||||
|
|
||||||
if (!remoteVideo) {
|
|
||||||
logger.debug('No remote video element to add stream');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteVideo.addRemoteStreamElement(stream);
|
|
||||||
|
|
||||||
this.onVideoMute(id);
|
|
||||||
remoteVideo.updateView();
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemoteStreamRemoved(stream) {
|
|
||||||
const id = stream.getParticipantId();
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
// Remote stream may be removed after participant left the conference.
|
|
||||||
if (remoteVideo) {
|
|
||||||
remoteVideo.removeRemoteStreamElement(stream);
|
|
||||||
remoteVideo.updateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateVideoMutedForNoTracks(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FIXME get rid of this method once muted indicator are reactified (by
|
* FIXME get rid of this method once muted indicator are reactified (by
|
||||||
* making sure that user with no tracks is displayed as muted )
|
* making sure that user with no tracks is displayed as muted )
|
||||||
|
@ -180,7 +75,7 @@ const VideoLayout = {
|
||||||
const participant = APP.conference.getParticipantById(participantId);
|
const participant = APP.conference.getParticipantById(participantId);
|
||||||
|
|
||||||
if (participant && !participant.getTracksByMediaType('video').length) {
|
if (participant && !participant.getTracksByMediaType('video').length) {
|
||||||
APP.UI.setVideoMuted(participantId);
|
VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -202,110 +97,12 @@ const VideoLayout = {
|
||||||
return videoTrack?.videoType;
|
return videoTrack?.videoType;
|
||||||
},
|
},
|
||||||
|
|
||||||
isPinned(id) {
|
|
||||||
return id === this.getPinnedId();
|
|
||||||
},
|
|
||||||
|
|
||||||
getPinnedId() {
|
getPinnedId() {
|
||||||
const { id } = getPinnedParticipant(APP.store.getState()) || {};
|
const { id } = getPinnedParticipant(APP.store.getState()) || {};
|
||||||
|
|
||||||
return id || null;
|
return id || null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers a thumbnail to pin or unpin itself.
|
|
||||||
*
|
|
||||||
* @param {number} videoNumber - The index of the video to toggle pin on.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
togglePin(videoNumber) {
|
|
||||||
const videos = getAllThumbnails();
|
|
||||||
const videoView = videos[videoNumber];
|
|
||||||
|
|
||||||
videoView && videoView.togglePin();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked to update display when the pin participant has changed.
|
|
||||||
*
|
|
||||||
* @paramn {string|null} pinnedParticipantID - The participant ID of the
|
|
||||||
* participant that is pinned or null if no one is pinned.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
onPinChange(pinnedParticipantID) {
|
|
||||||
getAllThumbnails().forEach(thumbnail =>
|
|
||||||
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a participant container for the given id.
|
|
||||||
*
|
|
||||||
* @param {Object} participant - The redux representation of a remote
|
|
||||||
* participant.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
addRemoteParticipantContainer(participant) {
|
|
||||||
if (!participant || participant.local) {
|
|
||||||
return;
|
|
||||||
} else if (participant.isFakeParticipant) {
|
|
||||||
const sharedVideoThumb = new SharedVideoThumb(participant);
|
|
||||||
|
|
||||||
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = participant.id;
|
|
||||||
const jitsiParticipant = APP.conference.getParticipantById(id);
|
|
||||||
const remoteVideo = new RemoteVideo(jitsiParticipant);
|
|
||||||
|
|
||||||
this.addRemoteVideoContainer(id, remoteVideo);
|
|
||||||
this.updateVideoMutedForNoTracks(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
|
|
||||||
*
|
|
||||||
* @param {string} the id of the video to add
|
|
||||||
* @param {SmallVideo} smallVideo the small video instance to add as a
|
|
||||||
* remote video
|
|
||||||
*/
|
|
||||||
addRemoteVideoContainer(id, remoteVideo) {
|
|
||||||
remoteVideos[id] = remoteVideo;
|
|
||||||
|
|
||||||
// Initialize the view
|
|
||||||
remoteVideo.updateView();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On video muted event.
|
|
||||||
*/
|
|
||||||
onVideoMute(id) {
|
|
||||||
if (APP.conference.isLocalId(id)) {
|
|
||||||
localVideoThumbnail && localVideoThumbnail.updateView();
|
|
||||||
} else {
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
if (remoteVideo) {
|
|
||||||
remoteVideo.updateView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// large video will show avatar instead of muted stream
|
|
||||||
this._updateLargeVideoIfDisplayed(id, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On dominant speaker changed event.
|
|
||||||
*
|
|
||||||
* @param {string} id - The participant ID of the new dominant speaker.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
onDominantSpeakerChanged(id) {
|
|
||||||
getAllThumbnails().forEach(thumbnail =>
|
|
||||||
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows/hides warning about a user's connectivity issues.
|
* Shows/hides warning about a user's connectivity issues.
|
||||||
*
|
*
|
||||||
|
@ -321,12 +118,6 @@ const VideoLayout = {
|
||||||
// We have to trigger full large video update to transition from
|
// We have to trigger full large video update to transition from
|
||||||
// avatar to video on connectivity restored.
|
// avatar to video on connectivity restored.
|
||||||
this._updateLargeVideoIfDisplayed(id, true);
|
this._updateLargeVideoIfDisplayed(id, true);
|
||||||
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
if (remoteVideo) {
|
|
||||||
remoteVideo.updateView();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -339,58 +130,14 @@ const VideoLayout = {
|
||||||
*/
|
*/
|
||||||
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
|
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
|
||||||
if (endpointsLeavingLastN) {
|
if (endpointsLeavingLastN) {
|
||||||
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
|
endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpointsEnteringLastN) {
|
if (endpointsEnteringLastN) {
|
||||||
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
|
endpointsEnteringLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates remote video by id if it exists.
|
|
||||||
* @param {string} id of the remote video
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_updateRemoteVideo(id) {
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
if (remoteVideo) {
|
|
||||||
remoteVideo.updateView();
|
|
||||||
this._updateLargeVideoIfDisplayed(id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeParticipantContainer(id) {
|
|
||||||
// Unlock large video
|
|
||||||
if (this.getPinnedId() === id) {
|
|
||||||
logger.info('Focused video owner has left the conference');
|
|
||||||
APP.store.dispatch(pinParticipant(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
if (remoteVideo) {
|
|
||||||
// Remove remote video
|
|
||||||
logger.info(`Removing remote video: ${id}`);
|
|
||||||
delete remoteVideos[id];
|
|
||||||
remoteVideo.remove();
|
|
||||||
} else {
|
|
||||||
logger.warn(`No remote video for ${id}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onVideoTypeChanged(id, newVideoType) {
|
|
||||||
const remoteVideo = remoteVideos[id];
|
|
||||||
|
|
||||||
if (!remoteVideo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Peer video type changed: ', id, newVideoType);
|
|
||||||
remoteVideo.updateView();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the video area.
|
* Resizes the video area.
|
||||||
*/
|
*/
|
||||||
|
@ -401,15 +148,6 @@ const VideoLayout = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getSmallVideo(id) {
|
|
||||||
if (APP.conference.isLocalId(id)) {
|
|
||||||
return localVideoThumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
return remoteVideos[id];
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
changeUserAvatar(id, avatarUrl) {
|
changeUserAvatar(id, avatarUrl) {
|
||||||
if (this.isCurrentlyOnLarge(id)) {
|
if (this.isCurrentlyOnLarge(id)) {
|
||||||
largeVideo.updateAvatar(avatarUrl);
|
largeVideo.updateAvatar(avatarUrl);
|
||||||
|
@ -432,24 +170,6 @@ const VideoLayout = {
|
||||||
return largeVideo && largeVideo.id === id;
|
return largeVideo && largeVideo.id === id;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers an update of remote video and large video displays so they may
|
|
||||||
* pick up any state changes that have occurred elsewhere.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
updateAllVideos() {
|
|
||||||
const displayedUserId = this.getLargeVideoID();
|
|
||||||
|
|
||||||
if (displayedUserId) {
|
|
||||||
this.updateLargeVideo(displayedUserId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(remoteVideos).forEach(video => {
|
|
||||||
remoteVideos[video].updateView();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateLargeVideo(id, forceUpdate) {
|
updateLargeVideo(id, forceUpdate) {
|
||||||
if (!largeVideo) {
|
if (!largeVideo) {
|
||||||
return;
|
return;
|
||||||
|
@ -510,13 +230,6 @@ const VideoLayout = {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentId = largeVideo.id;
|
|
||||||
let oldSmallVideo;
|
|
||||||
|
|
||||||
if (currentId) {
|
|
||||||
oldSmallVideo = this.getSmallVideo(currentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
let containerTypeToShow = type;
|
let containerTypeToShow = type;
|
||||||
|
|
||||||
// if we are hiding a container and there is focusedVideo
|
// if we are hiding a container and there is focusedVideo
|
||||||
|
@ -533,12 +246,7 @@ const VideoLayout = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return largeVideo.showContainer(containerTypeToShow)
|
return largeVideo.showContainer(containerTypeToShow);
|
||||||
.then(() => {
|
|
||||||
if (oldSmallVideo) {
|
|
||||||
oldSmallVideo && oldSmallVideo.updateView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isLargeContainerTypeVisible(type) {
|
isLargeContainerTypeVisible(type) {
|
||||||
|
@ -561,14 +269,6 @@ const VideoLayout = {
|
||||||
return largeVideo;
|
return largeVideo;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the flipX state of the local video.
|
|
||||||
* @param {boolean} true for flipped otherwise false;
|
|
||||||
*/
|
|
||||||
setLocalFlipX(val) {
|
|
||||||
this.localFlipX = val;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the wrapper jquery selector for the largeVideo
|
* Returns the wrapper jquery selector for the largeVideo
|
||||||
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
|
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
|
||||||
|
@ -577,15 +277,6 @@ const VideoLayout = {
|
||||||
return this.getCurrentlyOnLargeContainer().$wrapper;
|
return this.getCurrentlyOnLargeContainer().$wrapper;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of remove video ids.
|
|
||||||
*
|
|
||||||
* @returns {number} The number of remote videos.
|
|
||||||
*/
|
|
||||||
getRemoteVideosCount() {
|
|
||||||
return Object.keys(remoteVideos).length;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to invoke when the video layout has changed and elements
|
* Helper method to invoke when the video layout has changed and elements
|
||||||
* have to be re-arranged and resized.
|
* have to be re-arranged and resized.
|
||||||
|
@ -593,12 +284,7 @@ const VideoLayout = {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
refreshLayout() {
|
refreshLayout() {
|
||||||
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
|
|
||||||
VideoLayout.resizeVideoArea();
|
VideoLayout.resizeVideoArea();
|
||||||
|
|
||||||
// Rerender the thumbnails since they are dependent on the layout because of the tooltip positioning.
|
|
||||||
localVideoThumbnail && localVideoThumbnail.rerender();
|
|
||||||
Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -615,26 +301,6 @@ const VideoLayout = {
|
||||||
largeVideo = null;
|
largeVideo = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
|
|
||||||
* implementation is mainly for querying and manipulating the DOM while
|
|
||||||
* state mostly remains in {@code VideoLayout}.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_resetFilmstrip() {
|
|
||||||
Object.keys(remoteVideos).forEach(remoteVideoId => {
|
|
||||||
this.removeParticipantContainer(remoteVideoId);
|
|
||||||
delete remoteVideos[remoteVideoId];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (localVideoThumbnail) {
|
|
||||||
localVideoThumbnail.remove();
|
|
||||||
localVideoThumbnail = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers an update of large video if the passed in participant is
|
* Triggers an update of large video if the passed in participant is
|
||||||
* currently displayed on large video.
|
* currently displayed on large video.
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../react/features/analytics';
|
} from '../../react/features/analytics';
|
||||||
import { toggleDialog } from '../../react/features/base/dialog';
|
import { toggleDialog } from '../../react/features/base/dialog';
|
||||||
|
import { clickOnVideo } from '../../react/features/filmstrip/actions';
|
||||||
import { KeyboardShortcutsDialog }
|
import { KeyboardShortcutsDialog }
|
||||||
from '../../react/features/keyboard-shortcuts';
|
from '../../react/features/keyboard-shortcuts';
|
||||||
import { SpeakerStats } from '../../react/features/speaker-stats';
|
import { SpeakerStats } from '../../react/features/speaker-stats';
|
||||||
|
@ -54,7 +55,7 @@ const KeyboardShortcut = {
|
||||||
if (_shortcuts.has(key)) {
|
if (_shortcuts.has(key)) {
|
||||||
_shortcuts.get(key).function(e);
|
_shortcuts.get(key).function(e);
|
||||||
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
||||||
APP.UI.clickOnVideo(num);
|
APP.store.dispatch(clickOnVideo(num));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10090,11 +10090,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||||
},
|
},
|
||||||
"jquery-contextmenu": {
|
|
||||||
"version": "2.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/jquery-contextmenu/-/jquery-contextmenu-2.4.5.tgz",
|
|
||||||
"integrity": "sha1-5lrOBg2M2tTQ5d94FdDXA55RdFA="
|
|
||||||
},
|
|
||||||
"jquery-i18next": {
|
"jquery-i18next": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz",
|
||||||
|
|
|
@ -51,7 +51,6 @@
|
||||||
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
|
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
|
||||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
|
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
|
||||||
"jquery": "3.5.1",
|
"jquery": "3.5.1",
|
||||||
"jquery-contextmenu": "2.4.5",
|
|
||||||
"jquery-i18next": "1.2.1",
|
"jquery-i18next": "1.2.1",
|
||||||
"js-md5": "0.6.1",
|
"js-md5": "0.6.1",
|
||||||
"jwt-decode": "2.2.0",
|
"jwt-decode": "2.2.0",
|
||||||
|
|
|
@ -38,7 +38,103 @@ type Props = {
|
||||||
* Used to determine the value of the autoplay attribute of the underlying
|
* Used to determine the value of the autoplay attribute of the underlying
|
||||||
* video element.
|
* video element.
|
||||||
*/
|
*/
|
||||||
playsinline: boolean
|
playsinline: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of the event handlers for the video HTML element.
|
||||||
|
*/
|
||||||
|
eventHandlers?: {|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onAbort event handler.
|
||||||
|
*/
|
||||||
|
onAbort?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onCanPlay event handler.
|
||||||
|
*/
|
||||||
|
onCanPlay?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onCanPlayThrough event handler.
|
||||||
|
*/
|
||||||
|
onCanPlayThrough?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEmptied event handler.
|
||||||
|
*/
|
||||||
|
onEmptied?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEnded event handler.
|
||||||
|
*/
|
||||||
|
onEnded?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onError event handler.
|
||||||
|
*/
|
||||||
|
onError?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLoadedData event handler.
|
||||||
|
*/
|
||||||
|
onLoadedData?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLoadedMetadata event handler.
|
||||||
|
*/
|
||||||
|
onLoadedMetadata?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLoadStart event handler.
|
||||||
|
*/
|
||||||
|
onLoadStart?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPause event handler.
|
||||||
|
*/
|
||||||
|
onPause?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPlay event handler.
|
||||||
|
*/
|
||||||
|
onPlay?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPlaying event handler.
|
||||||
|
*/
|
||||||
|
onPlaying?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRateChange event handler.
|
||||||
|
*/
|
||||||
|
onRateChange?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onStalled event handler.
|
||||||
|
*/
|
||||||
|
onStalled?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onSuspend event handler.
|
||||||
|
*/
|
||||||
|
onSuspend?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onWaiting event handler.
|
||||||
|
*/
|
||||||
|
onWaiting?: ?Function
|
||||||
|
|},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styles that will be applied on the video element.
|
||||||
|
*/
|
||||||
|
style?: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the muted attribute for the underlying video element.
|
||||||
|
*/
|
||||||
|
muted?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,6 +235,10 @@ class Video extends Component<Props> {
|
||||||
this._attachTrack(nextProps.videoTrack);
|
this._attachTrack(nextProps.videoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,13 +249,26 @@ class Video extends Component<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
autoPlay,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
muted,
|
||||||
|
playsinline,
|
||||||
|
style,
|
||||||
|
eventHandlers
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
autoPlay = { this.props.autoPlay }
|
autoPlay = { autoPlay }
|
||||||
className = { this.props.className }
|
className = { className }
|
||||||
id = { this.props.id }
|
id = { id }
|
||||||
playsInline = { this.props.playsinline }
|
muted = { muted }
|
||||||
ref = { this._setVideoElement } />
|
playsInline = { playsinline }
|
||||||
|
ref = { this._setVideoElement }
|
||||||
|
style = { style }
|
||||||
|
{ ...eventHandlers } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,103 @@ type Props = AbstractVideoTrackProps & {
|
||||||
* Used to determine the value of the autoplay attribute of the underlying
|
* Used to determine the value of the autoplay attribute of the underlying
|
||||||
* video element.
|
* video element.
|
||||||
*/
|
*/
|
||||||
_noAutoPlayVideo: boolean
|
_noAutoPlayVideo: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of the event handlers for the video HTML element.
|
||||||
|
*/
|
||||||
|
eventHandlers?: {|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onAbort event handler.
|
||||||
|
*/
|
||||||
|
onAbort?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onCanPlay event handler.
|
||||||
|
*/
|
||||||
|
onCanPlay?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onCanPlayThrough event handler.
|
||||||
|
*/
|
||||||
|
onCanPlayThrough?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEmptied event handler.
|
||||||
|
*/
|
||||||
|
onEmptied?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEnded event handler.
|
||||||
|
*/
|
||||||
|
onEnded?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onError event handler.
|
||||||
|
*/
|
||||||
|
onError?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLoadedData event handler.
|
||||||
|
*/
|
||||||
|
onLoadedData?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLoadedMetadata event handler.
|
||||||
|
*/
|
||||||
|
onLoadedMetadata?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLoadStart event handler.
|
||||||
|
*/
|
||||||
|
onLoadStart?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPause event handler.
|
||||||
|
*/
|
||||||
|
onPause?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPlay event handler.
|
||||||
|
*/
|
||||||
|
onPlay?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onPlaying event handler.
|
||||||
|
*/
|
||||||
|
onPlaying?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRateChange event handler.
|
||||||
|
*/
|
||||||
|
onRateChange?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onStalled event handler.
|
||||||
|
*/
|
||||||
|
onStalled?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onSuspend event handler.
|
||||||
|
*/
|
||||||
|
onSuspend?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onWaiting event handler.
|
||||||
|
*/
|
||||||
|
onWaiting?: ?Function,
|
||||||
|
|},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styles that will be applied on the video element.
|
||||||
|
*/
|
||||||
|
style: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the muted attribute for the underlying element.
|
||||||
|
*/
|
||||||
|
muted?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,13 +153,27 @@ class VideoTrack extends AbstractVideoTrack<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
_noAutoPlayVideo,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
muted,
|
||||||
|
videoTrack,
|
||||||
|
style,
|
||||||
|
eventHandlers
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Video
|
<Video
|
||||||
autoPlay = { !this.props._noAutoPlayVideo }
|
autoPlay = { !_noAutoPlayVideo }
|
||||||
className = { this.props.className }
|
className = { className }
|
||||||
id = { this.props.id }
|
eventHandlers = { eventHandlers }
|
||||||
|
id = { id }
|
||||||
|
muted = { muted }
|
||||||
onVideoPlaying = { this._onVideoPlaying }
|
onVideoPlaying = { this._onVideoPlaying }
|
||||||
videoTrack = { this.props.videoTrack } />
|
style = { style }
|
||||||
|
videoTrack = { videoTrack } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { SETTINGS_UPDATED } from './actionTypes';
|
||||||
import { updateSettings } from './actions';
|
import { updateSettings } from './actions';
|
||||||
import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
|
import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The middleware of the feature base/settings. Distributes changes to the state
|
* The middleware of the feature base/settings. Distributes changes to the state
|
||||||
* of base/settings to the states of other features computed from the state of
|
* of base/settings to the states of other features computed from the state of
|
||||||
|
|
|
@ -75,6 +75,16 @@ export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE';
|
||||||
*/
|
*/
|
||||||
export const TRACK_REMOVED = 'TRACK_REMOVED';
|
export const TRACK_REMOVED = 'TRACK_REMOVED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of redux action dispatched when a track has stopped.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: TRACK_STOPPED,
|
||||||
|
* track: Track
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const TRACK_STOPPED = 'TRACK_STOPPED';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of redux action dispatched when a track's properties were updated.
|
* The type of redux action dispatched when a track's properties were updated.
|
||||||
*
|
*
|
||||||
|
|
|
@ -25,9 +25,10 @@ import {
|
||||||
TRACK_CREATE_ERROR,
|
TRACK_CREATE_ERROR,
|
||||||
TRACK_NO_DATA_FROM_SOURCE,
|
TRACK_NO_DATA_FROM_SOURCE,
|
||||||
TRACK_REMOVED,
|
TRACK_REMOVED,
|
||||||
|
TRACK_STOPPED,
|
||||||
TRACK_UPDATED,
|
TRACK_UPDATED,
|
||||||
TRACK_WILL_CREATE,
|
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
|
||||||
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT
|
TRACK_WILL_CREATE
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
createLocalTracksF,
|
createLocalTracksF,
|
||||||
|
@ -400,8 +401,15 @@ export function trackAdded(track) {
|
||||||
|
|
||||||
noDataFromSourceNotificationInfo = { timeout };
|
noDataFromSourceNotificationInfo = { timeout };
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||||
|
() => dispatch({
|
||||||
|
type: TRACK_STOPPED,
|
||||||
|
track: {
|
||||||
|
jitsiTrack: track
|
||||||
|
}
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
participantId = track.getParticipantId();
|
participantId = track.getParticipantId();
|
||||||
isReceivingData = true;
|
isReceivingData = true;
|
||||||
|
|
|
@ -163,7 +163,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
} else {
|
} else {
|
||||||
APP.UI.setVideoMuted(participantID);
|
APP.UI.setVideoMuted(participantID);
|
||||||
}
|
}
|
||||||
APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
|
|
||||||
} else if (jitsiTrack.isLocal()) {
|
} else if (jitsiTrack.isLocal()) {
|
||||||
APP.conference.setAudioMuteStatus(muted);
|
APP.conference.setAudioMuteStatus(muted);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -145,6 +145,18 @@ type Props = {
|
||||||
transport: Array<Object>
|
transport: Array<Object>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The click event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function onClick(event) {
|
||||||
|
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
|
||||||
|
// needs to be stopped.
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React {@code Component} for displaying connection statistics.
|
* React {@code Component} for displaying connection statistics.
|
||||||
*
|
*
|
||||||
|
@ -161,7 +173,9 @@ class ConnectionStatsTable extends Component<Props> {
|
||||||
const { isLocalVideo, enableSaveLogs } = this.props;
|
const { isLocalVideo, enableSaveLogs } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'connection-info'>
|
<div
|
||||||
|
className = 'connection-info'
|
||||||
|
onClick = { onClick }>
|
||||||
{ this._renderStatistics() }
|
{ this._renderStatistics() }
|
||||||
<div className = 'connection-actions'>
|
<div className = 'connection-actions'>
|
||||||
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
|
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { pinParticipant } from '../base/participants';
|
||||||
import { toState } from '../base/redux';
|
import { toState } from '../base/redux';
|
||||||
import { CHAT_SIZE } from '../chat/constants';
|
import { CHAT_SIZE } from '../chat/constants';
|
||||||
|
|
||||||
|
@ -69,4 +70,20 @@ export function setHorizontalViewDimensions(clientHeight: number = 0) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates a click on the n-th video.
|
||||||
|
*
|
||||||
|
* @param {number} n - Number that identifies the video.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function clickOnVideo(n: number) {
|
||||||
|
return (dispatch: Function, getState: Function) => {
|
||||||
|
const participants = getState()['features/base/participants'];
|
||||||
|
const nThParticipant = participants[n];
|
||||||
|
const { id, pinned } = nThParticipant;
|
||||||
|
|
||||||
|
dispatch(pinParticipant(pinned ? null : id));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export * from './actions.native';
|
export * from './actions.native';
|
||||||
|
|
|
@ -20,9 +20,9 @@ import { StyleType } from '../../../base/styles';
|
||||||
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||||
import { ConnectionIndicator } from '../../../connection-indicator';
|
import { ConnectionIndicator } from '../../../connection-indicator';
|
||||||
import { DisplayNameLabel } from '../../../display-name';
|
import { DisplayNameLabel } from '../../../display-name';
|
||||||
import { RemoteVideoMenu } from '../../../remote-video-menu';
|
|
||||||
import ConnectionStatusComponent from '../../../remote-video-menu/components/native/ConnectionStatusComponent';
|
|
||||||
import { toggleToolboxVisible } from '../../../toolbox/actions.native';
|
import { toggleToolboxVisible } from '../../../toolbox/actions.native';
|
||||||
|
import { RemoteVideoMenu } from '../../../video-menu';
|
||||||
|
import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
|
||||||
|
|
||||||
import AudioMutedIndicator from './AudioMutedIndicator';
|
import AudioMutedIndicator from './AudioMutedIndicator';
|
||||||
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
|
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
|
||||||
|
|
|
@ -11,12 +11,15 @@ import {
|
||||||
import { getToolbarButtons } from '../../../base/config';
|
import { getToolbarButtons } from '../../../base/config';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
||||||
|
import { getLocalParticipant } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
||||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||||
import { setFilmstripVisible } from '../../actions';
|
import { setFilmstripVisible } from '../../actions';
|
||||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||||
|
|
||||||
|
import Thumbnail from './Thumbnail';
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -60,6 +63,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_isFilmstripButtonEnabled: boolean,
|
_isFilmstripButtonEnabled: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participants in the call.
|
||||||
|
*/
|
||||||
|
_participants: Array<Object>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of rows in tile view.
|
* The number of rows in tile view.
|
||||||
*/
|
*/
|
||||||
|
@ -138,18 +146,15 @@ class Filmstrip extends Component <Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
// Note: Appending of {@code RemoteVideo} views is handled through
|
|
||||||
// VideoLayout. The views do not get blown away on render() because
|
|
||||||
// ReactDOMComponent is only aware of the given JSX and not new appended
|
|
||||||
// DOM. As such, when updateDOMProperties gets called, only attributes
|
|
||||||
// will get updated without replacing the DOM. If the known DOM gets
|
|
||||||
// modified, then the views will get blown away.
|
|
||||||
|
|
||||||
const filmstripStyle = { };
|
const filmstripStyle = { };
|
||||||
const filmstripRemoteVideosContainerStyle = {};
|
const filmstripRemoteVideosContainerStyle = {};
|
||||||
let remoteVideoContainerClassName = 'remote-videos-container';
|
let remoteVideoContainerClassName = 'remote-videos-container';
|
||||||
|
const { _currentLayout, _participants } = this.props;
|
||||||
|
const remoteParticipants = _participants.filter(p => !p.local);
|
||||||
|
const localParticipant = getLocalParticipant(_participants);
|
||||||
|
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||||
|
|
||||||
switch (this.props._currentLayout) {
|
switch (_currentLayout) {
|
||||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||||
// Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
|
// Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
|
||||||
// Also adding 7px for the scrollbar.
|
// Also adding 7px for the scrollbar.
|
||||||
|
@ -191,7 +196,13 @@ class Filmstrip extends Component <Props> {
|
||||||
<div
|
<div
|
||||||
className = 'filmstrip__videos'
|
className = 'filmstrip__videos'
|
||||||
id = 'filmstripLocalVideo'>
|
id = 'filmstripLocalVideo'>
|
||||||
<div id = 'filmstripLocalVideoThumbnail' />
|
<div id = 'filmstripLocalVideoThumbnail'>
|
||||||
|
{
|
||||||
|
!tileViewActive && <Thumbnail
|
||||||
|
key = 'local'
|
||||||
|
participantID = { localParticipant.id } />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className = { remoteVideosWrapperClassName }
|
className = { remoteVideosWrapperClassName }
|
||||||
|
@ -205,7 +216,21 @@ class Filmstrip extends Component <Props> {
|
||||||
className = { remoteVideoContainerClassName }
|
className = { remoteVideoContainerClassName }
|
||||||
id = 'filmstripRemoteVideosContainer'
|
id = 'filmstripRemoteVideosContainer'
|
||||||
style = { filmstripRemoteVideosContainerStyle }>
|
style = { filmstripRemoteVideosContainerStyle }>
|
||||||
<div id = 'localVideoTileViewContainer' />
|
{
|
||||||
|
remoteParticipants.map(
|
||||||
|
p => (
|
||||||
|
<Thumbnail
|
||||||
|
key = { `remote_${p.id}` }
|
||||||
|
participantID = { p.id } />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<div id = 'localVideoTileViewContainer'>
|
||||||
|
{
|
||||||
|
tileViewActive && <Thumbnail
|
||||||
|
key = 'local'
|
||||||
|
participantID = { localParticipant.id } />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -314,6 +339,7 @@ function _mapStateToProps(state) {
|
||||||
_hideScrollbar: Boolean(iAmSipGateway),
|
_hideScrollbar: Boolean(iAmSipGateway),
|
||||||
_hideToolbar: Boolean(iAmSipGateway),
|
_hideToolbar: Boolean(iAmSipGateway),
|
||||||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||||
|
_participants: state['features/base/participants'],
|
||||||
_rows: gridDimensions.rows,
|
_rows: gridDimensions.rows,
|
||||||
_videosClassName: videosClassName,
|
_videosClassName: videosClassName,
|
||||||
_visible: visible
|
_visible: visible
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
|
||||||
import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
||||||
import { Avatar } from '../../../base/avatar';
|
import { Avatar } from '../../../base/avatar';
|
||||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||||
|
@ -11,42 +11,72 @@ import AudioTrack from '../../../base/media/components/web/AudioTrack';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
getParticipantById,
|
getParticipantById,
|
||||||
getParticipantCount
|
getParticipantCount,
|
||||||
|
pinParticipant
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
import { isTestModeEnabled } from '../../../base/testing';
|
||||||
|
import {
|
||||||
|
getLocalAudioTrack,
|
||||||
|
getLocalVideoTrack,
|
||||||
|
getTrackByMediaTypeAndParticipant,
|
||||||
|
updateLastTrackVideoMediaEvent
|
||||||
|
} from '../../../base/tracks';
|
||||||
import { ConnectionIndicator } from '../../../connection-indicator';
|
import { ConnectionIndicator } from '../../../connection-indicator';
|
||||||
import { DisplayName } from '../../../display-name';
|
import { DisplayName } from '../../../display-name';
|
||||||
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
|
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
|
||||||
import { PresenceLabel } from '../../../presence-status';
|
import { PresenceLabel } from '../../../presence-status';
|
||||||
import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
|
|
||||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||||
|
import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
|
||||||
|
import {
|
||||||
|
DISPLAY_MODE_TO_CLASS_NAME,
|
||||||
|
DISPLAY_MODE_TO_STRING,
|
||||||
|
DISPLAY_VIDEO,
|
||||||
|
DISPLAY_VIDEO_WITH_NAME,
|
||||||
|
VIDEO_TEST_EVENTS
|
||||||
|
} from '../../constants';
|
||||||
|
import { isVideoPlayable, computeDisplayMode } from '../../functions';
|
||||||
|
import logger from '../../logger';
|
||||||
|
|
||||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} state of {@link Thumbnail}.
|
* The type of the React {@code Component} state of {@link Thumbnail}.
|
||||||
*/
|
*/
|
||||||
type State = {
|
export type State = {|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current audio level value for the Thumbnail.
|
* The current audio level value for the Thumbnail.
|
||||||
*/
|
*/
|
||||||
audioLevel: number,
|
audioLevel: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the canplay event has been received.
|
||||||
|
*/
|
||||||
|
canPlayEventReceived: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current display mode of the thumbnail.
|
||||||
|
*/
|
||||||
|
displayMode: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the thumbnail is hovered or not.
|
||||||
|
*/
|
||||||
|
isHovered: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current volume setting for the Thumbnail.
|
* The current volume setting for the Thumbnail.
|
||||||
*/
|
*/
|
||||||
volume: ?number
|
volume: ?number
|
||||||
};
|
|};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of {@link Thumbnail}.
|
* The type of the React {@code Component} props of {@link Thumbnail}.
|
||||||
*/
|
*/
|
||||||
type Props = {
|
export type Props = {|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The audio track related to the participant.
|
* The audio track related to the participant.
|
||||||
|
@ -73,11 +103,21 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_defaultLocalDisplayName: string,
|
_defaultLocalDisplayName: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the local video flip feature is disabled or not.
|
||||||
|
*/
|
||||||
|
_disableLocalVideoFlip: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether the profile functionality is disabled.
|
* Indicates whether the profile functionality is disabled.
|
||||||
*/
|
*/
|
||||||
_disableProfile: boolean,
|
_disableProfile: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The display mode of the thumbnail.
|
||||||
|
*/
|
||||||
|
_displayMode: number,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The height of the Thumbnail.
|
* The height of the Thumbnail.
|
||||||
*/
|
*/
|
||||||
|
@ -88,16 +128,51 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_heightToWidthPercent: number,
|
_heightToWidthPercent: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the thumbnail should be hidden or not.
|
||||||
|
*/
|
||||||
|
_isHidden: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether audio only mode is enabled.
|
||||||
|
*/
|
||||||
|
_isAudioOnly: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the participant associated with the thumbnail is displayed on the large video.
|
||||||
|
*/
|
||||||
|
_isCurrentlyOnLargeVideo: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the participant is screen sharing.
|
||||||
|
*/
|
||||||
|
_isScreenSharing: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the video associated with the thumbnail is playable.
|
||||||
|
*/
|
||||||
|
_isVideoPlayable: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable/enable the dominant speaker indicator.
|
* Disable/enable the dominant speaker indicator.
|
||||||
*/
|
*/
|
||||||
_isDominantSpeakerDisabled: boolean,
|
_isDominantSpeakerDisabled: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether testing mode is enabled.
|
||||||
|
*/
|
||||||
|
_isTestModeEnabled: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of the icon of indicators.
|
* The size of the icon of indicators.
|
||||||
*/
|
*/
|
||||||
_indicatorIconSize: number,
|
_indicatorIconSize: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current local video flip setting.
|
||||||
|
*/
|
||||||
|
_localFlipX: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object with information about the participant related to the thumbnaul.
|
* An object with information about the participant related to the thumbnaul.
|
||||||
*/
|
*/
|
||||||
|
@ -128,16 +203,23 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the thumbnail is hovered or not.
|
|
||||||
*/
|
|
||||||
isHovered: ?boolean,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the participant related to the thumbnail.
|
* The ID of the participant related to the thumbnail.
|
||||||
*/
|
*/
|
||||||
participantID: ?string
|
participantID: ?string
|
||||||
};
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for the display name container.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The click event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function onClick(event) {
|
||||||
|
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
|
||||||
|
// needs to be stopped.
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a thumbnail.
|
* Implements a thumbnail.
|
||||||
|
@ -145,7 +227,6 @@ type Props = {
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class Thumbnail extends Component<Props, State> {
|
class Thumbnail extends Component<Props, State> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new Thumbnail instance.
|
* Initializes a new Thumbnail instance.
|
||||||
*
|
*
|
||||||
|
@ -155,14 +236,27 @@ class Thumbnail extends Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
const state = {
|
||||||
audioLevel: 0,
|
audioLevel: 0,
|
||||||
volume: undefined
|
canPlayEventReceived: false,
|
||||||
|
isHovered: false,
|
||||||
|
volume: undefined,
|
||||||
|
displayMode: DISPLAY_VIDEO
|
||||||
|
};
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
...state,
|
||||||
|
displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state))
|
||||||
};
|
};
|
||||||
|
|
||||||
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
||||||
|
this._onCanPlay = this._onCanPlay.bind(this);
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
this._onVolumeChange = this._onVolumeChange.bind(this);
|
this._onVolumeChange = this._onVolumeChange.bind(this);
|
||||||
this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
|
this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
|
||||||
|
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||||
|
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||||
|
this._onTestingEvent = this._onTestingEvent.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -173,6 +267,7 @@ class Thumbnail extends Component<Props, State> {
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._listenForAudioUpdates();
|
this._listenForAudioUpdates();
|
||||||
|
this._onDisplayModeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,12 +277,121 @@ class Thumbnail extends Component<Props, State> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
if (prevProps._audioTrack !== this.props._audioTrack) {
|
if (prevProps._audioTrack !== this.props._audioTrack) {
|
||||||
this._stopListeningForAudioUpdates(prevProps._audioTrack);
|
this._stopListeningForAudioUpdates(prevProps._audioTrack);
|
||||||
this._listenForAudioUpdates();
|
this._listenForAudioUpdates();
|
||||||
this._updateAudioLevel(0);
|
this._updateAudioLevel(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevState.displayMode !== this.state.displayMode) {
|
||||||
|
this._onDisplayModeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles display mode changes.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDisplayModeChanged() {
|
||||||
|
const input = Thumbnail.getDisplayModeInput(this.props, this.state);
|
||||||
|
const displayModeString = DISPLAY_MODE_TO_STRING[this.state.displayMode];
|
||||||
|
const id = this.props._participant?.id;
|
||||||
|
|
||||||
|
this._maybeSendScreenSharingIssueEvents(input);
|
||||||
|
logger.debug(`Displaying ${displayModeString} for ${id}, data: [${JSON.stringify(input)}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends screen sharing issue event if an issue is detected.
|
||||||
|
*
|
||||||
|
* @param {Object} input - The input used to compute the thumbnail display mode.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_maybeSendScreenSharingIssueEvents(input) {
|
||||||
|
const {
|
||||||
|
_currentLayout,
|
||||||
|
_isAudioOnly,
|
||||||
|
_isScreenSharing
|
||||||
|
} = this.props;
|
||||||
|
const { displayMode } = this.state;
|
||||||
|
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||||
|
|
||||||
|
if (![ DISPLAY_VIDEO, DISPLAY_VIDEO_WITH_NAME ].includes(displayMode)
|
||||||
|
&& tileViewActive
|
||||||
|
&& _isScreenSharing
|
||||||
|
&& !_isAudioOnly) {
|
||||||
|
sendAnalytics(createScreenSharingIssueEvent({
|
||||||
|
source: 'thumbnail',
|
||||||
|
...input
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#getDerivedStateFromProps()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
static getDerivedStateFromProps(props: Props, prevState: State) {
|
||||||
|
if (!props._videoTrack && prevState.canPlayEventReceived) {
|
||||||
|
const newState = {
|
||||||
|
...prevState,
|
||||||
|
canPlayEventReceived: false
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...newState,
|
||||||
|
dispayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, newState))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDisplayMode = computeDisplayMode(Thumbnail.getDisplayModeInput(props, prevState));
|
||||||
|
|
||||||
|
if (newDisplayMode !== prevState.displayMode) {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
displayMode: newDisplayMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts information for props and state needed to compute the display mode.
|
||||||
|
*
|
||||||
|
* @param {Props} props - The component's props.
|
||||||
|
* @param {State} state - The component's state.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
static getDisplayModeInput(props: Props, state: State) {
|
||||||
|
const {
|
||||||
|
_currentLayout,
|
||||||
|
_isAudioOnly,
|
||||||
|
_isCurrentlyOnLargeVideo,
|
||||||
|
_isScreenSharing,
|
||||||
|
_isVideoPlayable,
|
||||||
|
_participant,
|
||||||
|
_videoTrack
|
||||||
|
} = props;
|
||||||
|
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||||
|
const { canPlayEventReceived, isHovered } = state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
|
||||||
|
isHovered,
|
||||||
|
isAudioOnly: _isAudioOnly,
|
||||||
|
tileViewActive,
|
||||||
|
isVideoPlayable: _isVideoPlayable,
|
||||||
|
connectionStatus: _participant?.connectionStatus,
|
||||||
|
canPlayEventReceived,
|
||||||
|
videoStream: Boolean(_videoTrack),
|
||||||
|
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
|
||||||
|
isScreenSharing: _isScreenSharing,
|
||||||
|
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -253,8 +457,14 @@ class Thumbnail extends Component<Props, State> {
|
||||||
* @returns {Object} - The styles for the thumbnail.
|
* @returns {Object} - The styles for the thumbnail.
|
||||||
*/
|
*/
|
||||||
_getStyles(): Object {
|
_getStyles(): Object {
|
||||||
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
|
const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
|
||||||
let styles;
|
let styles: {
|
||||||
|
thumbnail: Object,
|
||||||
|
avatar: Object
|
||||||
|
} = {
|
||||||
|
thumbnail: {},
|
||||||
|
avatar: {}
|
||||||
|
};
|
||||||
|
|
||||||
switch (_currentLayout) {
|
switch (_currentLayout) {
|
||||||
case LAYOUTS.TILE_VIEW:
|
case LAYOUTS.TILE_VIEW:
|
||||||
|
@ -262,7 +472,13 @@ class Thumbnail extends Component<Props, State> {
|
||||||
const avatarSize = _height / 2;
|
const avatarSize = _height / 2;
|
||||||
|
|
||||||
styles = {
|
styles = {
|
||||||
avatarContainer: {
|
thumbnail: {
|
||||||
|
height: `${_height}px`,
|
||||||
|
minHeight: `${_height}px`,
|
||||||
|
minWidth: `${_width}px`,
|
||||||
|
width: `${_width}px`
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
height: `${avatarSize}px`,
|
height: `${avatarSize}px`,
|
||||||
width: `${avatarSize}px`
|
width: `${avatarSize}px`
|
||||||
}
|
}
|
||||||
|
@ -271,7 +487,10 @@ class Thumbnail extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||||
styles = {
|
styles = {
|
||||||
avatarContainer: {
|
thumbnail: {
|
||||||
|
paddingTop: `${_heightToWidthPercent}%`
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
height: '50%',
|
height: '50%',
|
||||||
width: `${_heightToWidthPercent / 2}%`
|
width: `${_heightToWidthPercent / 2}%`
|
||||||
}
|
}
|
||||||
|
@ -280,9 +499,49 @@ class Thumbnail extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isHidden) {
|
||||||
|
styles.thumbnail.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onClick: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On click handler.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClick() {
|
||||||
|
const { _participant, dispatch } = this.props;
|
||||||
|
const { id, pinned } = _participant;
|
||||||
|
|
||||||
|
dispatch(pinParticipant(pinned ? null : id));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMouseEnter: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mouse enter handler.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onMouseEnter() {
|
||||||
|
this.setState({ isHovered: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMouseLeave: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mouse leave handler.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onMouseLeave() {
|
||||||
|
this.setState({ isHovered: false });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a fake participant (youtube video) thumbnail.
|
* Renders a fake participant (youtube video) thumbnail.
|
||||||
*
|
*
|
||||||
|
@ -292,9 +551,17 @@ class Thumbnail extends Component<Props, State> {
|
||||||
_renderFakeParticipant() {
|
_renderFakeParticipant() {
|
||||||
const { _participant } = this.props;
|
const { _participant } = this.props;
|
||||||
const { id } = _participant;
|
const { id } = _participant;
|
||||||
|
const styles = this._getStyles();
|
||||||
|
const containerClassName = this._getContainerClassName();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span
|
||||||
|
className = { containerClassName }
|
||||||
|
id = 'sharedVideoContainer'
|
||||||
|
onClick = { this._onClick }
|
||||||
|
onMouseEnter = { this._onMouseEnter }
|
||||||
|
onMouseLeave = { this._onMouseLeave }
|
||||||
|
style = { styles.thumbnail }>
|
||||||
<img
|
<img
|
||||||
className = 'sharedVideoAvatar'
|
className = 'sharedVideoAvatar'
|
||||||
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
|
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
|
||||||
|
@ -303,7 +570,7 @@ class Thumbnail extends Component<Props, State> {
|
||||||
elementID = 'sharedVideoContainer_name'
|
elementID = 'sharedVideoContainer_name'
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,9 +587,9 @@ class Thumbnail extends Component<Props, State> {
|
||||||
_isDominantSpeakerDisabled,
|
_isDominantSpeakerDisabled,
|
||||||
_indicatorIconSize: iconSize,
|
_indicatorIconSize: iconSize,
|
||||||
_participant,
|
_participant,
|
||||||
_participantCount,
|
_participantCount
|
||||||
isHovered
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { isHovered } = this.state;
|
||||||
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
||||||
const { id, local = false, dominantSpeaker = false } = _participant;
|
const { id, local = false, dominantSpeaker = false } = _participant;
|
||||||
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
|
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
|
||||||
|
@ -344,43 +611,41 @@ class Thumbnail extends Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AtlasKitThemeProvider mode = 'dark'>
|
{ !_connectionIndicatorDisabled
|
||||||
{ !_connectionIndicatorDisabled
|
&& <ConnectionIndicator
|
||||||
&& <ConnectionIndicator
|
alwaysVisible = { showConnectionIndicator }
|
||||||
alwaysVisible = { showConnectionIndicator }
|
enableStatsDisplay = { true }
|
||||||
enableStatsDisplay = { true }
|
|
||||||
iconSize = { iconSize }
|
|
||||||
isLocalVideo = { local }
|
|
||||||
participantId = { id }
|
|
||||||
statsPopoverPosition = { statsPopoverPosition } />
|
|
||||||
}
|
|
||||||
<RaisedHandIndicator
|
|
||||||
iconSize = { iconSize }
|
iconSize = { iconSize }
|
||||||
|
isLocalVideo = { local }
|
||||||
participantId = { id }
|
participantId = { id }
|
||||||
|
statsPopoverPosition = { statsPopoverPosition } />
|
||||||
|
}
|
||||||
|
<RaisedHandIndicator
|
||||||
|
iconSize = { iconSize }
|
||||||
|
participantId = { id }
|
||||||
|
tooltipPosition = { tooltipPosition } />
|
||||||
|
{ showDominantSpeaker && _participantCount > 2
|
||||||
|
&& <DominantSpeakerIndicator
|
||||||
|
iconSize = { iconSize }
|
||||||
tooltipPosition = { tooltipPosition } />
|
tooltipPosition = { tooltipPosition } />
|
||||||
{ showDominantSpeaker && _participantCount > 2
|
}
|
||||||
&& <DominantSpeakerIndicator
|
|
||||||
iconSize = { iconSize }
|
|
||||||
tooltipPosition = { tooltipPosition } />
|
|
||||||
}
|
|
||||||
</AtlasKitThemeProvider>
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the avatar.
|
* Renders the avatar.
|
||||||
*
|
*
|
||||||
|
* @param {Object} styles - The styles that will be applied to the avatar.
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
_renderAvatar() {
|
_renderAvatar(styles) {
|
||||||
const { _participant } = this.props;
|
const { _participant } = this.props;
|
||||||
const { id } = _participant;
|
const { id } = _participant;
|
||||||
const styles = this._getStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className = 'avatar-container'
|
className = 'avatar-container'
|
||||||
style = { styles.avatarContainer }>
|
style = { styles }>
|
||||||
<Avatar
|
<Avatar
|
||||||
className = 'userAvatar'
|
className = 'userAvatar'
|
||||||
participantId = { id } />
|
participantId = { id } />
|
||||||
|
@ -388,6 +653,38 @@ class Thumbnail extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the container class name.
|
||||||
|
*
|
||||||
|
* @returns {string} - The class name that will be used for the container.
|
||||||
|
*/
|
||||||
|
_getContainerClassName() {
|
||||||
|
let className = 'videocontainer';
|
||||||
|
const { displayMode } = this.state;
|
||||||
|
const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
|
||||||
|
const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
|
||||||
|
|
||||||
|
className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
|
||||||
|
|
||||||
|
if (_participant?.pinned) {
|
||||||
|
className += ' videoContainerFocused';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
|
||||||
|
className += ' active-speaker';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isHidden) {
|
||||||
|
className += ' hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRemoteParticipant && _isAudioOnly) {
|
||||||
|
className += ' audio-only';
|
||||||
|
}
|
||||||
|
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the local participant's thumbnail.
|
* Renders the local participant's thumbnail.
|
||||||
*
|
*
|
||||||
|
@ -396,19 +693,33 @@ class Thumbnail extends Component<Props, State> {
|
||||||
_renderLocalParticipant() {
|
_renderLocalParticipant() {
|
||||||
const {
|
const {
|
||||||
_defaultLocalDisplayName,
|
_defaultLocalDisplayName,
|
||||||
|
_disableLocalVideoFlip,
|
||||||
|
_isScreenSharing,
|
||||||
|
_localFlipX,
|
||||||
_disableProfile,
|
_disableProfile,
|
||||||
_participant,
|
_participant,
|
||||||
_videoTrack
|
_videoTrack
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { id } = _participant || {};
|
const { id } = _participant || {};
|
||||||
const { audioLevel } = this.state;
|
const { audioLevel } = this.state;
|
||||||
|
const styles = this._getStyles();
|
||||||
|
const containerClassName = this._getContainerClassName();
|
||||||
|
const videoTrackClassName
|
||||||
|
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span
|
||||||
|
className = { containerClassName }
|
||||||
|
id = 'localVideoContainer'
|
||||||
|
onClick = { this._onClick }
|
||||||
|
onMouseEnter = { this._onMouseEnter }
|
||||||
|
onMouseLeave = { this._onMouseLeave }
|
||||||
|
style = { styles.thumbnail }>
|
||||||
<div className = 'videocontainer__background' />
|
<div className = 'videocontainer__background' />
|
||||||
<span id = 'localVideoWrapper'>
|
<span id = 'localVideoWrapper'>
|
||||||
<VideoTrack
|
<VideoTrack
|
||||||
|
className = { videoTrackClassName }
|
||||||
id = 'localVideo_container'
|
id = 'localVideo_container'
|
||||||
videoTrack = { _videoTrack } />
|
videoTrack = { _videoTrack } />
|
||||||
</span>
|
</span>
|
||||||
|
@ -419,21 +730,64 @@ class Thumbnail extends Component<Props, State> {
|
||||||
{ this._renderTopIndicators() }
|
{ this._renderTopIndicators() }
|
||||||
</div>
|
</div>
|
||||||
<div className = 'videocontainer__hoverOverlay' />
|
<div className = 'videocontainer__hoverOverlay' />
|
||||||
<div className = 'displayNameContainer'>
|
<div
|
||||||
|
className = 'displayNameContainer'
|
||||||
|
onClick = { onClick }>
|
||||||
<DisplayName
|
<DisplayName
|
||||||
allowEditing = { !_disableProfile }
|
allowEditing = { !_disableProfile }
|
||||||
displayNameSuffix = { _defaultLocalDisplayName }
|
displayNameSuffix = { _defaultLocalDisplayName }
|
||||||
elementID = 'localDisplayName'
|
elementID = 'localDisplayName'
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</div>
|
</div>
|
||||||
{ this._renderAvatar() }
|
{ this._renderAvatar(styles.avatar) }
|
||||||
|
<span className = 'localvideomenu'>
|
||||||
|
<LocalVideoMenuTriggerButton />
|
||||||
|
</span>
|
||||||
<span className = 'audioindicator-container'>
|
<span className = 'audioindicator-container'>
|
||||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onCanPlay: Object => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canplay event listener.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onCanPlay(event) {
|
||||||
|
this.setState({ canPlayEventReceived: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
_isTestModeEnabled,
|
||||||
|
_videoTrack
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (_videoTrack && _isTestModeEnabled) {
|
||||||
|
this._onTestingEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onTestingEvent: Object => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for testing events.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onTestingEvent(event) {
|
||||||
|
const {
|
||||||
|
_videoTrack,
|
||||||
|
dispatch
|
||||||
|
} = this.props;
|
||||||
|
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
|
||||||
|
|
||||||
|
dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a remote participant's 'thumbnail.
|
* Renders a remote participant's 'thumbnail.
|
||||||
|
@ -443,29 +797,59 @@ class Thumbnail extends Component<Props, State> {
|
||||||
_renderRemoteParticipant() {
|
_renderRemoteParticipant() {
|
||||||
const {
|
const {
|
||||||
_audioTrack,
|
_audioTrack,
|
||||||
|
_isTestModeEnabled,
|
||||||
_participant,
|
_participant,
|
||||||
_startSilent
|
_startSilent,
|
||||||
|
_videoTrack
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { id } = _participant;
|
const { id } = _participant;
|
||||||
const { audioLevel, volume } = this.state;
|
const { audioLevel, canPlayEventReceived, volume } = this.state;
|
||||||
|
const styles = this._getStyles();
|
||||||
|
const containerClassName = this._getContainerClassName();
|
||||||
|
|
||||||
// hide volume when in silent mode
|
// hide volume when in silent mode
|
||||||
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
|
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
|
||||||
const jitsiTrack = _audioTrack?.jitsiTrack;
|
const jitsiAudioTrack = _audioTrack?.jitsiTrack;
|
||||||
const audioTrackId = jitsiTrack && jitsiTrack.getId();
|
const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
|
||||||
|
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
|
||||||
|
const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
|
||||||
|
const videoEventListeners = {};
|
||||||
|
|
||||||
|
if (_videoTrack && _isTestModeEnabled) {
|
||||||
|
VIDEO_TEST_EVENTS.forEach(attribute => {
|
||||||
|
videoEventListeners[attribute] = this._onTestingEvent;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
videoEventListeners.onCanPlay = this._onCanPlay;
|
||||||
|
|
||||||
|
const videoElementStyle = canPlayEventReceived ? null : {
|
||||||
|
display: 'none'
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span
|
||||||
|
className = { containerClassName }
|
||||||
|
id = { `participant_${id}` }
|
||||||
|
onClick = { this._onClick }
|
||||||
|
onMouseEnter = { this._onMouseEnter }
|
||||||
|
onMouseLeave = { this._onMouseLeave }
|
||||||
|
style = { styles.thumbnail }>
|
||||||
{
|
{
|
||||||
_audioTrack
|
_videoTrack && <VideoTrack
|
||||||
? <AudioTrack
|
eventHandlers = { videoEventListeners }
|
||||||
audioTrack = { _audioTrack }
|
id = { `remoteVideo_${videoTrackId || ''}` }
|
||||||
id = { `remoteAudio_${audioTrackId || ''}` }
|
muted = { true }
|
||||||
muted = { _startSilent }
|
style = { videoElementStyle }
|
||||||
onInitialVolumeSet = { this._onInitialVolumeSet }
|
videoTrack = { _videoTrack } />
|
||||||
volume = { this.state.volume } />
|
}
|
||||||
: null
|
{
|
||||||
|
_audioTrack && <AudioTrack
|
||||||
|
audioTrack = { _audioTrack }
|
||||||
|
id = { `remoteAudio_${audioTrackId || ''}` }
|
||||||
|
muted = { _startSilent }
|
||||||
|
onInitialVolumeSet = { this._onInitialVolumeSet }
|
||||||
|
volume = { volume } />
|
||||||
}
|
}
|
||||||
<div className = 'videocontainer__background' />
|
<div className = 'videocontainer__background' />
|
||||||
<div className = 'videocontainer__toptoolbar'>
|
<div className = 'videocontainer__toptoolbar'>
|
||||||
|
@ -480,24 +864,22 @@ class Thumbnail extends Component<Props, State> {
|
||||||
elementID = { `participant_${id}_name` }
|
elementID = { `participant_${id}_name` }
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</div>
|
</div>
|
||||||
{ this._renderAvatar() }
|
{ this._renderAvatar(styles.avatar) }
|
||||||
<div className = 'presence-label-container'>
|
<div className = 'presence-label-container'>
|
||||||
<PresenceLabel
|
<PresenceLabel
|
||||||
className = 'presence-label'
|
className = 'presence-label'
|
||||||
participantID = { id } />
|
participantID = { id } />
|
||||||
</div>
|
</div>
|
||||||
<span className = 'remotevideomenu'>
|
<span className = 'remotevideomenu'>
|
||||||
<AtlasKitThemeProvider mode = 'dark'>
|
<RemoteVideoMenuTriggerButton
|
||||||
<RemoteVideoMenuTriggerButton
|
initialVolumeValue = { volume }
|
||||||
initialVolumeValue = { volume }
|
onVolumeChange = { onVolumeChange }
|
||||||
onVolumeChange = { onVolumeChange }
|
participantID = { id } />
|
||||||
participantID = { id } />
|
|
||||||
</AtlasKitThemeProvider>
|
|
||||||
</span>
|
</span>
|
||||||
<span className = 'audioindicator-container'>
|
<span className = 'audioindicator-container'>
|
||||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -527,7 +909,6 @@ class Thumbnail extends Component<Props, State> {
|
||||||
this.setState({ volume: value });
|
this.setState({ volume: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -568,17 +949,24 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
|
|
||||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
// 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 participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
||||||
|
const { id } = participant;
|
||||||
const isLocal = participant?.local ?? true;
|
const isLocal = participant?.local ?? true;
|
||||||
|
const tracks = state['features/base/tracks'];
|
||||||
const _videoTrack = isLocal
|
const _videoTrack = isLocal
|
||||||
? getLocalVideoTrack(state['features/base/tracks'])
|
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
|
||||||
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
|
|
||||||
const _audioTrack = isLocal
|
const _audioTrack = isLocal
|
||||||
? getLocalAudioTrack(state['features/base/tracks'])
|
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
|
||||||
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
|
|
||||||
const _currentLayout = getCurrentLayout(state);
|
const _currentLayout = getCurrentLayout(state);
|
||||||
let size = {};
|
let size = {};
|
||||||
const { startSilent, disableProfile = false } = state['features/base/config'];
|
const {
|
||||||
|
startSilent,
|
||||||
|
disableLocalVideoFlip,
|
||||||
|
disableProfile,
|
||||||
|
iAmRecorder,
|
||||||
|
iAmSipGateway
|
||||||
|
} = state['features/base/config'];
|
||||||
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
|
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
|
||||||
|
const { localFlipX } = state['features/base/settings'];
|
||||||
|
|
||||||
|
|
||||||
switch (_currentLayout) {
|
switch (_currentLayout) {
|
||||||
|
@ -617,16 +1005,23 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_audioTrack,
|
_audioTrack,
|
||||||
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
||||||
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||||
_currentLayout,
|
_currentLayout,
|
||||||
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
||||||
|
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
|
||||||
_disableProfile: disableProfile,
|
_disableProfile: disableProfile,
|
||||||
|
_isHidden: isLocal && iAmRecorder && !iAmSipGateway,
|
||||||
|
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
|
||||||
|
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
|
||||||
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
|
||||||
|
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
||||||
|
_isTestModeEnabled: isTestModeEnabled(state),
|
||||||
|
_isVideoPlayable: isVideoPlayable(state, id),
|
||||||
_indicatorIconSize: NORMAL,
|
_indicatorIconSize: NORMAL,
|
||||||
|
_localFlipX: Boolean(localFlipX),
|
||||||
_participant: participant,
|
_participant: participant,
|
||||||
_participantCount: getParticipantCount(state),
|
_participantCount: getParticipantCount(state),
|
||||||
_startSilent: Boolean(startSilent),
|
_startSilent: Boolean(startSilent),
|
||||||
|
|
|
@ -47,3 +47,92 @@ export const DEFAULT_MAX_COLUMNS = 5;
|
||||||
* An extended number of columns for tile view.
|
* An extended number of columns for tile view.
|
||||||
*/
|
*/
|
||||||
export const ABSOLUTE_MAX_COLUMNS = 7;
|
export const ABSOLUTE_MAX_COLUMNS = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of attributes of the video element that will be used for adding a listener for every event in the list.
|
||||||
|
* The latest event will be stored in redux. This is currently used by torture only.
|
||||||
|
*/
|
||||||
|
export const VIDEO_TEST_EVENTS = [
|
||||||
|
'onAbort',
|
||||||
|
'onCanPlay',
|
||||||
|
'onCanPlayThrough',
|
||||||
|
'onEmptied',
|
||||||
|
'onEnded',
|
||||||
|
'onError',
|
||||||
|
'onLoadedData',
|
||||||
|
'onLoadedMetadata',
|
||||||
|
'onLoadStart',
|
||||||
|
'onPause',
|
||||||
|
'onPlay',
|
||||||
|
'onPlaying',
|
||||||
|
'onRateChange',
|
||||||
|
'onStalled',
|
||||||
|
'onSuspend',
|
||||||
|
'onWaiting'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode constant used when video is being displayed on the small video.
|
||||||
|
* @type {number}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_VIDEO = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode constant used when the user's avatar is being displayed on
|
||||||
|
* the small video.
|
||||||
|
* @type {number}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_AVATAR = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode constant used when neither video nor avatar is being displayed
|
||||||
|
* on the small video. And we just show the display name.
|
||||||
|
* @type {number}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_BLACKNESS_WITH_NAME = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode constant used when video is displayed and display name
|
||||||
|
* at the same time.
|
||||||
|
* @type {number}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_VIDEO_WITH_NAME = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode constant used when neither video nor avatar is being displayed
|
||||||
|
* on the small video. And we just show the display name.
|
||||||
|
* @type {number}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_AVATAR_WITH_NAME = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the display modes to class name that will be applied on the thumbnail container.
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_MODE_TO_CLASS_NAME = [
|
||||||
|
'display-video',
|
||||||
|
'display-avatar-only',
|
||||||
|
'display-name-on-black',
|
||||||
|
'display-name-on-video',
|
||||||
|
'display-avatar-with-name'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the display modes to string.
|
||||||
|
* @type {Array<string>}
|
||||||
|
* @constant
|
||||||
|
*/
|
||||||
|
export const DISPLAY_MODE_TO_STRING = [
|
||||||
|
'video',
|
||||||
|
'avatar',
|
||||||
|
'blackness-with-name',
|
||||||
|
'video-with-name',
|
||||||
|
'avatar-with-name'
|
||||||
|
];
|
||||||
|
|
|
@ -16,7 +16,16 @@ import {
|
||||||
isRemoteTrackMuted
|
isRemoteTrackMuted
|
||||||
} from '../base/tracks/functions';
|
} from '../base/tracks/functions';
|
||||||
|
|
||||||
import { ASPECT_RATIO_BREAKPOINT, SQUARE_TILE_ASPECT_RATIO, TILE_ASPECT_RATIO } from './constants';
|
import {
|
||||||
|
ASPECT_RATIO_BREAKPOINT,
|
||||||
|
DISPLAY_AVATAR,
|
||||||
|
DISPLAY_AVATAR_WITH_NAME,
|
||||||
|
DISPLAY_BLACKNESS_WITH_NAME,
|
||||||
|
DISPLAY_VIDEO,
|
||||||
|
DISPLAY_VIDEO_WITH_NAME,
|
||||||
|
SQUARE_TILE_ASPECT_RATIO,
|
||||||
|
TILE_ASPECT_RATIO
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -176,3 +185,36 @@ export function getVerticalFilmstripVisibleAreaWidth() {
|
||||||
|
|
||||||
return Math.min(filmstripMaxWidth, window.innerWidth);
|
return Math.min(filmstripMaxWidth, window.innerWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes information that determine the display mode.
|
||||||
|
*
|
||||||
|
* @param {Object} input - Obejct containing all necessary information for determining the display mode for
|
||||||
|
* the thumbnail.
|
||||||
|
* @returns {number} - One of <tt>DISPLAY_VIDEO</tt>, <tt>DISPLAY_AVATAR</tt> or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
|
||||||
|
*/
|
||||||
|
export function computeDisplayMode(input: Object) {
|
||||||
|
const {
|
||||||
|
isAudioOnly,
|
||||||
|
isCurrentlyOnLargeVideo,
|
||||||
|
isScreenSharing,
|
||||||
|
canPlayEventReceived,
|
||||||
|
isHovered,
|
||||||
|
isRemoteParticipant,
|
||||||
|
tileViewActive
|
||||||
|
} = input;
|
||||||
|
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
|
||||||
|
|
||||||
|
if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
|
||||||
|
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||||
|
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
|
||||||
|
// Display name is always and only displayed when user is on the stage
|
||||||
|
return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
||||||
|
} else if (adjustedIsVideoPlayable && !isAudioOnly) {
|
||||||
|
// check hovering and change state to video with name
|
||||||
|
return isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check hovering and change state to avatar with name
|
||||||
|
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { getLogger } from '../base/logging/functions';
|
||||||
|
|
||||||
|
export default getLogger('features/filmstrip');
|
|
@ -1,15 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
||||||
|
import { SETTINGS_UPDATED } from '../base/settings';
|
||||||
import {
|
import {
|
||||||
getCurrentLayout,
|
getCurrentLayout,
|
||||||
LAYOUTS,
|
LAYOUTS
|
||||||
shouldDisplayTileView
|
|
||||||
} from '../video-layout';
|
} from '../video-layout';
|
||||||
|
|
||||||
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
|
||||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||||
|
|
||||||
import './subscriber.web';
|
import './subscriber.web';
|
||||||
|
@ -48,29 +47,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SET_TILE_VIEW_DIMENSIONS: {
|
case SETTINGS_UPDATED: {
|
||||||
const state = store.getState();
|
if (typeof action.settings?.localFlipX === 'boolean') {
|
||||||
|
// TODO: This needs to be removed once the large video is Reactified.
|
||||||
if (shouldDisplayTileView(state)) {
|
VideoLayout.onLocalFlipXChanged();
|
||||||
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
|
|
||||||
|
|
||||||
// Once the thumbnails are reactified this should be moved there too.
|
|
||||||
Filmstrip.resizeThumbnailsForTileView(width, height, true);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SET_HORIZONTAL_VIEW_DIMENSIONS: {
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
if (getCurrentLayout(state) === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
|
|
||||||
const { horizontalViewDimensions = {} } = state['features/filmstrip'];
|
|
||||||
|
|
||||||
// Once the thumbnails are reactified this should be moved there too.
|
|
||||||
Filmstrip.resizeThumbnailsForHorizontalView(horizontalViewDimensions, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
|
||||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
|
||||||
import { StateListenerRegistry, equals } from '../base/redux';
|
import { StateListenerRegistry, equals } from '../base/redux';
|
||||||
import { setFilmstripVisible } from '../filmstrip/actions';
|
import { setFilmstripVisible } from '../filmstrip/actions';
|
||||||
import { setOverflowDrawer } from '../toolbox/actions.web';
|
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||||
|
@ -71,32 +69,9 @@ StateListenerRegistry.register(
|
||||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||||
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
|
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
|
||||||
break;
|
break;
|
||||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
|
||||||
// Once the thumbnails are reactified this should be moved there too.
|
|
||||||
Filmstrip.resizeThumbnailsForVerticalView();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles on stage participant updates.
|
|
||||||
*/
|
|
||||||
StateListenerRegistry.register(
|
|
||||||
/* selector */ state => state['features/large-video'].participantId,
|
|
||||||
/* listener */ (participantId, store, oldParticipantId) => {
|
|
||||||
const newThumbnail = VideoLayout.getSmallVideo(participantId);
|
|
||||||
const oldThumbnail = VideoLayout.getSmallVideo(oldParticipantId);
|
|
||||||
|
|
||||||
if (newThumbnail) {
|
|
||||||
newThumbnail.updateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldThumbnail) {
|
|
||||||
oldThumbnail.updateView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens for changes in the chat state to calculate the dimensions of the tile view grid and the tiles.
|
* Listens for changes in the chat state to calculate the dimensions of the tile view grid and the tiles.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -34,7 +34,7 @@ import { toggleScreensharing } from '../../base/tracks';
|
||||||
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
||||||
import { openChat } from '../../chat/actions';
|
import { openChat } from '../../chat/actions';
|
||||||
import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
|
import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
|
||||||
import { muteLocal } from '../../remote-video-menu/actions';
|
import { muteLocal } from '../../video-menu/actions';
|
||||||
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
|
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
|
||||||
|
|
||||||
import { setParticipantsWithScreenShare } from './actions';
|
import { setParticipantsWithScreenShare } from './actions';
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
/* @flow */
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the React {@code Component} props of {@link RemoteVideoMenu}.
|
|
||||||
*/
|
|
||||||
type Props = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The components to place as the body of the {@code RemoteVideoMenu}.
|
|
||||||
*/
|
|
||||||
children: React$Node,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The id attribute to be added to the component's DOM for retrieval when
|
|
||||||
* querying the DOM. Not used directly by the component.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React {@code Component} responsible for displaying other components as a menu
|
|
||||||
* for manipulating remote participant state.
|
|
||||||
*
|
|
||||||
* @extends {Component}
|
|
||||||
*/
|
|
||||||
export default class RemoteVideoMenu extends Component<Props> {
|
|
||||||
/**
|
|
||||||
* Implements React's {@link Component#render()}.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
className = 'popupmenu'
|
|
||||||
id = { this.props.id }>
|
|
||||||
{ this.props.children }
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ import { connect } from '../../base/redux';
|
||||||
import { AbstractAudioMuteButton } from '../../base/toolbox/components';
|
import { AbstractAudioMuteButton } from '../../base/toolbox/components';
|
||||||
import type { AbstractButtonProps } from '../../base/toolbox/components';
|
import type { AbstractButtonProps } from '../../base/toolbox/components';
|
||||||
import { isLocalTrackMuted } from '../../base/tracks';
|
import { isLocalTrackMuted } from '../../base/tracks';
|
||||||
import { muteLocal } from '../../remote-video-menu/actions';
|
import { muteLocal } from '../../video-menu/actions';
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { IconMuteEveryone } from '../../base/icons';
|
||||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||||
import { MuteEveryoneDialog } from '../../remote-video-menu/components';
|
import { MuteEveryoneDialog } from '../../video-menu/components';
|
||||||
|
|
||||||
type Props = AbstractButtonProps & {
|
type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { IconMuteVideoEveryone } from '../../base/icons';
|
||||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||||
import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
|
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
||||||
|
|
||||||
type Props = AbstractButtonProps & {
|
type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,12 @@ import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
|
||||||
import { CONFERENCE_WILL_LEAVE } from '../base/conference';
|
import { CONFERENCE_WILL_LEAVE } from '../base/conference';
|
||||||
import { MEDIA_TYPE } from '../base/media';
|
import { MEDIA_TYPE } from '../base/media';
|
||||||
import {
|
import {
|
||||||
DOMINANT_SPEAKER_CHANGED,
|
getLocalParticipant,
|
||||||
PARTICIPANT_JOINED,
|
PARTICIPANT_JOINED,
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_UPDATED
|
||||||
PARTICIPANT_UPDATED,
|
|
||||||
PIN_PARTICIPANT,
|
|
||||||
getParticipantById
|
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
import { TRACK_ADDED, TRACK_REMOVED } from '../base/tracks';
|
import { TRACK_ADDED, TRACK_REMOVED, TRACK_STOPPED } from '../base/tracks';
|
||||||
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
|
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
|
||||||
|
|
||||||
import './middleware.any';
|
import './middleware.any';
|
||||||
|
@ -40,15 +37,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
|
|
||||||
case PARTICIPANT_JOINED:
|
case PARTICIPANT_JOINED:
|
||||||
if (!action.participant.local) {
|
if (!action.participant.local) {
|
||||||
VideoLayout.addRemoteParticipantContainer(
|
VideoLayout.updateVideoMutedForNoTracks(action.participant.id);
|
||||||
getParticipantById(store.getState(), action.participant.id));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PARTICIPANT_LEFT:
|
|
||||||
VideoLayout.removeParticipantContainer(action.participant.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PARTICIPANT_UPDATED: {
|
case PARTICIPANT_UPDATED: {
|
||||||
// Look for actions that triggered a change to connectionStatus. This is
|
// Look for actions that triggered a change to connectionStatus. This is
|
||||||
// done instead of changing the connection status change action to be
|
// done instead of changing the connection status change action to be
|
||||||
|
@ -61,27 +53,28 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case DOMINANT_SPEAKER_CHANGED:
|
|
||||||
VideoLayout.onDominantSpeakerChanged(action.participant.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PIN_PARTICIPANT:
|
|
||||||
VideoLayout.onPinChange(action.participant?.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SET_FILMSTRIP_VISIBLE:
|
case SET_FILMSTRIP_VISIBLE:
|
||||||
VideoLayout.resizeVideoArea();
|
VideoLayout.resizeVideoArea();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TRACK_ADDED:
|
case TRACK_ADDED:
|
||||||
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
if (action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
VideoLayout._updateLargeVideoIfDisplayed(action.track.participantId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TRACK_STOPPED: {
|
||||||
|
if (action.track.jitsiTrack.isLocal()) {
|
||||||
|
const participant = getLocalParticipant(store.getState);
|
||||||
|
|
||||||
|
VideoLayout._updateLargeVideoIfDisplayed(participant?.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case TRACK_REMOVED:
|
case TRACK_REMOVED:
|
||||||
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||||
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
|
VideoLayout.updateVideoMutedForNoTracks(action.track.jitsiTrack.getParticipantId());
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
sendAnalytics,
|
sendAnalytics,
|
||||||
VIDEO_MUTE
|
VIDEO_MUTE
|
||||||
} from '../analytics';
|
} from '../analytics';
|
||||||
import { hideDialog } from '../base/dialog';
|
|
||||||
import {
|
import {
|
||||||
MEDIA_TYPE,
|
MEDIA_TYPE,
|
||||||
setAudioMuted,
|
setAudioMuted,
|
||||||
|
@ -22,21 +21,10 @@ import {
|
||||||
muteRemoteParticipant
|
muteRemoteParticipant
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
|
|
||||||
import { RemoteVideoMenu } from './components';
|
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
const logger = getLogger(__filename);
|
const logger = getLogger(__filename);
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the remote video menu.
|
|
||||||
*
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
export function hideRemoteVideoMenu() {
|
|
||||||
return hideDialog(RemoteVideoMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutes the local participant.
|
* Mutes the local participant.
|
||||||
*
|
*
|
|
@ -0,0 +1,15 @@
|
||||||
|
// @flow
|
||||||
|
import { hideDialog } from '../base/dialog';
|
||||||
|
|
||||||
|
import { RemoteVideoMenu } from './components/native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the remote video menu.
|
||||||
|
*
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function hideRemoteVideoMenu() {
|
||||||
|
return hideDialog(RemoteVideoMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './actions.any';
|
|
@ -0,0 +1,2 @@
|
||||||
|
// @flow
|
||||||
|
export * from './actions.any';
|
|
@ -11,7 +11,7 @@ import { getParticipantDisplayName } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { StyleType } from '../../../base/styles';
|
import { StyleType } from '../../../base/styles';
|
||||||
import { PrivateMessageButton } from '../../../chat';
|
import { PrivateMessageButton } from '../../../chat';
|
||||||
import { hideRemoteVideoMenu } from '../../actions';
|
import { hideRemoteVideoMenu } from '../../actions.native';
|
||||||
|
|
||||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||||
import GrantModeratorButton from './GrantModeratorButton';
|
import GrantModeratorButton from './GrantModeratorButton';
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import { updateSettings } from '../../../base/settings';
|
||||||
|
|
||||||
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current local flip x status.
|
||||||
|
*/
|
||||||
|
_localFlipX: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The redux dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link Component} which displays a button for flipping the local viedo.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class FlipLocalVideoButton extends PureComponent<Props> {
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code FlipLocalVideoButton} instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React Component props 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._onClick = this._onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {null|ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoMenuButton
|
||||||
|
buttonText = { t('videothumbnail.flip') }
|
||||||
|
displayClass = 'fliplink'
|
||||||
|
id = 'flipLocalVideoButton'
|
||||||
|
onClick = { this._onClick } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flips the local video.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClick() {
|
||||||
|
const { _localFlipX, dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch(updateSettings({
|
||||||
|
localFlipX: !_localFlipX
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the Redux state to the associated {@code FlipLocalVideoButton}'s props.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @private
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state) {
|
||||||
|
const { localFlipX } = state['features/base/settings'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
_localFlipX: Boolean(localFlipX)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(FlipLocalVideoButton));
|
|
@ -10,7 +10,7 @@ import AbstractGrantModeratorButton, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractGrantModeratorButton';
|
} from '../AbstractGrantModeratorButton';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class GrantModeratorButton extends AbstractGrantModeratorButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t('videothumbnail.grantModerator') }
|
buttonText = { t('videothumbnail.grantModerator') }
|
||||||
displayClass = 'grantmoderatorlink'
|
displayClass = 'grantmoderatorlink'
|
||||||
icon = { IconCrown }
|
icon = { IconCrown }
|
|
@ -9,7 +9,7 @@ import AbstractKickButton, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractKickButton';
|
} from '../AbstractKickButton';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which displays a button for kicking out
|
* Implements a React {@link Component} which displays a button for kicking out
|
||||||
|
@ -43,7 +43,7 @@ class KickButton extends AbstractKickButton {
|
||||||
const { participantID, t } = this.props;
|
const { participantID, t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t('videothumbnail.kick') }
|
buttonText = { t('videothumbnail.kick') }
|
||||||
displayClass = 'kicklink'
|
displayClass = 'kicklink'
|
||||||
icon = { IconKick }
|
icon = { IconKick }
|
|
@ -0,0 +1,100 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Icon, IconMenuThumb } from '../../../base/icons';
|
||||||
|
import { Popover } from '../../../base/popover';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import { getLocalVideoTrack } from '../../../base/tracks';
|
||||||
|
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||||
|
|
||||||
|
import FlipLocalVideoButton from './FlipLocalVideoButton';
|
||||||
|
import VideoMenu from './VideoMenu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of
|
||||||
|
* {@link LocalVideoMenuTriggerButton}.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position relative to the trigger the local video menu should display
|
||||||
|
* from. Valid values are those supported by AtlasKit
|
||||||
|
* {@code InlineDialog}.
|
||||||
|
*/
|
||||||
|
_menuPosition: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the Popover as a drawer.
|
||||||
|
*/
|
||||||
|
_overflowDrawer: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows/hides the local video flip button.
|
||||||
|
*/
|
||||||
|
_showLocalVideoFlipButton: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React Component for displaying an icon associated with opening the
|
||||||
|
* the video menu for the local participant.
|
||||||
|
*
|
||||||
|
* @param {Props} props - The props passed to the component.
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
function LocalVideoMenuTriggerButton(props: Props) {
|
||||||
|
return (
|
||||||
|
props._showLocalVideoFlipButton
|
||||||
|
? <Popover
|
||||||
|
content = {
|
||||||
|
<VideoMenu id = 'localVideoMenu'>
|
||||||
|
<FlipLocalVideoButton />
|
||||||
|
</VideoMenu>
|
||||||
|
}
|
||||||
|
overflowDrawer = { props._overflowDrawer }
|
||||||
|
position = { props._menuPosition }>
|
||||||
|
<span
|
||||||
|
className = 'popover-trigger local-video-menu-trigger'>
|
||||||
|
<Icon
|
||||||
|
size = '1em'
|
||||||
|
src = { IconMenuThumb }
|
||||||
|
title = 'Local user controls' />
|
||||||
|
</span>
|
||||||
|
</Popover>
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @private
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state) {
|
||||||
|
const currentLayout = getCurrentLayout(state);
|
||||||
|
const { disableLocalVideoFlip } = state['features/base/config'];
|
||||||
|
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||||
|
const { overflowDrawer } = state['features/toolbox'];
|
||||||
|
let _menuPosition;
|
||||||
|
|
||||||
|
switch (currentLayout) {
|
||||||
|
case LAYOUTS.TILE_VIEW:
|
||||||
|
_menuPosition = 'left-start';
|
||||||
|
break;
|
||||||
|
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||||
|
_menuPosition = 'left-end';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_menuPosition = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_menuPosition,
|
||||||
|
_showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
|
||||||
|
_overflowDrawer: overflowDrawer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(_mapStateToProps)(LocalVideoMenuTriggerButton);
|
|
@ -10,7 +10,7 @@ import AbstractMuteButton, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractMuteButton';
|
} from '../AbstractMuteButton';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which displays a button for audio muting
|
* Implements a React {@link Component} which displays a button for audio muting
|
||||||
|
@ -51,7 +51,7 @@ class MuteButton extends AbstractMuteButton {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t(muteConfig.translationKey) }
|
buttonText = { t(muteConfig.translationKey) }
|
||||||
displayClass = { muteConfig.muteClassName }
|
displayClass = { muteConfig.muteClassName }
|
||||||
icon = { IconMicDisabled }
|
icon = { IconMicDisabled }
|
|
@ -9,7 +9,7 @@ import AbstractMuteEveryoneElseButton, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractMuteEveryoneElseButton';
|
} from '../AbstractMuteEveryoneElseButton';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which displays a button for audio muting
|
* Implements a React {@link Component} which displays a button for audio muting
|
||||||
|
@ -38,7 +38,7 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
|
||||||
const { participantID, t } = this.props;
|
const { participantID, t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t('videothumbnail.domuteOthers') }
|
buttonText = { t('videothumbnail.domuteOthers') }
|
||||||
displayClass = { 'mutelink' }
|
displayClass = { 'mutelink' }
|
||||||
icon = { IconMuteEveryoneElse }
|
icon = { IconMuteEveryoneElse }
|
|
@ -9,7 +9,7 @@ import AbstractMuteEveryoneElsesVideoButton, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractMuteEveryoneElsesVideoButton';
|
} from '../AbstractMuteEveryoneElsesVideoButton';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which displays a button for audio muting
|
* Implements a React {@link Component} which displays a button for audio muting
|
||||||
|
@ -38,7 +38,7 @@ class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton
|
||||||
const { participantID, t } = this.props;
|
const { participantID, t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
|
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
|
||||||
displayClass = { 'mutelink' }
|
displayClass = { 'mutelink' }
|
||||||
icon = { IconMuteVideoEveryoneElse }
|
icon = { IconMuteVideoEveryoneElse }
|
|
@ -10,7 +10,7 @@ import AbstractMuteVideoButton, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractMuteVideoButton';
|
} from '../AbstractMuteVideoButton';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which displays a button for disabling
|
* Implements a React {@link Component} which displays a button for disabling
|
||||||
|
@ -51,7 +51,7 @@ class MuteVideoButton extends AbstractMuteVideoButton {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t(muteConfig.translationKey) }
|
buttonText = { t(muteConfig.translationKey) }
|
||||||
displayClass = { muteConfig.muteClassName }
|
displayClass = { muteConfig.muteClassName }
|
||||||
icon = { IconCameraDisabled }
|
icon = { IconCameraDisabled }
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '../../../chat/components/PrivateMessageButton';
|
} from '../../../chat/components/PrivateMessageButton';
|
||||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ class PrivateMessageMenuButton extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t('toolbar.privateMessage') }
|
buttonText = { t('toolbar.privateMessage') }
|
||||||
icon = { IconMessage }
|
icon = { IconMessage }
|
||||||
id = { `privmsglink_${participantID}` }
|
id = { `privmsglink_${participantID}` }
|
|
@ -9,7 +9,7 @@ import {
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons';
|
import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons';
|
||||||
|
|
||||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
import VideoMenuButton from './VideoMenuButton';
|
||||||
|
|
||||||
// TODO: Move these enums into the store after further reactification of the
|
// TODO: Move these enums into the store after further reactification of the
|
||||||
// non-react RemoteVideo component.
|
// non-react RemoteVideo component.
|
||||||
|
@ -102,7 +102,7 @@ class RemoteControlButton extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenuButton
|
<VideoMenuButton
|
||||||
buttonText = { t('videothumbnail.remoteControl') }
|
buttonText = { t('videothumbnail.remoteControl') }
|
||||||
displayClass = { className }
|
displayClass = { className }
|
||||||
icon = { icon }
|
icon = { icon }
|
|
@ -20,7 +20,7 @@ import {
|
||||||
KickButton,
|
KickButton,
|
||||||
PrivateMessageMenuButton,
|
PrivateMessageMenuButton,
|
||||||
RemoteControlButton,
|
RemoteControlButton,
|
||||||
RemoteVideoMenu,
|
VideoMenu,
|
||||||
VolumeSlider
|
VolumeSlider
|
||||||
} from './';
|
} from './';
|
||||||
|
|
||||||
|
@ -91,21 +91,11 @@ type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React {@code Component} for displaying an icon associated with opening the
|
* React {@code Component} for displaying an icon associated with opening the
|
||||||
* the {@code RemoteVideoMenu}.
|
* the {@code VideoMenu}.
|
||||||
*
|
*
|
||||||
* @extends {Component}
|
* @extends {Component}
|
||||||
*/
|
*/
|
||||||
class RemoteVideoMenuTriggerButton extends Component<Props> {
|
class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
/**
|
|
||||||
* The internal reference to topmost DOM/HTML element backing the React
|
|
||||||
* {@code Component}. Accessed directly for associating an element as
|
|
||||||
* the trigger for a popover.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @type {HTMLDivElement}
|
|
||||||
*/
|
|
||||||
_rootElement = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -136,7 +126,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with
|
* Creates a new {@code VideoMenu} with buttons for interacting with
|
||||||
* the remote participant.
|
* the remote participant.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
|
@ -230,9 +220,9 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
|
|
||||||
if (buttons.length > 0) {
|
if (buttons.length > 0) {
|
||||||
return (
|
return (
|
||||||
<RemoteVideoMenu id = { participantID }>
|
<VideoMenu id = { participantID }>
|
||||||
{ buttons }
|
{ buttons }
|
||||||
</RemoteVideoMenu>
|
</VideoMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of {@link VideoMenu}.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The components to place as the body of the {@code VideoMenu}.
|
||||||
|
*/
|
||||||
|
children: React$Node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id attribute to be added to the component's DOM for retrieval when
|
||||||
|
* querying the DOM. Not used directly by the component.
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler.
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event - The click event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function onClick(event) {
|
||||||
|
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
|
||||||
|
// needs to be stopped.
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React {@code Component} responsible for displaying other components as a menu
|
||||||
|
* for manipulating participant state.
|
||||||
|
*
|
||||||
|
* @param {Props} props - The component's props.
|
||||||
|
* @returns {Component}
|
||||||
|
*/
|
||||||
|
export default function VideoMenu(props: Props) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className = 'popupmenu'
|
||||||
|
id = { props.id }
|
||||||
|
onClick = { onClick }>
|
||||||
|
{ props.children }
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Icon } from '../../../base/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of
|
* The type of the React {@code Component} props of
|
||||||
* {@link RemoteVideoMenuButton}.
|
* {@link VideoMenuButton}.
|
||||||
*/
|
*/
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* The icon that will display within the component.
|
* The icon that will display within the component.
|
||||||
*/
|
*/
|
||||||
icon: Object,
|
icon?: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id attribute to be added to the component's DOM for retrieval when
|
* The id attribute to be added to the component's DOM for retrieval when
|
||||||
|
@ -38,11 +38,11 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React {@code Component} for displaying an action in {@code RemoteVideoMenu}.
|
* React {@code Component} for displaying an action in {@code VideoMenuButton}.
|
||||||
*
|
*
|
||||||
* @extends {Component}
|
* @extends {Component}
|
||||||
*/
|
*/
|
||||||
export default class RemoteVideoMenuButton extends Component<Props> {
|
export default class VideoMenuButton extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -67,7 +67,7 @@ export default class RemoteVideoMenuButton extends Component<Props> {
|
||||||
id = { id }
|
id = { id }
|
||||||
onClick = { onClick }>
|
onClick = { onClick }>
|
||||||
<span className = 'popupmenu__icon'>
|
<span className = 'popupmenu__icon'>
|
||||||
<Icon src = { icon } />
|
{ icon && <Icon src = { icon } /> }
|
||||||
</span>
|
</span>
|
||||||
<span className = 'popupmenu__text'>
|
<span className = 'popupmenu__text'>
|
||||||
{ buttonText }
|
{ buttonText }
|
|
@ -14,6 +14,7 @@ export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantD
|
||||||
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
||||||
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
|
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
|
||||||
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
|
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
|
||||||
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
export { default as VideoMenu } from './VideoMenu';
|
||||||
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
|
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
|
||||||
|
export { default as LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton';
|
||||||
export { default as VolumeSlider } from './VolumeSlider';
|
export { default as VolumeSlider } from './VolumeSlider';
|
|
@ -48,11 +48,6 @@ export default {
|
||||||
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
||||||
AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
|
AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that flipX property of the local video is changed.
|
|
||||||
*/
|
|
||||||
LOCAL_FLIPX_CHANGED: 'UI.local_flipx_changed',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies that the side toolbar container has been toggled. The actual
|
* Notifies that the side toolbar container has been toggled. The actual
|
||||||
* event must contain the identifier of the container that has been toggled
|
* event must contain the identifier of the container that has been toggled
|
||||||
|
@ -63,15 +58,5 @@ export default {
|
||||||
/**
|
/**
|
||||||
* Notifies that the raise hand has been changed.
|
* Notifies that the raise hand has been changed.
|
||||||
*/
|
*/
|
||||||
LOCAL_RAISE_HAND_CHANGED: 'UI.local_raise_hand_changed',
|
LOCAL_RAISE_HAND_CHANGED: 'UI.local_raise_hand_changed'
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that the avatar is displayed or not on the largeVideo.
|
|
||||||
*/
|
|
||||||
LARGE_VIDEO_AVATAR_VISIBLE: 'UI.large_video_avatar_visible',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that the displayed particpant id on the largeVideo is changed.
|
|
||||||
*/
|
|
||||||
LARGE_VIDEO_ID_CHANGED: 'UI.large_video_id_changed'
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue