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 */
|
||||
|
||||
import 'jquery';
|
||||
import 'jquery-contextmenu';
|
||||
import 'jQuery-Impromptu';
|
||||
|
||||
import 'olm';
|
||||
|
|
|
@ -1399,9 +1399,6 @@ export default {
|
|||
.then(() => {
|
||||
this.localVideo = newTrack;
|
||||
this._setSharingScreen(newTrack);
|
||||
if (newTrack) {
|
||||
APP.UI.addLocalVideoStream(newTrack);
|
||||
}
|
||||
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
||||
})
|
||||
.then(resolve)
|
||||
|
@ -2408,7 +2405,11 @@ export default {
|
|||
// There is no guarantee another event will trigger the update
|
||||
// immediately and in all situations, for example because a remote
|
||||
// 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(
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
**/
|
||||
|
||||
.popupmenu {
|
||||
min-width: 75px;
|
||||
min-width: 150px;
|
||||
text-align: left;
|
||||
padding: 0px;
|
||||
white-space: nowrap;
|
||||
|
@ -109,6 +109,6 @@ ul.popupmenu {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -400,7 +400,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger,
|
||||
.localvideomenu,
|
||||
.remotevideomenu
|
||||
{
|
||||
display: inline-block;
|
||||
|
@ -418,6 +420,7 @@
|
|||
cursor: hand;
|
||||
}
|
||||
}
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
0 0 3px $videoThumbnailSelected;
|
||||
}
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@
|
|||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||
0 0 3px $videoThumbnailHovered;
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
* specifically the various status icons.
|
||||
*/
|
||||
.remotevideomenu,
|
||||
.localvideomenu,
|
||||
.videocontainer__toptoolbar {
|
||||
z-index: auto;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
* and tooltips from getting a new location context due to translate3d.
|
||||
*/
|
||||
.connection-indicator,
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger,
|
||||
.indicator-icon-container {
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
@ -68,7 +69,9 @@
|
|||
* Move the remote video menu trigger to the bottom left of the video
|
||||
* thumbnail.
|
||||
*/
|
||||
.localvideomenu,
|
||||
.remotevideomenu,
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
@ -76,6 +79,7 @@
|
|||
right: auto;
|
||||
}
|
||||
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
margin-bottom: 7px;
|
||||
margin-left: $remoteVideoMenuIconMargin;
|
||||
|
|
|
@ -33,8 +33,8 @@ import {
|
|||
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
|
||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession } from '../../react/features/recording/functions';
|
||||
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
|
||||
import { toggleTileView } from '../../react/features/video-layout';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
import { setVideoQuality } from '../../react/features/video-quality';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
|
|
|
@ -115,7 +115,6 @@ UI.start = function() {
|
|||
// Set the defaults for prompt dialogs.
|
||||
$.prompt.setDefaults({ persistent: false });
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
VideoLayout.initLargeVideo();
|
||||
|
||||
// Do not animate the video area on UI start (second argument passed into
|
||||
|
@ -135,7 +134,6 @@ UI.start = function() {
|
|||
if (config.iAmRecorder) {
|
||||
// in case of iAmSipGateway keep local video visible
|
||||
if (!config.iAmSipGateway) {
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
}
|
||||
|
||||
|
@ -179,14 +177,6 @@ UI.unbindEvents = () => {
|
|||
$(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.
|
||||
* @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.
|
||||
*
|
||||
|
@ -289,19 +271,14 @@ UI.setAudioMuted = function(id) {
|
|||
* Sets muted video state for participant
|
||||
*/
|
||||
UI.setVideoMuted = function(id) {
|
||||
VideoLayout.onVideoMute(id);
|
||||
VideoLayout._updateLargeVideoIfDisplayed(id, true);
|
||||
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
APP.conference.updateVideoIconEnabled();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
UI.updateLargeVideo = (id, forceUpdate) => VideoLayout.updateLargeVideo(id, forceUpdate);
|
||||
|
||||
/**
|
||||
* 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.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
|
||||
|
||||
// Used by torture.
|
||||
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() {
|
||||
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 { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { createDeferred } from '../../util/helpers';
|
||||
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
|
||||
* container type.
|
||||
* @type {Object.<string, LargeContainer>}
|
||||
*/
|
||||
this.containers = {};
|
||||
this.eventEmitter = emitter;
|
||||
|
||||
this.state = VIDEO_CONTAINER_TYPE;
|
||||
|
||||
// FIXME: We are passing resizeContainer as parameter which is calling
|
||||
// Container.resize. Probably there's better way to implement this.
|
||||
this.videoContainer = new VideoContainer(
|
||||
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
|
||||
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
|
||||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
// 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
|
||||
// new streams.
|
||||
this.updateInProcess = false;
|
||||
this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
|
||||
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 { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
|
||||
import Filmstrip from './Filmstrip';
|
||||
|
@ -187,16 +186,13 @@ export class VideoContainer extends LargeContainer {
|
|||
* Creates new VideoContainer instance.
|
||||
* @param resizeContainer {Function} function that takes care of the size
|
||||
* of the video container.
|
||||
* @param emitter {EventEmitter} the event emitter that will be used by
|
||||
* this instance.
|
||||
*/
|
||||
constructor(resizeContainer, emitter) {
|
||||
constructor(resizeContainer) {
|
||||
super();
|
||||
this.stream = null;
|
||||
this.userId = null;
|
||||
this.videoType = null;
|
||||
this.localFlipX = true;
|
||||
this.emitter = emitter;
|
||||
this.resizeContainer = resizeContainer;
|
||||
|
||||
/**
|
||||
|
@ -492,7 +488,7 @@ export class VideoContainer extends LargeContainer {
|
|||
|
||||
stream.attach(this.$video[0]);
|
||||
|
||||
const flipX = stream.isLocal() && this.localFlipX;
|
||||
const flipX = stream.isLocal() && this.localFlipX && !this.isScreenSharing();
|
||||
|
||||
this.$video.css({
|
||||
transform: flipX ? 'scaleX(-1)' : 'none'
|
||||
|
@ -534,7 +530,6 @@ export class VideoContainer extends LargeContainer {
|
|||
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
|
||||
this.avatarDisplayed = show;
|
||||
|
||||
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, 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 {
|
||||
getLocalParticipant as getLocalParticipantFromStore,
|
||||
getPinnedParticipant,
|
||||
getParticipantById,
|
||||
pinParticipant
|
||||
getParticipantById
|
||||
} from '../../../react/features/base/participants';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
||||
|
||||
import LargeVideoManager from './LargeVideoManager';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
const remoteVideos = {};
|
||||
let localVideoThumbnail = null;
|
||||
|
||||
let eventEmitter = null;
|
||||
|
||||
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 = {
|
||||
init(emitter) {
|
||||
eventEmitter = emitter;
|
||||
|
||||
localVideoThumbnail = new LocalVideo(
|
||||
emitter,
|
||||
this._updateLargeVideoIfDisplayed.bind(this));
|
||||
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* Registering listeners for UI events in Video layout component.
|
||||
*
|
||||
* @returns {void}
|
||||
* Handler for local flip X changed event.
|
||||
*/
|
||||
registerListeners() {
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
||||
onLocalFlipXChanged);
|
||||
onLocalFlipXChanged() {
|
||||
if (largeVideo) {
|
||||
const { store } = APP;
|
||||
const { localFlipX } = store.getState()['features/base/settings'];
|
||||
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -95,14 +36,17 @@ const VideoLayout = {
|
|||
*/
|
||||
reset() {
|
||||
this._resetLargeVideo();
|
||||
this._resetFilmstrip();
|
||||
},
|
||||
|
||||
initLargeVideo() {
|
||||
this._resetLargeVideo();
|
||||
|
||||
largeVideo = new LargeVideoManager(eventEmitter);
|
||||
if (localFlipX) {
|
||||
largeVideo = new LargeVideoManager();
|
||||
|
||||
const { store } = APP;
|
||||
const { localFlipX } = store.getState()['features/base/settings'];
|
||||
|
||||
if (typeof localFlipX === 'boolean') {
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
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
|
||||
* making sure that user with no tracks is displayed as muted )
|
||||
|
@ -180,7 +75,7 @@ const VideoLayout = {
|
|||
const participant = APP.conference.getParticipantById(participantId);
|
||||
|
||||
if (participant && !participant.getTracksByMediaType('video').length) {
|
||||
APP.UI.setVideoMuted(participantId);
|
||||
VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -202,110 +97,12 @@ const VideoLayout = {
|
|||
return videoTrack?.videoType;
|
||||
},
|
||||
|
||||
isPinned(id) {
|
||||
return id === this.getPinnedId();
|
||||
},
|
||||
|
||||
getPinnedId() {
|
||||
const { id } = getPinnedParticipant(APP.store.getState()) || {};
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -321,12 +118,6 @@ const VideoLayout = {
|
|||
// We have to trigger full large video update to transition from
|
||||
// avatar to video on connectivity restored.
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -339,58 +130,14 @@ const VideoLayout = {
|
|||
*/
|
||||
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
|
||||
if (endpointsLeavingLastN) {
|
||||
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
|
||||
endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -401,15 +148,6 @@ const VideoLayout = {
|
|||
}
|
||||
},
|
||||
|
||||
getSmallVideo(id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return localVideoThumbnail;
|
||||
}
|
||||
|
||||
return remoteVideos[id];
|
||||
|
||||
},
|
||||
|
||||
changeUserAvatar(id, avatarUrl) {
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(avatarUrl);
|
||||
|
@ -432,24 +170,6 @@ const VideoLayout = {
|
|||
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) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
|
@ -510,13 +230,6 @@ const VideoLayout = {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const currentId = largeVideo.id;
|
||||
let oldSmallVideo;
|
||||
|
||||
if (currentId) {
|
||||
oldSmallVideo = this.getSmallVideo(currentId);
|
||||
}
|
||||
|
||||
let containerTypeToShow = type;
|
||||
|
||||
// if we are hiding a container and there is focusedVideo
|
||||
|
@ -533,12 +246,7 @@ const VideoLayout = {
|
|||
}
|
||||
}
|
||||
|
||||
return largeVideo.showContainer(containerTypeToShow)
|
||||
.then(() => {
|
||||
if (oldSmallVideo) {
|
||||
oldSmallVideo && oldSmallVideo.updateView();
|
||||
}
|
||||
});
|
||||
return largeVideo.showContainer(containerTypeToShow);
|
||||
},
|
||||
|
||||
isLargeContainerTypeVisible(type) {
|
||||
|
@ -561,14 +269,6 @@ const VideoLayout = {
|
|||
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 {JQuerySelector} the wrapper jquery selector for the largeVideo
|
||||
|
@ -577,15 +277,6 @@ const VideoLayout = {
|
|||
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
|
||||
* have to be re-arranged and resized.
|
||||
|
@ -593,12 +284,7 @@ const VideoLayout = {
|
|||
* @returns {void}
|
||||
*/
|
||||
refreshLayout() {
|
||||
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* currently displayed on large video.
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
sendAnalytics
|
||||
} from '../../react/features/analytics';
|
||||
import { toggleDialog } from '../../react/features/base/dialog';
|
||||
import { clickOnVideo } from '../../react/features/filmstrip/actions';
|
||||
import { KeyboardShortcutsDialog }
|
||||
from '../../react/features/keyboard-shortcuts';
|
||||
import { SpeakerStats } from '../../react/features/speaker-stats';
|
||||
|
@ -54,7 +55,7 @@ const KeyboardShortcut = {
|
|||
if (_shortcuts.has(key)) {
|
||||
_shortcuts.get(key).function(e);
|
||||
} 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",
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"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",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
|
|
|
@ -38,7 +38,103 @@ type Props = {
|
|||
* Used to determine the value of the autoplay attribute of the underlying
|
||||
* 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);
|
||||
}
|
||||
|
||||
if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -149,13 +249,26 @@ class Video extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
autoPlay,
|
||||
className,
|
||||
id,
|
||||
muted,
|
||||
playsinline,
|
||||
style,
|
||||
eventHandlers
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<video
|
||||
autoPlay = { this.props.autoPlay }
|
||||
className = { this.props.className }
|
||||
id = { this.props.id }
|
||||
playsInline = { this.props.playsinline }
|
||||
ref = { this._setVideoElement } />
|
||||
autoPlay = { autoPlay }
|
||||
className = { className }
|
||||
id = { id }
|
||||
muted = { muted }
|
||||
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
|
||||
* 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}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_noAutoPlayVideo,
|
||||
className,
|
||||
id,
|
||||
muted,
|
||||
videoTrack,
|
||||
style,
|
||||
eventHandlers
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
||||
<Video
|
||||
autoPlay = { !this.props._noAutoPlayVideo }
|
||||
className = { this.props.className }
|
||||
id = { this.props.id }
|
||||
autoPlay = { !_noAutoPlayVideo }
|
||||
className = { className }
|
||||
eventHandlers = { eventHandlers }
|
||||
id = { id }
|
||||
muted = { muted }
|
||||
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 { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
@ -75,6 +75,16 @@ export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE';
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -25,9 +25,10 @@ import {
|
|||
TRACK_CREATE_ERROR,
|
||||
TRACK_NO_DATA_FROM_SOURCE,
|
||||
TRACK_REMOVED,
|
||||
TRACK_STOPPED,
|
||||
TRACK_UPDATED,
|
||||
TRACK_WILL_CREATE,
|
||||
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT
|
||||
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
|
||||
TRACK_WILL_CREATE
|
||||
} from './actionTypes';
|
||||
import {
|
||||
createLocalTracksF,
|
||||
|
@ -400,8 +401,15 @@ export function trackAdded(track) {
|
|||
|
||||
noDataFromSourceNotificationInfo = { timeout };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => dispatch({
|
||||
type: TRACK_STOPPED,
|
||||
track: {
|
||||
jitsiTrack: track
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
participantId = track.getParticipantId();
|
||||
isReceivingData = true;
|
||||
|
|
|
@ -163,7 +163,6 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
} else {
|
||||
APP.UI.setVideoMuted(participantID);
|
||||
}
|
||||
APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
|
||||
} else if (jitsiTrack.isLocal()) {
|
||||
APP.conference.setAudioMuteStatus(muted);
|
||||
} else {
|
||||
|
|
|
@ -145,6 +145,18 @@ type Props = {
|
|||
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.
|
||||
*
|
||||
|
@ -161,7 +173,9 @@ class ConnectionStatsTable extends Component<Props> {
|
|||
const { isLocalVideo, enableSaveLogs } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'connection-info'>
|
||||
<div
|
||||
className = 'connection-info'
|
||||
onClick = { onClick }>
|
||||
{ this._renderStatistics() }
|
||||
<div className = 'connection-actions'>
|
||||
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { pinParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
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';
|
||||
|
|
|
@ -20,9 +20,9 @@ import { StyleType } from '../../../base/styles';
|
|||
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
|
||||
import { ConnectionIndicator } from '../../../connection-indicator';
|
||||
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 { RemoteVideoMenu } from '../../../video-menu';
|
||||
import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
|
||||
|
||||
import AudioMutedIndicator from './AudioMutedIndicator';
|
||||
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
|
||||
|
|
|
@ -11,12 +11,15 @@ import {
|
|||
import { getToolbarButtons } from '../../../base/config';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
|
||||
import { setFilmstripVisible } from '../../actions';
|
||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
@ -60,6 +63,11 @@ type Props = {
|
|||
*/
|
||||
_isFilmstripButtonEnabled: boolean,
|
||||
|
||||
/**
|
||||
* The participants in the call.
|
||||
*/
|
||||
_participants: Array<Object>,
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
|
@ -138,18 +146,15 @@ class Filmstrip extends Component <Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
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 filmstripRemoteVideosContainerStyle = {};
|
||||
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:
|
||||
// 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.
|
||||
|
@ -191,7 +196,13 @@ class Filmstrip extends Component <Props> {
|
|||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalVideo'>
|
||||
<div id = 'filmstripLocalVideoThumbnail' />
|
||||
<div id = 'filmstripLocalVideoThumbnail'>
|
||||
{
|
||||
!tileViewActive && <Thumbnail
|
||||
key = 'local'
|
||||
participantID = { localParticipant.id } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className = { remoteVideosWrapperClassName }
|
||||
|
@ -205,7 +216,21 @@ class Filmstrip extends Component <Props> {
|
|||
className = { remoteVideoContainerClassName }
|
||||
id = 'filmstripRemoteVideosContainer'
|
||||
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>
|
||||
|
@ -314,6 +339,7 @@ function _mapStateToProps(state) {
|
|||
_hideScrollbar: Boolean(iAmSipGateway),
|
||||
_hideToolbar: Boolean(iAmSipGateway),
|
||||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_participants: state['features/base/participants'],
|
||||
_rows: gridDimensions.rows,
|
||||
_videosClassName: videosClassName,
|
||||
_visible: visible
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
|
||||
import { AudioLevelIndicator } from '../../../audio-level-indicator';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
|
@ -11,42 +11,72 @@ import AudioTrack from '../../../base/media/components/web/AudioTrack';
|
|||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount
|
||||
getParticipantCount,
|
||||
pinParticipant
|
||||
} from '../../../base/participants';
|
||||
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 { DisplayName } from '../../../display-name';
|
||||
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
|
||||
import { PresenceLabel } from '../../../presence-status';
|
||||
import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
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;
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link Thumbnail}.
|
||||
*/
|
||||
type State = {
|
||||
export type State = {|
|
||||
|
||||
/**
|
||||
* The current audio level value for the Thumbnail.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
volume: ?number
|
||||
};
|
||||
|};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Thumbnail}.
|
||||
*/
|
||||
type Props = {
|
||||
export type Props = {|
|
||||
|
||||
/**
|
||||
* The audio track related to the participant.
|
||||
|
@ -73,11 +103,21 @@ type Props = {
|
|||
*/
|
||||
_defaultLocalDisplayName: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the local video flip feature is disabled or not.
|
||||
*/
|
||||
_disableLocalVideoFlip: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the profile functionality is disabled.
|
||||
*/
|
||||
_disableProfile: boolean,
|
||||
|
||||
/**
|
||||
* The display mode of the thumbnail.
|
||||
*/
|
||||
_displayMode: number,
|
||||
|
||||
/**
|
||||
* The height of the Thumbnail.
|
||||
*/
|
||||
|
@ -88,16 +128,51 @@ type Props = {
|
|||
*/
|
||||
_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.
|
||||
*/
|
||||
_isDominantSpeakerDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether testing mode is enabled.
|
||||
*/
|
||||
_isTestModeEnabled: boolean,
|
||||
|
||||
/**
|
||||
* The size of the icon of indicators.
|
||||
*/
|
||||
_indicatorIconSize: number,
|
||||
|
||||
/**
|
||||
* The current local video flip setting.
|
||||
*/
|
||||
_localFlipX: boolean,
|
||||
|
||||
/**
|
||||
* An object with information about the participant related to the thumbnaul.
|
||||
*/
|
||||
|
@ -128,16 +203,23 @@ type Props = {
|
|||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Indicates whether the thumbnail is hovered or not.
|
||||
*/
|
||||
isHovered: ?boolean,
|
||||
|
||||
/**
|
||||
* The ID of the participant related to the thumbnail.
|
||||
*/
|
||||
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.
|
||||
|
@ -145,7 +227,6 @@ type Props = {
|
|||
* @extends Component
|
||||
*/
|
||||
class Thumbnail extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Initializes a new Thumbnail instance.
|
||||
*
|
||||
|
@ -155,14 +236,27 @@ class Thumbnail extends Component<Props, State> {
|
|||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
const state = {
|
||||
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._onCanPlay = this._onCanPlay.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onVolumeChange = this._onVolumeChange.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() {
|
||||
this._listenForAudioUpdates();
|
||||
this._onDisplayModeChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -182,12 +277,121 @@ class Thumbnail extends Component<Props, State> {
|
|||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (prevProps._audioTrack !== this.props._audioTrack) {
|
||||
this._stopListeningForAudioUpdates(prevProps._audioTrack);
|
||||
this._listenForAudioUpdates();
|
||||
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.
|
||||
*/
|
||||
_getStyles(): Object {
|
||||
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
|
||||
let styles;
|
||||
const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
|
||||
let styles: {
|
||||
thumbnail: Object,
|
||||
avatar: Object
|
||||
} = {
|
||||
thumbnail: {},
|
||||
avatar: {}
|
||||
};
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
|
@ -262,7 +472,13 @@ class Thumbnail extends Component<Props, State> {
|
|||
const avatarSize = _height / 2;
|
||||
|
||||
styles = {
|
||||
avatarContainer: {
|
||||
thumbnail: {
|
||||
height: `${_height}px`,
|
||||
minHeight: `${_height}px`,
|
||||
minWidth: `${_width}px`,
|
||||
width: `${_width}px`
|
||||
},
|
||||
avatar: {
|
||||
height: `${avatarSize}px`,
|
||||
width: `${avatarSize}px`
|
||||
}
|
||||
|
@ -271,7 +487,10 @@ class Thumbnail extends Component<Props, State> {
|
|||
}
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
styles = {
|
||||
avatarContainer: {
|
||||
thumbnail: {
|
||||
paddingTop: `${_heightToWidthPercent}%`
|
||||
},
|
||||
avatar: {
|
||||
height: '50%',
|
||||
width: `${_heightToWidthPercent / 2}%`
|
||||
}
|
||||
|
@ -280,9 +499,49 @@ class Thumbnail extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
if (_isHidden) {
|
||||
styles.thumbnail.display = 'none';
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
@ -292,9 +551,17 @@ class Thumbnail extends Component<Props, State> {
|
|||
_renderFakeParticipant() {
|
||||
const { _participant } = this.props;
|
||||
const { id } = _participant;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className = { containerClassName }
|
||||
id = 'sharedVideoContainer'
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
style = { styles.thumbnail }>
|
||||
<img
|
||||
className = 'sharedVideoAvatar'
|
||||
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
|
||||
|
@ -303,7 +570,7 @@ class Thumbnail extends Component<Props, State> {
|
|||
elementID = 'sharedVideoContainer_name'
|
||||
participantID = { id } />
|
||||
</div>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -320,9 +587,9 @@ class Thumbnail extends Component<Props, State> {
|
|||
_isDominantSpeakerDisabled,
|
||||
_indicatorIconSize: iconSize,
|
||||
_participant,
|
||||
_participantCount,
|
||||
isHovered
|
||||
_participantCount
|
||||
} = this.props;
|
||||
const { isHovered } = this.state;
|
||||
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
|
||||
const { id, local = false, dominantSpeaker = false } = _participant;
|
||||
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
|
||||
|
@ -344,43 +611,41 @@ class Thumbnail extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
{ !_connectionIndicatorDisabled
|
||||
&& <ConnectionIndicator
|
||||
alwaysVisible = { showConnectionIndicator }
|
||||
enableStatsDisplay = { true }
|
||||
iconSize = { iconSize }
|
||||
isLocalVideo = { local }
|
||||
participantId = { id }
|
||||
statsPopoverPosition = { statsPopoverPosition } />
|
||||
}
|
||||
<RaisedHandIndicator
|
||||
{ !_connectionIndicatorDisabled
|
||||
&& <ConnectionIndicator
|
||||
alwaysVisible = { showConnectionIndicator }
|
||||
enableStatsDisplay = { true }
|
||||
iconSize = { iconSize }
|
||||
isLocalVideo = { local }
|
||||
participantId = { id }
|
||||
statsPopoverPosition = { statsPopoverPosition } />
|
||||
}
|
||||
<RaisedHandIndicator
|
||||
iconSize = { iconSize }
|
||||
participantId = { id }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
{ showDominantSpeaker && _participantCount > 2
|
||||
&& <DominantSpeakerIndicator
|
||||
iconSize = { iconSize }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
{ showDominantSpeaker && _participantCount > 2
|
||||
&& <DominantSpeakerIndicator
|
||||
iconSize = { iconSize }
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
}
|
||||
</AtlasKitThemeProvider>
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the avatar.
|
||||
*
|
||||
* @param {Object} styles - The styles that will be applied to the avatar.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAvatar() {
|
||||
_renderAvatar(styles) {
|
||||
const { _participant } = this.props;
|
||||
const { id } = _participant;
|
||||
const styles = this._getStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'avatar-container'
|
||||
style = { styles.avatarContainer }>
|
||||
style = { styles }>
|
||||
<Avatar
|
||||
className = 'userAvatar'
|
||||
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.
|
||||
*
|
||||
|
@ -396,19 +693,33 @@ class Thumbnail extends Component<Props, State> {
|
|||
_renderLocalParticipant() {
|
||||
const {
|
||||
_defaultLocalDisplayName,
|
||||
_disableLocalVideoFlip,
|
||||
_isScreenSharing,
|
||||
_localFlipX,
|
||||
_disableProfile,
|
||||
_participant,
|
||||
_videoTrack
|
||||
} = this.props;
|
||||
const { id } = _participant || {};
|
||||
const { audioLevel } = this.state;
|
||||
const styles = this._getStyles();
|
||||
const containerClassName = this._getContainerClassName();
|
||||
const videoTrackClassName
|
||||
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className = { containerClassName }
|
||||
id = 'localVideoContainer'
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
style = { styles.thumbnail }>
|
||||
<div className = 'videocontainer__background' />
|
||||
<span id = 'localVideoWrapper'>
|
||||
<VideoTrack
|
||||
className = { videoTrackClassName }
|
||||
id = 'localVideo_container'
|
||||
videoTrack = { _videoTrack } />
|
||||
</span>
|
||||
|
@ -419,21 +730,64 @@ class Thumbnail extends Component<Props, State> {
|
|||
{ this._renderTopIndicators() }
|
||||
</div>
|
||||
<div className = 'videocontainer__hoverOverlay' />
|
||||
<div className = 'displayNameContainer'>
|
||||
<div
|
||||
className = 'displayNameContainer'
|
||||
onClick = { onClick }>
|
||||
<DisplayName
|
||||
allowEditing = { !_disableProfile }
|
||||
displayNameSuffix = { _defaultLocalDisplayName }
|
||||
elementID = 'localDisplayName'
|
||||
participantID = { id } />
|
||||
</div>
|
||||
{ this._renderAvatar() }
|
||||
{ this._renderAvatar(styles.avatar) }
|
||||
<span className = 'localvideomenu'>
|
||||
<LocalVideoMenuTriggerButton />
|
||||
</span>
|
||||
<span className = 'audioindicator-container'>
|
||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||
</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.
|
||||
|
@ -443,29 +797,59 @@ class Thumbnail extends Component<Props, State> {
|
|||
_renderRemoteParticipant() {
|
||||
const {
|
||||
_audioTrack,
|
||||
_isTestModeEnabled,
|
||||
_participant,
|
||||
_startSilent
|
||||
_startSilent,
|
||||
_videoTrack
|
||||
} = this.props;
|
||||
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
|
||||
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
|
||||
const jitsiTrack = _audioTrack?.jitsiTrack;
|
||||
const audioTrackId = jitsiTrack && jitsiTrack.getId();
|
||||
const jitsiAudioTrack = _audioTrack?.jitsiTrack;
|
||||
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 (
|
||||
<>
|
||||
<span
|
||||
className = { containerClassName }
|
||||
id = { `participant_${id}` }
|
||||
onClick = { this._onClick }
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
style = { styles.thumbnail }>
|
||||
{
|
||||
_audioTrack
|
||||
? <AudioTrack
|
||||
audioTrack = { _audioTrack }
|
||||
id = { `remoteAudio_${audioTrackId || ''}` }
|
||||
muted = { _startSilent }
|
||||
onInitialVolumeSet = { this._onInitialVolumeSet }
|
||||
volume = { this.state.volume } />
|
||||
: null
|
||||
|
||||
_videoTrack && <VideoTrack
|
||||
eventHandlers = { videoEventListeners }
|
||||
id = { `remoteVideo_${videoTrackId || ''}` }
|
||||
muted = { true }
|
||||
style = { videoElementStyle }
|
||||
videoTrack = { _videoTrack } />
|
||||
}
|
||||
{
|
||||
_audioTrack && <AudioTrack
|
||||
audioTrack = { _audioTrack }
|
||||
id = { `remoteAudio_${audioTrackId || ''}` }
|
||||
muted = { _startSilent }
|
||||
onInitialVolumeSet = { this._onInitialVolumeSet }
|
||||
volume = { volume } />
|
||||
}
|
||||
<div className = 'videocontainer__background' />
|
||||
<div className = 'videocontainer__toptoolbar'>
|
||||
|
@ -480,24 +864,22 @@ class Thumbnail extends Component<Props, State> {
|
|||
elementID = { `participant_${id}_name` }
|
||||
participantID = { id } />
|
||||
</div>
|
||||
{ this._renderAvatar() }
|
||||
{ this._renderAvatar(styles.avatar) }
|
||||
<div className = 'presence-label-container'>
|
||||
<PresenceLabel
|
||||
className = 'presence-label'
|
||||
participantID = { id } />
|
||||
</div>
|
||||
<span className = 'remotevideomenu'>
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { volume }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { id } />
|
||||
</AtlasKitThemeProvider>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { volume }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { id } />
|
||||
</span>
|
||||
<span className = 'audioindicator-container'>
|
||||
<AudioLevelIndicator audioLevel = { audioLevel } />
|
||||
</span>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -527,7 +909,6 @@ class Thumbnail extends Component<Props, State> {
|
|||
this.setState({ volume: value });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
||||
const { id } = participant;
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const _videoTrack = isLocal
|
||||
? getLocalVideoTrack(state['features/base/tracks'])
|
||||
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
|
||||
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
|
||||
const _audioTrack = isLocal
|
||||
? getLocalAudioTrack(state['features/base/tracks'])
|
||||
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
|
||||
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
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 { localFlipX } = state['features/base/settings'];
|
||||
|
||||
|
||||
switch (_currentLayout) {
|
||||
|
@ -617,16 +1005,23 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
_audioTrack,
|
||||
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
|
||||
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
|
||||
_currentLayout,
|
||||
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
||||
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
|
||||
_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,
|
||||
_isScreenSharing: _videoTrack?.videoType === 'desktop',
|
||||
_isTestModeEnabled: isTestModeEnabled(state),
|
||||
_isVideoPlayable: isVideoPlayable(state, id),
|
||||
_indicatorIconSize: NORMAL,
|
||||
_localFlipX: Boolean(localFlipX),
|
||||
_participant: participant,
|
||||
_participantCount: getParticipantCount(state),
|
||||
_startSilent: Boolean(startSilent),
|
||||
|
|
|
@ -47,3 +47,92 @@ export const DEFAULT_MAX_COLUMNS = 5;
|
|||
* An extended number of columns for tile view.
|
||||
*/
|
||||
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
|
||||
} 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;
|
||||
|
||||
|
@ -176,3 +185,36 @@ export function getVerticalFilmstripVisibleAreaWidth() {
|
|||
|
||||
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
|
||||
|
||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
||||
import { SETTINGS_UPDATED } from '../base/settings';
|
||||
import {
|
||||
getCurrentLayout,
|
||||
LAYOUTS,
|
||||
shouldDisplayTileView
|
||||
LAYOUTS
|
||||
} from '../video-layout';
|
||||
|
||||
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||
|
||||
import './subscriber.web';
|
||||
|
@ -48,29 +47,13 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case SET_TILE_VIEW_DIMENSIONS: {
|
||||
const state = store.getState();
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
|
||||
|
||||
// Once the thumbnails are reactified this should be moved there too.
|
||||
Filmstrip.resizeThumbnailsForTileView(width, height, true);
|
||||
case SETTINGS_UPDATED: {
|
||||
if (typeof action.settings?.localFlipX === 'boolean') {
|
||||
// TODO: This needs to be removed once the large video is Reactified.
|
||||
VideoLayout.onLocalFlipXChanged();
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { StateListenerRegistry, equals } from '../base/redux';
|
||||
import { setFilmstripVisible } from '../filmstrip/actions';
|
||||
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||
|
@ -71,32 +69,9 @@ StateListenerRegistry.register(
|
|||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -34,7 +34,7 @@ import { toggleScreensharing } from '../../base/tracks';
|
|||
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
|
||||
import { openChat } from '../../chat/actions';
|
||||
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 { 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 type { AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { isLocalTrackMuted } from '../../base/tracks';
|
||||
import { muteLocal } from '../../remote-video-menu/actions';
|
||||
import { muteLocal } from '../../video-menu/actions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { IconMuteEveryone } from '../../base/icons';
|
|||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { MuteEveryoneDialog } from '../../remote-video-menu/components';
|
||||
import { MuteEveryoneDialog } from '../../video-menu/components';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { IconMuteVideoEveryone } from '../../base/icons';
|
|||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
|
||||
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
|
|
|
@ -4,15 +4,12 @@ import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
|
|||
import { CONFERENCE_WILL_LEAVE } from '../base/conference';
|
||||
import { MEDIA_TYPE } from '../base/media';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
getLocalParticipant,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT,
|
||||
getParticipantById
|
||||
PARTICIPANT_UPDATED
|
||||
} from '../base/participants';
|
||||
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 './middleware.any';
|
||||
|
@ -40,15 +37,10 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
case PARTICIPANT_JOINED:
|
||||
if (!action.participant.local) {
|
||||
VideoLayout.addRemoteParticipantContainer(
|
||||
getParticipantById(store.getState(), action.participant.id));
|
||||
VideoLayout.updateVideoMutedForNoTracks(action.participant.id);
|
||||
}
|
||||
break;
|
||||
|
||||
case PARTICIPANT_LEFT:
|
||||
VideoLayout.removeParticipantContainer(action.participant.id);
|
||||
break;
|
||||
|
||||
case PARTICIPANT_UPDATED: {
|
||||
// Look for actions that triggered a change to connectionStatus. This is
|
||||
// done instead of changing the connection status change action to be
|
||||
|
@ -61,27 +53,28 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
}
|
||||
|
||||
case DOMINANT_SPEAKER_CHANGED:
|
||||
VideoLayout.onDominantSpeakerChanged(action.participant.id);
|
||||
break;
|
||||
|
||||
case PIN_PARTICIPANT:
|
||||
VideoLayout.onPinChange(action.participant?.id);
|
||||
break;
|
||||
|
||||
case SET_FILMSTRIP_VISIBLE:
|
||||
VideoLayout.resizeVideoArea();
|
||||
break;
|
||||
|
||||
case TRACK_ADDED:
|
||||
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
||||
if (action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||
VideoLayout._updateLargeVideoIfDisplayed(action.track.participantId, true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TRACK_STOPPED: {
|
||||
if (action.track.jitsiTrack.isLocal()) {
|
||||
const participant = getLocalParticipant(store.getState);
|
||||
|
||||
VideoLayout._updateLargeVideoIfDisplayed(participant?.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TRACK_REMOVED:
|
||||
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
|
||||
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
|
||||
VideoLayout.updateVideoMutedForNoTracks(action.track.jitsiTrack.getParticipantId());
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
sendAnalytics,
|
||||
VIDEO_MUTE
|
||||
} from '../analytics';
|
||||
import { hideDialog } from '../base/dialog';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
setAudioMuted,
|
||||
|
@ -22,21 +21,10 @@ import {
|
|||
muteRemoteParticipant
|
||||
} from '../base/participants';
|
||||
|
||||
import { RemoteVideoMenu } from './components';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Hides the remote video menu.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideRemoteVideoMenu() {
|
||||
return hideDialog(RemoteVideoMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { StyleType } from '../../../base/styles';
|
||||
import { PrivateMessageButton } from '../../../chat';
|
||||
import { hideRemoteVideoMenu } from '../../actions';
|
||||
import { hideRemoteVideoMenu } from '../../actions.native';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
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
|
||||
} from '../AbstractGrantModeratorButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
@ -44,7 +44,7 @@ class GrantModeratorButton extends AbstractGrantModeratorButton {
|
|||
}
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t('videothumbnail.grantModerator') }
|
||||
displayClass = 'grantmoderatorlink'
|
||||
icon = { IconCrown }
|
|
@ -9,7 +9,7 @@ import AbstractKickButton, {
|
|||
type Props
|
||||
} from '../AbstractKickButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t('videothumbnail.kick') }
|
||||
displayClass = 'kicklink'
|
||||
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
|
||||
} from '../AbstractMuteButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
|
@ -51,7 +51,7 @@ class MuteButton extends AbstractMuteButton {
|
|||
};
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t(muteConfig.translationKey) }
|
||||
displayClass = { muteConfig.muteClassName }
|
||||
icon = { IconMicDisabled }
|
|
@ -9,7 +9,7 @@ import AbstractMuteEveryoneElseButton, {
|
|||
type Props
|
||||
} from '../AbstractMuteEveryoneElseButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t('videothumbnail.domuteOthers') }
|
||||
displayClass = { 'mutelink' }
|
||||
icon = { IconMuteEveryoneElse }
|
|
@ -9,7 +9,7 @@ import AbstractMuteEveryoneElsesVideoButton, {
|
|||
type Props
|
||||
} from '../AbstractMuteEveryoneElsesVideoButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
|
||||
displayClass = { 'mutelink' }
|
||||
icon = { IconMuteVideoEveryoneElse }
|
|
@ -10,7 +10,7 @@ import AbstractMuteVideoButton, {
|
|||
type Props
|
||||
} from '../AbstractMuteVideoButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for disabling
|
||||
|
@ -51,7 +51,7 @@ class MuteVideoButton extends AbstractMuteVideoButton {
|
|||
};
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t(muteConfig.translationKey) }
|
||||
displayClass = { muteConfig.muteClassName }
|
||||
icon = { IconCameraDisabled }
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../../chat/components/PrivateMessageButton';
|
||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
import VideoMenuButton from './VideoMenuButton';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
@ -56,7 +56,7 @@ class PrivateMessageMenuButton extends Component<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t('toolbar.privateMessage') }
|
||||
icon = { IconMessage }
|
||||
id = { `privmsglink_${participantID}` }
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { translate } from '../../../base/i18n';
|
||||
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
|
||||
// non-react RemoteVideo component.
|
||||
|
@ -102,7 +102,7 @@ class RemoteControlButton extends Component<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
<VideoMenuButton
|
||||
buttonText = { t('videothumbnail.remoteControl') }
|
||||
displayClass = { className }
|
||||
icon = { icon }
|
|
@ -20,7 +20,7 @@ import {
|
|||
KickButton,
|
||||
PrivateMessageMenuButton,
|
||||
RemoteControlButton,
|
||||
RemoteVideoMenu,
|
||||
VideoMenu,
|
||||
VolumeSlider
|
||||
} from './';
|
||||
|
||||
|
@ -91,21 +91,11 @@ type Props = {
|
|||
|
||||
/**
|
||||
* React {@code Component} for displaying an icon associated with opening the
|
||||
* the {@code RemoteVideoMenu}.
|
||||
* the {@code VideoMenu}.
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
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()}.
|
||||
*
|
||||
|
@ -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.
|
||||
*
|
||||
* @private
|
||||
|
@ -230,9 +220,9 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
|
||||
if (buttons.length > 0) {
|
||||
return (
|
||||
<RemoteVideoMenu id = { participantID }>
|
||||
<VideoMenu id = { participantID }>
|
||||
{ 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
|
||||
* {@link RemoteVideoMenuButton}.
|
||||
* {@link VideoMenuButton}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
|
@ -23,7 +23,7 @@ type Props = {
|
|||
/**
|
||||
* 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
|
||||
|
@ -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}
|
||||
*/
|
||||
export default class RemoteVideoMenuButton extends Component<Props> {
|
||||
export default class VideoMenuButton extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -67,7 +67,7 @@ export default class RemoteVideoMenuButton extends Component<Props> {
|
|||
id = { id }
|
||||
onClick = { onClick }>
|
||||
<span className = 'popupmenu__icon'>
|
||||
<Icon src = { icon } />
|
||||
{ icon && <Icon src = { icon } /> }
|
||||
</span>
|
||||
<span className = 'popupmenu__text'>
|
||||
{ buttonText }
|
|
@ -14,6 +14,7 @@ export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantD
|
|||
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
||||
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
|
||||
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 LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton';
|
||||
export { default as VolumeSlider } from './VolumeSlider';
|
|
@ -48,11 +48,6 @@ export default {
|
|||
VIDEO_DEVICE_CHANGED: 'UI.video_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
|
||||
* 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.
|
||||
*/
|
||||
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'
|
||||
LOCAL_RAISE_HAND_CHANGED: 'UI.local_raise_hand_changed'
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue