From f50872285dff523712a0513107639fbb608104d0 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Thu, 21 Jan 2021 14:46:47 -0600 Subject: [PATCH] ref(Filmstrip): Use Thumbnail component. --- app.js | 1 - conference.js | 9 +- css/_popup_menu.scss | 4 +- css/_videolayout_default.scss | 3 + css/filmstrip/_small_video.scss | 4 +- css/filmstrip/_tile_view_overrides.scss | 1 + .../_vertical_filmstrip_overrides.scss | 4 + modules/API/API.js | 2 +- modules/UI/UI.js | 31 +- modules/UI/shared_video/SharedVideoThumb.js | 67 --- modules/UI/videolayout/Filmstrip.js | 123 ---- modules/UI/videolayout/LargeVideoManager.js | 8 +- modules/UI/videolayout/LocalVideo.js | 213 ------- modules/UI/videolayout/RemoteVideo.js | 242 -------- modules/UI/videolayout/SmallVideo.js | 509 ---------------- modules/UI/videolayout/VideoContainer.js | 9 +- modules/UI/videolayout/VideoLayout.js | 372 +----------- modules/keyboardshortcut/keyboardshortcut.js | 3 +- package-lock.json | 5 - package.json | 1 - .../base/media/components/web/Video.js | 125 +++- .../base/media/components/web/VideoTrack.js | 120 +++- react/features/base/settings/middleware.js | 1 + react/features/base/tracks/actionTypes.js | 10 + react/features/base/tracks/actions.js | 14 +- react/features/base/tracks/middleware.js | 1 - .../components/ConnectionStatsTable.js | 16 +- react/features/filmstrip/actions.web.js | 17 + .../filmstrip/components/native/Thumbnail.js | 4 +- .../filmstrip/components/web/Filmstrip.js | 46 +- .../filmstrip/components/web/Thumbnail.js | 553 +++++++++++++++--- react/features/filmstrip/constants.js | 89 +++ react/features/filmstrip/functions.web.js | 44 +- react/features/filmstrip/logger.js | 5 + react/features/filmstrip/middleware.web.js | 31 +- react/features/filmstrip/subscriber.web.js | 25 - .../mobile/external-api/middleware.js | 2 +- .../components/web/RemoteVideoMenu.js | 44 -- .../toolbox/components/AudioMuteButton.js | 2 +- .../toolbox/components/MuteEveryoneButton.js | 2 +- .../components/MuteEveryonesVideoButton.js | 2 +- react/features/video-layout/middleware.web.js | 39 +- .../actions.js => video-menu/actions.any.js} | 12 - react/features/video-menu/actions.native.js | 15 + react/features/video-menu/actions.web.js | 2 + .../AbstractGrantModeratorButton.js | 0 .../AbstractGrantModeratorDialog.js | 0 .../components/AbstractKickButton.js | 0 .../AbstractKickRemoteParticipantDialog.js | 0 .../components/AbstractMuteButton.js | 0 .../components/AbstractMuteEveryoneDialog.js | 0 .../AbstractMuteEveryoneElseButton.js | 0 .../AbstractMuteEveryoneElsesVideoButton.js | 0 .../AbstractMuteEveryonesVideoDialog.js | 0 .../AbstractMuteRemoteParticipantDialog.js | 0 ...stractMuteRemoteParticipantsVideoDialog.js | 0 .../components/AbstractMuteVideoButton.js | 0 .../components/index.native.js | 0 .../components/index.web.js | 0 .../native/ConnectionStatusButton.js | 0 .../native/ConnectionStatusComponent.js | 0 .../components/native/GrantModeratorButton.js | 0 .../components/native/GrantModeratorDialog.js | 0 .../components/native/KickButton.js | 0 .../native/KickRemoteParticipantDialog.js | 0 .../components/native/MuteButton.js | 0 .../components/native/MuteEveryoneDialog.js | 0 .../native/MuteEveryoneElseButton.js | 0 .../native/MuteRemoteParticipantDialog.js | 0 .../components/native/PinButton.js | 0 .../components/native/RemoteVideoMenu.js | 2 +- .../components/native/index.js | 0 .../components/native/styles.js | 0 .../components/web/FlipLocalVideoButton.js | 103 ++++ .../components/web/GrantModeratorButton.js | 4 +- .../components/web/GrantModeratorDialog.js | 0 .../components/web/KickButton.js | 4 +- .../web/KickRemoteParticipantDialog.js | 0 .../web/LocalVideoMenuTriggerButton.js | 100 ++++ .../components/web/MuteButton.js | 4 +- .../components/web/MuteEveryoneDialog.js | 0 .../components/web/MuteEveryoneElseButton.js | 4 +- .../web/MuteEveryoneElsesVideoButton.js | 4 +- .../web/MuteEveryonesVideoDialog.js | 0 .../web/MuteRemoteParticipantDialog.js | 0 .../web/MuteRemoteParticipantsVideoDialog.js | 0 .../components/web/MuteVideoButton.js | 4 +- .../web/PrivateMessageMenuButton.js | 4 +- .../components/web/RemoteControlButton.js | 4 +- .../web/RemoteVideoMenuTriggerButton.js | 20 +- .../video-menu/components/web/VideoMenu.js | 51 ++ .../components/web/VideoMenuButton.js} | 10 +- .../components/web/VolumeSlider.js | 0 .../components/web/index.js | 3 +- .../index.js | 0 service/UI/UIEvents.js | 17 +- 96 files changed, 1311 insertions(+), 1859 deletions(-) delete mode 100644 modules/UI/shared_video/SharedVideoThumb.js delete mode 100644 modules/UI/videolayout/LocalVideo.js delete mode 100644 modules/UI/videolayout/RemoteVideo.js delete mode 100644 modules/UI/videolayout/SmallVideo.js create mode 100644 react/features/filmstrip/logger.js delete mode 100644 react/features/remote-video-menu/components/web/RemoteVideoMenu.js rename react/features/{remote-video-menu/actions.js => video-menu/actions.any.js} (92%) create mode 100644 react/features/video-menu/actions.native.js create mode 100644 react/features/video-menu/actions.web.js rename react/features/{remote-video-menu => video-menu}/components/AbstractGrantModeratorButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractGrantModeratorDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractKickButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractKickRemoteParticipantDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteEveryoneDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteEveryoneElseButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteEveryoneElsesVideoButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteEveryonesVideoDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteRemoteParticipantDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteRemoteParticipantsVideoDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/AbstractMuteVideoButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/index.native.js (100%) rename react/features/{remote-video-menu => video-menu}/components/index.web.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/ConnectionStatusButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/ConnectionStatusComponent.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/GrantModeratorButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/GrantModeratorDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/KickButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/KickRemoteParticipantDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/MuteButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/MuteEveryoneDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/MuteEveryoneElseButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/MuteRemoteParticipantDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/PinButton.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/RemoteVideoMenu.js (98%) rename react/features/{remote-video-menu => video-menu}/components/native/index.js (100%) rename react/features/{remote-video-menu => video-menu}/components/native/styles.js (100%) create mode 100644 react/features/video-menu/components/web/FlipLocalVideoButton.js rename react/features/{remote-video-menu => video-menu}/components/web/GrantModeratorButton.js (93%) rename react/features/{remote-video-menu => video-menu}/components/web/GrantModeratorDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/web/KickButton.js (94%) rename react/features/{remote-video-menu => video-menu}/components/web/KickRemoteParticipantDialog.js (100%) create mode 100644 react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js rename react/features/{remote-video-menu => video-menu}/components/web/MuteButton.js (95%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteEveryoneDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteEveryoneElseButton.js (93%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteEveryoneElsesVideoButton.js (93%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteEveryonesVideoDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteRemoteParticipantDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteRemoteParticipantsVideoDialog.js (100%) rename react/features/{remote-video-menu => video-menu}/components/web/MuteVideoButton.js (95%) rename react/features/{remote-video-menu => video-menu}/components/web/PrivateMessageMenuButton.js (96%) rename react/features/{remote-video-menu => video-menu}/components/web/RemoteControlButton.js (97%) rename react/features/{remote-video-menu => video-menu}/components/web/RemoteVideoMenuTriggerButton.js (94%) create mode 100644 react/features/video-menu/components/web/VideoMenu.js rename react/features/{remote-video-menu/components/web/RemoteVideoMenuButton.js => video-menu/components/web/VideoMenuButton.js} (86%) rename react/features/{remote-video-menu => video-menu}/components/web/VolumeSlider.js (100%) rename react/features/{remote-video-menu => video-menu}/components/web/index.js (89%) rename react/features/{remote-video-menu => video-menu}/index.js (100%) diff --git a/app.js b/app.js index fcc88becc..9bfb34570 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,6 @@ /* application specific logic */ import 'jquery'; -import 'jquery-contextmenu'; import 'jQuery-Impromptu'; import 'olm'; diff --git a/conference.js b/conference.js index f36a20035..36f0ed073 100644 --- a/conference.js +++ b/conference.js @@ -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( diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss index bf4ac8a9d..6201b6e43 100644 --- a/css/_popup_menu.scss +++ b/css/_popup_menu.scss @@ -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; } diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 87297d918..51aecc6c3 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -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; } diff --git a/css/filmstrip/_small_video.scss b/css/filmstrip/_small_video.scss index 166636a51..4f7e5ede2 100644 --- a/css/filmstrip/_small_video.scss +++ b/css/filmstrip/_small_video.scss @@ -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; } } diff --git a/css/filmstrip/_tile_view_overrides.scss b/css/filmstrip/_tile_view_overrides.scss index 79a9d3726..658f48918 100644 --- a/css/filmstrip/_tile_view_overrides.scss +++ b/css/filmstrip/_tile_view_overrides.scss @@ -43,6 +43,7 @@ * specifically the various status icons. */ .remotevideomenu, + .localvideomenu, .videocontainer__toptoolbar { z-index: auto; } diff --git a/css/filmstrip/_vertical_filmstrip_overrides.scss b/css/filmstrip/_vertical_filmstrip_overrides.scss index 1a21c6304..1c06c80df 100644 --- a/css/filmstrip/_vertical_filmstrip_overrides.scss +++ b/css/filmstrip/_vertical_filmstrip_overrides.scss @@ -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; diff --git a/modules/API/API.js b/modules/API/API.js index 0ab7d28ef..a23b78c8d 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -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'; diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 7a2a909e3..1c1dd98dc 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -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)); diff --git a/modules/UI/shared_video/SharedVideoThumb.js b/modules/UI/shared_video/SharedVideoThumb.js deleted file mode 100644 index 14eb94bde..000000000 --- a/modules/UI/shared_video/SharedVideoThumb.js +++ /dev/null @@ -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( - - - - - , this.container); - } -} diff --git a/modules/UI/videolayout/Filmstrip.js b/modules/UI/videolayout/Filmstrip.js index 38065ea7e..c9ec6e141 100644 --- a/modules/UI/videolayout/Filmstrip.js +++ b/modules/UI/videolayout/Filmstrip.js @@ -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 }; - } }; diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 0fd7b065d..966b10e19 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -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 LargeContainers where the key is the video * container type. * @type {Object.} */ 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(); }); } diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js deleted file mode 100644 index 1bcfa8b4f..000000000 --- a/modules/UI/videolayout/LocalVideo.js +++ /dev/null @@ -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( - - - - - , 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); - } -} diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js deleted file mode 100644 index cf84c81dd..000000000 --- a/modules/UI/videolayout/RemoteVideo.js +++ /dev/null @@ -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 RemoteVideo. - * @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 true after the 'canplay' event has been - * triggered on the current video element. It goes back to false - * 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( - - - - - , this.container); - } - - /** - * Removes the remote stream element corresponding to the given stream and - * parent container. - * - * @param stream the MediaStream - * @param isVideo true if given stream 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)); - }); - } - } -} diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js deleted file mode 100644 index f495305b6..000000000 --- a/modules/UI/videolayout/SmallVideo.js +++ /dev/null @@ -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 true if this small video isn't currently visible and - * false - 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 SmallVideo is currently - * being displayed on the "large video". - * - * @return {boolean} true if the user is displayed on the large video - * or false 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 SmallVideo. - * - * @return {boolean} true if there is a playable video stream available - * or false otherwise. - */ - isVideoPlayable() { - return isVideoPlayable(APP.store.getState(), this.id); - } - - /** - * Determines what should be display on the thumbnail. - * - * @return {number} one of DISPLAY_VIDEO,DISPLAY_AVATAR - * or DISPLAY_BLACKNESS_WITH_NAME. - */ - 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; - } - } - } -} diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index da5b6ddbc..393a9f453 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -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); } diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index d935602b9..8af8d7935 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -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 SmallVideo. - * - * @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. diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js index 9d2487186..f0ec6518b 100644 --- a/modules/keyboardshortcut/keyboardshortcut.js +++ b/modules/keyboardshortcut/keyboardshortcut.js @@ -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)); } } diff --git a/package-lock.json b/package-lock.json index dd79bfb17..24f2d5983 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 358e0559e..eeb60ec10 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/react/features/base/media/components/web/Video.js b/react/features/base/media/components/web/Video.js index 0c87ea53b..c140228ca 100644 --- a/react/features/base/media/components/web/Video.js +++ b/react/features/base/media/components/web/Video.js @@ -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 { 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 { * @returns {ReactElement} */ render() { + const { + autoPlay, + className, + id, + muted, + playsinline, + style, + eventHandlers + } = this.props; + return (