Maximise the active speaker when the window is small

This commit is contained in:
Robin Townsend 2022-09-14 19:05:05 -04:00
parent 2c052c162f
commit 9e367db324
6 changed files with 86 additions and 52 deletions

View file

@ -43,7 +43,7 @@ limitations under the License.
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 64px; height: calc(50px + 2 * 8px);
} }
.footer > * { .footer > * {
@ -54,7 +54,7 @@ limitations under the License.
margin-right: 0px; margin-right: 0px;
} }
.footerFullscreen { .maximised .footer {
position: absolute; position: absolute;
width: 100%; width: 100%;
bottom: 0; bottom: 0;
@ -67,8 +67,14 @@ limitations under the License.
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
@media (min-height: 300px) {
.footer {
height: calc(50px + 2 * 24px);
}
}
@media (min-width: 800px) { @media (min-width: 800px) {
.footer { .footer {
height: 118px; height: calc(50px + 2 * 32px);
} }
} }

View file

@ -16,6 +16,8 @@ limitations under the License.
import React, { useEffect, useCallback, useMemo, useRef } from "react"; import React, { useEffect, useCallback, useMemo, useRef } from "react";
import { usePreventScroll } from "@react-aria/overlays"; import { usePreventScroll } from "@react-aria/overlays";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
@ -111,9 +113,20 @@ export function InCallView({
hideHeader, hideHeader,
}: Props) { }: Props) {
usePreventScroll(); usePreventScroll();
const elementRef = useRef<HTMLDivElement>(); const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
// Merge the refs so they can attach to the same element
const containerRef = useCallback(
(el: HTMLDivElement) => {
containerRef1.current = el;
containerRef2(el);
},
[containerRef1, containerRef2]
);
const { layout, setLayout } = useVideoGridLayout(screenshareFeeds.length > 0); const { layout, setLayout } = useVideoGridLayout(screenshareFeeds.length > 0);
const { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef); const { toggleFullscreen, fullscreenParticipant } =
useFullscreen(containerRef1);
const [spatialAudio] = useSpatialAudio(); const [spatialAudio] = useSpatialAudio();
@ -170,9 +183,7 @@ export function InCallView({
id: callFeed.stream.id, id: callFeed.stream.id,
callFeed, callFeed,
focused: focused:
screenshareFeeds.length === 0 && layout === "spotlight" screenshareFeeds.length === 0 && callFeed.userId === activeSpeaker,
? callFeed.userId === activeSpeaker
: false,
isLocal: callFeed.isLocal(), isLocal: callFeed.isLocal(),
presenter: false, presenter: false,
}); });
@ -197,7 +208,20 @@ export function InCallView({
} }
return participants; return participants;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]); }, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
// The maximised participant: either the participant that the user has
// manually put in fullscreen, or the focused (active) participant if the
// window is too small to show everyone
const maximisedParticipant = useMemo(
() =>
fullscreenParticipant ?? bounds.height <= 500
? items.find((item) => item.focused) ??
items.find((item) => item.callFeed) ??
null
: null,
[fullscreenParticipant, bounds, items]
);
const renderAvatar = useCallback( const renderAvatar = useCallback(
(roomMember: RoomMember, width: number, height: number) => { (roomMember: RoomMember, width: number, height: number) => {
@ -217,7 +241,7 @@ export function InCallView({
[] []
); );
const renderContent = useCallback((): JSX.Element => { const renderContent = (): JSX.Element => {
if (items.length === 0) { if (items.length === 0) {
return ( return (
<div className={styles.centerMessage}> <div className={styles.centerMessage}>
@ -225,16 +249,19 @@ export function InCallView({
</div> </div>
); );
} }
if (fullscreenParticipant) { if (maximisedParticipant) {
return ( return (
<VideoTileContainer <VideoTileContainer
key={fullscreenParticipant.id} height={bounds.height}
item={fullscreenParticipant} width={bounds.width}
key={maximisedParticipant.id}
item={maximisedParticipant}
getAvatar={renderAvatar} getAvatar={renderAvatar}
audioContext={audioContext} audioContext={audioContext}
audioDestination={audioDestination} audioDestination={audioDestination}
disableSpeakingIndicator={true} disableSpeakingIndicator={true}
isFullscreen={!!fullscreenParticipant} maximised={Boolean(maximisedParticipant)}
fullscreen={maximisedParticipant === fullscreenParticipant}
onFullscreen={toggleFullscreen} onFullscreen={toggleFullscreen}
/> />
); );
@ -250,43 +277,36 @@ export function InCallView({
audioContext={audioContext} audioContext={audioContext}
audioDestination={audioDestination} audioDestination={audioDestination}
disableSpeakingIndicator={items.length < 3} disableSpeakingIndicator={items.length < 3}
isFullscreen={!!fullscreenParticipant} maximised={false}
fullscreen={false}
onFullscreen={toggleFullscreen} onFullscreen={toggleFullscreen}
{...rest} {...rest}
/> />
)} )}
</VideoGrid> </VideoGrid>
); );
}, [ };
fullscreenParticipant,
items,
audioContext,
audioDestination,
layout,
renderAvatar,
toggleFullscreen,
]);
const { const {
modalState: rageshakeRequestModalState, modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps, modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId); } = useRageshakeRequestModal(groupCall.room.roomId);
const footerClassNames = classNames(styles.footer, { const containerClasses = classNames(styles.inRoom, {
[styles.footerFullscreen]: fullscreenParticipant, [styles.maximised]: maximisedParticipant,
}); });
return ( return (
<div className={styles.inRoom} ref={elementRef}> <div className={containerClasses} ref={containerRef}>
<audio ref={audioRef} /> <audio ref={audioRef} />
{(!spatialAudio || fullscreenParticipant) && ( {(!spatialAudio || maximisedParticipant) && (
<AudioContainer <AudioContainer
items={items} items={items}
audioContext={audioContext} audioContext={audioContext}
audioDestination={audioDestination} audioDestination={audioDestination}
/> />
)} )}
{!hideHeader && !fullscreenParticipant && ( {!hideHeader && !maximisedParticipant && (
<Header> <Header>
<LeftNav> <LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} /> <RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
@ -302,16 +322,16 @@ export function InCallView({
</Header> </Header>
)} )}
{renderContent()} {renderContent()}
<div className={footerClassNames}> <div className={styles.footer}>
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} /> <MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} /> <VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
{canScreenshare && !isSafari && !fullscreenParticipant && ( {canScreenshare && !isSafari && !maximisedParticipant && (
<ScreenshareButton <ScreenshareButton
enabled={isScreensharing} enabled={isScreensharing}
onPress={toggleScreensharing} onPress={toggleScreensharing}
/> />
)} )}
{!fullscreenParticipant && ( {!maximisedParticipant && (
<OverflowMenu <OverflowMenu
inCall inCall
roomIdOrAlias={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}

View file

@ -77,6 +77,7 @@ export const ParticipantsTest = () => {
key={item.id} key={item.id}
name={`User ${item.id}`} name={`User ${item.id}`}
disableSpeakingIndicator={items.length < 3} disableSpeakingIndicator={items.length < 3}
maximised={false}
{...rest} {...rest}
/> />
)} )}

View file

@ -40,7 +40,7 @@
box-shadow: inset 0 0 0 4px var(--accent) !important; box-shadow: inset 0 0 0 4px var(--accent) !important;
} }
.videoTile.fullscreen { .videoTile.maximised {
position: relative; position: relative;
border-radius: 0; border-radius: 0;
} }

View file

@ -33,7 +33,8 @@ interface Props {
mediaRef?: React.RefObject<MediaElement>; mediaRef?: React.RefObject<MediaElement>;
onOptionsPress?: () => void; onOptionsPress?: () => void;
localVolume?: number; localVolume?: number;
isFullscreen?: boolean; maximised: boolean;
fullscreen?: boolean;
onFullscreen?: () => void; onFullscreen?: () => void;
className?: string; className?: string;
showOptions?: boolean; showOptions?: boolean;
@ -53,7 +54,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
mediaRef, mediaRef,
onOptionsPress, onOptionsPress,
localVolume, localVolume,
isFullscreen, maximised,
fullscreen,
onFullscreen, onFullscreen,
className, className,
showOptions, showOptions,
@ -71,7 +73,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
[styles.speaking]: speaking, [styles.speaking]: speaking,
[styles.muted]: audioMuted, [styles.muted]: audioMuted,
[styles.screenshare]: screenshare, [styles.screenshare]: screenshare,
[styles.fullscreen]: isFullscreen, [styles.maximised]: maximised,
})} })}
ref={ref} ref={ref}
{...rest} {...rest}
@ -88,7 +90,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
{screenshare && ( {screenshare && (
<FullscreenButton <FullscreenButton
className={styles.button} className={styles.button}
fullscreen={isFullscreen} fullscreen={fullscreen}
onPress={onFullscreen} onPress={onFullscreen}
/> />
)} )}
@ -100,7 +102,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
{avatar} {avatar}
</> </>
)} )}
{screenshare ? ( {!maximised &&
(screenshare ? (
<div className={styles.presenterLabel}> <div className={styles.presenterLabel}>
<span>{`${name} is presenting`}</span> <span>{`${name} is presenting`}</span>
</div> </div>
@ -110,7 +113,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
{videoMuted && <VideoMutedIcon />} {videoMuted && <VideoMutedIcon />}
<span title={name}>{name}</span> <span title={name}>{name}</span>
</div> </div>
)} ))}
<video ref={mediaRef} playsInline disablePictureInPicture /> <video ref={mediaRef} playsInline disablePictureInPicture />
</animated.div> </animated.div>
); );

