feat(tile-view): initial implementation for tile view (#3317)
* feat(tile-view): initial implementation for tile view - Modify the classname on the app root so layout can adjust depending on the desired layout mode--vertical filmstrip, horizontal filmstrip, and tile view. - Create a button for toggling tile view. - Add a StateListenerRegistry to automatically update the selected participant and max receiver frame height on tile view toggle. - Rezise thumbnails when switching in and out of tile view. - Move the local video when switching in and out of tile view. - Update reactified pieces of thumbnails when switching in and out of tile view. - Cap the max receiver video quality in tile view based on tile size. - Use CSS to hide UI components that should not display in tile view. - Signal follow me changes. * change local video id for tests * change approach: leverage more css * squash: fix some formatting * squash: prevent pinning, hide pin border in tile view * squash: change logic for maxReceiverQuality due to sidestepping resizing logic * squash: fix typo, columns configurable, remove unused constants * squash: resize with js again * squash: use yana's math for calculating tile size
This commit is contained in:
parent
2f1223f721
commit
c353e9377f
|
@ -14,14 +14,9 @@
|
||||||
* Focused video thumbnail.
|
* Focused video thumbnail.
|
||||||
*/
|
*/
|
||||||
&.videoContainerFocused {
|
&.videoContainerFocused {
|
||||||
transition-duration: 0.5s;
|
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
|
||||||
-webkit-transition-duration: 0.5s;
|
|
||||||
-webkit-animation-name: greyPulse;
|
|
||||||
-webkit-animation-duration: 2s;
|
|
||||||
-webkit-animation-iteration-count: 1;
|
|
||||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
|
|
||||||
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
||||||
0 0 3px $videoThumbnailSelected !important;
|
0 0 3px $videoThumbnailSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remotevideomenu > .icon-menu {
|
.remotevideomenu > .icon-menu {
|
||||||
|
@ -31,7 +26,7 @@
|
||||||
/**
|
/**
|
||||||
* Hovered video thumbnail.
|
* Hovered video thumbnail.
|
||||||
*/
|
*/
|
||||||
&:hover {
|
&:hover:not(.videoContainerFocused):not(.active-speaker) {
|
||||||
cursor: hand;
|
cursor: hand;
|
||||||
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
|
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
|
||||||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
|
||||||
|
*/
|
||||||
|
.tile-view {
|
||||||
|
/**
|
||||||
|
* Add a border around the active speaker to make the thumbnail easier to
|
||||||
|
* see.
|
||||||
|
*/
|
||||||
|
.active-speaker {
|
||||||
|
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
|
||||||
|
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
||||||
|
0 0 3px $videoThumbnailSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filmstripRemoteVideos {
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filmstrip__videos .videocontainer {
|
||||||
|
&:not(.active-speaker),
|
||||||
|
&:hover:not(.active-speaker) {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#remoteVideos {
|
||||||
|
/**
|
||||||
|
* Height is modified with an inline style in horizontal filmstrip mode
|
||||||
|
* so !important is used to override that.
|
||||||
|
*/
|
||||||
|
height: 100% !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filmstrip {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: $filmstripVideosZ
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regardless of the user setting, do not let the filmstrip be in a hidden
|
||||||
|
* state.
|
||||||
|
*/
|
||||||
|
.filmstrip__videos.hidden {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filmstripRemoteVideos {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow scrolling of the thumbnails.
|
||||||
|
*/
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the thumbnails should be set with javascript, based on
|
||||||
|
* desired column count and window width. The rows are created using flex
|
||||||
|
* and allowing the thumbnails to wrap.
|
||||||
|
*/
|
||||||
|
#filmstripRemoteVideosContainer {
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100px 0;
|
||||||
|
|
||||||
|
.videocontainer {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-overflow#filmstripRemoteVideosContainer {
|
||||||
|
align-content: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-overflow .videocontainer {
|
||||||
|
align-self: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firefox flex acts a little differently. To make sure the bottom row of
|
||||||
|
* thumbnails is not overlapped by the horizontal toolbar, margin is added
|
||||||
|
* to the local thumbnail to keep it from the bottom of the screen. It is
|
||||||
|
* assumed the local thumbnail will always be on the bottom row.
|
||||||
|
*/
|
||||||
|
.has-overflow #localVideoContainer {
|
||||||
|
margin-bottom: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Various overrides outside of the filmstrip to style the app to support a
|
||||||
|
* tiled thumbnail experience.
|
||||||
|
*/
|
||||||
|
.tile-view {
|
||||||
|
/**
|
||||||
|
* Let the avatar grow with the tile.
|
||||||
|
*/
|
||||||
|
.userAvatar {
|
||||||
|
max-height: initial;
|
||||||
|
max-width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide various features that should not be displayed while in tile view.
|
||||||
|
*/
|
||||||
|
#dominantSpeaker,
|
||||||
|
#filmstripLocalVideoThumbnail,
|
||||||
|
#largeVideoElementsContainer,
|
||||||
|
#sharedVideo,
|
||||||
|
.filmstrip__toolbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#localConnectionMessage,
|
||||||
|
#remoteConnectionMessage,
|
||||||
|
.watermark {
|
||||||
|
z-index: $filmstripVideosZ + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The follow styling uses !important to override inline styles set with
|
||||||
|
* javascript.
|
||||||
|
*
|
||||||
|
* TODO: These overrides should be more easy to remove and should be removed
|
||||||
|
* when the components are in react so their rendering done declaratively,
|
||||||
|
* making conditional styling easier to apply.
|
||||||
|
*/
|
||||||
|
#largeVideoElementsContainer,
|
||||||
|
#remoteConnectionMessage,
|
||||||
|
#remotePresenceMessage {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
#largeVideoContainer {
|
||||||
|
background-color: $defaultBackground !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,6 +72,8 @@
|
||||||
@import 'filmstrip/filmstrip_toolbar';
|
@import 'filmstrip/filmstrip_toolbar';
|
||||||
@import 'filmstrip/horizontal_filmstrip';
|
@import 'filmstrip/horizontal_filmstrip';
|
||||||
@import 'filmstrip/small_video';
|
@import 'filmstrip/small_video';
|
||||||
|
@import 'filmstrip/tile_view';
|
||||||
|
@import 'filmstrip/tile_view_overrides';
|
||||||
@import 'filmstrip/vertical_filmstrip';
|
@import 'filmstrip/vertical_filmstrip';
|
||||||
@import 'filmstrip/vertical_filmstrip_overrides';
|
@import 'filmstrip/vertical_filmstrip_overrides';
|
||||||
@import 'unsupported-browser/main';
|
@import 'unsupported-browser/main';
|
||||||
|
|
|
@ -48,7 +48,8 @@ var interfaceConfig = {
|
||||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||||
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
|
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
|
||||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
|
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||||
|
'tileview'
|
||||||
],
|
],
|
||||||
|
|
||||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
|
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
|
||||||
|
@ -172,6 +173,12 @@ var interfaceConfig = {
|
||||||
*/
|
*/
|
||||||
RECENT_LIST_ENABLED: true
|
RECENT_LIST_ENABLED: true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many columns the tile view can expand to. The respected range is
|
||||||
|
* between 1 and 5.
|
||||||
|
*/
|
||||||
|
// TILE_VIEW_MAX_COLUMNS: 5,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify custom URL for downloading android mobile app.
|
* Specify custom URL for downloading android mobile app.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -102,6 +102,7 @@
|
||||||
"shortcuts": "Toggle shortcuts",
|
"shortcuts": "Toggle shortcuts",
|
||||||
"speakerStats": "Toggle speaker statistics",
|
"speakerStats": "Toggle speaker statistics",
|
||||||
"toggleCamera": "Toggle camera",
|
"toggleCamera": "Toggle camera",
|
||||||
|
"tileView": "Toggle tile view",
|
||||||
"videomute": "Toggle mute video"
|
"videomute": "Toggle mute video"
|
||||||
},
|
},
|
||||||
"addPeople": "Add people to your call",
|
"addPeople": "Add people to your call",
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
"raiseHand": "Raise / Lower your hand",
|
"raiseHand": "Raise / Lower your hand",
|
||||||
"shortcuts": "View shortcuts",
|
"shortcuts": "View shortcuts",
|
||||||
"speakerStats": "Speaker stats",
|
"speakerStats": "Speaker stats",
|
||||||
|
"tileViewToggle": "Toggle tile view",
|
||||||
"invite": "Invite people"
|
"invite": "Invite people"
|
||||||
},
|
},
|
||||||
"chat":{
|
"chat":{
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
getPinnedParticipant,
|
getPinnedParticipant,
|
||||||
pinParticipant
|
pinParticipant
|
||||||
} from '../react/features/base/participants';
|
} from '../react/features/base/participants';
|
||||||
|
import { setTileView } from '../react/features/video-layout';
|
||||||
import UIEvents from '../service/UI/UIEvents';
|
import UIEvents from '../service/UI/UIEvents';
|
||||||
import VideoLayout from './UI/videolayout/VideoLayout';
|
import VideoLayout from './UI/videolayout/VideoLayout';
|
||||||
|
|
||||||
|
@ -117,6 +118,31 @@ class State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A getter for this object instance to know the state of tile view.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if tile view is enabled.
|
||||||
|
*/
|
||||||
|
get tileViewEnabled() {
|
||||||
|
return this._tileViewEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setter for {@link tileViewEnabled}. Fires a property change event for
|
||||||
|
* other participants to follow.
|
||||||
|
*
|
||||||
|
* @param {boolean} b - Whether or not tile view is enabled.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
set tileViewEnabled(b) {
|
||||||
|
const oldValue = this._tileViewEnabled;
|
||||||
|
|
||||||
|
if (oldValue !== b) {
|
||||||
|
this._tileViewEnabled = b;
|
||||||
|
this._firePropertyChange('tileViewEnabled', oldValue, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invokes {_propertyChangeCallback} to notify it that {property} had its
|
* Invokes {_propertyChangeCallback} to notify it that {property} had its
|
||||||
* value changed from {oldValue} to {newValue}.
|
* value changed from {oldValue} to {newValue}.
|
||||||
|
@ -189,6 +215,10 @@ class FollowMe {
|
||||||
this._sharedDocumentToggled
|
this._sharedDocumentToggled
|
||||||
.bind(this, this._UI.getSharedDocumentManager().isVisible());
|
.bind(this, this._UI.getSharedDocumentManager().isVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._tileViewToggled.bind(
|
||||||
|
this,
|
||||||
|
APP.store.getState()['features/video-layout'].tileViewEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -214,6 +244,10 @@ class FollowMe {
|
||||||
this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
|
this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
|
||||||
this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
|
this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
|
||||||
this.sharedDocEventHandler);
|
this.sharedDocEventHandler);
|
||||||
|
|
||||||
|
this.tileViewEventHandler = this._tileViewToggled.bind(this);
|
||||||
|
this._UI.addListener(UIEvents.TOGGLED_TILE_VIEW,
|
||||||
|
this.tileViewEventHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -227,6 +261,8 @@ class FollowMe {
|
||||||
this.sharedDocEventHandler);
|
this.sharedDocEventHandler);
|
||||||
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
|
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
|
||||||
this.pinnedEndpointEventHandler);
|
this.pinnedEndpointEventHandler);
|
||||||
|
this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
|
||||||
|
this.tileViewEventHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -266,6 +302,18 @@ class FollowMe {
|
||||||
this._local.sharedDocumentVisible = sharedDocumentVisible;
|
this._local.sharedDocumentVisible = sharedDocumentVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies this instance that the tile view mode has been enabled or
|
||||||
|
* disabled.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - True if tile view has been enabled, false
|
||||||
|
* if has been disabled.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_tileViewToggled(enabled) {
|
||||||
|
this._local.tileViewEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the nextOnStage property value.
|
* Changes the nextOnStage property value.
|
||||||
*
|
*
|
||||||
|
@ -316,7 +364,8 @@ class FollowMe {
|
||||||
attributes: {
|
attributes: {
|
||||||
filmstripVisible: local.filmstripVisible,
|
filmstripVisible: local.filmstripVisible,
|
||||||
nextOnStage: local.nextOnStage,
|
nextOnStage: local.nextOnStage,
|
||||||
sharedDocumentVisible: local.sharedDocumentVisible
|
sharedDocumentVisible: local.sharedDocumentVisible,
|
||||||
|
tileViewEnabled: local.tileViewEnabled
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -355,6 +404,7 @@ class FollowMe {
|
||||||
this._onFilmstripVisible(attributes.filmstripVisible);
|
this._onFilmstripVisible(attributes.filmstripVisible);
|
||||||
this._onNextOnStage(attributes.nextOnStage);
|
this._onNextOnStage(attributes.nextOnStage);
|
||||||
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
|
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
|
||||||
|
this._onTileViewEnabled(attributes.tileViewEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -434,6 +484,21 @@ class FollowMe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a tile view enabled / disabled event received from FOLLOW-ME.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - Whether or not tile view should be shown.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onTileViewEnabled(enabled) {
|
||||||
|
if (typeof enabled === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
APP.store.dispatch(setTileView(enabled === 'true'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pins / unpins the video thumbnail given by clickId.
|
* Pins / unpins the video thumbnail given by clickId.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global $ */
|
/* global $, APP */
|
||||||
|
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||||
|
|
||||||
import SmallVideo from '../videolayout/SmallVideo';
|
import SmallVideo from '../videolayout/SmallVideo';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
@ -64,7 +66,9 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
|
||||||
* The thumb click handler.
|
* The thumb click handler.
|
||||||
*/
|
*/
|
||||||
SharedVideoThumb.prototype.videoClick = function() {
|
SharedVideoThumb.prototype.videoClick = function() {
|
||||||
|
if (!shouldDisplayTileView(APP.store.getState())) {
|
||||||
this._togglePin();
|
this._togglePin();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
/* global $, APP, interfaceConfig */
|
/* global $, APP, interfaceConfig */
|
||||||
|
|
||||||
import { setFilmstripVisible } from '../../../react/features/filmstrip';
|
import { setFilmstripVisible } from '../../../react/features/filmstrip';
|
||||||
|
import {
|
||||||
|
LAYOUTS,
|
||||||
|
getCurrentLayout,
|
||||||
|
getMaxColumnCount,
|
||||||
|
getTileViewGridDimensions,
|
||||||
|
shouldDisplayTileView
|
||||||
|
} from '../../../react/features/video-layout';
|
||||||
|
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
import UIUtil from '../util/UIUtil';
|
import UIUtil from '../util/UIUtil';
|
||||||
|
@ -233,6 +240,10 @@ const Filmstrip = {
|
||||||
* @returns {*|{localVideo, remoteVideo}}
|
* @returns {*|{localVideo, remoteVideo}}
|
||||||
*/
|
*/
|
||||||
calculateThumbnailSize() {
|
calculateThumbnailSize() {
|
||||||
|
if (shouldDisplayTileView(APP.store.getState())) {
|
||||||
|
return this._calculateThumbnailSizeForTileView();
|
||||||
|
}
|
||||||
|
|
||||||
const availableSizes = this.calculateAvailableSize();
|
const availableSizes = this.calculateAvailableSize();
|
||||||
const width = availableSizes.availableWidth;
|
const width = availableSizes.availableWidth;
|
||||||
const height = availableSizes.availableHeight;
|
const height = availableSizes.availableHeight;
|
||||||
|
@ -247,11 +258,10 @@ const Filmstrip = {
|
||||||
* @returns {{availableWidth: number, availableHeight: number}}
|
* @returns {{availableWidth: number, availableHeight: number}}
|
||||||
*/
|
*/
|
||||||
calculateAvailableSize() {
|
calculateAvailableSize() {
|
||||||
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
|
const state = APP.store.getState();
|
||||||
const thumbs = this.getThumbs(true);
|
const currentLayout = getCurrentLayout(state);
|
||||||
const numvids = thumbs.remoteThumbs.length;
|
const isHorizontalFilmstripView
|
||||||
|
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
|
||||||
const localVideoContainer = $('#localVideoContainer');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the videoAreaAvailableWidth is set we use this one to calculate
|
* If the videoAreaAvailableWidth is set we use this one to calculate
|
||||||
|
@ -268,10 +278,15 @@ const Filmstrip = {
|
||||||
- UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
|
- UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
|
||||||
- 5;
|
- 5;
|
||||||
|
|
||||||
|
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
|
||||||
let availableWidth = videoAreaAvailableWidth;
|
let availableWidth = videoAreaAvailableWidth;
|
||||||
|
|
||||||
|
const thumbs = this.getThumbs(true);
|
||||||
|
|
||||||
// If local thumb is not hidden
|
// If local thumb is not hidden
|
||||||
if (thumbs.localThumb) {
|
if (thumbs.localThumb) {
|
||||||
|
const localVideoContainer = $('#localVideoContainer');
|
||||||
|
|
||||||
availableWidth = Math.floor(
|
availableWidth = Math.floor(
|
||||||
videoAreaAvailableWidth - (
|
videoAreaAvailableWidth - (
|
||||||
UIUtil.parseCssInt(
|
UIUtil.parseCssInt(
|
||||||
|
@ -289,10 +304,12 @@ const Filmstrip = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the number of videos is 0 or undefined or we're in vertical
|
// If the number of videos is 0 or undefined or we're not in horizontal
|
||||||
// filmstrip mode we don't need to calculate further any adjustments
|
// filmstrip mode we don't need to calculate further any adjustments
|
||||||
// to width based on the number of videos present.
|
// to width based on the number of videos present.
|
||||||
if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
|
const numvids = thumbs.remoteThumbs.length;
|
||||||
|
|
||||||
|
if (numvids && isHorizontalFilmstripView) {
|
||||||
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
|
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
|
||||||
|
|
||||||
availableWidth = Math.floor(
|
availableWidth = Math.floor(
|
||||||
|
@ -322,8 +339,10 @@ const Filmstrip = {
|
||||||
availableHeight
|
availableHeight
|
||||||
= Math.min(maxHeight, window.innerHeight - 18);
|
= Math.min(maxHeight, window.innerHeight - 18);
|
||||||
|
|
||||||
return { availableWidth,
|
return {
|
||||||
availableHeight };
|
availableHeight,
|
||||||
|
availableWidth
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -434,6 +453,51 @@ const Filmstrip = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the size for thumbnails when in tile view layout.
|
||||||
|
*
|
||||||
|
* @returns {{localVideo, remoteVideo}}
|
||||||
|
*/
|
||||||
|
_calculateThumbnailSizeForTileView() {
|
||||||
|
const tileAspectRatio = 16 / 9;
|
||||||
|
|
||||||
|
// The distance from the top and bottom of the screen, as set by CSS, to
|
||||||
|
// avoid overlapping UI elements.
|
||||||
|
const topBottomPadding = 200;
|
||||||
|
|
||||||
|
// Minimum space to keep between the sides of the tiles and the sides
|
||||||
|
// of the window.
|
||||||
|
const sideMargins = 30 * 2;
|
||||||
|
|
||||||
|
const state = APP.store.getState();
|
||||||
|
|
||||||
|
const viewWidth = document.body.clientWidth - sideMargins;
|
||||||
|
const viewHeight = document.body.clientHeight - topBottomPadding;
|
||||||
|
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
visibleRows
|
||||||
|
} = getTileViewGridDimensions(state, getMaxColumnCount());
|
||||||
|
const initialWidth = viewWidth / columns;
|
||||||
|
const aspectRatioHeight = initialWidth / tileAspectRatio;
|
||||||
|
|
||||||
|
const heightOfEach = Math.min(
|
||||||
|
aspectRatioHeight,
|
||||||
|
viewHeight / visibleRows);
|
||||||
|
const widthOfEach = tileAspectRatio * heightOfEach;
|
||||||
|
|
||||||
|
return {
|
||||||
|
localVideo: {
|
||||||
|
thumbWidth: widthOfEach,
|
||||||
|
thumbHeight: heightOfEach
|
||||||
|
},
|
||||||
|
remoteVideo: {
|
||||||
|
thumbWidth: widthOfEach,
|
||||||
|
thumbHeight: heightOfEach
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes thumbnails
|
* Resizes thumbnails
|
||||||
* @param local
|
* @param local
|
||||||
|
@ -443,6 +507,28 @@ const Filmstrip = {
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line max-params
|
// eslint-disable-next-line max-params
|
||||||
resizeThumbnails(local, remote, forceUpdate = false) {
|
resizeThumbnails(local, remote, forceUpdate = false) {
|
||||||
|
const state = APP.store.getState();
|
||||||
|
|
||||||
|
if (shouldDisplayTileView(state)) {
|
||||||
|
// The size of the side margins for each tile as set in CSS.
|
||||||
|
const sideMargins = 10 * 2;
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
rows
|
||||||
|
} = getTileViewGridDimensions(state, getMaxColumnCount());
|
||||||
|
const hasOverflow = rows > columns;
|
||||||
|
|
||||||
|
// Width is set so that the flex layout can automatically wrap
|
||||||
|
// tiles onto new rows.
|
||||||
|
this.filmstripRemoteVideos.css({
|
||||||
|
width: (local.thumbWidth * columns) + (columns * sideMargins)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
|
||||||
|
} else {
|
||||||
|
this.filmstripRemoteVideos.css('width', '');
|
||||||
|
}
|
||||||
|
|
||||||
const thumbs = this.getThumbs(!forceUpdate);
|
const thumbs = this.getThumbs(!forceUpdate);
|
||||||
|
|
||||||
if (thumbs.localThumb) {
|
if (thumbs.localThumb) {
|
||||||
|
@ -466,13 +552,15 @@ const Filmstrip = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||||
|
|
||||||
// Let CSS take care of height in vertical filmstrip mode.
|
// Let CSS take care of height in vertical filmstrip mode.
|
||||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||||
$('#filmstripLocalVideo').css({
|
$('#filmstripLocalVideo').css({
|
||||||
// adds 4 px because of small video 2px border
|
// adds 4 px because of small video 2px border
|
||||||
width: `${local.thumbWidth + 4}px`
|
width: `${local.thumbWidth + 4}px`
|
||||||
});
|
});
|
||||||
} else {
|
} else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
|
||||||
this.filmstrip.css({
|
this.filmstrip.css({
|
||||||
// adds 4 px because of small video 2px border
|
// adds 4 px because of small video 2px border
|
||||||
height: `${remote.thumbHeight + 4}px`
|
height: `${remote.thumbHeight + 4}px`
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
getAvatarURLByParticipantId
|
getAvatarURLByParticipantId
|
||||||
} from '../../../react/features/base/participants';
|
} from '../../../react/features/base/participants';
|
||||||
import { updateSettings } from '../../../react/features/base/settings';
|
import { updateSettings } from '../../../react/features/base/settings';
|
||||||
|
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
@ -26,7 +27,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||||
this.streamEndedCallback = streamEndedCallback;
|
this.streamEndedCallback = streamEndedCallback;
|
||||||
this.container = this.createContainer();
|
this.container = this.createContainer();
|
||||||
this.$container = $(this.container);
|
this.$container = $(this.container);
|
||||||
$('#filmstripLocalVideoThumbnail').append(this.container);
|
this.updateDOMLocation();
|
||||||
|
|
||||||
this.localVideoId = null;
|
this.localVideoId = null;
|
||||||
this.bindHoverHandler();
|
this.bindHoverHandler();
|
||||||
|
@ -109,16 +110,7 @@ LocalVideo.prototype.changeVideo = function(stream) {
|
||||||
|
|
||||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||||
|
|
||||||
const localVideoContainer = document.getElementById('localVideoWrapper');
|
this._updateVideoElement();
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<Provider store = { APP.store }>
|
|
||||||
<VideoTrack
|
|
||||||
id = { this.localVideoId }
|
|
||||||
videoTrack = {{ jitsiTrack: stream }} />
|
|
||||||
</Provider>,
|
|
||||||
localVideoContainer
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line eqeqeq
|
||||||
const isVideo = stream.videoType != 'desktop';
|
const isVideo = stream.videoType != 'desktop';
|
||||||
|
@ -128,12 +120,14 @@ LocalVideo.prototype.changeVideo = function(stream) {
|
||||||
this.setFlipX(isVideo ? settings.localFlipX : false);
|
this.setFlipX(isVideo ? settings.localFlipX : false);
|
||||||
|
|
||||||
const endedHandler = () => {
|
const endedHandler = () => {
|
||||||
|
const localVideoContainer
|
||||||
|
= document.getElementById('localVideoWrapper');
|
||||||
|
|
||||||
// Only remove if there is no video and not a transition state.
|
// Only remove if there is no video and not a transition state.
|
||||||
// Previous non-react logic created a new video element with each track
|
// Previous non-react logic created a new video element with each track
|
||||||
// removal whereas react reuses the video component so it could be the
|
// removal whereas react reuses the video component so it could be the
|
||||||
// stream ended but a new one is being used.
|
// stream ended but a new one is being used.
|
||||||
if (this.videoStream.isEnded()) {
|
if (localVideoContainer && this.videoStream.isEnded()) {
|
||||||
ReactDOM.unmountComponentAtNode(localVideoContainer);
|
ReactDOM.unmountComponentAtNode(localVideoContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +229,29 @@ LocalVideo.prototype._enableDisableContextMenu = function(enable) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places the {@code LocalVideo} in the DOM based on the current video layout.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
LocalVideo.prototype.updateDOMLocation = function() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
this._updateVideoElement();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback invoked when the thumbnail is clicked. Will directly call
|
* Callback invoked when the thumbnail is clicked. Will directly call
|
||||||
* VideoLayout to handle thumbnail click if certain elements have not been
|
* VideoLayout to handle thumbnail click if certain elements have not been
|
||||||
|
@ -258,7 +275,9 @@ LocalVideo.prototype._onContainerClick = function(event) {
|
||||||
= $source.parents('.displayNameContainer').length > 0;
|
= $source.parents('.displayNameContainer').length > 0;
|
||||||
const clickedOnPopover = $source.parents('.popover').length > 0
|
const clickedOnPopover = $source.parents('.popover').length > 0
|
||||||
|| classList.contains('popover');
|
|| classList.contains('popover');
|
||||||
const ignoreClick = clickedOnDisplayName || clickedOnPopover;
|
const ignoreClick = clickedOnDisplayName
|
||||||
|
|| clickedOnPopover
|
||||||
|
|| shouldDisplayTileView(APP.store.getState());
|
||||||
|
|
||||||
if (event.stopPropagation && !ignoreClick) {
|
if (event.stopPropagation && !ignoreClick) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -269,4 +288,28 @@ LocalVideo.prototype._onContainerClick = function(event) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the React Element for displaying video in {@code LocalVideo}.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
LocalVideo.prototype._updateVideoElement = function() {
|
||||||
|
const localVideoContainer = document.getElementById('localVideoWrapper');
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<Provider store = { APP.store }>
|
||||||
|
<VideoTrack
|
||||||
|
id = 'localVideo_container'
|
||||||
|
videoTrack = {{ jitsiTrack: this.videoStream }} />
|
||||||
|
</Provider>,
|
||||||
|
localVideoContainer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure the video gets play() called on it. This may be necessary in the
|
||||||
|
// case where the local video container was moved and re-attached, in which
|
||||||
|
// case video does not autoplay.
|
||||||
|
const video = this.container.querySelector('video');
|
||||||
|
|
||||||
|
video && video.play();
|
||||||
|
};
|
||||||
|
|
||||||
export default LocalVideo;
|
export default LocalVideo;
|
||||||
|
|
|
@ -20,6 +20,11 @@ import {
|
||||||
REMOTE_CONTROL_MENU_STATES,
|
REMOTE_CONTROL_MENU_STATES,
|
||||||
RemoteVideoMenuTriggerButton
|
RemoteVideoMenuTriggerButton
|
||||||
} from '../../../react/features/remote-video-menu';
|
} from '../../../react/features/remote-video-menu';
|
||||||
|
import {
|
||||||
|
LAYOUTS,
|
||||||
|
getCurrentLayout,
|
||||||
|
shouldDisplayTileView
|
||||||
|
} from '../../../react/features/video-layout';
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
@ -163,8 +168,17 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||||
const onVolumeChange = this._setAudioVolume;
|
const onVolumeChange = this._setAudioVolume;
|
||||||
const { isModerator } = APP.conference;
|
const { isModerator } = APP.conference;
|
||||||
const participantID = this.id;
|
const participantID = this.id;
|
||||||
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
|
|
||||||
? 'left bottom' : 'top center';
|
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||||
|
let remoteMenuPosition;
|
||||||
|
|
||||||
|
if (currentLayout === LAYOUTS.TILE_VIEW) {
|
||||||
|
remoteMenuPosition = 'left top';
|
||||||
|
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||||
|
remoteMenuPosition = 'left bottom';
|
||||||
|
} else {
|
||||||
|
remoteMenuPosition = 'top center';
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store = { APP.store }>
|
<Provider store = { APP.store }>
|
||||||
|
@ -174,7 +188,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||||
initialVolumeValue = { initialVolumeValue }
|
initialVolumeValue = { initialVolumeValue }
|
||||||
isAudioMuted = { this.isAudioMuted }
|
isAudioMuted = { this.isAudioMuted }
|
||||||
isModerator = { isModerator }
|
isModerator = { isModerator }
|
||||||
menuPosition = { menuPosition }
|
menuPosition = { remoteMenuPosition }
|
||||||
onMenuDisplay
|
onMenuDisplay
|
||||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
||||||
onRemoteControlToggle = { onRemoteControlToggle }
|
onRemoteControlToggle = { onRemoteControlToggle }
|
||||||
|
@ -613,7 +627,8 @@ RemoteVideo.prototype._onContainerClick = function(event) {
|
||||||
const { classList } = event.target;
|
const { classList } = event.target;
|
||||||
|
|
||||||
const ignoreClick = $source.parents('.popover').length > 0
|
const ignoreClick = $source.parents('.popover').length > 0
|
||||||
|| classList.contains('popover');
|
|| classList.contains('popover')
|
||||||
|
|| shouldDisplayTileView(APP.store.getState());
|
||||||
|
|
||||||
if (!ignoreClick) {
|
if (!ignoreClick) {
|
||||||
this._togglePin();
|
this._togglePin();
|
||||||
|
|
|
@ -27,6 +27,11 @@ import {
|
||||||
RaisedHandIndicator,
|
RaisedHandIndicator,
|
||||||
VideoMutedIndicator
|
VideoMutedIndicator
|
||||||
} from '../../../react/features/filmstrip';
|
} from '../../../react/features/filmstrip';
|
||||||
|
import {
|
||||||
|
LAYOUTS,
|
||||||
|
getCurrentLayout,
|
||||||
|
shouldDisplayTileView
|
||||||
|
} from '../../../react/features/video-layout';
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
@ -328,7 +333,21 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
|
||||||
SmallVideo.prototype.updateStatusBar = function() {
|
SmallVideo.prototype.updateStatusBar = function() {
|
||||||
const statusBarContainer
|
const statusBarContainer
|
||||||
= this.container.querySelector('.videocontainer__toolbar');
|
= this.container.querySelector('.videocontainer__toolbar');
|
||||||
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
|
|
||||||
|
if (!statusBarContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||||
|
let tooltipPosition;
|
||||||
|
|
||||||
|
if (currentLayout === LAYOUTS.TILE_VIEW) {
|
||||||
|
tooltipPosition = 'right';
|
||||||
|
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||||
|
tooltipPosition = 'left';
|
||||||
|
} else {
|
||||||
|
tooltipPosition = 'top';
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nextProvider i18n = { i18next }>
|
<I18nextProvider i18n = { i18next }>
|
||||||
|
@ -547,7 +566,8 @@ SmallVideo.prototype.isVideoPlayable = function() {
|
||||||
*/
|
*/
|
||||||
SmallVideo.prototype.selectDisplayMode = function() {
|
SmallVideo.prototype.selectDisplayMode = function() {
|
||||||
// Display name is always and only displayed when user is on the stage
|
// Display name is always and only displayed when user is on the stage
|
||||||
if (this.isCurrentlyOnLargeVideo()) {
|
if (this.isCurrentlyOnLargeVideo()
|
||||||
|
&& !shouldDisplayTileView(APP.store.getState())) {
|
||||||
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
|
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
|
||||||
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
||||||
} else if (this.isVideoPlayable()
|
} else if (this.isVideoPlayable()
|
||||||
|
@ -685,7 +705,10 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
|
||||||
|
|
||||||
this._showDominantSpeaker = show;
|
this._showDominantSpeaker = show;
|
||||||
|
|
||||||
|
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
|
||||||
|
|
||||||
this.updateIndicators();
|
this.updateIndicators();
|
||||||
|
this.updateView();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -765,6 +788,18 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for re-rendering multiple react components of the small
|
||||||
|
* video.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
SmallVideo.prototype.rerender = function() {
|
||||||
|
this.updateIndicators();
|
||||||
|
this.updateStatusBar();
|
||||||
|
this.updateView();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the React element responsible for showing connection status, dominant
|
* Updates the React element responsible for showing connection status, dominant
|
||||||
* speaker, and raised hand icons. Uses instance variables to get the necessary
|
* speaker, and raised hand icons. Uses instance variables to get the necessary
|
||||||
|
@ -784,7 +819,19 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||||
const iconSize = UIUtil.getIndicatorFontSize();
|
const iconSize = UIUtil.getIndicatorFontSize();
|
||||||
const showConnectionIndicator = this.videoIsHovered
|
const showConnectionIndicator = this.videoIsHovered
|
||||||
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
|
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
|
||||||
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
|
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||||
|
let statsPopoverPosition, tooltipPosition;
|
||||||
|
|
||||||
|
if (currentLayout === LAYOUTS.TILE_VIEW) {
|
||||||
|
statsPopoverPosition = 'right top';
|
||||||
|
tooltipPosition = 'right';
|
||||||
|
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||||
|
statsPopoverPosition = this.statsPopoverLocation;
|
||||||
|
tooltipPosition = 'left';
|
||||||
|
} else {
|
||||||
|
statsPopoverPosition = this.statsPopoverLocation;
|
||||||
|
tooltipPosition = 'top';
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nextProvider i18n = { i18next }>
|
<I18nextProvider i18n = { i18next }>
|
||||||
|
@ -799,7 +846,7 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||||
enableStatsDisplay
|
enableStatsDisplay
|
||||||
= { !interfaceConfig.filmStripOnly }
|
= { !interfaceConfig.filmStripOnly }
|
||||||
statsPopoverPosition
|
statsPopoverPosition
|
||||||
= { this.statsPopoverLocation }
|
= { statsPopoverPosition }
|
||||||
userID = { this.id } />
|
userID = { this.id } />
|
||||||
: null }
|
: null }
|
||||||
{ this._showRaisedHand
|
{ this._showRaisedHand
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
/* global APP, $, interfaceConfig */
|
/* global APP, $, interfaceConfig */
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
import {
|
||||||
|
getNearestReceiverVideoQualityLevel,
|
||||||
|
setMaxReceiverVideoQuality
|
||||||
|
} from '../../../react/features/base/conference';
|
||||||
import {
|
import {
|
||||||
JitsiParticipantConnectionStatus
|
JitsiParticipantConnectionStatus
|
||||||
} from '../../../react/features/base/lib-jitsi-meet';
|
} from '../../../react/features/base/lib-jitsi-meet';
|
||||||
|
@ -9,6 +13,9 @@ import {
|
||||||
getPinnedParticipant,
|
getPinnedParticipant,
|
||||||
pinParticipant
|
pinParticipant
|
||||||
} from '../../../react/features/base/participants';
|
} from '../../../react/features/base/participants';
|
||||||
|
import {
|
||||||
|
shouldDisplayTileView
|
||||||
|
} from '../../../react/features/video-layout';
|
||||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
||||||
|
|
||||||
|
@ -594,12 +601,19 @@ const VideoLayout = {
|
||||||
|
|
||||||
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
|
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
|
||||||
|
|
||||||
|
if (shouldDisplayTileView(APP.store.getState())) {
|
||||||
|
const height
|
||||||
|
= (localVideo && localVideo.thumbHeight)
|
||||||
|
|| (remoteVideo && remoteVideo.thumbnHeight)
|
||||||
|
|| 0;
|
||||||
|
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
|
||||||
|
|
||||||
|
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
|
||||||
|
}
|
||||||
|
|
||||||
if (onComplete && typeof onComplete === 'function') {
|
if (onComplete && typeof onComplete === 'function') {
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { localVideo,
|
|
||||||
remoteVideo };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1142,6 +1156,22 @@ const VideoLayout = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to invoke when the video layout has changed and elements
|
||||||
|
* have to be re-arranged and resized.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
refreshLayout() {
|
||||||
|
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
|
||||||
|
VideoLayout.resizeVideoArea();
|
||||||
|
|
||||||
|
localVideoThumbnail && localVideoThumbnail.rerender();
|
||||||
|
Object.values(remoteVideos).forEach(
|
||||||
|
remoteVideo => remoteVideo.rerender()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers an update of large video if the passed in participant is
|
* Triggers an update of large video if the passed in participant is
|
||||||
* currently displayed on large video.
|
* currently displayed on large video.
|
||||||
|
|
|
@ -8,7 +8,8 @@ import {
|
||||||
AVATAR_ID_COMMAND,
|
AVATAR_ID_COMMAND,
|
||||||
AVATAR_URL_COMMAND,
|
AVATAR_URL_COMMAND,
|
||||||
EMAIL_COMMAND,
|
EMAIL_COMMAND,
|
||||||
JITSI_CONFERENCE_URL_KEY
|
JITSI_CONFERENCE_URL_KEY,
|
||||||
|
VIDEO_QUALITY_LEVELS
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
@ -102,6 +103,38 @@ export function getCurrentConference(stateful: Function | Object) {
|
||||||
: joining);
|
: joining);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the nearest match for the passed in {@link availableHeight} to am
|
||||||
|
* enumerated value in {@code VIDEO_QUALITY_LEVELS}.
|
||||||
|
*
|
||||||
|
* @param {number} availableHeight - The height to which a matching video
|
||||||
|
* quality level should be found.
|
||||||
|
* @returns {number} The closest matching value from
|
||||||
|
* {@code VIDEO_QUALITY_LEVELS}.
|
||||||
|
*/
|
||||||
|
export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
|
||||||
|
const qualityLevels = [
|
||||||
|
VIDEO_QUALITY_LEVELS.HIGH,
|
||||||
|
VIDEO_QUALITY_LEVELS.STANDARD,
|
||||||
|
VIDEO_QUALITY_LEVELS.LOW
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedLevel = qualityLevels[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < qualityLevels.length; i++) {
|
||||||
|
const previousValue = qualityLevels[i - 1];
|
||||||
|
const currentValue = qualityLevels[i];
|
||||||
|
const diffWithCurrent = Math.abs(availableHeight - currentValue);
|
||||||
|
const diffWithPrevious = Math.abs(availableHeight - previousValue);
|
||||||
|
|
||||||
|
if (diffWithCurrent < diffWithPrevious) {
|
||||||
|
selectedLevel = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedLevel;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
|
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
|
||||||
* manipulating a conference participant (e.g. pin or select participant).
|
* manipulating a conference participant (e.g. pin or select participant).
|
||||||
|
|
|
@ -4,6 +4,8 @@ import _ from 'lodash';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect as reactReduxConnect } from 'react-redux';
|
import { connect as reactReduxConnect } from 'react-redux';
|
||||||
|
|
||||||
|
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
||||||
|
|
||||||
import { obtainConfig } from '../../base/config';
|
import { obtainConfig } from '../../base/config';
|
||||||
import { connect, disconnect } from '../../base/connection';
|
import { connect, disconnect } from '../../base/connection';
|
||||||
import { DialogContainer } from '../../base/dialog';
|
import { DialogContainer } from '../../base/dialog';
|
||||||
|
@ -13,6 +15,12 @@ import { CalleeInfoContainer } from '../../invite';
|
||||||
import { LargeVideo } from '../../large-video';
|
import { LargeVideo } from '../../large-video';
|
||||||
import { NotificationsContainer } from '../../notifications';
|
import { NotificationsContainer } from '../../notifications';
|
||||||
import { SidePanel } from '../../side-panel';
|
import { SidePanel } from '../../side-panel';
|
||||||
|
import {
|
||||||
|
LAYOUTS,
|
||||||
|
getCurrentLayout,
|
||||||
|
shouldDisplayTileView
|
||||||
|
} from '../../video-layout';
|
||||||
|
|
||||||
import { default as Notice } from './Notice';
|
import { default as Notice } from './Notice';
|
||||||
import {
|
import {
|
||||||
Toolbox,
|
Toolbox,
|
||||||
|
@ -49,9 +57,10 @@ const FULL_SCREEN_EVENTS = [
|
||||||
* @private
|
* @private
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
const LAYOUT_CLASSES = {
|
const LAYOUT_CLASSNAMES = {
|
||||||
HORIZONTAL_FILMSTRIP: 'horizontal-filmstrip',
|
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
|
||||||
VERTICAL_FILMSTRIP: 'vertical-filmstrip'
|
[LAYOUTS.TILE_VIEW]: 'tile-view',
|
||||||
|
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,13 +77,18 @@ type Props = {
|
||||||
* The CSS class to apply to the root of {@link Conference} to modify the
|
* The CSS class to apply to the root of {@link Conference} to modify the
|
||||||
* application layout.
|
* application layout.
|
||||||
*/
|
*/
|
||||||
_layoutModeClassName: string,
|
_layoutClassName: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conference room name.
|
* Conference room name.
|
||||||
*/
|
*/
|
||||||
_room: string,
|
_room: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current UI layout should be in tile view.
|
||||||
|
*/
|
||||||
|
_shouldDisplayTileView: boolean,
|
||||||
|
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
t: Function
|
t: Function
|
||||||
}
|
}
|
||||||
|
@ -143,6 +157,25 @@ class Conference extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls into legacy UI to update the application layout, if necessary.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* returns {void}
|
||||||
|
*/
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props._shouldDisplayTileView
|
||||||
|
=== prevProps._shouldDisplayTileView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: For now VideoLayout is being called as LargeVideo and Filmstrip
|
||||||
|
// sizing logic is still handled outside of React. Once all components
|
||||||
|
// are in react they should calculate size on their own as much as
|
||||||
|
// possible and pass down sizings.
|
||||||
|
VideoLayout.refreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect from the conference when component will be
|
* Disconnect from the conference when component will be
|
||||||
* unmounted.
|
* unmounted.
|
||||||
|
@ -180,7 +213,7 @@ class Conference extends Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className = { this.props._layoutModeClassName }
|
className = { this.props._layoutClassName }
|
||||||
id = 'videoconference_page'
|
id = 'videoconference_page'
|
||||||
onMouseMove = { this._onShowToolbar }>
|
onMouseMove = { this._onShowToolbar }>
|
||||||
<Notice />
|
<Notice />
|
||||||
|
@ -257,29 +290,19 @@ class Conference extends Component<Props> {
|
||||||
* @private
|
* @private
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* _iAmRecorder: boolean,
|
* _iAmRecorder: boolean,
|
||||||
* _room: ?string
|
* _layoutClassName: string,
|
||||||
|
* _room: ?string,
|
||||||
|
* _shouldDisplayTileView: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
const { room } = state['features/base/conference'];
|
const currentLayout = getCurrentLayout(state);
|
||||||
const { iAmRecorder } = state['features/base/config'];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||||
* Whether the local participant is recording the conference.
|
_layoutClassName: LAYOUT_CLASSNAMES[currentLayout],
|
||||||
*
|
_room: state['features/base/conference'].room,
|
||||||
* @private
|
_shouldDisplayTileView: shouldDisplayTileView(state)
|
||||||
*/
|
|
||||||
_iAmRecorder: iAmRecorder,
|
|
||||||
|
|
||||||
_layoutModeClassName: interfaceConfig.VERTICAL_FILMSTRIP
|
|
||||||
? LAYOUT_CLASSES.VERTICAL_FILMSTRIP
|
|
||||||
: LAYOUT_CLASSES.HORIZONTAL_FILMSTRIP,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conference room name.
|
|
||||||
*/
|
|
||||||
_room: room
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { dockToolbox } from '../../../toolbox';
|
||||||
|
|
||||||
import { setFilmstripHovered } from '../../actions';
|
import { setFilmstripHovered } from '../../actions';
|
||||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||||
|
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
|
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
@ -185,9 +186,8 @@ function _mapStateToProps(state) {
|
||||||
&& state['features/toolbox'].visible
|
&& state['features/toolbox'].visible
|
||||||
&& interfaceConfig.TOOLBAR_BUTTONS.length;
|
&& interfaceConfig.TOOLBAR_BUTTONS.length;
|
||||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||||
|
|
||||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||||
reduceHeight ? 'reduce-height' : ''}`;
|
reduceHeight ? 'reduce-height' : ''}`.trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_className: className,
|
_className: className,
|
||||||
|
|
|
@ -6,7 +6,9 @@ import {
|
||||||
} from '../analytics';
|
} from '../analytics';
|
||||||
import { _handleParticipantError } from '../base/conference';
|
import { _handleParticipantError } from '../base/conference';
|
||||||
import { MEDIA_TYPE } from '../base/media';
|
import { MEDIA_TYPE } from '../base/media';
|
||||||
|
import { getParticipants } from '../base/participants';
|
||||||
import { reportError } from '../base/util';
|
import { reportError } from '../base/util';
|
||||||
|
import { shouldDisplayTileView } from '../video-layout';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SELECT_LARGE_VIDEO_PARTICIPANT,
|
SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||||
|
@ -26,17 +28,19 @@ export function selectParticipant() {
|
||||||
const { conference } = state['features/base/conference'];
|
const { conference } = state['features/base/conference'];
|
||||||
|
|
||||||
if (conference) {
|
if (conference) {
|
||||||
const largeVideo = state['features/large-video'];
|
const ids = shouldDisplayTileView(state)
|
||||||
const id = largeVideo.participantId;
|
? getParticipants(state).map(participant => participant.id)
|
||||||
|
: [ state['features/large-video'].participantId ];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
conference.selectParticipant(id);
|
conference.selectParticipants(ids);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_handleParticipantError(err);
|
_handleParticipantError(err);
|
||||||
|
|
||||||
sendAnalytics(createSelectParticipantFailedEvent(err));
|
sendAnalytics(createSelectParticipantFailedEvent(err));
|
||||||
|
|
||||||
reportError(err, `Failed to select participant ${id}`);
|
reportError(
|
||||||
|
err, `Failed to select participants ${ids.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||||
|
|
||||||
import { isFilmstripVisible } from '../../filmstrip';
|
import { isFilmstripVisible } from '../../filmstrip';
|
||||||
import { RecordingLabel } from '../../recording';
|
import { RecordingLabel } from '../../recording';
|
||||||
|
import { shouldDisplayTileView } from '../../video-layout';
|
||||||
import { VideoQualityLabel } from '../../video-quality';
|
import { VideoQualityLabel } from '../../video-quality';
|
||||||
import { TranscribingLabel } from '../../transcribing/';
|
import { TranscribingLabel } from '../../transcribing/';
|
||||||
|
|
||||||
|
@ -17,6 +18,11 @@ export type Props = {
|
||||||
* determine display classes to set.
|
* determine display classes to set.
|
||||||
*/
|
*/
|
||||||
_filmstripVisible: boolean,
|
_filmstripVisible: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the video quality label should be displayed.
|
||||||
|
*/
|
||||||
|
_showVideoQualityLabel: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,11 +78,13 @@ export default class AbstractLabels<P: Props, S> extends Component<P, S> {
|
||||||
* @param {Object} state - The Redux state.
|
* @param {Object} state - The Redux state.
|
||||||
* @private
|
* @private
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* _filmstripVisible: boolean
|
* _filmstripVisible: boolean,
|
||||||
|
* _showVideoQualityLabel: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function _abstractMapStateToProps(state: Object) {
|
export function _abstractMapStateToProps(state: Object) {
|
||||||
return {
|
return {
|
||||||
_filmstripVisible: isFilmstripVisible(state)
|
_filmstripVisible: isFilmstripVisible(state),
|
||||||
|
_showVideoQualityLabel: !shouldDisplayTileView(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,8 @@ class Labels extends AbstractLabels<Props, State> {
|
||||||
this._renderTranscribingLabel()
|
this._renderTranscribingLabel()
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this._renderVideoQualityLabel()
|
this.props._showVideoQualityLabel
|
||||||
|
&& this._renderVideoQualityLabel()
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default class LargeVideo extends Component<*> {
|
||||||
</div>
|
</div>
|
||||||
<div id = 'remotePresenceMessage' />
|
<div id = 'remotePresenceMessage' />
|
||||||
<span id = 'remoteConnectionMessage' />
|
<span id = 'remoteConnectionMessage' />
|
||||||
<div>
|
<div id = 'largeVideoElementsContainer'>
|
||||||
<div id = 'largeVideoBackgroundContainer' />
|
<div id = 'largeVideoBackgroundContainer' />
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ import {
|
||||||
import { toggleSharedVideo } from '../../../shared-video';
|
import { toggleSharedVideo } from '../../../shared-video';
|
||||||
import { toggleChat } from '../../../side-panel';
|
import { toggleChat } from '../../../side-panel';
|
||||||
import { SpeakerStats } from '../../../speaker-stats';
|
import { SpeakerStats } from '../../../speaker-stats';
|
||||||
|
import { TileViewButton } from '../../../video-layout';
|
||||||
import {
|
import {
|
||||||
OverflowMenuVideoQualityItem,
|
OverflowMenuVideoQualityItem,
|
||||||
VideoQualityDialog
|
VideoQualityDialog
|
||||||
|
@ -369,6 +370,8 @@ class Toolbox extends Component<Props> {
|
||||||
visible = { this._shouldShowButton('camera') } />
|
visible = { this._shouldShowButton('camera') } />
|
||||||
</div>
|
</div>
|
||||||
<div className = 'button-group-right'>
|
<div className = 'button-group-right'>
|
||||||
|
{ this._shouldShowButton('tileview')
|
||||||
|
&& <TileViewButton /> }
|
||||||
{ this._shouldShowButton('invite')
|
{ this._shouldShowButton('invite')
|
||||||
&& !_hideInviteButton
|
&& !_hideInviteButton
|
||||||
&& <ToolbarButton
|
&& <ToolbarButton
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* The type of the action which enables or disables the feature for showing
|
||||||
|
* video thumbnails in a two-axis tile view.
|
||||||
|
*
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_TILE_VIEW,
|
||||||
|
* enabled: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const SET_TILE_VIEW = Symbol('SET_TILE_VIEW');
|
|
@ -0,0 +1,20 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { SET_TILE_VIEW } from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a (redux) action which signals to set the UI layout to be tiled view
|
||||||
|
* or not.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - Whether or not tile view should be shown.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_TILE_VIEW,
|
||||||
|
* enabled: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setTileView(enabled: boolean) {
|
||||||
|
return {
|
||||||
|
type: SET_TILE_VIEW,
|
||||||
|
enabled
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createToolbarEvent,
|
||||||
|
sendAnalytics
|
||||||
|
} from '../../analytics';
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
import {
|
||||||
|
AbstractButton,
|
||||||
|
type AbstractButtonProps
|
||||||
|
} from '../../base/toolbox';
|
||||||
|
|
||||||
|
import { setTileView } from '../actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of {@link TileViewButton}.
|
||||||
|
*/
|
||||||
|
type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not tile view layout has been enabled as the user preference.
|
||||||
|
*/
|
||||||
|
_tileViewEnabled: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to dispatch actions from the buttons.
|
||||||
|
*/
|
||||||
|
dispatch: Dispatch<*>
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders a toolbar button for toggling the tile layout view.
|
||||||
|
*
|
||||||
|
* @extends AbstractButton
|
||||||
|
*/
|
||||||
|
class TileViewButton<P: Props> extends AbstractButton<P, *> {
|
||||||
|
accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
|
||||||
|
iconName = 'icon-tiles-many';
|
||||||
|
toggledIconName = 'icon-tiles-many toggled';
|
||||||
|
tooltip = 'toolbar.tileViewToggle';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clicking / pressing the button.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_handleClick() {
|
||||||
|
const { _tileViewEnabled, dispatch } = this.props;
|
||||||
|
|
||||||
|
sendAnalytics(createToolbarEvent(
|
||||||
|
'tileview.button',
|
||||||
|
{
|
||||||
|
'is_enabled': _tileViewEnabled
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch(setTileView(!_tileViewEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether this button is in toggled state or not.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
* @protected
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_isToggled() {
|
||||||
|
return this.props._tileViewEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the redux state to the associated props for the
|
||||||
|
* {@code TileViewButton} component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @returns {{
|
||||||
|
* _tileViewEnabled: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
_tileViewEnabled: state['features/video-layout'].tileViewEnabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(TileViewButton));
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as TileViewButton } from './TileViewButton';
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* An enumeration of the different display layouts supported by the application.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
export const LAYOUTS = {
|
||||||
|
HORIZONTAL_FILMSTRIP_VIEW: 'horizontal-filmstrip-view',
|
||||||
|
TILE_VIEW: 'tile-view',
|
||||||
|
VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view'
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { LAYOUTS } from './constants';
|
||||||
|
|
||||||
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@code LAYOUTS} constant associated with the layout
|
||||||
|
* the application should currently be in.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getCurrentLayout(state: Object) {
|
||||||
|
if (shouldDisplayTileView(state)) {
|
||||||
|
return LAYOUTS.TILE_VIEW;
|
||||||
|
} else if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||||
|
return LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns how many columns should be displayed in tile view. The number
|
||||||
|
* returned will be between 1 and 5, inclusive.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function getMaxColumnCount() {
|
||||||
|
const configuredMax = interfaceConfig.TILE_VIEW_MAX_COLUMNS || 5;
|
||||||
|
|
||||||
|
return Math.max(Math.min(configuredMax, 1), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cell count dimensions for tile view. Tile view tries to uphold
|
||||||
|
* equal count of tiles for height and width, until maxColumn is reached in
|
||||||
|
* which rows will be added but no more columns.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {number} maxColumns - The maximum number of columns that can be
|
||||||
|
* displayed.
|
||||||
|
* @returns {Object} An object is return with the desired number of columns,
|
||||||
|
* rows, and visible rows (the rest should overflow) for the tile view layout.
|
||||||
|
*/
|
||||||
|
export function getTileViewGridDimensions(state: Object, maxColumns: number) {
|
||||||
|
// Purposefully include all participants, which includes fake participants
|
||||||
|
// that should show a thumbnail.
|
||||||
|
const potentialThumbnails = state['features/base/participants'].length;
|
||||||
|
|
||||||
|
const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails));
|
||||||
|
const columns = Math.min(columnsToMaintainASquare, maxColumns);
|
||||||
|
const rows = Math.ceil(potentialThumbnails / columns);
|
||||||
|
const visibleRows = Math.min(maxColumns, rows);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
visibleRows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selector for determining if the UI layout should be in tile view. Tile view
|
||||||
|
* is determined by more than just having the tile view setting enabled, as
|
||||||
|
* one-on-one calls should not be in tile view, as well as etherpad editing.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @returns {boolean} True if tile view should be displayed.
|
||||||
|
*/
|
||||||
|
export function shouldDisplayTileView(state: Object = {}) {
|
||||||
|
return Boolean(
|
||||||
|
state['features/video-layout']
|
||||||
|
&& state['features/video-layout'].tileViewEnabled
|
||||||
|
&& !state['features/etherpad'].editing
|
||||||
|
);
|
||||||
|
}
|
|
@ -1 +1,9 @@
|
||||||
|
export * from './actions';
|
||||||
|
export * from './actionTypes';
|
||||||
|
export * from './components';
|
||||||
|
export * from './constants';
|
||||||
|
export * from './functions';
|
||||||
|
|
||||||
import './middleware';
|
import './middleware';
|
||||||
|
import './reducer';
|
||||||
|
import './subscriber';
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
import { TRACK_ADDED } from '../base/tracks';
|
import { TRACK_ADDED } from '../base/tracks';
|
||||||
|
|
||||||
|
import { SET_TILE_VIEW } from './actionTypes';
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,6 +73,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
Boolean(action.participant.id));
|
Boolean(action.participant.id));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case SET_TILE_VIEW:
|
||||||
|
APP.UI.emitEvent(UIEvents.TOGGLED_TILE_VIEW, action.enabled);
|
||||||
|
break;
|
||||||
|
|
||||||
case TRACK_ADDED:
|
case TRACK_ADDED:
|
||||||
if (!action.track.local) {
|
if (!action.track.local) {
|
||||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { SET_TILE_VIEW } from './actionTypes';
|
||||||
|
|
||||||
|
ReducerRegistry.register('features/video-layout', (state = {}, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_TILE_VIEW:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tileViewEnabled: action.enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
VIDEO_QUALITY_LEVELS,
|
||||||
|
setMaxReceiverVideoQuality
|
||||||
|
} from '../base/conference';
|
||||||
|
import { StateListenerRegistry } from '../base/redux';
|
||||||
|
import { selectParticipant } from '../large-video';
|
||||||
|
import { shouldDisplayTileView } from './functions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StateListenerRegistry provides a reliable way of detecting changes to
|
||||||
|
* preferred layout state and dispatching additional actions.
|
||||||
|
*/
|
||||||
|
StateListenerRegistry.register(
|
||||||
|
/* selector */ state => shouldDisplayTileView(state),
|
||||||
|
/* listener */ (displayTileView, { dispatch }) => {
|
||||||
|
dispatch(selectParticipant());
|
||||||
|
|
||||||
|
if (!displayTileView) {
|
||||||
|
dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
|
@ -59,6 +59,7 @@ export default {
|
||||||
TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
|
TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
|
||||||
TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
|
TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
|
||||||
TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
|
TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
|
||||||
|
TOGGLED_TILE_VIEW: 'UI.toggled_tile_view',
|
||||||
HANGUP: 'UI.hangup',
|
HANGUP: 'UI.hangup',
|
||||||
LOGOUT: 'UI.logout',
|
LOGOUT: 'UI.logout',
|
||||||
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
||||||
|
|
Loading…
Reference in New Issue