Compare commits

...

30 Commits

Author SHA1 Message Date
Horatiu Muresan 00cb0a80a1 fix(background-alpha) Fix setting background opacity 2022-03-30 12:14:45 -04:00
Saúl Ibarra Corretgé 8e0ad97860 fix(video-layout) fix incorrect import of isStageFilmstripEnabled 2022-03-30 12:14:45 -04:00
Saúl Ibarra Corretgé 09d686fcd3 fix(rn,lobby) fix lobby not showing up on subsequent tries
We need to make sure to hide it explicitly so the Redux state is in sync
with reality.
2022-03-30 12:14:45 -04:00
robertpin f0a59934a5 fix(settings-dialog) Add back CSS classes used by tests 2022-03-30 12:14:45 -04:00
Saúl Ibarra Corretgé 38a1354877 fix(android,back-button) rework back button handling on Android
We used to have a registry which registered a single handlerwith RN.
THis was registered really early in the app.

When React Navigation was introduced we ddidn't realize it interacts
with the back button. In a stack nagigator it will navigate to the
previous screen. This meant our back button handling was broken.

This commit removes our previous registry and uses the RN back button
handler directly in the 2 components that use it: the conference and
bottom sheets.

Since these handlers are registered after navigation, our handlers are
going to run first so we cna implement the behavior we need, namely to
dismiss an open botom sheet or set the conference in PiP mode.
2022-03-30 12:14:45 -04:00
Calin Chitu aed23a8cd1 feat(participants/native) - fix joining breakout room 2022-03-30 12:14:45 -04:00
Robert Pintilii 5add987711 fix(rn) Fix native after stage filmstrip merge (#11247) 2022-03-30 12:14:45 -04:00
Shahab 636c16f269 refactor(connection-stats): use jss instead of sass in ConnectionStatsTable (#11156) 2022-03-30 12:14:33 -04:00
Shahab f7e4d83a05 refactor(virtual-background): use jss instead of sass (#11152) 2022-03-30 12:12:05 -04:00
Shahab cca984048e refactor: move chat component outside of videoconference_page (#11138) 2022-03-30 12:12:05 -04:00
Shahab 5baacaed7e refactor(dialog): use jss instead of sass for mute-dialog style (#11154) 2022-03-30 12:12:05 -04:00
Shahab 736667d0e9 refactor(settings): use jss instead of css (#11149) 2022-03-30 12:12:05 -04:00
Robert Pintilii e3c2823655 feat(stage) Add stage filmstrip (multiple participants on stage) (#11145) 2022-03-30 11:31:01 -04:00
Дамян Минков b65428aa02 feat: Adds a hint for cors headers in default prosody config. 2022-03-29 20:07:07 -04:00
Saúl Ibarra Corretgé ddf90dd6f1 fix(redux) fix not working with Redux Devtools 2022-03-29 20:07:07 -04:00
Saúl Ibarra Corretgé de5c83d53a fix(debian) support installing the prosody-0.12 upstream package 2022-03-29 20:07:07 -04:00
Avram Tudor fce2ffc3e3 fix(face-centering) fix face centering on browsers with no offscreencanvas support (#11234) 2022-03-29 20:07:07 -04:00
gpatel-fr 90d330921e fix(lang): update french translation 2022-03-29 20:07:07 -04:00
William Liang c3938e8915 small cleanup 2022-03-27 10:53:39 -04:00
William Liang 75fcc01576 fix lint errors 2022-03-27 10:44:50 -04:00
William Liang 733e703afd rename state to sortedRemoteFakeScreenShareParticipants 2022-03-27 10:33:55 -04:00
William Liang 6f56d8f9d8 refactor getter for screenshare tracks 2022-03-27 10:25:30 -04:00
William Liang 2e39df49fa address PR comments 2022-03-27 10:05:42 -04:00
William Liang 34b52cf26e refactor: update key values for sortedFakeScreenShareParticipants 2022-03-27 08:38:02 -04:00
William Liang 639f794a43 fix: screenshare not display in large video 2022-03-27 08:34:48 -04:00
William Liang d2dab7e6b2 fix: handle screenshare muted change and track removal 2022-03-27 08:29:36 -04:00
William Liang 4f21cca7d7 remove fake ss participant creation on track update 2022-03-25 15:52:03 -04:00
William Liang f87a1a0e3a update reciver constraints to include SS source names 2022-03-25 15:52:03 -04:00
pangrr 56ae2a56e4 cleanup (#4)
* cleanup

* .

* .

* .

* add function description
2022-03-25 15:52:03 -04:00
William Liang 4608e60325 feat(screenshare) Add fake participant for screen share
prioritize participants with screen shares

support local screen share track

auto pin screen share

revert using fake screen share participant component

support screen share for large video

ensure fake screen share participants are sorted

fix local screen share in vertical filmstrip

fix local screen share in tile mode

use FakeScreenShareParticipant component for screen share thumbnails

ensure changes are behind feature flag and update jsdocs

fix bug where local screen share was not rendering
2022-03-25 15:52:03 -04:00
97 changed files with 2670 additions and 976 deletions

View File

@ -1308,6 +1308,10 @@ var config = {
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
// // (width, tiles aspect ratios) through the interfaceConfig options.
// disableResizable: false,
// // Disables the stage filmstrip
// // (displaying multiple participants on stage besides the vertical filmstrip)
// disableStageFilmstrip: false
// },
// Tile view related config options.
@ -1317,7 +1321,6 @@ var config = {
// numberOfVisibleTiles: 25
// },
// Specifies whether the chat emoticons are disabled or not
// disableChatSmileys: false,

View File

@ -43,18 +43,6 @@ body {
outline: none;
}
/**
* AtlasKit sets a default margin on the rendered modals, so
* when the shift-right class is set when the chat opens, we
* pad the modal container in order for the modals to be centered
* while also taking the chat size into consideration.
*/
@media (min-width: 581px) {
.shift-right .atlaskit-portal > div:not(.Tooltip) {
padding-left: $sidebarWidth;
}
}
.jitsi-icon {
&-default svg {
fill: white;

View File

@ -1,15 +1,20 @@
#sideToolbarContainer {
background-color: $chatBackgroundColor;
box-sizing: border-box;
color: #FFF;
height: 100%;
position: absolute;
top: 0;
flex-shrink: 0;
overflow: hidden;
position: relative;
transition: width .16s ease-in-out;
width: $sidebarWidth;
z-index: $sideToolbarContainerZ;
@media (max-width: 580px) {
width: 100%;
height: 100vh;
height: -webkit-fill-available;
left: 0;
position: fixed;
right: 0;
top: 0;
width: auto;
}
}

View File

@ -1,50 +0,0 @@
%connection-info {
font-size: 12px;
font-weight: 400;
td {
padding: 2px 0;
}
}
.connection-info
{
@extend %connection-info;
> table {
white-space: nowrap;
@extend %connection-info;
}
td:nth-child(n-1) {
padding-left: 5px;
}
&__icon {
margin-right: 2px;
}
&__download
{
@extend .connection-info__icon;
}
&__status
{
font-weight: bold;
}
&__upload
{
@extend .connection-info__icon;
}
&__mobile {
margin: 15px;
}
.connection-actions {
margin: 10px auto;
text-align: center;
}
}

View File

@ -193,3 +193,17 @@
@mixin transparentBg($color, $alpha) {
background-color: rgba(red($color), green($color), blue($color), $alpha);
}
/**
* Change the direction of the current element to LTR, but do not change the direction
* of its children; Keep them RTL.
*/
@mixin ltr {
body[dir=rtl] & {
direction: ltr;
& > * {
direction: rtl;
}
}
}

View File

@ -6,10 +6,6 @@
transition: width .16s ease-in-out;
width: 315px;
z-index: $zindex0;
&--closed {
width: 0;
}
}
.participants_pane-content {
@ -34,11 +30,6 @@
right: 0;
top: 0;
width: auto;
&--closed {
display: none;
width: auto;
}
}
.participants_pane-content {

View File

@ -88,10 +88,6 @@
max-width: calc(100% - 24px);
}
.shift-right .details-container {
margin-left: calc(#{$sidebarWidth} / 2);
}
@keyframes hideSubject {
0% {
max-width: 100%;

View File

@ -39,13 +39,6 @@
&.no-buttons {
display: none;
}
@media (min-width: 581px) {
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
}
}
}
.toolbox-content {
@ -99,6 +92,10 @@
max-width: 100%;
pointer-events: all;
border-radius: 6px;
.toolbox-content-items {
@include ltr;
}
}
.toolbox-content-wrapper::after {
@ -183,6 +180,7 @@
}
.toolbox-content-items {
@include ltr;
border-radius: 0;
display: flex;
justify-content: space-evenly;

View File

@ -6,6 +6,7 @@
}
#layout_wrapper {
@include ltr;
display: flex;
height: 100%;
}
@ -44,15 +45,6 @@
position: relative;
text-align: center;
overflow: 'hidden';
@media (min-width: 581px) {
&.shift-right {
&#largeVideoContainer {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
}
}
}
}
#localVideoWrapper {

View File

@ -48,7 +48,8 @@
/**
* The local video identifier.
*/
&#filmstripLocalVideo {
&#filmstripLocalVideo,
&#filmstripLocalScreenShare {
align-self: flex-end;
display: block;
margin-bottom: 8px;

View File

@ -41,17 +41,6 @@
top: 0;
width: 100%;
@media (min-width: 581px) {
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
.remote-videos {
width: calc(100vw - #{$sidebarWidth});
}
}
}
&.collapse {
#remoteVideos {
height: calc(100% - #{$newToolbarSizeMobile}) !important;

View File

@ -2,7 +2,7 @@
* Various overrides outside of the filmstrip to style the app to support a
* tiled thumbnail experience.
*/
.tile-view {
.tile-view, .stage-filmstrip {
/**
* Let the avatar grow with the tile.
*/
@ -15,9 +15,10 @@
* Hide various features that should not be displayed while in tile view.
*/
#dominantSpeaker,
#filmstripLocalVideoThumbnail,
#filmstripLocalScreenShareThumbnail,
#largeVideoElementsContainer,
#sharedVideo {
#sharedVideo,
.stage-participant-label {
display: none;
}

View File

@ -1,4 +1,4 @@
.vertical-filmstrip .filmstrip {
.vertical-filmstrip span:not(.tile-view) .filmstrip {
&.hide-videos {
.remote-videos {
& > div {
@ -87,9 +87,27 @@
.videocontainer {
height: 0px;
width: 100%;
}
}
}
}
#filmstripLocalScreenShare {
align-self: initial;
margin-bottom: 5px;
display: flex;
flex-direction: column-reverse;
height: auto;
justify-content: flex-start;
width: 100%;
#filmstripLocalScreenShareThumbnail {
width: calc(100% - 15px);
.videocontainer {
height: 0px;
width: 100%;
}
}
}
/**
@ -97,6 +115,7 @@
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripLocalScreenShare,
.remote-videos {
padding: 0;
}

View File

@ -38,10 +38,8 @@ $flagsImagePath: "../images/";
@import 'modals/embed-meeting/embed-meeting';
@import 'modals/feedback/feedback';
@import 'modals/invite/info';
@import 'modals/settings/settings';
@import 'modals/screen-share/share-audio';
@import 'modals/screen-share/share-screen-warning';
@import 'modals/virtual-background/virtual-background';
@import 'modals/local-recording/local-recording';
@import 'videolayout_default';
@import 'notice';
@ -61,7 +59,6 @@ $flagsImagePath: "../images/";
@import 'components/button-control';
@import 'components/input-control';
@import 'components/input-slider';
@import "connection-info";
@import '404';
@import 'policy';
@import 'popover';
@ -89,7 +86,6 @@ $flagsImagePath: "../images/";
@import 'country-picker';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'modals/mute/mute-dialog';
@import 'e2ee';
@import 'responsive';
@import 'drawer';

View File

@ -1,19 +0,0 @@
.mute-dialog {
.separator-line {
margin: 24px 0 24px -20px;
padding: 0 20px;
width: 100%;
height: 1px;
background: #5E6D7A;
}
.control-row {
display: flex;
justify-content: space-between;
margin-top: 15px;
label {
font-size: 14px;
}
}
}

View File

@ -1,102 +0,0 @@
.settings-pane {
display: flex;
width: 100%;
&.profile-pane {
flex-direction: column;
}
.auth-name {
margin-bottom: 4px;
}
.calendar-tab,
.device-selection {
margin-top: 20px;
}
.mock-atlaskit-label {
color: #b8c7e0;
font-size: 12px;
font-weight: 600;
line-height: 1.33;
padding: 20px 0px 4px 0px;
}
input[type="checkbox"]:checked + svg {
--checkbox-background-color: #6492e7;
--checkbox-border-color: #6492e7;
}
input[type="checkbox"] + svg + span {
color: #b8c7e0;
}
input[type="checkbox"] + svg + span {
color: #9FB0CC;
}
.calendar-tab,
.more-tab,
.box {
display: flex;
justify-content: space-between;
width: 100%;
}
.profile-edit {
display: flex;
width: 100%;
}
.profile-edit-field {
flex: .5;
}
.settings-sub-pane {
flex: 1;
}
.settings-sub-pane .right {
flex: 1;
}
.settings-sub-pane .left {
flex: 1;
}
.settings-sub-pane-element {
text-align: left;
flex: 1;
}
.moderator-settings-wrapper {
padding-top: 20px;
}
.profile-edit-field {
margin-right: 20px;
}
.calendar-tab {
align-items: center;
flex-direction: column;
font-size: 14px;
min-height: 100px;
text-align: center;
}
.calendar-tab-sign-in {
margin-top: 20px;
}
.sign-out-cta {
margin-bottom: 20px;
}
@media only screen and (max-width: $smallScreen) {
.device-selection {
display: flex;
flex-direction: column;
}
.more-tab {
flex-direction: column;
}
}
}

View File

@ -1,207 +0,0 @@
.virtual-background-dialog {
margin-left: -10px;
position: relative;
max-height: 300px;
color: white;
display: inline-grid;
grid-template-columns: auto auto auto auto auto;
column-gap: 9px;
cursor: pointer;
.desktop-share:hover,
.thumbnail:hover,
.blur:hover,
.slight-blur:hover,
.virtual-background-none:hover {
opacity: 0.5;
border: 2px solid #99bbf3;
@media (max-width: 632px) {
height: 60px;
width: 60px;
}
}
.background-option {
margin-top: 8px;
border-radius: 6px;
height: 60px;
width: 107px;
text-align: center;
justify-content: center;
font-weight: bold;
box-sizing: border-box;
display: flex;
align-items: center;
}
.thumbnail {
object-fit: cover;
}
.thumbnail:hover ~ .delete-image-icon {
display: block;
}
.thumbnail-selected {
object-fit: cover;
border: 2px solid #246fe5;
}
.blur {
box-shadow: inset 0 0 12px #000000;
background: #7e8287;
padding: 0 10px;
}
.blur-selected {
box-shadow: inset 0 0 12px #000000;
background: #7e8287;
border: 2px solid #246fe5;
padding: 0 10px;
}
.slight-blur {
box-shadow: inset 0 0 12px #000000;
background: #a4a4a4;
padding: 0 10px;
}
.slight-blur-selected {
box-shadow: inset 0 0 12px #000000;
background: #a4a4a4;
border: 2px solid #246fe5;
padding: 0 10px;
}
.virtual-background-none {
background: #525252;
padding: 0 10px;
}
.none-selected {
background: #525252;
border: 2px solid #246fe5;
padding: 0 10px;
}
.desktop-share {
background: #525252;
}
.desktop-share-selected {
background: #525252;
border: 2px solid #246fe5;
padding: 0 10px;
}
@media (max-width: 632px) {
font-size: 1.5vw;
.desktop-share,
.virtual-background-none,
.thumbnail,
.blur,
.slight-blur {
height: 60px;
width: 60px;
}
.desktop-share-selected,
.thumbnail-selected,
.none-selected,
.blur-selected,
.slight-blur-selected {
height: 60px;
width: 60px;
}
}
@media (max-width: 360px) {
grid-template-columns: auto auto auto;
}
}
.modal-dialog-form .virtual-background-loading {
overflow: hidden;
position: fixed;
left: 50%;
margin-top: 10px;
transform: translateX(-50%);
}
.modal-dialog-form .video-preview {
height: 250px;
}
.file-upload-btn {
display: none;
}
.file-upload-label {
font-size: 14px;
font-weight: 600;
line-height: 20px;
margin-left: -10px;
margin-top: 16px;
margin-bottom: 8px;
color: #669aec;
display: inline-flex;
cursor: pointer;
}
.delete-image-icon {
background: #3d3d3d;
position: absolute;
display: none;
left: 96;
bottom: 51;
@media (max-width: 632px) {
left: 51px;
}
}
.delete-image-icon:hover {
display: block;
}
.thumbnail-container {
position: relative;
&:focus-within {
.thumbnail ~ .delete-image-icon {
display: block;
}
}
}
.add-background {
margin-right: 8px;
}
.apply-background-btn {
margin-top: 16px;
float: right;
}
.video-background-preview-entry {
margin-left: -10px;
height: 250px;
width: 570px;
margin-bottom: 8px;
z-index: 2;
@media (max-width: 632px) {
max-width: 336;
}
}
.virtual-background-preview-video {
margin-left: -10;
border-radius: 6px;
height: 100%;
object-fit: cover;
width: 100%;
}
.video-preview-loader {
border-radius: 6px;
background-color: transparent;
height: 250px;
margin-bottom: 8px;
width: 572px;
position: fixed;
z-index: 2;
@media (min-width: 432px) and (max-width: 632px) {
width: 340px;
}
}
.video-preview-loader svg {
position: absolute;
top: 40%;
left: 45%;
}
.dialog-margin-top{
margin-top: 44px;
}

View File

@ -137,6 +137,7 @@
}
.toolbox-content-items {
@include ltr;
background: transparent;
box-shadow: none;
display: flex;

2
debian/control vendored
View File

@ -33,7 +33,7 @@ Description: Configuration for web serving of Jitsi Meet
Package: jitsi-meet-prosody
Architecture: all
Depends: openssl, prosody (>= 0.11.0) | prosody-trunk | prosody-0.11, lua-sec
Depends: openssl, prosody (>= 0.11.0) | prosody-trunk | prosody-0.12 | prosody-0.11, lua-sec
Replaces: jitsi-meet-tokens
Description: Prosody configuration for Jitsi Meet
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi

View File

@ -14,6 +14,16 @@ cross_domain_bosh = false;
consider_bosh_secure = true;
-- https_ports = { }; -- Remove this line to prevent listening on port 5284
-- by default prosody 0.12 sends cors headers, if you want to disable it uncomment the following (the config is available on 0.12.1)
--http_cors_override = {
-- bosh = {
-- enabled = false;
-- };
-- websocket = {
-- enabled = false;
-- };
--}
-- https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
ssl = {
protocol = "tlsv1_2+";

View File

@ -39,9 +39,6 @@
"audioOnly": {
"audioOnly": "Bande passante faible"
},
"blankPage": {
"meetingEnded": "Réunion terminée."
},
"breakoutRooms": {
"actions": {
"add": "Ajouter salle annexe",
@ -559,6 +556,7 @@
"errorMissingPassword": "Veuillez saisir le mot de passe de la réunion",
"invalidPassword": "Mot de passe invalide",
"joinRejectedMessage": "Votre requête pour rejoindre une réunion a été refusée par un modérateur.",
"joinRejectedTitle": "Demande d'accès rejetée.",
"joinTitle": "Rejoindre une réunion",
"joinWithPasswordMessage": "Tentative de rejoindre avec mot de passe, patientez s'il vous plait ...",
"joiningMessage": "Vous allez rejoindre une réunion dès que quelqu'un aura accepté votre demande",
@ -865,6 +863,11 @@
"expandedPending": "Démarrage de l'enregistrement ...",
"failedToStart": "L'enregistrement n'a pas réussi à démarrer",
"fileSharingdescription": "Partager l'enregistrement avec les participants de la réunion",
"highlight": "Souligner",
"highlightMoment": "Souligner un moment",
"highlightMomentDisabled": "Vous ne pouvez souligner des moments que pendant une réunion",
"highlightMomentSuccess": "Moment souligné",
"highlightMomentSucessDescription": "Votre moment souligné sera ajouté au résumé de la réunion.",
"inProgress": "Enregistrement ou diffusion en cours",
"limitNotificationDescriptionNative": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <3> {{app}} </3>.",
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",

View File

@ -722,6 +722,7 @@
},
"passwordDigitsOnly": "Up to {{number}} digits",
"passwordSetRemotely": "Set by another participant",
"pinnedParticipant": "The participant is pinned",
"polls": {
"answer": {
"skip": "Skip",
@ -1213,10 +1214,12 @@
"moderator": "Moderator",
"mute": "Participant is muted",
"muted": "Muted",
"pinToStage": "Pin to stage",
"remoteControl": "Start / Stop remote control",
"screenSharing": "Participant is sharing their screen",
"show": "Show on stage",
"showSelfView": "Show self view",
"unpinFromStage": "Unpin",
"videoMuted": "Camera disabled",
"videomute": "Participant has stopped the camera"
},

View File

@ -108,13 +108,13 @@ UI.start = function() {
$('body').addClass('mobile-browser');
} else {
$('body').addClass('desktop-browser');
}
if (config.backgroundAlpha !== undefined) {
const backgroundColor = $('body').css('background-color');
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
if (config.backgroundAlpha !== undefined) {
const backgroundColor = $('body').css('background-color');
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
$('body').css('background-color', alphaColor);
}
$('body').css('background-color', alphaColor);
}
if (config.iAmRecorder) {

View File

@ -11,12 +11,14 @@ import { Avatar } from '../../../react/features/base/avatar';
import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { i18next } from '../../../react/features/base/i18n';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import { VIDEO_TYPE } from '../../../react/features/base/media';
import {
getParticipantById,
getParticipantDisplayName
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import {
getVideoTrackByParticipant
} from '../../../react/features/base/tracks';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
isParticipantConnectionStatusActive,
@ -237,11 +239,13 @@ export default class LargeVideoManager {
let isVideoRenderable;
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isTrackStreamingStatusActive(videoTrack));
isVideoRenderable = !isVideoMuted && (
APP.conference.isLocalId(id)
|| participant?.isLocalScreenShare
|| isTrackStreamingStatusActive(videoTrack)
);
} else {
isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || isParticipantConnectionStatusActive(participant));
@ -268,8 +272,8 @@ export default class LargeVideoManager {
&& participant && !participant.local && !participant.isFakeParticipant) {
// remote participant only
const track = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const track = getVideoTrackByParticipant(state['features/base/tracks'], participant);
const isScreenSharing = track?.videoType === 'desktop';
if (isScreenSharing) {
@ -300,8 +304,7 @@ export default class LargeVideoManager {
let messageKey;
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
messageKey = isTrackStreamingStatusInactive(videoTrack) ? 'connection.LOW_BANDWIDTH' : null;
} else {
@ -541,8 +544,7 @@ export default class LargeVideoManager {
const state = APP.store.getState();
if (getSourceNameSignalingFeatureFlag(state)) {
const videoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id);
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)

View File

@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip';
import { FILMSTRIP_BREAKPOINT, shouldDisplayStageFilmstrip } from '../../../react/features/filmstrip';
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
@ -414,7 +414,7 @@ export class VideoContainer extends LargeContainer {
const verticalFilmstripWidth = state['features/filmstrip'].width?.current;
if (currentLayout === LAYOUTS.TILE_VIEW) {
if (currentLayout === LAYOUTS.TILE_VIEW || shouldDisplayStageFilmstrip(state)) {
// We don't need to resize the large video since it won't be displayed and we'll resize when returning back
// to stage view.
return;

View File

@ -2,12 +2,16 @@
import Logger from '@jitsi/logger';
import { getSourceNameSignalingFeatureFlag } from '../../../react/features/base/config';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import {
getPinnedParticipant,
getParticipantById
} from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import {
getTrackByMediaTypeAndParticipant,
getVideoTrackByParticipant
} from '../../../react/features/base/tracks';
import LargeVideoManager from './LargeVideoManager';
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
@ -91,6 +95,10 @@ const VideoLayout = {
return VIDEO_TYPE.CAMERA;
}
if (getSourceNameSignalingFeatureFlag(state) && participant?.isFakeScreenShareParticipant) {
return VIDEO_TYPE.DESKTOP;
}
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
return videoTrack?.videoType;
@ -177,7 +185,8 @@ const VideoLayout = {
const currentContainerType = largeVideo.getCurrentContainerType();
const isOnLarge = this.isCurrentlyOnLarge(id);
const state = APP.store.getState();
const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const participant = getParticipantById(state, id);
const videoTrack = getVideoTrackByParticipant(state['features/base/tracks'], participant);
const videoStream = videoTrack?.jitsiTrack;
if (isOnLarge && !forceUpdate

View File

@ -2,7 +2,6 @@
import '../authentication/middleware';
import '../mobile/audio-mode/middleware';
import '../mobile/back-button/middleware';
import '../mobile/background/middleware';
import '../mobile/call-integration/middleware';
import '../mobile/external-api/middleware';

View File

@ -218,19 +218,9 @@ export default class BaseApp extends Component<*, State> {
// additional 3rd party middleware:
// - Thunk - allows us to dispatch async actions easily. For more info
// @see https://github.com/gaearon/redux-thunk.
let middleware = MiddlewareRegistry.applyMiddleware(Thunk);
// Try to enable Redux DevTools Chrome extension in order to make it
// available for the purposes of facilitating development.
let devToolsExtension;
if (typeof window === 'object'
&& (devToolsExtension = window.devToolsExtension)) {
middleware = compose(middleware, devToolsExtension());
}
const store = createStore(
reducer, PersistenceRegistry.getPersistedState(), middleware);
const middleware = MiddlewareRegistry.applyMiddleware(Thunk);
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, PersistenceRegistry.getPersistedState(), composeEnhancers(middleware));
// StateListenerRegistry
StateListenerRegistry.subscribe(store);

View File

@ -93,6 +93,7 @@ export { default as IconOutlook } from './office365.svg';
export { default as IconParticipants } from './participants.svg';
export { default as IconPhone } from './phone.svg';
export { default as IconPin } from './enlarge.svg';
export { default as IconPinParticipant } from './pin.svg';
export { default as IconPlane } from './paper-plane.svg';
export { default as IconPresentation } from './presentation.svg';
export { default as IconRaisedHand } from './raised-hand.svg';
@ -128,6 +129,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
export { default as IconTileView } from './tiles-many.svg';
export { default as IconToggleRecording } from './camera-take-picture.svg';
export { default as IconTrash } from './trash.svg';
export { default as IconUnpin } from './unpin.svg';
export { default as IconVideoOff } from './video-off.svg';
export { default as IconVideoQualityAudioOnly } from './AUD.svg';
export { default as IconVideoQualityHD } from './HD.svg';

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.69428 17.1265L5.98398 13.9439L7.92634 15.8862C8.57722 16.5371 9.63249 16.5371 10.2834 15.8862L10.8726 15.297C11.5235 14.6461 11.5235 13.5908 10.8726 12.9399L10.578 12.6453L11.2691 11.4935C11.396 11.2819 11.5684 11.1012 11.7737 10.9643L13.5832 9.75796L13.8189 9.99366C14.4698 10.6445 15.525 10.6445 16.1759 9.99366L16.7652 9.4044C17.416 8.75353 17.416 7.69825 16.7652 7.04738L10.8726 1.15482C10.2217 0.503949 9.16647 0.503949 8.5156 1.15482L7.92634 1.74408C7.27547 2.39495 7.27547 3.45023 7.92634 4.1011L8.27989 4.45465L7.3088 6.25811C7.13602 6.579 6.86281 6.83441 6.53102 6.98522L5.42201 7.48932L4.98006 7.04738C4.32919 6.39651 3.27391 6.39651 2.62304 7.04738L2.03379 7.63664C1.38291 8.28751 1.38291 9.34278 2.03379 9.99366L3.97615 11.936L0.793463 16.2257C0.603279 16.4821 0.629578 16.839 0.855274 17.0647C1.08097 17.2904 1.43794 17.3167 1.69428 17.1265ZM13.7956 7.6133L10.8492 9.57753C10.4386 9.8513 10.0938 10.2128 9.8399 10.636L8.47933 12.9037L9.69411 14.1184L9.10485 14.7077L3.2123 8.81515L3.80155 8.22589L5.0602 9.48454L7.2207 8.5025C7.88427 8.20088 8.43068 7.69006 8.77626 7.04828L10.3353 4.15299L9.10485 2.92259L9.69411 2.33333L15.5867 8.22589L14.9974 8.81515L13.7956 7.6133Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.36765 7.1488L8.36029 7.16248L3.11269 1.91488C2.78182 1.58401 2.24545 1.58392 1.91468 1.91469C1.58391 2.24546 1.58399 2.78183 1.91487 3.1127L7.01977 8.21761L6.422 8.48932L5.98005 8.04738C5.32918 7.39651 4.27391 7.39651 3.62303 8.04738L3.03378 8.63664C2.3829 9.28751 2.3829 10.3428 3.03378 10.9937L4.97614 12.936L1.79345 17.2257C1.60327 17.4821 1.62957 17.839 1.85526 18.0647C2.08096 18.2904 2.43793 18.3167 2.69427 18.1265L6.98397 14.9439L8.92633 16.8862C9.57721 17.5371 10.6325 17.5371 11.2834 16.8862L11.8726 16.297C12.5235 15.6461 12.5235 14.5908 11.8726 13.9399L11.578 13.6453L11.904 13.1019L16.8873 18.0851C17.2182 18.416 17.7545 18.4161 18.0853 18.0853C18.4161 17.7545 18.416 17.2182 18.0851 16.8873L13.0067 11.8089L13.0194 11.8005L11.8177 10.5988C11.8135 10.6017 11.8093 10.6045 11.8052 10.6074L9.57425 8.37644C9.57714 8.37231 9.58001 8.36817 9.58288 8.36403L8.36765 7.1488ZM13.2549 9.6404L14.7956 8.6133L15.9974 9.81515L16.5867 9.22589L10.6941 3.33333L10.1048 3.92259L11.3352 5.15299L10.4365 6.82203L9.20613 5.59163L9.27989 5.45465L8.92633 5.1011C8.27546 4.45023 8.27546 3.39495 8.92633 2.74408L9.51559 2.15482C10.1665 1.50395 11.2217 1.50395 11.8726 2.15482L17.7652 8.04738C18.416 8.69825 18.416 9.75353 17.7652 10.4044L17.1759 10.9937C16.525 11.6445 15.4698 11.6445 14.8189 10.9937L14.5832 10.758L14.4567 10.8422L13.2549 9.6404ZM9.47932 13.9037L10.6893 11.8871L8.27797 9.4758C8.25897 9.48488 8.23988 9.49378 8.22069 9.5025L6.06019 10.4845L4.80154 9.22589L4.21229 9.81515L10.1048 15.7077L10.6941 15.1184L9.47932 13.9037Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -5,7 +5,11 @@ import debounce from 'lodash/debounce';
import { SET_FILMSTRIP_ENABLED } from '../../filmstrip/actionTypes';
import { SELECT_LARGE_VIDEO_PARTICIPANT } from '../../large-video/actionTypes';
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-layout/actionTypes';
import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from '../../video-layout/actionTypes';
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import {
@ -92,6 +96,7 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_STATE_CHANGED:
case CONFERENCE_JOINED:
case FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case PARTICIPANT_JOINED:
case PARTICIPANT_KICKED:
case PARTICIPANT_LEFT:

View File

@ -3,7 +3,9 @@
import { getGravatarURL } from '@jitsi/js-utils/avatar';
import type { Store } from 'redux';
import { isStageFilmstripEnabled } from '../../filmstrip/functions';
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
import { getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
import { toState } from '../redux';
@ -118,9 +120,11 @@ export function getNormalizedDisplayName(name: string) {
export function getParticipantById(
stateful: Object | Function, id: string): ?Object {
const state = toState(stateful)['features/base/participants'];
const { local, remote } = state;
const { local, localScreenShare, remote } = state;
return remote.get(id) || (local?.id === id ? local : undefined);
return remote.get(id)
|| (local?.id === id ? local : undefined)
|| (localScreenShare?.id === id ? localScreenShare : undefined);
}
/**
@ -147,10 +151,31 @@ export function getParticipantByIdOrUndefined(stateful: Object | Function, parti
* @returns {number}
*/
export function getParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { local, remote, fakeParticipants } = state;
const state = toState(stateful);
const {
local,
remote,
fakeParticipants,
sortedRemoteFakeScreenShareParticipants
} = state['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return remote.size - fakeParticipants.size - sortedRemoteFakeScreenShareParticipants.size + (local ? 1 : 0);
}
return remote.size - fakeParticipants.size + (local ? 1 : 0);
}
/**
* Returns participant ID of the owner of a fake screenshare participant.
*
* @param {string} id - The ID of the fake screenshare participant.
* @private
* @returns {(string|undefined)}
*/
export function getFakeScreenShareParticipantOwnerId(id: string) {
return id.split('-')[0];
}
/**
@ -176,6 +201,10 @@ export function getFakeParticipants(stateful: Object | Function) {
export function getRemoteParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return state.remote.size - state.sortedRemoteFakeScreenShareParticipants.size;
}
return state.remote.size;
}
@ -189,8 +218,12 @@ export function getRemoteParticipantCount(stateful: Object | Function) {
* @returns {number}
*/
export function getParticipantCountWithFake(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { local, remote } = state;
const state = toState(stateful);
const { local, localScreenShare, remote } = state['features/base/participants'];
if (getSourceNameSignalingFeatureFlag(state)) {
return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
}
return remote.size + (local ? 1 : 0);
}
@ -290,8 +323,16 @@ export function getRemoteParticipantsSorted(stateful: Object | Function) {
* @returns {(Participant|undefined)}
*/
export function getPinnedParticipant(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { pinnedParticipant } = state;
const state = toState(stateful);
const { pinnedParticipant } = state['features/base/participants'];
const stageFilmstrip = isStageFilmstripEnabled(state);
if (stageFilmstrip) {
const { activeParticipants } = state['features/filmstrip'];
const id = activeParticipants.find(p => p.pinned)?.participantId;
return id ? getParticipantById(stateful, id) : undefined;
}
if (!pinnedParticipant) {
return undefined;

View File

@ -1,6 +1,8 @@
// @flow
import { SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED } from '../../video-layout/actionTypes';
import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
} from '../../video-layout/actionTypes';
import { ReducerRegistry, set } from '../redux';
import {
@ -59,9 +61,11 @@ const DEFAULT_STATE = {
fakeParticipants: new Map(),
haveParticipantWithScreenSharingFeature: false,
local: undefined,
localScreenShare: undefined,
pinnedParticipant: undefined,
raisedHandsQueue: [],
remote: new Map(),
sortedRemoteFakeScreenShareParticipants: new Map(),
sortedRemoteParticipants: new Map(),
sortedRemoteScreenshares: new Map(),
speakersList: new Map()
@ -207,7 +211,7 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
}
case PARTICIPANT_JOINED: {
const participant = _participantJoined(action);
const { id, isFakeParticipant, name, pinned } = participant;
const { id, isFakeParticipant, isFakeScreenShareParticipant, isLocalScreenShare, name, pinned } = participant;
const { pinnedParticipant, dominantSpeaker } = state;
if (pinned) {
@ -241,6 +245,13 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
};
}
if (isLocalScreenShare) {
return {
...state,
localScreenShare: participant
};
}
state.remote.set(id, participant);
// Insert the new participant.
@ -253,6 +264,14 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
// The sort order of participants is preserved since Map remembers the original insertion order of the keys.
state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
if (isFakeScreenShareParticipant) {
const sortedRemoteFakeScreenShareParticipants = [ ...state.sortedRemoteFakeScreenShareParticipants ];
sortedRemoteFakeScreenShareParticipants.push([ id, name ]);
sortedRemoteFakeScreenShareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
}
if (isFakeParticipant) {
state.fakeParticipants.set(id, participant);
}
@ -267,7 +286,15 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
// (and the fact that the local participant "joins" at the beginning of
// the app and "leaves" at the end of the app).
const { conference, id } = action.participant;
const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
const {
fakeParticipants,
sortedRemoteFakeScreenShareParticipants,
remote,
local,
localScreenShare,
dominantSpeaker,
pinnedParticipant
} = state;
let oldParticipant = remote.get(id);
if (oldParticipant && oldParticipant.conference === conference) {
@ -275,6 +302,9 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
} else if (local?.id === id) {
oldParticipant = state.local;
delete state.local;
} else if (localScreenShare?.id === id) {
oldParticipant = state.local;
delete state.localScreenShare;
} else {
// no participant found
return state;
@ -324,6 +354,11 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
fakeParticipants.delete(id);
}
if (sortedRemoteFakeScreenShareParticipants.has(id)) {
sortedRemoteFakeScreenShareParticipants.delete(id);
state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
}
return { ...state };
}
case RAISE_HAND_UPDATED: {
@ -447,6 +482,8 @@ function _participantJoined({ participant }) {
dominantSpeaker,
email,
isFakeParticipant,
isFakeScreenShareParticipant,
isLocalScreenShare,
isReplacing,
isJigasi,
loadableAvatarUrl,
@ -479,6 +516,8 @@ function _participantJoined({ participant }) {
email,
id,
isFakeParticipant,
isFakeScreenShareParticipant,
isLocalScreenShare,
isReplacing,
isJigasi,
loadableAvatarUrl,
@ -500,7 +539,7 @@ function _participantJoined({ participant }) {
* @returns {boolean} - True if a participant was updated and false otherwise.
*/
function _updateParticipantProperty(state, id, property, value) {
const { remote, local } = state;
const { remote, local, localScreenShare } = state;
if (remote.has(id)) {
remote.set(id, set(remote.get(id), property, value));
@ -511,6 +550,11 @@ function _updateParticipantProperty(state, id, property, value) {
// not in a conference.
state.local = set(local, property, value);
return true;
} else if (localScreenShare?.id === id) {
state.localScreenShare = set(localScreenShare, property, value);
return true;
}

View File

@ -3,12 +3,12 @@
import React, { PureComponent, type Node } from 'react';
import {
Animated,
BackHandler,
Dimensions,
TouchableWithoutFeedback,
View
} from 'react-native';
import { BackButtonRegistry } from '../../../../mobile/back-button';
import { type StyleType } from '../../../styles';
import styles from './slidingviewstyles';
@ -121,7 +121,7 @@ export default class SlidingView extends PureComponent<Props, State> {
* @inheritdoc
*/
componentDidMount() {
BackButtonRegistry.addListener(this._onHardwareBackPress, true);
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
this._mounted = true;
this._setShow(this.props.show);
@ -146,7 +146,7 @@ export default class SlidingView extends PureComponent<Props, State> {
* @inheritdoc
*/
componentWillUnmount() {
BackButtonRegistry.removeListener(this._onHardwareBackPress);
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
this._mounted = false;
}
@ -229,13 +229,9 @@ export default class SlidingView extends PureComponent<Props, State> {
* @returns {boolean}
*/
_onHardwareBackPress() {
const { onHide } = this.props;
this._onHide();
if (typeof onHide === 'function') {
return onHide();
}
return false;
return true;
}
_onHide: () => void;

View File

@ -107,6 +107,18 @@ export const TRACK_STOPPED = 'TRACK_STOPPED';
*/
export const TRACK_UPDATED = 'TRACK_UPDATED';
/**
* The type of redux action dispatched when a screenshare track's muted property were updated.
*
* {
* type: SCREENSHARE_TRACK_MUTED_UPDATED,
* track: Track,
* muted: Boolean
*
* }
*/
export const SCREENSHARE_TRACK_MUTED_UPDATED = 'SCREENSHARE_TRACK_MUTED_UPDATED';
/**
* The type of redux action dispatched when a local track starts being created
* via a WebRTC {@code getUserMedia} call. The action's payload includes an

View File

@ -6,7 +6,7 @@ import {
} from '../../analytics';
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../../notifications';
import { getCurrentConference } from '../conference';
import { getMultipleVideoSupportFeatureFlag } from '../config';
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
import {
CAMERA_FACING_MODE,
@ -21,6 +21,7 @@ import { getLocalParticipant } from '../participants';
import { updateSettings } from '../settings';
import {
SCREENSHARE_TRACK_MUTED_UPDATED,
SET_NO_SRC_DATA_NOTIFICATION_UID,
TOGGLE_SCREENSHARING,
TRACK_ADDED,
@ -395,7 +396,12 @@ export function trackAdded(track) {
return async (dispatch, getState) => {
track.on(
JitsiTrackEvents.TRACK_MUTE_CHANGED,
() => dispatch(trackMutedChanged(track)));
() => {
if (getSourceNameSignalingFeatureFlag(getState()) && track.getVideoType() === VIDEO_TYPE.DESKTOP) {
dispatch(screenshareTrackMutedChanged(track));
}
dispatch(trackMutedChanged(track));
});
track.on(
JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
type => dispatch(trackVideoTypeChanged(track, type)));
@ -491,6 +497,24 @@ export function trackMutedChanged(track) {
};
}
/**
* Create an action for when a screenshare track's muted state has been signaled to be changed.
*
* @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
* @returns {{
* type: TRACK_UPDATED,
* track: Track,
* muted: boolean
* }}
*/
export function screenshareTrackMutedChanged(track) {
return {
type: SCREENSHARE_TRACK_MUTED_UPDATED,
track: { jitsiTrack: track },
muted: track.isMuted()
};
}
/**
* Create an action for when a track's muted state change action has failed. This could happen because of
* {@code getUserMedia} errors during unmute or replace track errors at the peerconnection level.

View File

@ -4,6 +4,7 @@ import { getMultipleVideoSupportFeatureFlag } from '../config/functions.any';
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
import { getFakeScreenShareParticipantOwnerId } from '../participants';
import { toState } from '../redux';
import {
getUserSelectedCameraDeviceId,
@ -410,6 +411,32 @@ export function getLocalJitsiAudioTrack(state) {
return track?.jitsiTrack;
}
/**
* Returns track of specified media type for specified participant.
*
* @param {Track[]} tracks - List of all tracks.
* @param {Object} participant - Participant Object.
* @returns {(Track|undefined)}
*/
export function getVideoTrackByParticipant(tracks, participant) {
if (!participant) {
return;
}
let participantId;
let mediaType;
if (participant?.isFakeScreenShareParticipant) {
participantId = getFakeScreenShareParticipantOwnerId(participant.id);
mediaType = MEDIA_TYPE.SCREENSHARE;
} else {
participantId = participant.id;
mediaType = MEDIA_TYPE.VIDEO;
}
return getTrackByMediaTypeAndParticipant(tracks, mediaType, participantId);
}
/**
* Returns track of specified media type for specified participant id.
*
@ -427,6 +454,19 @@ export function getTrackByMediaTypeAndParticipant(
);
}
/**
* Returns track of given fakeScreenshareParticipantId.
*
* @param {Track[]} tracks - List of all tracks.
* @param {string} fakeScreenshareParticipantId - Fake Screenshare Participant ID.
* @returns {(Track|undefined)}
*/
export function getFakeScreenshareParticipantTrack(tracks, fakeScreenshareParticipantId) {
const participantId = getFakeScreenShareParticipantOwnerId(fakeScreenshareParticipantId);
return getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.SCREENSHARE, participantId);
}
/**
* Returns track source name of specified media type for specified participant id.
*

View File

@ -8,7 +8,7 @@ import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
import { getMultipleVideoSupportFeatureFlag } from '../config';
import { getMultipleVideoSupportFeatureFlag, getSourceNameSignalingFeatureFlag } from '../config';
import { getAvailableDevices } from '../devices/actions';
import {
CAMERA_FACING_MODE,
@ -24,9 +24,11 @@ import {
setScreenshareMuted,
SCREENSHARE_MUTISM_AUTHORITY
} from '../media';
import { participantLeft, participantJoined, getParticipantById } from '../participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import {
SCREENSHARE_TRACK_MUTED_UPDATED,
TOGGLE_SCREENSHARING,
TRACK_ADDED,
TRACK_MUTE_UNMUTE_FAILED,
@ -50,6 +52,7 @@ import {
isUserInteractionRequiredForUnmute,
setTrackMuted
} from './functions';
import logger from './logger';
import './subscriber';
@ -72,6 +75,13 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(getAvailableDevices());
}
if (getSourceNameSignalingFeatureFlag(store.getState())
&& action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP
&& !action.track.jitsiTrack.isMuted()
) {
createFakeScreenShareParticipant(store, action);
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: {
@ -81,7 +91,40 @@ MiddlewareRegistry.register(store => next => action => {
return result;
}
case SCREENSHARE_TRACK_MUTED_UPDATED: {
const state = store.getState();
if (!getSourceNameSignalingFeatureFlag(state)) {
return;
}
const { track, muted } = action;
if (muted) {
const conference = getCurrentConference(state);
const participantId = track?.jitsiTrack.getSourceName();
store.dispatch(participantLeft(participantId, conference));
}
if (!muted) {
createFakeScreenShareParticipant(store, action);
}
break;
}
case TRACK_REMOVED: {
const state = store.getState();
if (getSourceNameSignalingFeatureFlag(state) && action.track.jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
const conference = getCurrentConference(state);
const participantId = action.track.jitsiTrack.getSourceName();
store.dispatch(participantLeft(participantId, conference));
}
_removeNoDataFromSourceNotification(store, action.track);
break;
}
@ -326,6 +369,32 @@ function _handleNoDataFromSourceErrors(store, action) {
}
}
/**
* Creates a fake participant for screen share using the track's source name as the participant id.
*
* @param {Store} store - The redux store in which the specified action is dispatched.
* @param {Action} action - The redux action dispatched in the specified store.
* @private
* @returns {void}
*/
function createFakeScreenShareParticipant({ dispatch, getState }, { track }) {
const state = getState();
const participantId = track.jitsiTrack?.getParticipantId?.();
const participant = getParticipantById(state, participantId);
if (participant.name) {
dispatch(participantJoined({
conference: state['features/base/conference'].conference,
id: track.jitsiTrack.getSourceName(),
isFakeScreenShareParticipant: true,
isLocalScreenShare: track?.jitsiTrack.isLocal(),
name: `${participant.name}'s screen`
}));
} else {
logger.error(`Failed to create a screenshare participant for participantId: ${participantId}`);
}
}
/**
* Gets the local track associated with a specific {@code MEDIA_TYPE} in a
* specific redux store.

View File

@ -7,6 +7,7 @@
*/
export const commonClassName = {
emptyList: 'empty-list',
muteDialog: 'mute-dialog',
overflowMenuItem: 'overflow-menu-item',
overflowMenuItemIcon: 'overflow-menu-item-icon',
participantAvatar: 'participant-avatar',
@ -30,6 +31,25 @@ export const commonStyles = (theme: Object) => {
margin: 0,
padding: 0
},
[commonClassName.muteDialog]: {
'& .separator-line': {
margin: `${theme.spacing(4)}px 0 ${theme.spacing(4)}px -20px`,
padding: '0 20px',
width: '100%',
height: '1px',
background: '#5E6D7A'
},
'& .control-row': {
display: 'flex',
justifyContent: 'space-between',
marginTop: `${theme.spacing(3)}px`,
'& label': {
fontSize: '14px'
}
}
},
[commonClassName.overflowMenuItem]: {
alignItems: 'center',
color: theme.palette.text01,
@ -98,7 +118,7 @@ export const commonStyles = (theme: Object) => {
}
},
[commonClassName.participantAvatar]: {
margin: `${theme.spacing(2)} ${theme.spacing(3)} ${theme.spacing(2)} 0`
margin: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(2)}px 0`
},
[commonClassName.toolboxIcon]: {
display: 'flex',

View File

@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { appNavigate } from '../../../app/actions';
@ -22,7 +22,6 @@ import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby/components/native';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { BackButtonRegistry } from '../../../mobile/back-button';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
@ -166,7 +165,7 @@ class Conference extends AbstractConference<Props, State> {
* @returns {void}
*/
componentDidMount() {
BackButtonRegistry.addListener(this._onHardwareBackPress);
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
}
/**
@ -196,7 +195,7 @@ class Conference extends AbstractConference<Props, State> {
*/
componentWillUnmount() {
// Tear handling any hardware button presses for back navigation down.
BackButtonRegistry.removeListener(this._onHardwareBackPress);
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
clearTimeout(this._expandedLabelTimeout.current);
}

View File

@ -1,5 +1,6 @@
// @flow
import clsx from 'clsx';
import _ from 'lodash';
import React from 'react';
@ -11,7 +12,7 @@ import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip';
import { MainFilmstrip, StageFilmstrip, shouldDisplayStageFilmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { LobbyScreen } from '../../../lobby';
@ -55,7 +56,7 @@ const FULL_SCREEN_EVENTS = [
* @private
* @type {Object}
*/
const LAYOUT_CLASSNAMES = {
export const LAYOUT_CLASSNAMES = {
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
[LAYOUTS.TILE_VIEW]: 'tile-view',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
@ -95,13 +96,18 @@ type Props = AbstractProps & {
/**
* If lobby page is visible or not.
*/
_showLobby: boolean,
_showLobby: boolean,
/**
* If prejoin page is visible or not.
*/
_showPrejoin: boolean,
/**
* Whether or not the stage filmstrip should be displayed.
*/
_showStageFilmstrip: boolean,
dispatch: Function,
t: Function
}
@ -214,7 +220,8 @@ class Conference extends AbstractConference<Props, *> {
_notificationsVisible,
_overflowDrawer,
_showLobby,
_showPrejoin
_showPrejoin,
_showStageFilmstrip
} = this.props;
return (
@ -222,24 +229,24 @@ class Conference extends AbstractConference<Props, *> {
id = 'layout_wrapper'
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove } >
onMouseMove = { this._onMouseMove }
ref = { this._setBackground }>
<Chat />
<div
className = { _layoutClassName }
className = { clsx(_layoutClassName, _showStageFilmstrip && 'stage-filmstrip') }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }
ref = { this._setBackground }>
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
<ConferenceInfo />
<Notice />
<div
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo />
<Filmstrip />
{_showStageFilmstrip && <StageFilmstrip />}
<MainFilmstrip />
</div>
{ _showPrejoin || _showLobby || <Toolbox /> }
<Chat />
{_notificationsVisible && (_overflowDrawer
? <JitsiPortal className = 'notification-portal'>
@ -395,7 +402,8 @@ function _mapStateToProps(state) {
_overflowDrawer: overflowDrawer,
_roomName: getConferenceNameForTitle(state),
_showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state)
_showPrejoin: isPrejoinPageVisible(state),
_showStageFilmstrip: shouldDisplayStageFilmstrip(state)
};
}

View File

@ -11,7 +11,9 @@ import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import {
getFakeScreenshareParticipantTrack,
getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import {
isParticipantConnectionStatusInactive,
isParticipantConnectionStatusInterrupted,
@ -366,12 +368,18 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { participantId } = ownProps;
const tracks = state['features/base/tracks'];
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
const participant = participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
let firstVideoTrack;
if (sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant) {
firstVideoTrack = getFakeScreenshareParticipantTrack(tracks, participantId);
} else {
firstVideoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
const _isConnectionStatusInactive = sourceNameSignalingEnabled
? isTrackStreamingStatusInactive(firstVideoTrack)
: isParticipantConnectionStatusInactive(participant);

View File

@ -87,6 +87,12 @@ type Props = AbstractProps & {
*/
_enableSaveLogs: boolean,
/**
* Whether or not the displays stats are for screen share. This prop is behind the sourceNameSignaling feature
* flag.
*/
_isFakeScreenShareParticipant: Boolean,
/**
* Whether or not the displays stats are for local video.
*/
@ -199,6 +205,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
e2eRtt = { e2eRtt }
enableSaveLogs = { this.props._enableSaveLogs }
framerate = { framerate }
isFakeScreenShareParticipant = { this.props._isFakeScreenShareParticipant }
isLocalVideo = { this.props._isLocalVideo }
maxEnabledResolution = { maxEnabledResolution }
onSaveLogs = { this.props._onSaveLogs }
@ -334,10 +341,11 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
_connectionStatus: participant?.connectionStatus,
_enableSaveLogs: state['features/base/config'].enableSaveLogs,
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
_isLocalVideo: participant?.local,
_region: participant?.region,
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
_isConnectionStatusInterrupted,
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
_isLocalVideo: participant?.local,
_region: participant?.region
};
if (conference) {

View File

@ -1,6 +1,7 @@
/* @flow */
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { Component } from 'react';
import { isMobileBrowser } from '../../../features/base/environment/utils';
@ -90,6 +91,11 @@ type Props = {
*/
isLocalVideo: boolean,
/**
* Whether or not the statistics are for screen share.
*/
isFakeScreenShareParticipant: boolean,
/**
* The send-side max enabled resolution (aka the highest layer that is not
* suspended on the send-side).
@ -172,15 +178,48 @@ function onClick(event) {
const styles = theme => {
return {
actions: {
margin: '10px auto',
textAlign: 'center'
},
connectionStatsTable: {
'&, & > table': {
fontSize: '12px',
fontWeight: '400',
'& td': {
padding: '2px 0'
}
},
'& > table': {
whiteSpace: 'nowrap'
},
'& td:nth-child(n-1)': {
paddingLeft: '5px'
},
'& $upload, & $download': {
marginRight: '2px'
}
},
contextMenu: {
position: 'relative',
marginTop: 0,
right: 'auto',
padding: `${theme.spacing(2)}px ${theme.spacing(1)}px`,
marginLeft: '4px',
marginRight: '4px',
marginBottom: '4px'
}
marginLeft: `${theme.spacing(1)}px`,
marginRight: `${theme.spacing(1)}px`,
marginBottom: `${theme.spacing(1)}px`
},
download: {},
mobile: {
margin: `${theme.spacing(3)}px`
},
status: {
fontWeight: 'bold'
},
upload: {}
};
};
@ -197,8 +236,18 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { isLocalVideo, enableSaveLogs, disableShowMoreStats, classes } = this.props;
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
const {
classes,
disableShowMoreStats,
enableSaveLogs,
isFakeScreenShareParticipant,
isLocalVideo
} = this.props;
const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() });
if (isFakeScreenShareParticipant) {
return this._renderScreenShareStatus();
}
return (
<ContextMenu
@ -209,7 +258,7 @@ class ConnectionStatsTable extends Component<Props> {
className = { className }
onClick = { onClick }>
{ this._renderStatistics() }
<div className = 'connection-actions'>
<div className = { classes.actions }>
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
{ !disableShowMoreStats && this._renderShowMoreLink() }
</div>
@ -219,6 +268,34 @@ class ConnectionStatsTable extends Component<Props> {
);
}
/**
* Creates a ReactElement that will display connection statistics for a screen share thumbnail.
*
* @private
* @returns {ReactElement}
*/
_renderScreenShareStatus() {
const { classes } = this.props;
const className = isMobileBrowser() ? 'connection-info connection-info__mobile' : 'connection-info';
return (<ContextMenu
className = { classes.contextMenu }
hidden = { false }
inDrawer = { true }>
<div
className = { className }
onClick = { onClick }>
{ <table className = 'connection-info__container'>
<tbody>
{ this._renderResolution() }
{ this._renderFrameRate() }
</tbody>
</table> }
</div>
</ContextMenu>);
}
/**
* Creates a table as ReactElement that will display additional statistics
* related to bandwidth and transport for the local user.
@ -230,7 +307,7 @@ class ConnectionStatsTable extends Component<Props> {
const { isLocalVideo } = this.props;
return (
<table className = 'connection-info__container'>
<table>
<tbody>
{ isLocalVideo ? this._renderBandwidth() : null }
{ isLocalVideo ? this._renderTransport() : null }
@ -251,6 +328,7 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
_renderBandwidth() {
const { classes } = this.props;
const { download, upload } = this.props.bandwidth || {};
return (
@ -259,11 +337,11 @@ class ConnectionStatsTable extends Component<Props> {
{ this.props.t('connectionindicator.bandwidth') }
</td>
<td>
<span className = 'connection-info__download'>
<span className = { classes.download }>
&darr;
</span>
{ download ? `${download} Kbps` : 'N/A' }
<span className = 'connection-info__upload'>
<span className = { classes.upload }>
&uarr;
</span>
{ upload ? `${upload} Kbps` : 'N/A' }
@ -280,6 +358,7 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
_renderBitrate() {
const { classes } = this.props;
const { download, upload } = this.props.bitrate || {};
return (
@ -290,11 +369,11 @@ class ConnectionStatsTable extends Component<Props> {
</span>
</td>
<td>
<span className = 'connection-info__download'>
<span className = { classes.download }>
&darr;
</span>
{ download ? `${download} Kbps` : 'N/A' }
<span className = 'connection-info__upload'>
<span className = { classes.upload }>
&uarr;
</span>
{ upload ? `${upload} Kbps` : 'N/A' }
@ -410,8 +489,10 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
_renderConnectionSummary() {
const { classes } = this.props;
return (
<tr className = 'connection-info__status'>
<tr className = { classes.status }>
<td>
<span>{ this.props.t('connectionindicator.status') }</span>
</td>
@ -527,7 +608,7 @@ class ConnectionStatsTable extends Component<Props> {
* @returns {ReactElement}
*/
_renderPacketLoss() {
const { packetLoss, t } = this.props;
const { classes, packetLoss, t } = this.props;
let packetLossTableData;
if (packetLoss) {
@ -535,11 +616,11 @@ class ConnectionStatsTable extends Component<Props> {
packetLossTableData = (
<td>
<span className = 'connection-info__download'>
<span className = { classes.download }>
&darr;
</span>
{ download === null ? 'N/A' : `${download}%` }
<span className = 'connection-info__upload'>
<span className = { classes.upload }>
&uarr;
</span>
{ upload === null ? 'N/A' : `${upload}%` }
@ -650,7 +731,7 @@ class ConnectionStatsTable extends Component<Props> {
const isRemoteVideo = !this.props.isLocalVideo;
return (
<table className = 'connection-info__container'>
<table>
<tbody>
{ this._renderConnectionSummary() }
{ this._renderBitrate() }

View File

@ -1,6 +1,7 @@
// @flow
import { makeStyles } from '@material-ui/core/styles';
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
@ -40,7 +41,7 @@ const useStyles = makeStyles(theme => {
*
* @returns {ReactElement|null}
*/
const DominantSpeakerName = () => {
const StageParticipantNameLabel = () => {
const classes = useStyles();
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
const nameToDisplay = largeVideoParticipant?.name;
@ -56,7 +57,11 @@ const DominantSpeakerName = () => {
if (showDisplayName && nameToDisplay && selectedId !== localId && !isTileView) {
return (
<div
className = { `${classes.badgeContainer}${toolboxVisible ? ` ${classes.containerElevated}` : ''}` }>
className = { clsx(
'stage-participant-label',
classes.badgeContainer,
toolboxVisible && classes.containerElevated
) }>
<DisplayNameBadge name = { nameToDisplay } />
</div>
);
@ -65,4 +70,4 @@ const DominantSpeakerName = () => {
return null;
};
export default DominantSpeakerName;
export default StageParticipantNameLabel;

View File

@ -2,4 +2,4 @@
export { default as DisplayName } from './DisplayName';
export { default as DisplayNamePrompt } from './DisplayNamePrompt';
export { default as DominantSpeakerName } from './DominantSpeakerName';
export { default as StageParticipantNameLabel } from './StageParticipantNameLabel';

View File

@ -35,7 +35,7 @@ const queue = [];
let lastValidFaceBox;
const detect = async message => {
const { baseUrl, imageBitmap, isHorizontallyFlipped, threshold } = message.data;
const { baseUrl, image, isHorizontallyFlipped, threshold } = message.data;
if (initInProgress || initError) {
return;
@ -70,8 +70,8 @@ const detect = async message => {
tf.engine().startScope();
const image = tf.browser.fromPixels(imageBitmap);
const detections = await model.estimateFaces(image, false, isHorizontallyFlipped, false);
const imageTensor = tf.browser.fromPixels(image);
const detections = await model.estimateFaces(imageTensor, false, isHorizontallyFlipped, false);
tf.engine().endScope();
@ -80,10 +80,10 @@ const detect = async message => {
if (detections.length) {
faceBox = {
// normalize to percentage based
left: Math.round(Math.min(...detections.map(d => d.topLeft[0])) * 100 / imageBitmap.width),
right: Math.round(Math.max(...detections.map(d => d.bottomRight[0])) * 100 / imageBitmap.width),
top: Math.round(Math.min(...detections.map(d => d.topLeft[1])) * 100 / imageBitmap.height),
bottom: Math.round(Math.max(...detections.map(d => d.bottomRight[1])) * 100 / imageBitmap.height)
left: Math.round(Math.min(...detections.map(d => d.topLeft[0])) * 100 / image.width),
right: Math.round(Math.max(...detections.map(d => d.bottomRight[0])) * 100 / image.width),
top: Math.round(Math.min(...detections.map(d => d.topLeft[1])) * 100 / image.height),
bottom: Math.round(Math.max(...detections.map(d => d.bottomRight[1])) * 100 / image.height)
};
if (lastValidFaceBox && Math.abs(lastValidFaceBox.left - faceBox.left) < threshold) {

View File

@ -44,6 +44,7 @@ export async function sendDataToWorker(
}
let imageBitmap;
let image;
try {
imageBitmap = await imageCapture.grabFrame();
@ -53,13 +54,28 @@ export async function sendDataToWorker(
return;
}
if (typeof OffscreenCanvas === 'undefined') {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
context.drawImage(imageBitmap, 0, 0);
image = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
} else {
image = imageBitmap;
}
worker.postMessage({
id: DETECT_FACE_BOX,
baseUrl: getBaseUrl(),
imageBitmap,
image,
threshold,
isHorizontallyFlipped
});
imageBitmap.close();
}
/**

View File

@ -118,3 +118,44 @@ export const SET_USER_FILMSTRIP_WIDTH = 'SET_USER_FILMSTRIP_WIDTH';
* }
*/
export const SET_USER_IS_RESIZING = 'SET_USER_IS_RESIZING';
/**
* The type of (redux) action which sets the dimensions of the thumbnails in stage filmstrip view.
*
* {
* type: SET_STAGE_FILMSTRIP_DIMENSIONS,
* dimensions: Object
* }
*/
export const SET_STAGE_FILMSTRIP_DIMENSIONS = 'SET_STAGE_FILMSTRIP_DIMENSIONS';
/**
* The type of Redux action which adds a participant to the active list
* (the participants displayed on the stage filmstrip).
* {
* type: ADD_STAGE_PARTICIPANT,
* participantId: string,
* pinned: boolean
* }
*/
export const ADD_STAGE_PARTICIPANT = 'ADD_STAGE_PARTICIPANT';
/**
* The type of Redux action which removes a participant from the active list
* (the participants displayed on the stage filmstrip).
* {
* type: REMOVE_STAGE_PARTICIPANT,
* participantId: string,
* }
*/
export const REMOVE_STAGE_PARTICIPANT = 'REMOVE_STAGE_PARTICIPANT';
/**
* The type of Redux action which sets the active participants list
* (the participants displayed on the stage filmstrip).
* {
* type: SET_STAGE_PARTICIPANTS,
* queue: Array<Object>
* }
*/
export const SET_STAGE_PARTICIPANTS = 'SET_STAGE_PARTICIPANTS';

View File

@ -1,6 +1,7 @@
// @flow
import type { Dispatch } from 'redux';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import {
getLocalParticipant,
getParticipantById,
@ -11,8 +12,12 @@ import { shouldHideSelfView } from '../base/settings/functions.any';
import { getMaxColumnCount } from '../video-layout';
import {
ADD_STAGE_PARTICIPANT,
REMOVE_STAGE_PARTICIPANT,
SET_STAGE_PARTICIPANTS,
SET_FILMSTRIP_WIDTH,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_STAGE_FILMSTRIP_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_USER_FILMSTRIP_WIDTH,
SET_USER_IS_RESIZING,
@ -21,6 +26,7 @@ import {
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
MAX_ACTIVE_PARTICIPANTS,
SCROLL_SIZE,
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
TILE_HORIZONTAL_MARGIN,
@ -32,11 +38,12 @@ import {
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
} from './constants';
import {
calculateNotResponsiveTileViewDimensions,
calculateNonResponsiveTileViewDimensions,
calculateResponsiveTileViewDimensions,
calculateThumbnailSizeForHorizontalView,
calculateThumbnailSizeForVerticalView,
getNumberOfPartipantsForTileView,
getVerticalViewMaxWidth,
isFilmstripResizable,
showGridInVerticalView
} from './functions';
@ -46,9 +53,6 @@ export * from './actions.any';
/**
* Sets the dimensions of the tile view grid.
*
* @param {Object} dimensions - Whether the filmstrip is visible.
* @param {Object | Function} stateful - An object or function that can be
* resolved to Redux state using the {@code toState} function.
* @returns {Function}
*/
export function setTileViewDimensions() {
@ -70,7 +74,7 @@ export function setTileViewDimensions() {
columns,
rows
} = disableResponsiveTiles
? calculateNotResponsiveTileViewDimensions(state)
? calculateNonResponsiveTileViewDimensions(state)
: calculateResponsiveTileViewDimensions({
clientWidth,
clientHeight,
@ -119,6 +123,7 @@ export function setVerticalViewDimensions() {
const resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
const numberOfRemoteParticipants = getRemoteParticipantCount(state);
const { localScreenShare } = state['features/base/participants'];
let gridView = {};
let thumbnails = {};
@ -142,8 +147,8 @@ export function setVerticalViewDimensions() {
clientWidth: filmstripWidth.current,
clientHeight,
disableTileEnlargement: false,
isVerticalFilmstrip: true,
maxColumns,
noHorizontalContainerMargin: true,
numberOfParticipants,
numberOfVisibleTiles
});
@ -176,6 +181,20 @@ export function setVerticalViewDimensions() {
= thumbnails?.local?.width + TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN + SCROLL_SIZE;
remoteVideosContainerHeight
= clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height) - VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
if (getSourceNameSignalingFeatureFlag(state)) {
// Account for the height of the local screen share thumbnail when calculating the height of the remote
// videos container.
const localCameraThumbnailHeight = thumbnails?.local?.height;
const localScreenShareThumbnailHeight
= localScreenShare && !disableSelfView ? thumbnails?.local?.height : 0;
remoteVideosContainerHeight = clientHeight
- localCameraThumbnailHeight
- localScreenShareThumbnailHeight
- VERTICAL_FILMSTRIP_VERTICAL_MARGIN;
}
hasScroll
= remoteVideosContainerHeight
< (thumbnails?.remote.height + TILE_VERTICAL_MARGIN) * numberOfRemoteParticipants;
@ -230,6 +249,68 @@ export function setHorizontalViewDimensions() {
};
}
/**
* Sets the dimensions of the stage filmstrip tile view grid.
*
* @returns {Function}
*/
export function setStageFilmstripViewDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const {
disableResponsiveTiles,
disableTileEnlargement,
tileView = {}
} = state['features/base/config'];
const { visible } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
const maxColumns = getMaxColumnCount(state);
const {
height,
width,
columns,
rows
} = disableResponsiveTiles
? calculateNonResponsiveTileViewDimensions(state, true)
: calculateResponsiveTileViewDimensions({
clientWidth: clientWidth - verticalWidth,
clientHeight,
disableTileEnlargement,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
const filmstripWidth
= Math.min(clientWidth - TILE_VIEW_GRID_HORIZONTAL_MARGIN, columns * (TILE_HORIZONTAL_MARGIN + width))
+ (hasScroll ? SCROLL_SIZE : 0);
const filmstripHeight = Math.min(clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN, thumbnailsTotalHeight);
dispatch({
type: SET_STAGE_FILMSTRIP_DIMENSIONS,
dimensions: {
gridDimensions: {
columns,
rows
},
thumbnailSize: {
height,
width
},
filmstripHeight,
filmstripWidth,
hasScroll
}
});
};
}
/**
* Emulates a click on the n-th video.
*
@ -313,3 +394,44 @@ export function setUserIsResizing(resizing: boolean) {
resizing
};
}
/**
* Add participant to the active participants list.
*
* @param {string} participantId - The Id of the participant to be added.
* @param {boolean?} pinned - Whether the participant is pinned or not.
* @returns {Object}
*/
export function addStageParticipant(participantId, pinned = false) {
return {
type: ADD_STAGE_PARTICIPANT,
participantId,
pinned
};
}
/**
* Remove participant from the active participants list.
*
* @param {string} participantId - The Id of the participant to be removed.
* @returns {Object}
*/
export function removeStageParticipant(participantId) {
return {
type: REMOVE_STAGE_PARTICIPANT,
participantId
};
}
/**
* Sets the active participants list.
*
* @param {Array<Object>} queue - The new list.
* @returns {Object}
*/
export function setStageParticipants(queue) {
return {
type: SET_STAGE_PARTICIPANTS,
queue
};
}

View File

@ -0,0 +1,157 @@
// @flow
import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { VideoTrack } from '../../../base/media';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
type Props = {
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The class name that will be used for the container.
*/
containerClassName: string,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: boolean,
/**
* Indicates whether we are currently running in a mobile browser.
*/
isMobile: boolean,
/**
* Click handler.
*/
onClick: Function,
/**
* Mouse enter handler.
*/
onMouseEnter: Function,
/**
* Mouse leave handler.
*/
onMouseLeave: Function,
/**
* Mouse move handler.
*/
onMouseMove: Function,
/**
* Touch end handler.
*/
onTouchEnd: Function,
/**
* Touch move handler.
*/
onTouchMove: Function,
/**
* Touch start handler.
*/
onTouchStart: Function,
/**
* The ID of the fake screen share participant.
*/
participantId: string,
/**
* An object with the styles for thumbnail.
*/
styles: Object,
/**
* JitsiTrack instance.
*/
videoTrack: Object
}
const FakeScreenShareParticipant = ({
classes,
containerClassName,
isHovered,
isMobile,
onClick,
onMouseEnter,
onMouseLeave,
onMouseMove,
onTouchEnd,
onTouchMove,
onTouchStart,
participantId,
styles,
videoTrack
}: Props) => {
const currentLayout = useSelector(getCurrentLayout);
const videoTrackId = videoTrack?.jitsiTrack?.getId();
const video = videoTrack && <VideoTrack
id = { `remoteVideo_${videoTrackId || ''}` }
muted = { true }
style = { styles.video }
videoTrack = { videoTrack } />;
return (
<span
className = { containerClassName }
id = { `participant_${participantId}` }
{ ...(isMobile
? {
onTouchEnd,
onTouchMove,
onTouchStart
}
: {
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave
}
) }
style = { styles.thumbnail }>
{video}
<div className = { classes.containerBackground } />
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsTopContainer,
currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
) }>
<ThumbnailTopIndicators
currentLayout = { currentLayout }
isFakeScreenShareParticipant = { true }
isHovered = { isHovered }
participantId = { participantId } />
</div>
<div
className = { clsx(classes.indicatorsContainer,
classes.indicatorsBottomContainer,
currentLayout === LAYOUTS.TILE_VIEW && 'tile-view-mode'
) }>
<ThumbnailBottomIndicators
className = { classes.indicatorsBackground }
currentLayout = { currentLayout }
local = { false }
participantId = { participantId }
showStatusIndicators = { false } />
</div>
</span>);
};
export default FakeScreenShareParticipant;

View File

@ -12,15 +12,16 @@ import {
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { getToolbarButtons } from '../../../base/config';
import { getSourceNameSignalingFeatureFlag, getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { CHAT_SIZE } from '../../../chat';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { LAYOUTS } from '../../../video-layout';
import {
setFilmstripVisible,
setVisibleRemoteParticipants,
@ -30,19 +31,13 @@ import {
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
MIN_STAGE_VIEW_WIDTH,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE
TILE_VERTICAL_MARGIN
} from '../../constants';
import {
getVerticalViewMaxWidth,
isFilmstripResizable,
shouldRemoteVideosBeVisible,
showGridInVerticalView
shouldRemoteVideosBeVisible
} from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
@ -63,6 +58,11 @@ type Props = {
*/
_className: string,
/**
* Whether or not the chat is open.
*/
_chatOpen: boolean,
/**
* The current layout of the filmstrip.
*/
@ -113,6 +113,11 @@ type Props = {
*/
_isVerticalFilmstrip: boolean,
/**
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
*/
_localScreenShare: Object,
/**
* The maximum width of the vertical filmstrip.
*/
@ -138,6 +143,11 @@ type Props = {
*/
_rows: number,
/**
* Whether or not this is the stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The height of the thumbnail.
*/
@ -158,6 +168,11 @@ type Props = {
*/
_verticalFilmstripWidth: ?number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
@ -295,11 +310,14 @@ class Filmstrip extends PureComponent <Props, State> {
render() {
const filmstripStyle = { };
const {
_chatOpen,
_currentLayout,
_disableSelfView,
_localScreenShare,
_resizableFilmstrip,
_verticalFilmstripWidth,
_stageFilmstrip,
_visible,
_verticalViewBackground,
_verticalViewGrid,
_verticalViewMaxWidth,
classes
@ -308,13 +326,20 @@ class Filmstrip extends PureComponent <Props, State> {
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
filmstripStyle.maxWidth = _verticalViewMaxWidth;
if (!_visible) {
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
}
break;
}
case LAYOUTS.TILE_VIEW: {
if (_stageFilmstrip && _visible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px - ${_chatOpen ? CHAT_SIZE : 0}px)`;
}
break;
}
}
let toolbar = null;
@ -332,10 +357,24 @@ class Filmstrip extends PureComponent <Props, State> {
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
<div id = 'filmstripLocalVideoThumbnail'>
{
!tileViewActive && <div id = 'filmstripLocalVideoThumbnail'>
<Thumbnail
key = 'local' />
</div>
}
</div>
)}
{_localScreenShare && !_disableSelfView && !_verticalViewGrid && (
<div
className = 'filmstrip__videos'
id = 'filmstripLocalScreenShare'>
<div id = 'filmstripLocalScreenShareThumbnail'>
{
!tileViewActive && <Thumbnail
key = 'local' />
key = 'localScreenShare'
participantID = { _localScreenShare.id } />
}
</div>
</div>
@ -352,8 +391,7 @@ class Filmstrip extends PureComponent <Props, State> {
this.props._className,
classes.filmstrip,
_verticalViewGrid && 'no-vertical-padding',
_verticalFilmstripWidth + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
&& classes.filmstripBackground) }
_verticalViewBackground && classes.filmstripBackground) }
style = { filmstripStyle }>
{ toolbar }
{_resizableFilmstrip
@ -576,6 +614,7 @@ class Filmstrip extends PureComponent <Props, State> {
_remoteParticipantsLength,
_resizableFilmstrip,
_rows,
_stageFilmstrip,
_thumbnailHeight,
_thumbnailWidth,
_verticalViewGrid
@ -596,6 +635,7 @@ class Filmstrip extends PureComponent <Props, State> {
height = { _filmstripHeight }
initialScrollLeft = { 0 }
initialScrollTop = { 0 }
itemData = {{ stageFilmstrip: _stageFilmstrip }}
itemKey = { this._gridItemKey }
onItemsRendered = { this._onGridItemsRendered }
overscanRowCount = { 1 }
@ -759,129 +799,49 @@ class Filmstrip extends PureComponent <Props, State> {
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state, ownProps) {
const { _hasScroll = false } = ownProps;
const toolbarButtons = getToolbarButtons(state);
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { localScreenShare } = state['features/base/participants'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const {
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
hasScroll: tileViewHasScroll,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
const disableSelfView = shouldHideSelfView(state);
const _resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
let gridDimensions = dimensions;
let _hasScroll = false;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const { clientWidth } = state['features/base/responsive-ui'];
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && (
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
const shouldReduceHeight = reduceHeight && isMobileBrowser();
let videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const className = `${remoteVideosVisible || _verticalViewGrid ? '' : 'hide-videos'} ${
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
const className = `${remoteVideosVisible || ownProps._verticalViewGrid ? '' : 'hide-videos'} ${
shouldReduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_hasScroll = Boolean(tileViewHasScroll);
if (_hasScroll) {
videosClassName += ' has-scroll';
}
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const {
remote,
remoteVideosContainer,
gridView,
hasScroll
} = state['features/filmstrip'].verticalViewDimensions;
_hasScroll = Boolean(hasScroll);
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
? TOOLBAR_HEIGHT : 0);
remoteFilmstripWidth = remoteVideosContainer?.width;
if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
_thumbnailSize = gridView.thumbnailSize;
if (gridView.hasScroll) {
videosClassName += ' has-scroll';
}
} else {
_thumbnailSize = remote;
}
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
_hasScroll = Boolean(hasScroll);
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height;
remoteFilmstripWidth = remoteVideosContainer?.width;
break;
}
}
return {
_className: className,
_columns: gridDimensions.columns,
_currentLayout,
_chatOpen: state['features/chat'].isOpen,
_disableSelfView: disableSelfView,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_hasScroll,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip: _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_isVerticalFilmstrip: ownProps._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW,
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_resizableFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_thumbnailsReordered: enableThumbnailReordering,
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_videosClassName: videosClassName,
_visible: visible,
_verticalViewGrid,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state)
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_videosClassName: videosClassName
};
}

View File

@ -0,0 +1,208 @@
// @flow
import React from 'react';
import { getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { connect } from '../../../base/redux';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE } from '../../constants';
import { isFilmstripResizable, showGridInVerticalView } from '../../functions.web';
import Filmstrip from './Filmstrip';
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
_columns: number,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
* The height of the filmstrip.
*/
_filmstripHeight: number,
/**
* Whether the filmstrip has scroll or not.
*/
_hasScroll: boolean,
/**
* Whether or not the current layout is vertical filmstrip.
*/
_isVerticalFilmstrip: boolean,
/**
* The participants in the call.
*/
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* Whether or not the filmstrip should be user-resizable.
*/
_resizableFilmstrip: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
_verticalViewGrid: boolean,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean
};
const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
hasScroll: tileViewHasScroll,
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
const _resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
let gridDimensions = dimensions;
let _hasScroll = false;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && (
isMobileBrowser() || _currentLayout !== LAYOUTS.VERTICAL_FILMSTRIP_VIEW);
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_hasScroll = Boolean(tileViewHasScroll);
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
const {
remote,
remoteVideosContainer,
gridView,
hasScroll
} = state['features/filmstrip'].verticalViewDimensions;
_hasScroll = Boolean(hasScroll);
remoteFilmstripHeight = remoteVideosContainer?.height - (!_verticalViewGrid && shouldReduceHeight
? TOOLBAR_HEIGHT : 0);
remoteFilmstripWidth = remoteVideosContainer?.width;
if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
_thumbnailSize = gridView.thumbnailSize;
_hasScroll = gridView.hasScroll;
} else {
_thumbnailSize = remote;
}
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const { remote, remoteVideosContainer, hasScroll } = state['features/filmstrip'].horizontalViewDimensions;
_hasScroll = Boolean(hasScroll);
_thumbnailSize = remote;
remoteFilmstripHeight = remoteVideosContainer?.height;
remoteFilmstripWidth = remoteVideosContainer?.width;
break;
}
}
return {
_columns: gridDimensions.columns,
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_hasScroll,
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_resizableFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_visible: visible,
_verticalViewGrid,
_verticalViewBackground: verticalFilmstripWidth.current + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
};
}
export default connect(_mapStateToProps)(MainFilmstrip);

View File

@ -0,0 +1,74 @@
/* @flow */
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { useSelector } from 'react-redux';
import { IconPinParticipant } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
import { getActiveParticipantsIds } from '../../functions.web';
/**
* The type of the React {@code Component} props of {@link PinnedIndicator}.
*/
type Props = {
/**
* The font-size for the icon.
*/
iconSize: number,
/**
* The participant id who we want to render the raised hand indicator
* for.
*/
participantId: string,
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
};
const useStyles = makeStyles(() => {
return {
pinnedIndicator: {
backgroundColor: 'rgba(0, 0, 0, .7)',
padding: '2px',
zIndex: 3,
display: 'inline-block',
borderRadius: '4px',
boxSizing: 'border-box'
}
};
});
/**
* Thumbnail badge showing that the participant would like to speak.
*
* @returns {ReactElement}
*/
const PinnedIndicator = ({
iconSize,
participantId,
tooltipPosition
}: Props) => {
const isPinned = useSelector(getActiveParticipantsIds).find(id => id === participantId);
const styles = useStyles();
if (!isPinned) {
return null;
}
return (
<div className = { styles.pinnedIndicator }>
<BaseIndicator
icon = { IconPinParticipant }
iconSize = { `${iconSize}px` }
tooltipKey = 'pinnedParticipant'
tooltipPosition = { tooltipPosition } />
</div>
);
};
export default PinnedIndicator;

View File

@ -0,0 +1,161 @@
// @flow
import React from 'react';
import { getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { connect } from '../../../base/redux';
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
TOOLBAR_HEIGHT_MOBILE
} from '../../constants';
import { getActiveParticipantsIds } from '../../functions';
import Filmstrip from './Filmstrip';
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
_columns: number,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
* The height of the filmstrip.
*/
_filmstripHeight: number,
/**
* Whether or not the current layout is vertical filmstrip.
*/
_isVerticalFilmstrip: boolean,
/**
* The participants in the call.
*/
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* Whether or not the filmstrip should be user-resizable.
*/
_resizableFilmstrip: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
_verticalViewGrid: boolean,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean
};
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
_currentLayout = { LAYOUTS.TILE_VIEW }
_stageFilmstrip = { true } />
</span>
);
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible } = state['features/filmstrip'];
const activeParticipants = getActiveParticipantsIds(state);
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
gridDimensions: dimensions = {},
filmstripHeight,
filmstripWidth,
thumbnailSize
} = state['features/filmstrip'].stageFilmstripDimensions;
const gridDimensions = dimensions;
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
return {
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: filmstripWidth,
_remoteParticipantsLength: activeParticipants.length,
_remoteParticipants: activeParticipants,
_resizableFilmstrip: false,
_rows: gridDimensions.rows,
_thumbnailWidth: thumbnailSize?.width,
_thumbnailHeight: thumbnailSize?.height,
_visible: visible,
_verticalViewGrid: false,
_verticalViewBackground: false
};
}
export default connect(_mapStateToProps)(StageFilmstrip);

View File

@ -7,6 +7,7 @@ import React, { Component } from 'react';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
import { Avatar } from '../../../base/avatar';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
@ -21,6 +22,7 @@ import {
getLocalAudioTrack,
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
getFakeScreenshareParticipantTrack,
updateLastTrackVideoMediaEvent
} from '../../../base/tracks';
import { getVideoObjectPosition } from '../../../face-centering/functions';
@ -28,6 +30,7 @@ import { hideGif, showGif } from '../../../gifs/actions';
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { addStageParticipant } from '../../actions.web';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
@ -36,11 +39,14 @@ import {
} from '../../constants';
import {
computeDisplayModeFromInput,
getActiveParticipantsIds,
getDisplayModeInput,
isVideoPlayable,
showGridInVerticalView
} from '../../functions';
import { isStageFilmstripEnabled } from '../../functions.web';
import FakeScreenShareParticipant from './FakeScreenShareParticipant';
import ThumbnailAudioIndicator from './ThumbnailAudioIndicator';
import ThumbnailBottomIndicators from './ThumbnailBottomIndicators';
import ThumbnailTopIndicators from './ThumbnailTopIndicators';
@ -108,16 +114,17 @@ export type Props = {|
*/
_height: number,
/**
* Whether or not the participant is displayed on the stage filmstrip.
* Used to hide the video from the vertical filmstrip.
*/
_isActiveParticipant: boolean,
/**
* Indicates whether the thumbnail should be hidden or not.
*/
_isHidden: boolean,
/**
* Whether or not there is a pinned participant.
*/
_isAnyParticipantPinned: boolean,
/**
* Indicates whether audio only mode is enabled.
*/
@ -128,6 +135,12 @@ export type Props = {|
*/
_isCurrentlyOnLargeVideo: boolean,
/**
* Indicates whether the participant is a fake screen share participant. This prop is behind the
* sourceNameSignaling feature flag.
*/
_isFakeScreenShareParticipant: boolean,
/**
* Whether we are currently running in a mobile browser.
*/
@ -173,6 +186,11 @@ export type Props = {|
*/
_raisedHand: boolean,
/**
* Whether or not the stage filmstrip is disabled.
*/
_stageFilmstripDisabled: boolean,
/**
* The video object position for the participant.
*/
@ -208,6 +226,11 @@ export type Props = {|
*/
participantID: ?string,
/**
* Whether the tile is displayed in the stage filmstrip or not.
*/
stageFilmstrip: boolean,
/**
* Styles that will be set to the Thumbnail's main span element.
*/
@ -498,6 +521,13 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_hidePopover() {
const { _currentLayout } = this.props;
if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
this.setState({
isHovered: false
});
}
this.setState({
popoverVisible: false
});
@ -514,6 +544,7 @@ class Thumbnail extends Component<Props, State> {
_currentLayout,
_disableTileEnlargement,
_height,
_isFakeScreenShareParticipant,
_isHidden,
_isScreenSharing,
_participant,
@ -551,7 +582,7 @@ class Thumbnail extends Component<Props, State> {
|| _disableTileEnlargement
|| _isScreenSharing;
if (canPlayEventReceived || _participant.local) {
if (canPlayEventReceived || _participant.local || _isFakeScreenShareParticipant) {
videoStyles = {
objectFit: doNotStretchVideo ? 'contain' : 'cover'
};
@ -596,10 +627,14 @@ class Thumbnail extends Component<Props, State> {
* @returns {void}
*/
_onClick() {
const { _participant, dispatch } = this.props;
const { _participant, dispatch, _stageFilmstripDisabled } = this.props;
const { id, pinned } = _participant;
dispatch(pinParticipant(pinned ? null : id));
if (_stageFilmstripDisabled) {
dispatch(pinParticipant(pinned ? null : id));
} else {
dispatch(addStageParticipant(id, true));
}
}
_onMouseEnter: () => void;
@ -747,7 +782,6 @@ class Thumbnail extends Component<Props, State> {
_isDominantSpeakerDisabled,
_participant,
_currentLayout,
_isAnyParticipantPinned,
_raisedHand,
classes
} = this.props;
@ -758,17 +792,12 @@ class Thumbnail extends Component<Props, State> {
className += ` ${classes.raisedHand}`;
}
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
} else if (_isAnyParticipantPinned) {
if (_participant?.pinned) {
className += ` videoContainerFocused ${classes.activeSpeaker}`;
}
} else if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
className += ` ${classes.activeSpeaker} dominant-speaker`;
}
if (_currentLayout !== LAYOUTS.TILE_VIEW && _participant?.pinned) {
className += ' videoContainerFocused';
}
return className;
}
@ -876,8 +905,9 @@ class Thumbnail extends Component<Props, State> {
_localFlipX,
_participant,
_videoTrack,
_gifSrc,
classes,
_gifSrc
stageFilmstrip
} = this.props;
const { id } = _participant || {};
const { isHovered, popoverVisible } = this.state;
@ -914,7 +944,10 @@ class Thumbnail extends Component<Props, State> {
return (
<span
className = { containerClassName }
id = { local ? 'localVideoContainer' : `participant_${id}` }
id = { local
? `localVideoContainer${stageFilmstrip ? '_stage' : ''}`
: `participant_${id}${stageFilmstrip ? '_stage' : ''}`
}
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
@ -988,7 +1021,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _participant } = this.props;
const { _participant, _isFakeScreenShareParticipant } = this.props;
if (!_participant) {
return null;
@ -1004,6 +1037,29 @@ class Thumbnail extends Component<Props, State> {
return this._renderFakeParticipant();
}
if (_isFakeScreenShareParticipant) {
const { isHovered } = this.state;
const { _videoTrack, _isMobile, classes } = this.props;
return (
<FakeScreenShareParticipant
classes = { classes }
containerClassName = { this._getContainerClassName() }
isHovered = { isHovered }
isMobile = { _isMobile }
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove }
onTouchEnd = { this._onTouchEnd }
onTouchMove = { this._onTouchMove }
onTouchStart = { this._onTouchStart }
participantId = { _participant.id }
styles = { this._getStyles() }
videoTrack = { _videoTrack } />
);
}
return this._renderParticipant();
}
}
@ -1017,17 +1073,25 @@ class Thumbnail extends Component<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps;
const { participantID, stageFilmstrip } = ownProps;
const participant = getParticipantByIdOrUndefined(state, participantID);
const id = participant?.id;
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const _videoTrack = isLocal
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
let _videoTrack;
if (sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant) {
_videoTrack = getFakeScreenshareParticipantTrack(tracks, id);
} else {
_videoTrack = isLocal
? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
}
const _audioTrack = isLocal
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
const _currentLayout = stageFilmstrip ? LAYOUTS.TILE_VIEW : getCurrentLayout(state);
let size = {};
let _isMobilePortrait = false;
const {
@ -1039,6 +1103,7 @@ function _mapStateToProps(state, ownProps): Object {
} = state['features/base/config'];
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
const activeParticipants = getActiveParticipantsIds(state);
switch (_currentLayout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
@ -1079,12 +1144,26 @@ function _mapStateToProps(state, ownProps): Object {
break;
}
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
const {
stageFilmstripDimensions = {
thumbnailSize: {}
}
} = state['features/filmstrip'];
size = {
_width: width,
_height: height
_width: thumbnailSize?.width,
_height: thumbnailSize?.height
};
if (stageFilmstrip) {
const { width: _width, height: _height } = stageFilmstripDimensions.thumbnailSize;
size = {
_width,
_height
};
}
break;
}
}
@ -1098,10 +1177,12 @@ function _mapStateToProps(state, ownProps): Object {
_defaultLocalDisplayName: defaultLocalDisplayName,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
_disableTileEnlargement: Boolean(disableTileEnlargement),
_isActiveParticipant: activeParticipants.find(pId => pId === participantID),
_isHidden: isLocal && iAmRecorder && !iAmSipGateway,
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_isFakeScreenShareParticipant: sourceNameSignalingEnabled && participant?.isFakeScreenShareParticipant,
_isMobile,
_isMobilePortrait,
_isScreenSharing: _videoTrack?.videoType === 'desktop',
@ -1110,6 +1191,7 @@ function _mapStateToProps(state, ownProps): Object {
_localFlipX: Boolean(localFlipX),
_participant: participant,
_raisedHand: hasRaisedHand(participant),
_stageFilmstripDisabled: !isStageFilmstripEnabled(state),
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
_videoTrack,
...size,

View File

@ -32,7 +32,12 @@ type Props = {
/**
* Id of the participant for which the component is displayed.
*/
participantId: string
participantId: string,
/**
* Whether or not to show the status indicators.
*/
showStatusIndicators: string
}
const useStyles = makeStyles(() => {
@ -58,7 +63,8 @@ const ThumbnailBottomIndicators = ({
className,
currentLayout,
local,
participantId
participantId,
showStatusIndicators = true
}: Props) => {
const styles = useStyles();
const _allowEditing = !useSelector(isNameReadOnly);
@ -66,11 +72,13 @@ const ThumbnailBottomIndicators = ({
const _showDisplayName = useSelector(isDisplayNameVisible);
return (<div className = { className }>
<StatusIndicators
audio = { true }
moderator = { true }
participantID = { participantId }
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
{
showStatusIndicators && <StatusIndicators
audio = { true }
moderator = { true }
participantID = { participantId }
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
}
{
_showDisplayName && (
<span className = { styles.nameContainer }>

View File

@ -5,12 +5,14 @@ import clsx from 'clsx';
import React from 'react';
import { useSelector } from 'react-redux';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import ConnectionIndicator from '../../../connection-indicator/components/web/ConnectionIndicator';
import { LAYOUTS } from '../../../video-layout';
import { STATS_POPOVER_POSITION } from '../../constants';
import { getIndicatorsTooltipPosition } from '../../functions.web';
import PinnedIndicator from './PinnedIndicator';
import RaisedHandIndicator from './RaisedHandIndicator';
import StatusIndicators from './StatusIndicators';
import VideoMenuTriggerButton from './VideoMenuTriggerButton';
@ -39,6 +41,11 @@ type Props = {
*/
isHovered: boolean,
/**
* Whether or not the thumbnail is a fake screen share participant.
*/
isFakeScreenShareParticipant: boolean,
/**
* Whether or not the indicators are for the local participant.
*/
@ -76,6 +83,7 @@ const ThumbnailTopIndicators = ({
currentLayout,
hidePopover,
indicatorsClassName,
isFakeScreenShareParticipant,
isHovered,
local,
participantId,
@ -91,12 +99,31 @@ const ThumbnailTopIndicators = ({
useSelector(state => state['features/base/config'].connectionIndicators?.autoHide) ?? true);
const _connectionIndicatorDisabled = _isMobile
|| Boolean(useSelector(state => state['features/base/config'].connectionIndicators?.disabled));
const sourceNameSignalingEnabled = useSelector(getSourceNameSignalingFeatureFlag);
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
if (sourceNameSignalingEnabled && isFakeScreenShareParticipant) {
return (
<div className = { styles.container }>
{!_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { _indicatorIconSize }
participantId = { participantId }
statsPopoverPosition = { STATS_POPOVER_POSITION[currentLayout] } />
}
</div>
);
}
return (
<>
<div className = { styles.container }>
<PinnedIndicator
iconSize = { _indicatorIconSize }
participantId = { participantId }
tooltipPosition = { getIndicatorsTooltipPosition(currentLayout) } />
{!_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
@ -119,6 +146,7 @@ const ThumbnailTopIndicators = ({
</div>
<div className = { styles.container }>
<VideoMenuTriggerButton
currentLayout = { currentLayout }
hidePopover = { hidePopover }
local = { local }
participantId = { participantId }

View File

@ -2,11 +2,12 @@
import React, { Component } from 'react';
import { shouldComponentUpdate } from 'react-window';
import { getPinnedParticipant } from '../../../base/participants';
import { getSourceNameSignalingFeatureFlag } from '../../../base/config';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { showGridInVerticalView } from '../../functions';
import { showGridInVerticalView, getActiveParticipantsIds } from '../../functions';
import Thumbnail from './Thumbnail';
@ -26,14 +27,19 @@ type Props = {
_horizontalOffset: number,
/**
* Whether or not there is a pinned participant.
* The ID of the participant associated with the Thumbnail.
*/
_isAnyParticipantPinned: boolean,
_participantID: ?string,
/**
* Whether or not the thumbnail is a local screen share.
*/
_isLocalScreenShare: boolean,
/**
* The ID of the participant associated with the Thumbnail.
*/
_participantID: ?string,
_stageFilmstrip: boolean,
/**
* The index of the column in tile view.
@ -82,7 +88,14 @@ class ThumbnailWrapper extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _participantID, style, _horizontalOffset = 0, _isAnyParticipantPinned, _disableSelfView } = this.props;
const {
_disableSelfView,
_horizontalOffset = 0,
_isLocalScreenShare = false,
_participantID,
_stageFilmstrip,
style
} = this.props;
if (typeof _participantID !== 'string') {
return null;
@ -91,18 +104,28 @@ class ThumbnailWrapper extends Component<Props> {
if (_participantID === 'local') {
return _disableSelfView ? null : (
<Thumbnail
_isAnyParticipantPinned = { _isAnyParticipantPinned }
horizontalOffset = { _horizontalOffset }
key = 'local'
stageFilmstrip = { _stageFilmstrip }
style = { style } />);
}
if (_isLocalScreenShare) {
return _disableSelfView ? null : (
<Thumbnail
_isAnyParticipantPinned = { _isAnyParticipantPinned }
horizontalOffset = { _horizontalOffset }
key = 'localScreenShare'
participantID = { _participantID }
style = { style } />);
}
return (
<Thumbnail
_isAnyParticipantPinned = { _isAnyParticipantPinned }
horizontalOffset = { _horizontalOffset }
key = { `remote_${_participantID}` }
participantID = { _participantID }
stageFilmstrip = { _stageFilmstrip }
style = { style } />);
}
}
@ -117,47 +140,103 @@ class ThumbnailWrapper extends Component<Props> {
*/
function _mapStateToProps(state, ownProps) {
const _currentLayout = getCurrentLayout(state);
const { remoteParticipants } = state['features/filmstrip'];
const remoteParticipantsLength = remoteParticipants.length;
const { remoteParticipants: remote } = state['features/filmstrip'];
const activeParticipants = getActiveParticipantsIds(state);
const { testing = {} } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const _verticalViewGrid = showGridInVerticalView(state);
const _isAnyParticipantPinned = Boolean(getPinnedParticipant(state));
const stageFilmstrip = ownProps.data?.stageFilmstrip;
const remoteParticipants = stageFilmstrip ? activeParticipants : remote;
const remoteParticipantsLength = remoteParticipants.length;
const localId = getLocalParticipant(state).id;
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || stageFilmstrip) {
const { columnIndex, rowIndex } = ownProps;
const { gridDimensions: dimensions = {}, thumbnailSize: size } = state['features/filmstrip'].tileViewDimensions;
const { gridView } = state['features/filmstrip'].verticalViewDimensions;
const gridDimensions = _verticalViewGrid ? gridView.gridDimensions : dimensions;
const thumbnailSize = _verticalViewGrid ? gridView.thumbnailSize : size;
const { tileViewDimensions, stageFilmstripDimensions, verticalViewDimensions } = state['features/filmstrip'];
const { gridView } = verticalViewDimensions;
let gridDimensions = tileViewDimensions.gridDimensions,
thumbnailSize = tileViewDimensions.thumbnailSize;
if (stageFilmstrip) {
gridDimensions = stageFilmstripDimensions.gridDimensions;
thumbnailSize = stageFilmstripDimensions.thumbnailSize;
} else if (_verticalViewGrid) {
gridDimensions = gridView.gridDimensions;
thumbnailSize = gridView.thumbnailSize;
}
const { columns, rows } = gridDimensions;
const index = (rowIndex * columns) + columnIndex;
let horizontalOffset;
const { iAmRecorder } = state['features/base/config'];
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
const { localScreenShare } = state['features/base/participants'];
const localParticipantsLength = localScreenShare ? 2 : 1;
let participantsLength;
if (sourceNameSignalingEnabled) {
participantsLength = remoteParticipantsLength
// Add local camera and screen share to total participant count when self view is not disabled.
+ (disableSelfView ? 0 : localParticipantsLength)
// Removes iAmRecorder from the total participants count.
- (iAmRecorder ? 1 : 0);
} else {
participantsLength = stageFilmstrip ? remoteParticipantsLength
: remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
}
if (rowIndex === rows - 1) { // center the last row
const { width: thumbnailWidth } = thumbnailSize;
const partialLastRowParticipantsNumber = participantsLenght % columns;
const partialLastRowParticipantsNumber = participantsLength % columns;
if (partialLastRowParticipantsNumber > 0) {
horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2);
}
}
if (index > participantsLenght - 1) {
if (index > participantsLength - 1) {
return {};
}
if (stageFilmstrip) {
return {
_disableSelfView: disableSelfView,
_participantID: remoteParticipants[index] === localId ? 'local' : remoteParticipants[index],
_horizontalOffset: horizontalOffset,
_stageFilmstrip: stageFilmstrip
};
}
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = enableThumbnailReordering && !disableSelfView ? 0 : remoteParticipantsLength;
const remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
// Local screen share is inserted at index 1 after the local camera.
const localScreenShareIndex = enableThumbnailReordering && !disableSelfView ? 1 : remoteParticipantsLength;
let remoteIndex;
if (sourceNameSignalingEnabled) {
remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView
? index - localParticipantsLength : index;
} else {
remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
}
if (!iAmRecorder && index === localIndex) {
return {
_disableSelfView: disableSelfView,
_participantID: 'local',
_horizontalOffset: horizontalOffset
};
}
if (sourceNameSignalingEnabled && !iAmRecorder && localScreenShare && index === localScreenShareIndex) {
return {
_disableSelfView: disableSelfView,
_isLocalScreenShare: true,
_participantID: localScreenShare?.id,
_horizontalOffset: horizontalOffset,
_isAnyParticipantPinned: _verticalViewGrid && _isAnyParticipantPinned
};
@ -165,8 +244,7 @@ function _mapStateToProps(state, ownProps) {
return {
_participantID: remoteParticipants[remoteIndex],
_horizontalOffset: horizontalOffset,
_isAnyParticipantPinned: _verticalViewGrid && _isAnyParticipantPinned
_horizontalOffset: horizontalOffset
};
}
@ -177,8 +255,7 @@ function _mapStateToProps(state, ownProps) {
}
return {
_participantID: remoteParticipants[index],
_isAnyParticipantPinned
_participantID: remoteParticipants[index]
};
}

View File

@ -6,6 +6,11 @@ import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../..
type Props = {
/**
* The current layout of the filmstrip.
*/
currentLayout: string,
/**
* Hide popover callback.
*/
@ -39,6 +44,7 @@ type Props = {
// eslint-disable-next-line no-confusing-arrow
const VideoMenuTriggerButton = ({
currentLayout,
hidePopover,
local,
participantId,
@ -50,6 +56,7 @@ const VideoMenuTriggerButton = ({
<span id = 'localvideomenu'>
<LocalVideoMenuTriggerButton
buttonVisible = { visible }
currentLayout = { currentLayout }
hidePopover = { hidePopover }
popoverVisible = { popoverVisible }
showPopover = { showPopover } />
@ -59,6 +66,7 @@ const VideoMenuTriggerButton = ({
<span id = 'remotevideomenu'>
<RemoteVideoMenuTriggerButton
buttonVisible = { visible }
currentLayout = { currentLayout }
hidePopover = { hidePopover }
participantID = { participantId }
popoverVisible = { popoverVisible }

View File

@ -2,7 +2,9 @@
export { default as AudioMutedIndicator } from './AudioMutedIndicator';
export { default as Filmstrip } from './Filmstrip';
export { default as MainFilmstrip } from './MainFilmstrip';
export { default as ModeratorIndicator } from './ModeratorIndicator';
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
export { default as StageFilmstrip } from './StageFilmstrip';
export { default as StatusIndicators } from './StatusIndicators';
export { default as Thumbnail } from './Thumbnail';

View File

@ -284,3 +284,13 @@ export const MIN_STAGE_VIEW_WIDTH = 800;
*/
export const VERTICAL_VIEW_HORIZONTAL_MARGIN = VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
+ SCROLL_SIZE + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
/**
* The time after which a participant should be removed from active participants.
*/
export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60;
/**
* The max number of participants to be displayed on the stage filmstrip.
*/
export const MAX_ACTIVE_PARTICIPANTS = 4;

View File

@ -1,5 +1,8 @@
// @flow
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { getFakeScreenShareParticipantOwnerId } from '../base/participants';
import { setRemoteParticipants } from './actions';
import { isReorderingEnabled } from './functions';
@ -15,7 +18,9 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
const state = store.getState();
let reorderedParticipants = [];
if (!isReorderingEnabled(state)) {
const { sortedRemoteFakeScreenShareParticipants } = state['features/base/participants'];
if (!isReorderingEnabled(state) && !sortedRemoteFakeScreenShareParticipants.size) {
if (participantId) {
const { remoteParticipants } = state['features/filmstrip'];
@ -34,13 +39,28 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
} = state['features/base/participants'];
const remoteParticipants = new Map(sortedRemoteParticipants);
const screenShares = new Map(sortedRemoteScreenshares);
const screenShareParticipants = sortedRemoteFakeScreenShareParticipants
? [ ...sortedRemoteFakeScreenShareParticipants.keys() ] : [];
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
const speakers = new Map(speakersList);
for (const screenshare of screenShares.keys()) {
remoteParticipants.delete(screenshare);
speakers.delete(screenshare);
if (getSourceNameSignalingFeatureFlag(state)) {
for (const screenshare of screenShareParticipants) {
const ownerId = getFakeScreenShareParticipantOwnerId(screenshare);
remoteParticipants.delete(ownerId);
remoteParticipants.delete(screenshare);
speakers.delete(ownerId);
speakers.delete(screenshare);
}
} else {
for (const screenshare of screenShares.keys()) {
remoteParticipants.delete(screenshare);
speakers.delete(screenshare);
}
}
for (const sharedVideo of sharedVideos) {
remoteParticipants.delete(sharedVideo);
speakers.delete(sharedVideo);
@ -49,13 +69,32 @@ export function updateRemoteParticipants(store: Object, participantId: ?number)
remoteParticipants.delete(speaker);
}
// Always update the order of the thumnails.
reorderedParticipants = [
...Array.from(screenShares.keys()),
...sharedVideos,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
if (getSourceNameSignalingFeatureFlag(state)) {
// Always update the order of the thumnails.
const participantsWithScreenShare = screenShareParticipants.reduce((acc, screenshare) => {
const ownerId = getFakeScreenShareParticipantOwnerId(screenshare);
acc.push(ownerId);
acc.push(screenshare);
return acc;
}, []);
reorderedParticipants = [
...participantsWithScreenShare,
...sharedVideos,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
} else {
// Always update the order of the thumnails.
reorderedParticipants = [
...Array.from(screenShares.keys()),
...sharedVideos,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
}
store.dispatch(setRemoteParticipants(reorderedParticipants));
}

View File

@ -102,3 +102,12 @@ export function isReorderingEnabled(state) {
return enableThumbnailReordering;
}
/**
* Whether the stage filmstrip is disabled or not.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isStageFilmstripEnabled() {
return false;
}

View File

@ -18,6 +18,7 @@ import {
isRemoteTrackMuted
} from '../base/tracks/functions';
import { isTrackStreamingStatusActive, isParticipantConnectionStatusActive } from '../connection-indicator/functions';
import { isSharingStatus } from '../shared-video/functions';
import {
getCurrentLayout,
getNotResponsiveTileViewGridDimensions,
@ -228,9 +229,11 @@ export function getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnl
export function getNumberOfPartipantsForTileView(state) {
const { iAmRecorder } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const { localScreenShare } = state['features/base/participants'];
const localParticipantsCount = getSourceNameSignalingFeatureFlag(state) && localScreenShare ? 2 : 1;
const numberOfParticipants = getParticipantCountWithFake(state)
- (iAmRecorder ? 1 : 0)
- (disableSelfView ? 1 : 0);
- (disableSelfView ? localParticipantsCount : 0);
return numberOfParticipants;
}
@ -240,16 +243,18 @@ export function getNumberOfPartipantsForTileView(state) {
* disabled.
*
* @param {Object} state - The redux store state.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @returns {Object} - The dimensions.
*/
export function calculateNotResponsiveTileViewDimensions(state) {
export function calculateNonResponsiveTileViewDimensions(state, stageFilmstrip = false) {
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { disableTileEnlargement } = state['features/base/config'];
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state);
const { columns: c, minVisibleRows, rows: r } = getNotResponsiveTileViewGridDimensions(state, stageFilmstrip);
const filmstripWidth = getVerticalViewMaxWidth(state);
const size = calculateThumbnailSizeForTileView({
columns: c,
minVisibleRows,
clientWidth,
clientWidth: clientWidth - (stageFilmstrip ? filmstripWidth : 0),
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: true
@ -289,7 +294,7 @@ export function calculateResponsiveTileViewDimensions({
clientWidth,
clientHeight,
disableTileEnlargement = false,
isVerticalFilmstrip = false,
noHorizontalContainerMargin = false,
maxColumns,
numberOfParticipants,
numberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
@ -320,7 +325,7 @@ export function calculateResponsiveTileViewDimensions({
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: false,
isVerticalFilmstrip
noHorizontalContainerMargin
});
if (size) {
@ -389,12 +394,12 @@ export function calculateThumbnailSizeForTileView({
clientHeight,
disableResponsiveTiles = false,
disableTileEnlargement = false,
isVerticalFilmstrip = false
noHorizontalContainerMargin = false
}: Object) {
const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
const minHeight = getThumbnailMinHeight(clientWidth);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (isVerticalFilmstrip ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
const initialWidth = viewWidth / columns;
let initialHeight = viewHeight / minVisibleRows;
@ -486,8 +491,10 @@ export function getVerticalFilmstripVisibleAreaWidth() {
*/
export function computeDisplayModeFromInput(input: Object) {
const {
isActiveParticipant,
isAudioOnly,
isCurrentlyOnLargeVideo,
isFakeScreenShareParticipant,
isScreenSharing,
canPlayEventReceived,
isRemoteParticipant,
@ -495,7 +502,11 @@ export function computeDisplayModeFromInput(input: Object) {
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
if (isFakeScreenShareParticipant) {
return DISPLAY_VIDEO;
}
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant) || isActiveParticipant)) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
@ -519,8 +530,10 @@ export function computeDisplayModeFromInput(input: Object) {
export function getDisplayModeInput(props: Object, state: Object) {
const {
_currentLayout,
_isActiveParticipant,
_isAudioOnly,
_isCurrentlyOnLargeVideo,
_isFakeScreenShareParticipant,
_isScreenSharing,
_isVideoPlayable,
_participant,
@ -530,6 +543,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
const { canPlayEventReceived } = state;
return {
isActiveParticipant: _isActiveParticipant,
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isAudioOnly: _isAudioOnly,
tileViewActive,
@ -539,6 +553,7 @@ export function getDisplayModeInput(props: Object, state: Object) {
videoStream: Boolean(_videoTrack),
isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
isScreenSharing: _isScreenSharing,
isFakeScreenShareParticipant: _isFakeScreenShareParticipant,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
@ -613,7 +628,7 @@ export function isReorderingEnabled(state) {
const { testing = {} } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
return enableThumbnailReordering && isFilmstripScollVisible(state);
return enableThumbnailReordering && isFilmstripScrollVisible(state);
}
/**
@ -622,7 +637,7 @@ export function isReorderingEnabled(state) {
* @param {Object} state - The redux state.
* @returns {boolean} - True if the scroll is displayed and false otherwise.
*/
export function isFilmstripScollVisible(state) {
export function isFilmstripScrollVisible(state) {
const _currentLayout = getCurrentLayout(state);
let hasScroll = false;
@ -642,3 +657,43 @@ export function isFilmstripScollVisible(state) {
return hasScroll;
}
/**
* Gets the ids of the active participants.
*
* @param {Object} state - Redux state.
* @returns {Array<string>}
*/
export function getActiveParticipantsIds(state) {
const { activeParticipants } = state['features/filmstrip'];
return activeParticipants.map(p => p.participantId);
}
/**
* Get whether or not the stage filmstrip should be displayed.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function shouldDisplayStageFilmstrip(state) {
const { activeParticipants } = state['features/filmstrip'];
const { remoteScreenShares } = state['features/video-layout'];
const currentLayout = getCurrentLayout(state);
const sharedVideo = isSharingStatus(state['features/shared-video']?.status);
return isStageFilmstripEnabled(state) && remoteScreenShares.length === 0 && !sharedVideo
&& activeParticipants.length > 1 && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
}
/**
* Whether the stage filmstrip is disabled or not.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isStageFilmstripEnabled(state) {
const { filmstrip } = state['features/base/config'];
return !filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP;
}

View File

@ -1,26 +1,52 @@
// @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import {
DOMINANT_SPEAKER_CHANGED,
getDominantSpeakerParticipant,
getLocalParticipant,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { CLIENT_RESIZED } from '../base/responsive-ui';
import { SETTINGS_UPDATED } from '../base/settings';
import {
getCurrentLayout,
LAYOUTS
LAYOUTS,
setTileView
} from '../video-layout';
import { SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
import { ADD_STAGE_PARTICIPANT, REMOVE_STAGE_PARTICIPANT, SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
import {
addStageParticipant,
removeStageParticipant,
setFilmstripWidth,
setHorizontalViewDimensions,
setStageParticipants,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions';
import { DEFAULT_FILMSTRIP_WIDTH, MIN_STAGE_VIEW_WIDTH } from './constants';
import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
import { isFilmstripResizable } from './functions.web';
import {
ACTIVE_PARTICIPANT_TIMEOUT,
DEFAULT_FILMSTRIP_WIDTH,
MAX_ACTIVE_PARTICIPANTS,
MIN_STAGE_VIEW_WIDTH
} from './constants';
import {
isFilmstripResizable,
updateRemoteParticipants,
updateRemoteParticipantsOnLeave
} from './functions';
import './subscriber';
import { getActiveParticipantsIds, isStageFilmstripEnabled } from './functions.web';
/**
* Map of timers.
*
* @type {Map}
*/
const timers = new Map();
/**
* The middleware of the feature Filmstrip.
@ -35,7 +61,7 @@ MiddlewareRegistry.register(store => next => action => {
updateRemoteParticipantsOnLeave(store, action.participant?.id);
}
const result = next(action);
let result;
switch (action.type) {
case CLIENT_RESIZED: {
@ -74,6 +100,10 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PARTICIPANT_JOINED: {
if (action.participant?.isLocalScreenShare) {
break;
}
result = next(action);
updateRemoteParticipants(store, action.participant?.id);
break;
}
@ -82,12 +112,114 @@ MiddlewareRegistry.register(store => next => action => {
// TODO: This needs to be removed once the large video is Reactified.
VideoLayout.onLocalFlipXChanged();
}
if (action.settings?.disableSelfView) {
const state = store.getState();
const local = getLocalParticipant(state);
const activeParticipantsIds = getActiveParticipantsIds(state);
if (activeParticipantsIds.find(id => id === local.id)) {
store.dispatch(removeStageParticipant(local.id));
}
}
break;
}
case SET_USER_FILMSTRIP_WIDTH: {
VideoLayout.refreshLayout();
break;
}
case ADD_STAGE_PARTICIPANT: {
const { dispatch, getState } = store;
const { participantId, pinned } = action;
const state = getState();
const { activeParticipants } = state['features/filmstrip'];
let queue;
if (activeParticipants.find(p => p.participantId === participantId)) {
queue = activeParticipants.filter(p => p.participantId !== participantId);
queue.push({
participantId,
pinned
});
const tid = timers.get(participantId);
clearTimeout(tid);
} else if (activeParticipants.length < MAX_ACTIVE_PARTICIPANTS) {
queue = [ ...activeParticipants, {
participantId,
pinned
} ];
} else {
const notPinnedIndex = activeParticipants.findIndex(p => !p.pinned);
if (notPinnedIndex === -1) {
if (pinned) {
queue = [ ...activeParticipants, {
participantId,
pinned
} ];
queue.shift();
}
} else {
queue = [ ...activeParticipants, {
participantId,
pinned
} ];
queue.splice(notPinnedIndex, 1);
}
}
dispatch(setStageParticipants(queue));
if (!pinned) {
const timeoutId = setTimeout(() => dispatch(removeStageParticipant(participantId)),
ACTIVE_PARTICIPANT_TIMEOUT);
timers.set(participantId, timeoutId);
}
if (getCurrentLayout(state) === LAYOUTS.TILE_VIEW) {
dispatch(setTileView(false));
}
break;
}
case REMOVE_STAGE_PARTICIPANT: {
const state = store.getState();
const { participantId } = action;
const tid = timers.get(participantId);
clearTimeout(tid);
timers.delete(participantId);
const dominant = getDominantSpeakerParticipant(state);
if (participantId === dominant?.id) {
const timeoutId = setTimeout(() => store.dispatch(removeStageParticipant(participantId)),
ACTIVE_PARTICIPANT_TIMEOUT);
timers.set(participantId, timeoutId);
return;
}
break;
}
case DOMINANT_SPEAKER_CHANGED: {
const { id } = action.participant;
const state = store.getState();
const stageFilmstrip = isStageFilmstripEnabled(state);
const currentLayout = getCurrentLayout(state);
if (stageFilmstrip && currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
store.dispatch(addStageParticipant(id));
}
break;
}
case PARTICIPANT_LEFT: {
const { id } = action.participant;
const activeParticipantsIds = getActiveParticipantsIds(store.getState());
if (activeParticipantsIds.find(pId => pId === id)) {
store.dispatch(removeStageParticipant(id));
}
break;
}
}
return result;
return result ?? next(action);
});

View File

@ -4,11 +4,14 @@ import { PARTICIPANT_LEFT } from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import {
REMOVE_STAGE_PARTICIPANT,
SET_STAGE_PARTICIPANTS,
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE,
SET_FILMSTRIP_WIDTH,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_REMOTE_PARTICIPANTS,
SET_STAGE_FILMSTRIP_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_USER_FILMSTRIP_WIDTH,
SET_USER_IS_RESIZING,
@ -18,6 +21,12 @@ import {
} from './actionTypes';
const DEFAULT_STATE = {
/**
* The list of participants to be displayed on the stage filmstrip.
*/
activeParticipants: [],
/**
* The indicator which determines whether the {@link Filmstrip} is enabled.
*
@ -57,6 +66,14 @@ const DEFAULT_STATE = {
*/
remoteParticipants: [],
/**
* The stage filmstrip view dimensions.
*
* @public
* @type {Object}
*/
stageFilmstripDimensions: {},
/**
* The tile view dimensions.
*
@ -223,6 +240,24 @@ ReducerRegistry.register(
isResizing: action.resizing
};
}
case SET_STAGE_FILMSTRIP_DIMENSIONS: {
return {
...state,
stageFilmstripDimensions: action.dimensions
};
}
case SET_STAGE_PARTICIPANTS: {
return {
...state,
activeParticipants: action.queue
};
}
case REMOVE_STAGE_PARTICIPANT: {
return {
...state,
activeParticipants: state.activeParticipants.filter(p => p.participantId !== action.participantId)
};
}
}
return state;

View File

@ -6,12 +6,14 @@ import { StateListenerRegistry } from '../base/redux';
import { clientResized } from '../base/responsive-ui';
import { shouldHideSelfView } from '../base/settings';
import { setFilmstripVisible } from '../filmstrip/actions';
import { selectParticipantInLargeVideo } from '../large-video/actions.any';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import {
setHorizontalViewDimensions,
setStageFilmstripViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
} from './actions';
@ -19,7 +21,13 @@ import {
ASPECT_RATIO_BREAKPOINT,
DISPLAY_DRAWER_THRESHOLD
} from './constants';
import { isFilmstripResizable, isFilmstripScollVisible, updateRemoteParticipants } from './functions';
import {
isFilmstripResizable,
isFilmstripScrollVisible,
shouldDisplayStageFilmstrip,
updateRemoteParticipants
} from './functions';
import './subscriber.any';
@ -30,7 +38,8 @@ StateListenerRegistry.register(
/* selector */ state => {
return {
numberOfParticipants: getParticipantCountWithFake(state),
disableSelfView: shouldHideSelfView(state)
disableSelfView: shouldHideSelfView(state),
localScreenShare: state['features/base/participants'].localScreenShare
};
},
/* listener */ (currentState, store) => {
@ -145,5 +154,39 @@ StateListenerRegistry.register(
* Listens for changes in the filmstrip scroll visibility.
*/
StateListenerRegistry.register(
/* selector */ state => isFilmstripScollVisible(state),
/* selector */ state => isFilmstripScrollVisible(state),
/* listener */ (_, store) => updateRemoteParticipants(store));
/**
* Listens for changes to determine the size of the stage filmstrip tiles.
*/
StateListenerRegistry.register(
/* selector */ state => {
return {
remoteScreenShares: state['features/video-layout'].remoteScreenShares.length,
length: state['features/filmstrip'].activeParticipants.length,
width: state['features/filmstrip'].width?.current,
visible: state['features/filmstrip'].visible,
clientWidth: state['features/base/responsive-ui'].clientWidth,
tileView: state['features/video-layout'].tileViewEnabled
};
},
/* listener */(_, store) => {
if (shouldDisplayStageFilmstrip(store.getState())) {
store.dispatch(setStageFilmstripViewDimensions());
}
}, {
deepEquals: true
});
/**
* Listens for changes in the active participants count determine the stage participant (when
* there's just one).
*/
StateListenerRegistry.register(
/* selector */ state => state['features/filmstrip'].activeParticipants.length,
/* listener */(length, store) => {
if (length <= 1) {
store.dispatch(selectParticipantInLargeVideo());
}
});

View File

@ -102,7 +102,14 @@ function _electLastVisibleRemoteVideo(tracks) {
* @returns {(string|undefined)}
*/
function _electParticipantInLargeVideo(state) {
// 1. If a participant is pinned, they will be shown in the LargeVideo
// 1. If there's a remote screenshare, pick the most recent one that was added to the conference.
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
if (remoteScreenShares?.length) {
return remoteScreenShares[remoteScreenShares.length - 1];
}
// 2. Next, if a participant is pinned, they will be shown in the LargeVideo
// (regardless of whether they are local or remote).
let participant = getPinnedParticipant(state);
@ -110,13 +117,6 @@ function _electParticipantInLargeVideo(state) {
return participant.id;
}
// 2. Next, pick the most recent remote screenshare that was added to the conference.
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
if (remoteScreenShares?.length) {
return remoteScreenShares[remoteScreenShares.length - 1];
}
// 3. Next, pick the dominant speaker (other than self).
participant = getDominantSpeakerParticipant(state);
if (participant && !participant.local) {

View File

@ -6,7 +6,7 @@ import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { setColorAlpha } from '../../base/util';
import { DominantSpeakerName } from '../../display-name';
import { StageParticipantNameLabel } from '../../display-name';
import { FILMSTRIP_BREAKPOINT, isFilmstripResizable } from '../../filmstrip';
import { getVerticalViewMaxWidth } from '../../filmstrip/functions.web';
import { SharedVideo } from '../../shared-video/components/web';
@ -175,7 +175,7 @@ class LargeVideo extends Component<Props> {
</div>
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|| <Captions /> }
{_showDominantSpeakerBadge && <DominantSpeakerName />}
{_showDominantSpeakerBadge && <StageParticipantNameLabel />}
</div>
);
}

View File

@ -1,9 +1,9 @@
// @flow
import { type Dispatch } from 'redux';
import { batch } from 'react-redux';
import { appNavigate } from '../app/actions';
import { hideLobbyScreen, setKnockingState } from './actions.any';
export * from './actions.any';
/**
@ -12,8 +12,11 @@ export * from './actions.any';
* @returns {Function}
*/
export function cancelKnocking() {
return async (dispatch: Dispatch<any>) => {
dispatch(appNavigate(undefined));
return dispatch => {
batch(() => {
dispatch(setKnockingState(false));
dispatch(hideLobbyScreen());
dispatch(appNavigate(undefined));
});
};
}

View File

@ -58,7 +58,7 @@ export function showLobbyChatButton(
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
const conference = getCurrentConference(state);
const lobbyLocalId = conference.myLobbyUserId();
const lobbyLocalId = conference?.myLobbyUserId();
if (!enableLobbyChat) {
return false;

View File

@ -1,66 +0,0 @@
// @flow
/**
* An registry that dispatches hardware back button events for subscribers with a custom logic.
*/
class BackButtonRegistry {
_listeners: Array<Function>;
/**
* Instantiates a new instance of the registry.
*/
constructor() {
this._listeners = [];
}
/**
* Adds a listener to the registry.
*
* NOTE: Due to the different order of component mounts, we allow a component to register
* its listener to the top of the list, so then that will be invoked before other, 'non-top'
* listeners. For example a 'non-top' listener can be the one that puts the app into PiP mode,
* while a 'top' listener is the one that closes a modal in a conference.
*
* @param {Function} listener - The listener function.
* @param {boolean?} top - If true, the listener will be put on the top (eg for modal-like components).
* @returns {void}
*/
addListener(listener: Function, top: boolean = false) {
if (top) {
this._listeners.splice(0, 0, listener);
} else {
this._listeners.push(listener);
}
}
/**
* Removes a listener from the registry.
*
* @param {Function} listener - The listener to remove.
* @returns {void}
*/
removeListener(listener: Function) {
this._listeners = this._listeners.filter(f => f !== listener);
}
onHardwareBackPress: () => boolean;
/**
* Callback for the back button press event.
*
* @returns {boolean}
*/
onHardwareBackPress() {
for (const listener of this._listeners) {
const result = listener();
if (result === true) {
return true;
}
}
return false;
}
}
export default new BackButtonRegistry();

View File

@ -1,3 +0,0 @@
// @flow
export { default as BackButtonRegistry } from './BackButtonRegistry';

View File

@ -1,36 +0,0 @@
// @flow
import { BackHandler } from 'react-native';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import BackButtonRegistry from './BackButtonRegistry';
// Binding function to the proper instance, so then the event emitter won't replace the
// underlying instance.
BackButtonRegistry.onHardwareBackPress = BackButtonRegistry.onHardwareBackPress.bind(BackButtonRegistry);
/**
* Middleware that captures App lifetime actions and subscribes to application
* state changes. When the application state changes it will fire the action
* required to mute or unmute the local video in case the application goes to
* the background or comes back from it.
*
* @param {Store} store - The redux store.
* @returns {Function}
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
*/
MiddlewareRegistry.register(() => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
BackHandler.addEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
break;
case APP_WILL_UNMOUNT:
BackHandler.removeEventListener('hardwareBackPress', BackButtonRegistry.onHardwareBackPress);
break;
}
return next(action);
});

View File

@ -3,6 +3,7 @@
import { FlagGroupContext } from '@atlaskit/flag/flag-group';
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { Component } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
@ -67,10 +68,6 @@ const useStyles = theme => {
maxWidth: 'calc(100% - 32px)'
},
containerChatOpen: {
left: '331px'
},
transitionGroup: {
'& > *': {
marginBottom: '20px',
@ -166,12 +163,9 @@ class NotificationsContainer extends Component<Props> {
<AtlasKitThemeProvider mode = 'light'>
<FlagGroupContext.Provider value = { this._api }>
<div
className = { `${this.props.classes.container} ${this.props.portal
? this.props.classes.containerPortal
: this.props._isChatOpen
? this.props.classes.containerChatOpen
: ''}`
}
className = { clsx(this.props.classes.container, {
[this.props.classes.containerPortal]: this.props.portal
}) }
id = 'notifications-container'>
<TransitionGroup className = { this.props.classes.transitionGroup }>
{this._renderFlags()}

View File

@ -71,8 +71,8 @@ export default {
},
transparentButton: {
...baseButton,
backgroundColor: 'transparent'
backgroundColor: 'transparent',
marginTop: BaseTheme.spacing[3]
},
leaveButtonLabel: {

View File

@ -207,7 +207,7 @@ class MeetingParticipantList extends PureComponent<Props> {
// If there are only meeting participants available,
// we take the full container height
const onlyMeetingParticipants
= breakoutRooms.length === 0 && lobbyParticipants.length === 0;
= breakoutRooms?.length === 0 && lobbyParticipants.length === 0;
const containerStyle
= onlyMeetingParticipants
? styles.meetingListFullContainer : styles.meetingListContainer;

View File

@ -12,7 +12,7 @@ import { connect } from '../../../base/redux';
import { getBreakoutRoomsConfig } from '../../../breakout-rooms/functions';
import { MuteEveryoneDialog } from '../../../video-menu/components/';
import { close } from '../../actions';
import { classList, findAncestorByClass, getParticipantsPaneOpen } from '../../functions';
import { findAncestorByClass, getParticipantsPaneOpen } from '../../functions';
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
import { RoomList } from '../breakout-rooms/components/web/RoomList';
@ -214,7 +214,7 @@ class ParticipantsPane extends Component<Props, State> {
}
return (
<div className = { classList('participants_pane', !_paneOpen && 'participants_pane--closed') }>
<div className = 'participants_pane'>
<div className = 'participants_pane-content'>
<div className = { classes.header }>
<div

View File

@ -22,14 +22,6 @@ import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { QUICK_ACTION_BUTTON, REDUCER_KEY, MEDIA_STATE } from './constants';
/**
* Generates a class attribute value.
*
* @param {Iterable<string>} args - String iterable.
* @returns {string} Class attribute value.
*/
export const classList = (...args: Array<string | boolean>) => args.filter(Boolean).join(' ');
/**
* Find the first styled ancestor component of an element.
*

View File

@ -1,5 +1,6 @@
// @flow
import { withStyles } from '@material-ui/styles';
import React, { Component } from 'react';
import { getAvailableDevices } from '../../../base/devices';
@ -39,6 +40,11 @@ declare var interfaceConfig: Object;
*/
type Props = {
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* Which settings tab should be initially displayed. If not defined then
* the first tab will be displayed.
@ -56,6 +62,116 @@ type Props = {
dispatch: Function
};
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = theme => {
return {
settingsDialog: {
display: 'flex',
width: '100%',
'&.profile-pane': {
flexDirection: 'column'
},
'& .auth-name': {
marginBottom: `${theme.spacing(1)}px`
},
'& .calendar-tab, & .device-selection': {
marginTop: '20px'
},
'& .mock-atlaskit-label': {
color: '#b8c7e0',
fontSize: '12px',
fontWeight: 600,
lineHeight: 1.33,
padding: `20px 0px ${theme.spacing(1)}px 0px`
},
'& input[type="checkbox"]:checked + svg': {
'--checkbox-background-color': '#6492e7',
'--checkbox-border-color': '#6492e7'
},
'& input[type="checkbox"] + svg + span': {
color: '#9FB0CC'
},
[[ '& .calendar-tab',
'& .more-tab',
'& .box' ]]: {
display: 'flex',
justifyContent: 'space-between',
width: '100%'
},
'& .profile-edit': {
display: 'flex',
width: '100%'
},
'& .profile-edit-field': {
flex: 0.5,
marginRight: '20px'
},
'& .settings-sub-pane': {
flex: 1
},
'& .settings-sub-pane .right': {
flex: 1
},
'& .settings-sub-pane .left': {
flex: 1
},
'& .settings-sub-pane-element': {
textAlign: 'left',
flex: 1
},
'& .moderator-settings-wrapper': {
paddingTop: '20px'
},
'& .calendar-tab': {
alignItems: 'center',
flexDirection: 'column',
fontSize: '14px',
minHeight: '100px',
textAlign: 'center'
},
'& .calendar-tab-sign-in': {
marginTop: '20px'
},
'& .sign-out-cta': {
marginBottom: '20px'
},
'@media only screen and (max-width: 700px)': {
'& .device-selection': {
display: 'flex',
flexDirection: 'column'
},
'& .more-tab': {
flexDirection: 'column'
}
}
}
};
};
/**
* A React {@code Component} for displaying a dialog to modify local settings
* and conference-wide (moderator) settings. This version is connected to
@ -130,12 +246,14 @@ class SettingsDialog extends Component<Props> {
* {@code ConnectedSettingsDialog} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The props passed to the component.
* @private
* @returns {{
* tabs: Array<Object>
* }}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state, ownProps) {
const { classes } = ownProps;
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
// The settings sections to display.
@ -173,7 +291,7 @@ function _mapStateToProps(state) {
selectedVideoInputId: tabState.selectedVideoInputId
};
},
styles: 'settings-pane devices-pane',
styles: `settings-pane ${classes.settingsDialog} devices-pane`,
submit: submitDeviceSelectionTab
});
}
@ -184,7 +302,7 @@ function _mapStateToProps(state) {
component: ProfileTab,
label: 'profile.title',
props: getProfileTabProps(state),
styles: 'settings-pane profile-pane',
styles: `settings-pane ${classes.settingsDialog} profile-pane`,
submit: submitProfileTab
});
}
@ -206,7 +324,7 @@ function _mapStateToProps(state) {
startReactionsMuted: tabState?.startReactionsMuted
};
},
styles: 'settings-pane moderator-pane',
styles: `settings-pane ${classes.settingsDialog} moderator-pane`,
submit: submitModeratorTab
});
}
@ -215,8 +333,8 @@ function _mapStateToProps(state) {
tabs.push({
name: SETTINGS_TABS.CALENDAR,
component: CalendarTab,
label: 'settings.calendar.title',
styles: 'settings-pane calendar-pane'
label: 'settings-pane settings.calendar.title',
styles: `${classes.settingsDialog} calendar-pane`
});
}
@ -226,7 +344,7 @@ function _mapStateToProps(state) {
component: SoundsTab,
label: 'settings.sounds',
props: getSoundsTabProps(state),
styles: 'settings-pane profile-pane',
styles: `settings-pane ${classes.settingsDialog} profile-pane`,
submit: submitSoundsTab
});
}
@ -249,7 +367,7 @@ function _mapStateToProps(state) {
enabledNotifications: tabState?.enabledNotifications
};
},
styles: 'settings-pane more-pane',
styles: `settings-pane ${classes.settingsDialog} more-pane`,
submit: submitMoreTab
});
}
@ -257,4 +375,4 @@ function _mapStateToProps(state) {
return { _tabs: tabs };
}
export default connect(_mapStateToProps)(SettingsDialog);
export default withStyles(styles)(connect(_mapStateToProps)(SettingsDialog));

View File

@ -10,6 +10,16 @@
export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
= 'SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
/**
* The type of the action which sets the list of known remote fake screen share participant IDs.
*
* @returns {{
* type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
* participantIds: Array<string>
* }}
*/
export const FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED = 'FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
/**
* The type of the action which enables or disables the feature for showing
* video thumbnails in a two-axis tile view.

View File

@ -3,6 +3,7 @@
import type { Dispatch } from 'redux';
import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from './actionTypes';
@ -26,6 +27,22 @@ export function setRemoteParticipantsWithScreenShare(participantIds: Array<strin
};
}
/**
* Creates a (redux) action which signals that the list of known remote fake screen share participant ids has changed.
*
* @param {string} participantIds - The remote fake screen share participants.
* @returns {{
* type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
* participantIds: Array<string>
* }}
*/
export function fakeScreenshareParticipantsUpdated(participantIds: Array<string>) {
return {
type: FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
participantIds
};
}
/**
* Creates a (redux) action which signals to set the UI layout to be tiled view
* or not.

View File

@ -73,13 +73,14 @@ export function getMaxColumnCount() {
* which rows will be added but no more columns.
*
* @param {Object} state - The redux store state.
* @param {number} width - Custom width to use for calculation.
* @param {boolean} stageFilmstrip - Whether the dimensions should be calculated for the stage filmstrip.
* @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 getNotResponsiveTileViewGridDimensions(state: Object) {
export function getNotResponsiveTileViewGridDimensions(state: Object, stageFilmstrip: boolean = false) {
const maxColumns = getMaxColumnCount(state);
const numberOfParticipants = getNumberOfPartipantsForTileView(state);
const { activeParticipants } = state['features/filmstrip'];
const numberOfParticipants = stageFilmstrip ? activeParticipants.length : getNumberOfPartipantsForTileView(state);
const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(numberOfParticipants / columns);
@ -240,3 +241,15 @@ export function getVideoQualityForLargeVideo() {
return getVideoQualityForHeight(wrapper.clientHeight);
}
/**
* Gets the video quality level for the thumbnails in the stage filmstrip.
*
* @param {Object} state - Redux state.
* @returns {number}
*/
export function getVideoQualityForStageThumbnails(state) {
const height = state['features/filmstrip'].stageFilmstripDimensions?.thumbnailSize?.height;
return getVideoQualityForHeight(height);
}

View File

@ -12,6 +12,7 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { TRACK_REMOVED } from '../base/tracks';
import { SET_DOCUMENT_EDITING_STATUS } from '../etherpad';
import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me';
import { SET_TILE_VIEW } from './actionTypes';
@ -68,11 +69,15 @@ MiddlewareRegistry.register(store => next => action => {
break;
// Things to update when tile view state changes
case SET_TILE_VIEW:
if (action.enabled && getPinnedParticipant(store)) {
case SET_TILE_VIEW: {
const state = store.getState();
const stageFilmstrip = isStageFilmstripEnabled(state);
if (action.enabled && !stageFilmstrip && getPinnedParticipant(state)) {
store.dispatch(pinParticipant(null));
}
break;
}
// Update the remoteScreenShares.
// Because of the debounce in the subscriber which updates the remoteScreenShares we need to handle

View File

@ -3,6 +3,7 @@
import { ReducerRegistry } from '../base/redux';
import {
FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_TILE_VIEW
} from './actionTypes';
@ -27,6 +28,7 @@ const STORE_NAME = 'features/video-layout';
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case FAKE_SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
return {
...state,

View File

@ -2,12 +2,45 @@
import debounce from 'lodash/debounce';
import { getSourceNameSignalingFeatureFlag } from '../base/config';
import { StateListenerRegistry, equals } from '../base/redux';
import { isFollowMeActive } from '../follow-me';
import { setRemoteParticipantsWithScreenShare } from './actions';
import { setRemoteParticipantsWithScreenShare, fakeScreenshareParticipantsUpdated } from './actions';
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].sortedRemoteFakeScreenShareParticipants,
/* listener */ (sortedRemoteFakeScreenShareParticipants, store) => {
if (!getAutoPinSetting() || isFollowMeActive(store) || !getSourceNameSignalingFeatureFlag(store.getState())) {
return;
}
const oldScreenSharesOrder = store.getState()['features/video-layout'].remoteScreenShares || [];
const knownSharingParticipantIds = [ ...sortedRemoteFakeScreenShareParticipants.keys() ];
// Filter out any participants which are no longer screen sharing
// by looping through the known sharing participants and removing any
// participant IDs which are no longer sharing.
const newScreenSharesOrder = oldScreenSharesOrder.filter(
participantId => knownSharingParticipantIds.includes(participantId));
// Make sure all new sharing participant get added to the end of the
// known screen shares.
knownSharingParticipantIds.forEach(participantId => {
if (!newScreenSharesOrder.includes(participantId)) {
newScreenSharesOrder.push(participantId);
}
});
if (!equals(oldScreenSharesOrder, newScreenSharesOrder)) {
store.dispatch(fakeScreenshareParticipantsUpdated(newScreenSharesOrder));
updateAutoPinnedParticipant(oldScreenSharesOrder, store);
}
});
/**
* For auto-pin mode, listen for changes to the known media tracks and look
* for updates to screen shares. The listener is debounced to avoid state
@ -20,7 +53,7 @@ StateListenerRegistry.register(
// possible to have screen sharing participant that has already left in the remoteScreenShares array.
// This can lead to rendering a thumbnails for already left participants since the remoteScreenShares
// array is used for building the ordered list of remote participants.
if (!getAutoPinSetting() || isFollowMeActive(store)) {
if (!getAutoPinSetting() || isFollowMeActive(store) || getSourceNameSignalingFeatureFlag(store.getState())) {
return;
}

View File

@ -18,12 +18,14 @@ import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actio
import { getHideSelfView } from '../../../base/settings';
import { getLocalVideoTrack } from '../../../base/tracks';
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
import { LAYOUTS } from '../../../video-layout';
import { renderConnectionStatus } from '../../actions.web';
import ConnectionStatusButton from './ConnectionStatusButton';
import FlipLocalVideoButton from './FlipLocalVideoButton';
import HideSelfViewVideoButton from './HideSelfViewVideoButton';
import TogglePinToStageButton from './TogglePinToStageButton';
/**
* The type of the React {@code Component} props of
@ -93,6 +95,11 @@ type Props = {
*/
_showLocalVideoFlipButton: boolean,
/**
* Whether to render the pin to stage button.
*/
_showPinToStage: boolean,
/**
* Invoked to obtain translated strings.
*/
@ -158,6 +165,7 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
_showConnectionInfo,
_showHideSelfViewButton,
_showLocalVideoFlipButton,
_showPinToStage,
buttonVisible,
classes,
hidePopover,
@ -183,8 +191,15 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
className = { _overflowDrawer ? classes.flipText : '' }
onClick = { hidePopover } />
}
{
_showPinToStage && <TogglePinToStageButton
className = { _overflowDrawer ? classes.flipText : '' }
noIcon = { true }
onClick = { hidePopover }
participantID = { _localParticipantId } />
}
{ isMobileBrowser()
&& <ConnectionStatusButton participantId = { _localParticipantId } />
&& <ConnectionStatusButton participantId = { _localParticipantId } />
}
</ContextMenuItemGroup>
</ContextMenu>
@ -254,11 +269,12 @@ class LocalVideoMenuTriggerButton extends Component<Props> {
* Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const currentLayout = getCurrentLayout(state);
function _mapStateToProps(state, ownProps) {
const { currentLayout } = ownProps;
const localParticipant = getLocalParticipant(state);
const { disableLocalVideoFlip, disableSelfViewSettings } = state['features/base/config'];
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
@ -288,7 +304,8 @@ function _mapStateToProps(state) {
_showHideSelfViewButton: showHideSelfViewButton,
_overflowDrawer: overflowDrawer,
_localParticipantId: localParticipant.id,
_showConnectionInfo: showConnectionInfo
_showConnectionInfo: showConnectionInfo,
_showPinToStage: isStageFilmstripEnabled(state)
};
}

View File

@ -16,6 +16,7 @@ import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participant
import { isParticipantAudioMuted } from '../../../base/tracks';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { setVolume } from '../../../filmstrip/actions.web';
import { isStageFilmstripEnabled } from '../../../filmstrip/functions.web';
import { isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { stopSharedVideo } from '../../../shared-video/actions.any';
@ -35,6 +36,7 @@ import {
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
TogglePinToStageButton,
VolumeSlider
} from './';
@ -144,6 +146,7 @@ const ParticipantContextMenu = ({
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerationSupported = useSelector(isAvModerationSupported());
const stageFilmstrip = useSelector(isStageFilmstripEnabled);
const _currentRoomId = useSelector(getCurrentRoomId);
const _rooms = Object.values(useSelector(getBreakoutRooms));
@ -231,6 +234,12 @@ const ParticipantContextMenu = ({
}
}
if (stageFilmstrip) {
buttons2.push(<TogglePinToStageButton
key = 'pinToStage'
participantID = { _getCurrentParticipantId() } />);
}
if (!disablePrivateChat) {
buttons2.push(<PrivateMessageMenuButton
key = 'privateMessage'

View File

@ -14,7 +14,7 @@ import { getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { LAYOUTS } from '../../../video-layout';
import { renderConnectionStatus } from '../../actions.web';
import ParticipantContextMenu from './ParticipantContextMenu';
@ -265,7 +265,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const { participantID, currentLayout } = ownProps;
let _remoteControlState = null;
const participant = getParticipantById(state, participantID);
const _participantDisplayName = participant?.name;
@ -289,7 +289,6 @@ function _mapStateToProps(state, ownProps) {
}
}
const currentLayout = getCurrentLayout(state);
let _menuPosition;
switch (currentLayout) {

View File

@ -0,0 +1,62 @@
// @flow
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import ContextMenuItem from '../../../base/components/context-menu/ContextMenuItem';
import { IconPinParticipant, IconUnpin } from '../../../base/icons';
import { addStageParticipant, removeStageParticipant } from '../../../filmstrip/actions.web';
import { getActiveParticipantsIds } from '../../../filmstrip/functions';
type Props = {
/**
* Button text class name.
*/
className: string,
/**
* Whether the icon should be hidden or not.
*/
noIcon: boolean,
/**
* Click handler executed aside from the main action.
*/
onClick?: Function,
/**
* The ID for the participant on which the button will act.
*/
participantID: string
}
const TogglePinToStageButton = ({ className, noIcon = false, onClick, participantID }: Props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const isActive = Boolean(useSelector(getActiveParticipantsIds).find(p => p === participantID));
const _onClick = useCallback(() => {
dispatch(isActive
? removeStageParticipant(participantID)
: addStageParticipant(participantID, true));
onClick && onClick();
}, [ participantID, isActive ]);
const text = isActive
? t('videothumbnail.unpinFromStage')
: t('videothumbnail.pinToStage');
const icon = isActive ? IconUnpin : IconPinParticipant;
return (
<ContextMenuItem
accessibilityLabel = { text }
icon = { noIcon ? null : icon }
onClick = { _onClick }
text = { text }
textClassName = { className } />
);
};
export default TogglePinToStageButton;

View File

@ -13,6 +13,7 @@ export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog'
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as TogglePinToStageButton } from './TogglePinToStageButton';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';

View File

@ -9,9 +9,11 @@ import { getLocalParticipant, getParticipantCount } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks';
import { reportError } from '../base/util';
import { getActiveParticipantsIds } from '../filmstrip/functions.web';
import {
getVideoQualityForLargeVideo,
getVideoQualityForResizableFilmstripThumbnails,
getVideoQualityForStageThumbnails,
shouldDisplayTileView
} from '../video-layout';
@ -92,6 +94,17 @@ StateListenerRegistry.register(
}
);
/**
* Updates the receiver constraints when the stage participants change.
*/
StateListenerRegistry.register(
state => getActiveParticipantsIds(state).sort()
.join(),
(_, store) => {
_updateReceiverVideoConstraints(store);
}
);
/**
* StateListenerRegistry provides a reliable way of detecting changes to
* maxReceiverVideoQuality and preferredVideoQuality state and dispatching additional actions.
@ -219,6 +232,7 @@ function _updateReceiverVideoConstraints({ getState }) {
const tracks = state['features/base/tracks'];
const sourceNameSignaling = getSourceNameSignalingFeatureFlag(state);
const localParticipantId = getLocalParticipant(state).id;
const activeParticipantsIds = getActiveParticipantsIds(state);
let receiverConstraints;
@ -232,22 +246,36 @@ function _updateReceiverVideoConstraints({ getState }) {
};
const visibleRemoteTrackSourceNames = [];
let largeVideoSourceName;
const activeParticipantsSources = [];
if (visibleRemoteParticipants?.size) {
visibleRemoteParticipants.forEach(participantId => {
const sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
let sourceName;
if (remoteScreenShares.includes(participantId)) {
sourceName = participantId;
} else {
sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
if (sourceName) {
visibleRemoteTrackSourceNames.push(sourceName);
if (activeParticipantsIds.find(id => id === participantId)) {
activeParticipantsSources.push(sourceName);
}
}
});
}
if (localParticipantId !== largeVideoParticipantId) {
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.VIDEO,
largeVideoParticipantId
);
if (remoteScreenShares.includes(largeVideoParticipantId)) {
largeVideoSourceName = largeVideoParticipantId;
} else {
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.VIDEO,
largeVideoParticipantId
);
}
}
// Tile view.
@ -262,11 +290,7 @@ function _updateReceiverVideoConstraints({ getState }) {
// Prioritize screenshare in tile view.
if (remoteScreenShares?.length) {
const remoteScreenShareSourceNames = remoteScreenShares.map(remoteScreenShare =>
getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, remoteScreenShare)
);
receiverConstraints.selectedSources = remoteScreenShareSourceNames;
receiverConstraints.selectedSources = remoteScreenShares;
}
// Stage view.
@ -277,10 +301,14 @@ function _updateReceiverVideoConstraints({ getState }) {
if (visibleRemoteTrackSourceNames?.length) {
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
const stageParticipantsLevel = getVideoQualityForStageThumbnails(state);
visibleRemoteTrackSourceNames.forEach(sourceName => {
receiverConstraints.constraints[sourceName] = { 'maxHeight': Math.min(qualityLevel,
maxFrameHeight) };
const isStageParticipant = activeParticipantsSources.find(name => name === sourceName);
const quality = Math.min(maxFrameHeight, isStageParticipant
? stageParticipantsLevel : qualityLevel);
receiverConstraints.constraints[sourceName] = { 'maxHeight': quality };
});
}
@ -326,10 +354,14 @@ function _updateReceiverVideoConstraints({ getState }) {
if (visibleRemoteParticipants?.size > 0) {
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
const stageParticipantsLevel = getVideoQualityForStageThumbnails(state);
visibleRemoteParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': Math.min(qualityLevel,
maxFrameHeight) };
const isStageParticipant = activeParticipantsIds.find(id => id === participantId);
const quality = Math.min(maxFrameHeight, isStageParticipant
? stageParticipantsLevel : qualityLevel);
receiverConstraints.constraints[participantId] = { 'maxHeight': quality };
});
}

View File

@ -1,5 +1,6 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React, { useCallback, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
@ -42,6 +43,28 @@ type Props = {
t: Function
}
const useStyles = makeStyles(theme => {
return {
addBackground: {
marginRight: `${theme.spacing(2)}px`
},
button: {
display: 'none'
},
label: {
fontSize: '14px',
fontWeight: '600',
lineHeight: '20px',
marginLeft: '-10px',
marginTop: `${theme.spacing(3)}px`,
marginBottom: `${theme.spacing(2)}px`,
color: '#669aec',
display: 'inline-flex',
cursor: 'pointer'
}
};
});
/**
* Component used to upload an image.
*
@ -56,6 +79,7 @@ function UploadImageButton({
storedImages,
t
}: Props) {
const classes = useStyles();
const uploadImageButton: Object = useRef(null);
const uploadImageKeyPress = useCallback(e => {
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
@ -100,12 +124,12 @@ function UploadImageButton({
<>
{showLabel && <label
aria-label = { t('virtualBackground.uploadImage') }
className = 'file-upload-label'
className = { classes.label }
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }
tabIndex = { 0 } >
<Icon
className = { 'add-background' }
className = { classes.addBackground }
size = { 20 }
src = { IconPlusCircle } />
{t('virtualBackground.addBackground')}
@ -113,7 +137,7 @@ function UploadImageButton({
<input
accept = 'image/*'
className = 'file-upload-btn'
className = { classes.button }
id = 'file-upload'
onChange = { uploadImage }
ref = { uploadImageButton }

View File

@ -3,6 +3,8 @@
import Spinner from '@atlaskit/spinner';
import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import { makeStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { useState, useEffect, useCallback } from 'react';
import { useSelector } from 'react-redux';
@ -106,6 +108,142 @@ function _mapStateToProps(state): Object {
const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackground));
const useStyles = makeStyles(theme => {
return {
dialog: {
marginLeft: '-10px',
position: 'relative',
maxHeight: '300px',
color: 'white',
display: 'inline-grid',
gridTemplateColumns: 'auto auto auto auto auto',
columnGap: '9px',
cursor: 'pointer',
[[ '& .desktop-share:hover',
'& .thumbnail:hover',
'& .blur:hover',
'& .slight-blur:hover',
'& .virtual-background-none:hover' ]]: {
opacity: 0.5,
border: '2px solid #99bbf3'
},
'& .background-option': {
marginTop: `${theme.spacing(2)}px`,
borderRadius: `${theme.shape.borderRadius}px`,
height: '60px',
width: '107px',
textAlign: 'center',
justifyContent: 'center',
fontWeight: 'bold',
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center'
},
'& thumbnail-container': {
position: 'relative',
'&:focus-within .thumbnail ~ .delete-image-icon': {
display: 'block'
}
},
'& .thumbnail': {
objectFit: 'cover'
},
'& .thumbnail:hover ~ .delete-image-icon': {
display: 'block'
},
'& .thumbnail-selected': {
objectFit: 'cover',
border: '2px solid #246fe5'
},
'& .blur': {
boxShadow: 'inset 0 0 12px #000000',
background: '#7e8287',
padding: '0 10px'
},
'& .blur-selected': {
border: '2px solid #246fe5'
},
'& .slight-blur': {
boxShadow: 'inset 0 0 12px #000000',
background: '#a4a4a4',
padding: '0 10px'
},
'& .slight-blur-selected': {
border: '2px solid #246fe5'
},
'& .virtual-background-none': {
background: '#525252',
padding: '0 10px'
},
'& .none-selected': {
border: '2px solid #246fe5'
},
'& .desktop-share': {
background: '#525252'
},
'& .desktop-share-selected': {
border: '2px solid #246fe5',
padding: '0 10px'
},
'& delete-image-icon': {
background: '#3d3d3d',
position: 'absolute',
display: 'none',
left: '96px',
bottom: '51px',
'&:hover': {
display: 'block'
},
'@media (max-width: 632px)': {
left: '51px'
}
},
'@media (max-width: 360px)': {
gridTemplateColumns: 'auto auto auto'
},
'@media (max-width: 632px)': {
fontSize: '1.5vw',
[[ '& .desktop-share:hover',
'& .thumbnail:hover',
'& .blur:hover',
'& .slight-blur:hover',
'& .virtual-background-none:hover' ]]: {
height: '60px',
width: '60px'
},
[[ '& .desktop-share',
'& .virtual-background-none,',
'& .thumbnail,',
'& .blur,',
'& .slight-blur' ]]: {
height: '60px',
width: '60px'
},
[[ '& .desktop-share-selected',
'& .thumbnail-selected',
'& .none-selected',
'& .blur-selected',
'& .slight-blur-selected' ]]: {
height: '60px',
width: '60px'
}
}
},
dialogMarginTop: {
marginTop: '44px'
},
virtualBackgroundLoading: {
overflow: 'hidden',
position: 'fixed',
left: '50%',
marginTop: '10px',
transform: 'translateX(-50%)'
}
};
});
/**
* Renders virtual background dialog.
*
@ -122,6 +260,7 @@ function VirtualBackground({
initialOptions,
t
}: Props) {
const classes = useStyles();
const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
const [ options, setOptions ] = useState({ ...initialOptions });
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
@ -382,7 +521,7 @@ function VirtualBackground({
loadedPreview = { loadedPreviewState }
options = { options } />
{loading ? (
<div className = 'virtual-background-loading'>
<div className = { classes.virtualBackgroundLoading }>
<Spinner
isCompleting = { false }
size = 'medium' />
@ -397,7 +536,7 @@ function VirtualBackground({
showLabel = { previewIsLoaded }
storedImages = { storedImages } />}
<div
className = { `virtual-background-dialog${previewIsLoaded ? '' : ' dialog-margin-top'}` }
className = { clsx(classes.dialog, { [classes.dialogMarginTop]: previewIsLoaded }) }
role = 'radiogroup'
tabIndex = '-1'>
<Tooltip
@ -406,8 +545,9 @@ function VirtualBackground({
<div
aria-checked = { _selectedThumbnail === 'none' }
aria-label = { t('virtualBackground.removeBackground') }
className = { _selectedThumbnail === 'none' ? 'background-option none-selected'
: 'background-option virtual-background-none' }
className = { clsx('background-option', 'virtual-background-none', {
'none-selected': _selectedThumbnail === 'none'
}) }
onClick = { removeBackground }
onKeyPress = { removeBackgroundKeyPress }
role = 'radio'
@ -421,8 +561,9 @@ function VirtualBackground({
<div
aria-checked = { _selectedThumbnail === 'slight-blur' }
aria-label = { t('virtualBackground.slightBlur') }
className = { _selectedThumbnail === 'slight-blur'
? 'background-option slight-blur-selected' : 'background-option slight-blur' }
className = { clsx('background-option', 'slight-blur', {
'slight-blur-selected': _selectedThumbnail === 'slight-blur'
}) }
onClick = { enableSlideBlur }
onKeyPress = { enableSlideBlurKeyPress }
role = 'radio'
@ -436,8 +577,9 @@ function VirtualBackground({
<div
aria-checked = { _selectedThumbnail === 'blur' }
aria-label = { t('virtualBackground.blur') }
className = { _selectedThumbnail === 'blur' ? 'background-option blur-selected'
: 'background-option blur' }
className = { clsx('background-option', 'blur', {
'blur-selected': _selectedThumbnail === 'blur'
}) }
onClick = { enableBlur }
onKeyPress = { enableBlurKeyPress }
role = 'radio'
@ -452,9 +594,9 @@ function VirtualBackground({
<div
aria-checked = { _selectedThumbnail === 'desktop-share' }
aria-label = { t('virtualBackground.desktopShare') }
className = { _selectedThumbnail === 'desktop-share'
? 'background-option desktop-share-selected'
: 'background-option desktop-share' }
className = { clsx('background-option', 'desktop-share', {
'desktop-share-selected': _selectedThumbnail === 'desktop-share'
}) }
onClick = { shareDesktop }
onKeyPress = { shareDesktopKeyPress }
role = 'radio'
@ -494,8 +636,10 @@ function VirtualBackground({
<img
alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
aria-checked = { _selectedThumbnail === image.id }
className = { _selectedThumbnail === image.id
? 'background-option thumbnail-selected' : 'background-option thumbnail' }
className = { clsx('background-option', {
'thumbnail-selected': _selectedThumbnail === image.id,
'thumbnail': _selectedThumbnail !== image.id
}) }
data-imageid = { image.id }
onClick = { setUploadedImageBackground }
onError = { onError }

View File

@ -1,6 +1,7 @@
// @flow
import Spinner from '@atlaskit/spinner';
import { withStyles } from '@material-ui/core/styles';
import React, { PureComponent } from 'react';
import { hideDialog } from '../../base/dialog';
@ -29,6 +30,11 @@ export type Props = {
*/
_currentCameraDeviceId: string,
/**
* An object containing the CSS classes.
*/
classes: Object,
/**
* The redux {@code dispatch} function.
*/
@ -71,6 +77,55 @@ type State = {
jitsiTrack: Object
};
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = theme => {
return {
virtualBackgroundPreview: {
'& .video-preview': {
height: '250px'
},
'& .video-background-preview-entry': {
marginLeft: '-10px',
height: '250px',
width: '570px',
marginBottom: `${theme.spacing(2)}px`,
zIndex: 2,
'@media (max-width: 632px)': {
maxWidth: '336px'
}
},
'& .video-preview-loader': {
borderRadius: '6px',
backgroundColor: 'transparent',
height: '250px',
marginBottom: `${theme.spacing(2)}px`,
width: '572px',
position: 'fixed',
zIndex: 2,
'& svg': {
position: 'absolute',
top: '40%',
left: '45%'
},
'@media (min-width: 432px) and (max-width: 632px)': {
width: '340px'
}
}
}
};
};
/**
* Implements a React {@link PureComponent} which displays the virtual
* background preview.
@ -265,11 +320,13 @@ class VirtualBackgroundPreview extends PureComponent<Props, State> {
*/
render() {
const { jitsiTrack } = this.state;
const { classes } = this.props;
return jitsiTrack
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
;
return (<div className = { classes.virtualBackgroundPreview }>
{jitsiTrack
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
}</div>);
}
}
@ -287,4 +344,4 @@ function _mapStateToProps(state): Object {
};
}
export default translate(connect(_mapStateToProps)(VirtualBackgroundPreview));
export default translate(connect(_mapStateToProps)(withStyles(styles)(VirtualBackgroundPreview)));