View file

@ -39,9 +39,11 @@ interface Props {
audioContext: AudioContext; audioContext: AudioContext;
audioDestination: AudioNode; audioDestination: AudioNode;
disableSpeakingIndicator: boolean; disableSpeakingIndicator: boolean;
isFullscreen: boolean; maximised: boolean;
fullscreen: boolean;
onFullscreen: (item: Participant) => void; onFullscreen: (item: Participant) => void;
} }
export function VideoTileContainer({ export function VideoTileContainer({
item, item,
width, width,
@ -50,7 +52,8 @@ export function VideoTileContainer({
audioContext, audioContext,
audioDestination, audioDestination,
disableSpeakingIndicator, disableSpeakingIndicator,
isFullscreen, maximised,
fullscreen,
onFullscreen, onFullscreen,
...rest ...rest
}: Props) { }: Props) {
@ -101,7 +104,8 @@ export function VideoTileContainer({
avatar={getAvatar && getAvatar(member, width, height)} avatar={getAvatar && getAvatar(member, width, height)}
onOptionsPress={onOptionsPress} onOptionsPress={onOptionsPress}
localVolume={localVolume} localVolume={localVolume}
isFullscreen={isFullscreen} maximised={maximised}
fullscreen={fullscreen}
onFullscreen={onFullscreenCallback} onFullscreen={onFullscreenCallback}
{...rest} {...rest}
/> />