22// SPDX-License-Identifier: AGPL-3.0-only
33
44import React , { ReactNode } from 'react' ;
5+ import Measure from 'react-measure' ;
6+ import { debounce } from 'lodash' ;
57import {
68 SetLocalAudioType ,
79 SetLocalPreviewType ,
@@ -15,6 +17,15 @@ import { Spinner } from './Spinner';
1517import { ColorType } from '../types/Colors' ;
1618import { LocalizerType } from '../types/Util' ;
1719import { ConversationType } from '../state/ducks/conversations' ;
20+ import {
21+ REQUESTED_VIDEO_WIDTH ,
22+ REQUESTED_VIDEO_HEIGHT ,
23+ } from '../calling/constants' ;
24+
25+ // We request dimensions but may not get them depending on the user's webcam. This is our
26+ // fallback while we don't know.
27+ const VIDEO_ASPECT_RATIO_FALLBACK =
28+ REQUESTED_VIDEO_WIDTH / REQUESTED_VIDEO_HEIGHT ;
1829
1930export type PropsType = {
2031 availableCameras : Array < MediaDeviceInfo > ;
@@ -61,7 +72,18 @@ export const CallingLobby = ({
6172 toggleParticipants,
6273 toggleSettings,
6374} : PropsType ) : JSX . Element => {
64- const localVideoRef = React . useRef ( null ) ;
75+ const [
76+ localPreviewContainerWidth ,
77+ setLocalPreviewContainerWidth ,
78+ ] = React . useState < null | number > ( null ) ;
79+ const [
80+ localPreviewContainerHeight ,
81+ setLocalPreviewContainerHeight ,
82+ ] = React . useState < null | number > ( null ) ;
83+ const [ localVideoAspectRatio , setLocalVideoAspectRatio ] = React . useState (
84+ VIDEO_ASPECT_RATIO_FALLBACK
85+ ) ;
86+ const localVideoRef = React . useRef < null | HTMLVideoElement > ( null ) ;
6587
6688 const toggleAudio = React . useCallback ( ( ) : void => {
6789 setLocalAudio ( { enabled : ! hasLocalAudio } ) ;
@@ -71,6 +93,24 @@ export const CallingLobby = ({
7193 setLocalVideo ( { enabled : ! hasLocalVideo } ) ;
7294 } , [ hasLocalVideo , setLocalVideo ] ) ;
7395
96+ const hasEverMeasured =
97+ localPreviewContainerWidth !== null && localPreviewContainerHeight !== null ;
98+ const setLocalPreviewContainerDimensions = React . useMemo ( ( ) => {
99+ const set = ( bounds : Readonly < { width : number ; height : number } > ) => {
100+ setLocalPreviewContainerWidth ( bounds . width ) ;
101+ setLocalPreviewContainerHeight ( bounds . height ) ;
102+ } ;
103+
104+ if ( hasEverMeasured ) {
105+ return debounce ( set , 100 , { maxWait : 3000 } ) ;
106+ }
107+ return set ;
108+ } , [
109+ hasEverMeasured ,
110+ setLocalPreviewContainerWidth ,
111+ setLocalPreviewContainerHeight ,
112+ ] ) ;
113+
74114 React . useEffect ( ( ) => {
75115 setLocalPreview ( { element : localVideoRef } ) ;
76116
@@ -79,6 +119,21 @@ export const CallingLobby = ({
79119 } ;
80120 } , [ setLocalPreview ] ) ;
81121
122+ // This isn't perfect because it doesn't react to changes in the webcam's aspect ratio.
123+ // For example, if you changed from Webcam A to Webcam B and Webcam B had a different
124+ // aspect ratio, we wouldn't update.
125+ //
126+ // Unfortunately, RingRTC (1) doesn't update these dimensions with the "real" camera
127+ // dimensions (2) doesn't give us any hooks or callbacks. For now, this works okay.
128+ // We have `object-fit: contain` in the CSS in case we're wrong; not ideal, but
129+ // usable.
130+ React . useEffect ( ( ) => {
131+ const videoEl = localVideoRef . current ;
132+ if ( hasLocalVideo && videoEl && videoEl . width && videoEl . height ) {
133+ setLocalVideoAspectRatio ( videoEl . width / videoEl . height ) ;
134+ }
135+ } , [ hasLocalVideo , setLocalVideoAspectRatio ] ) ;
136+
82137 React . useEffect ( ( ) => {
83138 function handleKeyDown ( event : KeyboardEvent ) : void {
84139 let eventHandled = false ;
@@ -141,6 +196,33 @@ export const CallingLobby = ({
141196 joinButtonChildren = i18n ( 'calling__start' ) ;
142197 }
143198
199+ let localPreviewStyles : React . CSSProperties ;
200+ // It'd be nice to use `hasEverMeasured` here, too, but TypeScript isn't smart enough
201+ // to understand the logic here.
202+ if (
203+ localPreviewContainerWidth !== null &&
204+ localPreviewContainerHeight !== null
205+ ) {
206+ const containerAspectRatio =
207+ localPreviewContainerWidth / localPreviewContainerHeight ;
208+ localPreviewStyles =
209+ containerAspectRatio < localVideoAspectRatio
210+ ? {
211+ width : '100%' ,
212+ height : Math . floor (
213+ localPreviewContainerWidth / localVideoAspectRatio
214+ ) ,
215+ }
216+ : {
217+ width : Math . floor (
218+ localPreviewContainerHeight * localVideoAspectRatio
219+ ) ,
220+ height : '100%' ,
221+ } ;
222+ } else {
223+ localPreviewStyles = { display : 'none' } ;
224+ }
225+
144226 return (
145227 < div className = "module-calling__container" >
146228 < CallingHeader
@@ -153,37 +235,58 @@ export const CallingLobby = ({
153235 toggleSettings = { toggleSettings }
154236 />
155237
156- < div className = "module-calling-lobby__video" >
157- { hasLocalVideo && availableCameras . length > 0 ? (
158- < video
159- className = "module-calling-lobby__video-on__video"
160- ref = { localVideoRef }
161- autoPlay
162- />
163- ) : (
164- < CallBackgroundBlur avatarPath = { me . avatarPath } color = { me . color } >
165- < div className = "module-calling__video-off--icon" />
166- < span className = "module-calling__video-off--text" >
167- { i18n ( 'calling__your-video-is-off' ) }
168- </ span >
169- </ CallBackgroundBlur >
170- ) }
238+ < Measure
239+ bounds
240+ onResize = { ( { bounds } ) => {
241+ if ( ! bounds ) {
242+ window . log . error ( 'We should be measuring bounds' ) ;
243+ return ;
244+ }
245+ setLocalPreviewContainerDimensions ( bounds ) ;
246+ } }
247+ >
248+ { ( { measureRef } ) => (
249+ < div
250+ ref = { measureRef }
251+ className = "module-calling-lobby__local-preview-container"
252+ >
253+ < div
254+ className = "module-calling-lobby__local-preview"
255+ style = { localPreviewStyles }
256+ >
257+ { hasLocalVideo && availableCameras . length > 0 ? (
258+ < video
259+ className = "module-calling-lobby__local-preview__video-on"
260+ ref = { localVideoRef }
261+ autoPlay
262+ />
263+ ) : (
264+ < CallBackgroundBlur avatarPath = { me . avatarPath } color = { me . color } >
265+ < div className = "module-calling-lobby__local-preview__video-off__icon" />
266+ < span className = "module-calling-lobby__local-preview__video-off__text" >
267+ { i18n ( 'calling__your-video-is-off' ) }
268+ </ span >
269+ </ CallBackgroundBlur >
270+ ) }
171271
172- < div className = "module-calling__buttons" >
173- < CallingButton
174- buttonType = { videoButtonType }
175- i18n = { i18n }
176- onClick = { toggleVideo }
177- tooltipDirection = { TooltipPlacement . Top }
178- />
179- < CallingButton
180- buttonType = { audioButtonType }
181- i18n = { i18n }
182- onClick = { toggleAudio }
183- tooltipDirection = { TooltipPlacement . Top }
184- />
185- </ div >
186- </ div >
272+ < div className = "module-calling__buttons" >
273+ < CallingButton
274+ buttonType = { videoButtonType }
275+ i18n = { i18n }
276+ onClick = { toggleVideo }
277+ tooltipDirection = { TooltipPlacement . Top }
278+ />
279+ < CallingButton
280+ buttonType = { audioButtonType }
281+ i18n = { i18n }
282+ onClick = { toggleAudio }
283+ tooltipDirection = { TooltipPlacement . Top }
284+ />
285+ </ div >
286+ </ div >
287+ </ div >
288+ ) }
289+ </ Measure >
187290
188291 { isGroupCall ? (
189292 < div className = "module-calling-lobby__info" >
0 commit comments