Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ deploy-init:
cp $(LIBJITSIMEET_DIR)/models/RTC/Encoder.onnx \
$(LIBJITSIMEET_DIR)/models/RTC/Decoder.onnx \
$(DEPLOY_DIR)/models
cp -r $(ONNX_DIR)/dist \
$(DEPLOY_DIR)
mkdir -p $(DEPLOY_DIR)/dist
cp $(ONNX_DIR)/dist/ort-wasm-simd-threaded.mjs \
$(ONNX_DIR)/dist/ort-wasm-simd-threaded.wasm \
$(DEPLOY_DIR)/dist

deploy-appbundle:
cp $(BUILD_DIR)/*.min.js $(DEPLOY_DIR)
Expand Down
2 changes: 2 additions & 0 deletions conference.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise'
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
import { transcriberJoined, transcriberLeft } from './react/features/transcribing/actions';
import { muteLocal } from './react/features/video-menu/actions.any';
import { setTileView } from './react/features/video-layout/actions.any';

const logger = Logger.getLogger('app:conference-web');
let room;
Expand Down Expand Up @@ -1882,6 +1883,7 @@ export default {
APP.UI.initConference();

dispatch(conferenceJoined(room));
dispatch(setTileView(true));
const participantCount = room?.getParticipants()?.length;
if (participantCount === 0) {
dispatch(toggleE2EE(true));
Expand Down
7 changes: 7 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,13 @@ var config = {
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
// Use -1 to disable.
// maxFullResolutionParticipants: 2,
maxFullResolutionParticipants: -1,
videoQuality: {
minHeightForQualityLvl: {
240: 'standard',
720: 'high',
},
},

// w3c spec-compliant video constraints to use for video capture. Currently
// used by browsers that return true from lib-jitsi-meet's
Expand Down
3 changes: 3 additions & 0 deletions modules/UI/videolayout/LargeVideoManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,9 @@ export default class LargeVideoManager {
* Updates the src of the dominant speaker avatar
*/
updateAvatar() {
if (!this._dominantSpeakerAvatarContainer) {
return;
}
if (!this._avatarRoot) {
this._avatarRoot = createRoot(this._dominantSpeakerAvatarContainer);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-meet": "https://github.com/internxt/lib-meet/releases/download/v.1.0.2/lib-meet-1.0.2.tgz",
"lib-meet": "https://github.com/internxt/lib-meet/releases/download/v.1.0.3/lib-meet-1.0.3.tgz",
"lodash-es": "4.18.1",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { useAspectRatio } from "../../../general/hooks/useAspectRatio";
import React, { useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import { setMeasuredTileViewThumbnailSize } from "../../../../../filmstrip/actions.web";
import { VideoParticipantType } from "../types";
import VideoParticipant from "./VideoParticipant";

Expand All @@ -10,8 +11,10 @@ export interface VideoGalleryProps {
}

const VideoGallery = ({ participants, flipX, translate }: VideoGalleryProps) => {
const dispatch = useDispatch();
const gridRef = useRef<HTMLDivElement>(null);
const maxSeen = useRef(0);
const participantsNumber = participants.length;
const { containerStyle } = useAspectRatio();
const hasOneParticipant = participantsNumber === 1;

const sortedParticipants = [...participants].sort((a, b) => {
Expand All @@ -22,41 +25,73 @@ const VideoGallery = ({ participants, flipX, translate }: VideoGalleryProps) =>
return a.name.localeCompare(b.name);
});

const getParticipantClasses = () => {
let widthClass = "";
let heightClass = "";

const getLayout = () => {
if (hasOneParticipant) {
return "relative aspect-square sm:aspect-video h-full max-w-full";
return { aspect: "sm:aspect-video", width: "h-full max-w-full" };
} else if (participantsNumber === 2) {
widthClass = "w-[calc(50%-5px)]";
heightClass = "sm:max-h-[75%]";
} else if (participantsNumber === 3) {
widthClass = "w-[calc(33.333%-5px)] sm:w-[calc(33%-10px)]";
heightClass = "sm:max-h-[36%]";
} else if (participantsNumber === 4) {
widthClass = "w-[calc(50%-5px)]";
heightClass = "sm:max-h-[36%]";
} else if (participantsNumber > 4 && participantsNumber < 10) {
widthClass = "w-[calc(50%-5px)] sm:w-[calc(33.333%-7px)]";
heightClass = "sm:max-h-[24%]";
} else {
widthClass = "w-[calc(50%-5px)] sm:w-[calc(25%-7px)]";
heightClass = "sm:max-h-[18%]";
// 2 cols x 1 row
return { aspect: "sm:aspect-[32/9]", width: "w-[calc(50%-5px)]" };
} else if (participantsNumber <= 4) {
// 2 cols x 2 rows -> 3 participants wrap to 2 on top, 1 centered below
return { aspect: "sm:aspect-video", width: "w-[calc(50%-5px)]" };
} else if (participantsNumber <= 6) {
// 3 cols x 2 rows
return { aspect: "sm:aspect-[8/3]", width: "w-[calc(50%-5px)] sm:w-[calc(33.333%-7px)]" };
} else if (participantsNumber <= 9) {
// 3 cols x 3 rows
return { aspect: "sm:aspect-video", width: "w-[calc(50%-5px)] sm:w-[calc(33.333%-7px)]" };
}
// 10 (room max capacity) -> 4 cols x 3 rows, last row has 2 centered
return { aspect: "sm:aspect-[64/27]", width: "w-[calc(50%-5px)] sm:w-[calc(25%-8px)]" };
};

const mobileHeightClass = participantsNumber > 4 ? "max-h-[120px]" : "";
const { aspect, width } = getLayout();
const mobileHeightClass = participantsNumber > 4 ? "max-h-[120px] sm:max-h-none" : "";
const participantClasses = `relative ${width} ${mobileHeightClass} aspect-square sm:aspect-video`;

return `relative ${widthClass} ${heightClass} ${mobileHeightClass} aspect-square sm:aspect-video`;
};
useEffect(() => {
const grid = gridRef.current;

if (!grid || participantsNumber === 0) {
return;
}

const measure = () => {
const tile = grid.firstElementChild;

if (!tile) {
return;
}

const { height, width: tileWidth } = tile.getBoundingClientRect();

const dpr = window.devicePixelRatio || 1;

if (height > 0 && tileWidth > 0 && height * dpr > maxSeen.current) {
maxSeen.current = height * dpr;
dispatch(setMeasuredTileViewThumbnailSize(
Math.round(height * dpr),
Math.round(tileWidth * dpr)
));
}
};
Comment on lines +70 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t quite understand this. Does maxSeen simply store the maximum tile size seen throughout the entire call and never decrease? Does this mean the resolution never decreases, or what effect does it have? Because it always just stores the largest value

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jitsi reduced the video quality after a few minutes, despite all other changes. It has some smart way of figuring out what quality to use, which is not working well with our customization.

I can try to remove maxSeen and check again, but paradoxically, it was the only way to stop Jitsi from reducing the video quality

@TamaraFinogina TamaraFinogina Aug 31, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CandelR I tried removing maxSeen and resetting it to 0 in useEffect, but both resulted in quality degradation after a few minutes of the call. maxSeen blocks this degradation, but we really need to figure out how Jitsi decides video quality (highly non-trivial btw).

P.S. Video bridge ignores this and sends what it can. I think this whole quality calculation is more of an artificial limitation Jitsi sets to reduce load on the video bridge. Since we only have 10 participants (and not 100 as they support), I think we can ignore this optimization


const observer = new ResizeObserver(measure);

observer.observe(grid);
measure();

return () => observer.disconnect();
}, [ dispatch, participantsNumber ]);

return (
<div className="h-full w-full flex items-center justify-center overflow-hidden bg-gray-950">
<div
className={`max-h-[85vh] sm:h-[90%] w-[95%] sm:w-[90%] flex justify-center items-center`}
style={containerStyle}
className={`max-h-[85vh] sm:h-[88%] w-[95%] sm:w-auto ${aspect} sm:max-w-[90%]
flex justify-center items-center`}
>
<div
ref={gridRef}
className={`${
hasOneParticipant ? "h-full" : ""
} w-full flex flex-wrap justify-center items-start content-start gap-2.5`}
Expand All @@ -65,7 +100,7 @@ const VideoGallery = ({ participants, flipX, translate }: VideoGalleryProps) =>
<VideoParticipant
key={participant.id}
participant={participant}
className={getParticipantClasses()}
className={participantClasses}
translate={translate}
flipX={flipX}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const ConferenceControls = ({
inviteUrl={_inviteUrl}
/>
<div className="flex absolute bottom-5 left-2/4 -translate-x-2/4 z-[100]">
<div className="flex flex-row space-x-3 p-3 justify-center items-center bg-black/50 border border-white/10 rounded-full">
<div className="flex flex-row space-x-3 p-3 justify-center items-center bg-black/20 backdrop-blur-md border border-white/10 rounded-full">
<MediaControlsWrapper />
<CircleButton variant="default" onClick={handleToggleChat} active={_chatOpen}>
<ChatIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class Conference extends AbstractConference<IProps, any> {
isVisible={this.state.isBannerVisible}
onClose={() => this.setState({ isBannerVisible: false })}
/>
<div className="flex-1 flex justify-center items-center">
<div className="flex-1 flex justify-center items-center pb-24">
{/* <LargeVideoWeb /> */}
<VideoGalleryWrapper videoMode={viewMode} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { startScreenShareFlow } from "../../../../../screen-share/actions.web";
import { isScreenVideoShared } from "../../../../../screen-share/functions";
import { getCurrentConference } from "../../../../conference/functions";
import { translate } from "../../../../i18n/functions";
import { useAspectRatio } from "../../../general/hooks/useAspectRatio";
import { useParticipantAvatar } from "../../PreMeeting/hooks/useParticipantAvatar";
import VideoGallery from "../components/VideoGallery";
import VideoParticipant from "../components/VideoParticipant";
Expand All @@ -29,7 +28,6 @@ interface GalleryVideoWrapperProps extends WithTranslation, OwnProps, MappedStat
}

const GalleryVideoWrapper = ({ videoMode, t, dispatch }: GalleryVideoWrapperProps) => {
const { containerStyle } = useAspectRatio();
const participantsScrollRef = useRef<HTMLDivElement>(null);
const [canScrollUp, setCanScrollUp] = useState(false);
const [canScrollDown, setCanScrollDown] = useState(false);
Expand All @@ -41,7 +39,6 @@ const GalleryVideoWrapper = ({ videoMode, t, dispatch }: GalleryVideoWrapperProp
const flipX = useSelector((state: IReduxState) => state["features/base/settings"].localFlipX);
const isLocalSharing = useSelector((state: IReduxState) => isScreenVideoShared(state));

const contStyle = videoMode === "gallery" ? containerStyle : {};
const hasScreenShare = screenShareParticipants.length > 0;
const sharingParticipant = screenShareParticipants[0];

Expand Down Expand Up @@ -85,7 +82,7 @@ const GalleryVideoWrapper = ({ videoMode, t, dispatch }: GalleryVideoWrapperProp
}, [participants]);

return (
<div className="h-full w-full bg-gray-950" style={contStyle}>
<div className="h-full w-full bg-gray-950">
<AudioTracksContainer />

{/* Screen Sharing Notification */}
Expand Down
5 changes: 5 additions & 0 deletions react/features/chat/components/web/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ const useStyles = makeStyles<{
userSelect: 'text',
WebkitUserSelect: 'text',
},

'&::selection': {
backgroundColor: '#FFFFFF',
color: '#000000'
},

"@keyframes chatSlideIn": {
"0%": {
Expand Down
2 changes: 1 addition & 1 deletion react/features/conference/components/web/Conference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ class Conference extends AbstractConference<IProps, any> {
<Notice />
<div onTouchStart={this._onVideospaceTouchStart}>
<Header mode={viewMode} translate={t} onSetModeClicked={this._onSetVideoModeClicked} />
<div className="flex">
<div className="flex pb-24">
{/* <LargeVideoWeb /> */}
<VideoGalleryWrapper videoMode={viewMode} />
</div>
Expand Down
4 changes: 1 addition & 3 deletions react/features/e2ee/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,7 @@ StateListenerRegistry.register(
const handlers = new Map<string, Function>();

const sasAvailableHandler = (sas: object) => {
if (ConfigService.instance.isDevelopment()) {
dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { sas }));
}
dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { sas }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the check has to appear in prod?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That will be the best way for you guys to rule out crypto problems. If it gives problems, disable it again. But it should work just fine

};
handlers.set(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, sasAvailableHandler);
conference.on(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, sasAvailableHandler);
Expand Down
34 changes: 32 additions & 2 deletions react/features/filmstrip/actions.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ export function setTileViewDimensions() {
numberOfParticipants,
desiredNumberOfVisibleTiles: numberOfVisibleTiles
});
const measured = state['features/filmstrip'].tileViewDimensions?.measured
? state['features/filmstrip'].tileViewDimensions?.thumbnailSize
: null;
const thumbnailsTotalHeight = (rows ?? 1) * (TILE_VERTICAL_MARGIN + (height ?? 0));
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
const hasScroll = availableHeight < thumbnailsTotalHeight;
Expand All @@ -127,9 +130,10 @@ export function setTileViewDimensions() {
rows
},
thumbnailSize: {
height,
width
height: measured?.height ?? height,
width: measured?.width ?? width
},
measured: state['features/filmstrip'].tileViewDimensions?.measured ?? false,
filmstripHeight,
filmstripWidth,
hasScroll
Expand All @@ -138,6 +142,32 @@ export function setTileViewDimensions() {
};
}

/**
* Overrides the tile thumbnail size with one measured from the rendered grid.
* setTileViewDimensions() derives it from the legacy filmstrip geometry, which
* this UI does not render, so it under-reports and the receiver constraints end
* up lower than the tiles actually are.
*/
export function setMeasuredTileViewThumbnailSize(height: number, width: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { tileViewDimensions } = getState()['features/filmstrip'];

if (tileViewDimensions?.thumbnailSize?.height === height
&& tileViewDimensions?.thumbnailSize?.width === width) {
return;
}

dispatch({
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
...tileViewDimensions,
measured: true,
thumbnailSize: { height, width }
}
});
};
}

/**
* Sets the dimensions of the thumbnails in vertical view.
*
Expand Down
1 change: 1 addition & 0 deletions react/features/filmstrip/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ interface IFilmstripDimensions {
rows: number;
};
hasScroll?: boolean;
measured?: boolean;
thumbnailSize?: IDimensions;
}

Expand Down
4 changes: 4 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ function devServerProxyBypass({ path, headers }) {
return "/index.html";
}

if (tpath === "/config.js" || tpath === "/interface_config.js") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise, it was using the Jitsi config in make dev (because the path to our config was defined for prod only), and I couldn't see the difference untill changes were pushed to remote

return tpath;
}

if (
tpath.startsWith("/css/") ||
tpath.startsWith("/doc/") ||
Expand Down
Loading
Loading