Merge pull request #581 from robintown/maximise

Maximise the active speaker when the window is small
This commit is contained in:
Robin 2022-09-16 10:25:55 -04:00 committed by GitHub
commit 3b181224fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 85 additions and 52 deletions

View file

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

View file

@ -16,6 +16,8 @@ limitations under the License.
import React, { useEffect, useCallback, useMemo, useRef } from "react";
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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
@ -111,9 +113,20 @@ export function InCallView({
hideHeader,
}: Props) {
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 { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef);
const { toggleFullscreen, fullscreenParticipant } =
useFullscreen(containerRef1);
const [spatialAudio] = useSpatialAudio();
@ -170,9 +183,7 @@ export function InCallView({
id: callFeed.stream.id,
callFeed,
focused:
screenshareFeeds.length === 0 && layout === "spotlight"
? callFeed.userId === activeSpeaker
: false,
screenshareFeeds.length === 0 && callFeed.userId === activeSpeaker,
isLocal: callFeed.isLocal(),
presenter: false,
});
@ -197,7 +208,20 @@ export function InCallView({
}
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 && bounds.width <= 500)
? items.find((item) => item.focused) ??
items.find((item) => item.callFeed) ??
null
: null,
[fullscreenParticipant, bounds, items]
);
const renderAvatar = useCallback(
(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) {
return (
<div className={styles.centerMessage}>
@ -225,16 +249,19 @@ export function InCallView({
</div>
);
}
if (fullscreenParticipant) {
if (maximisedParticipant) {
return (
<VideoTileContainer
key={fullscreenParticipant.id}
item={fullscreenParticipant}
height={bounds.height}
width={bounds.width}
key={maximisedParticipant.id}
item={maximisedParticipant}
getAvatar={renderAvatar}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={true}
isFullscreen={!!fullscreenParticipant}
maximised={Boolean(maximisedParticipant)}
fullscreen={maximisedParticipant === fullscreenParticipant}
onFullscreen={toggleFullscreen}
/>
);
@ -250,43 +277,36 @@ export function InCallView({
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={items.length < 3}
isFullscreen={!!fullscreenParticipant}
maximised={false}
fullscreen={false}
onFullscreen={toggleFullscreen}
{...rest}
/>
)}
</VideoGrid>
);
}, [
fullscreenParticipant,
items,
audioContext,
audioDestination,
layout,
renderAvatar,
toggleFullscreen,
]);
};
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
const footerClassNames = classNames(styles.footer, {
[styles.footerFullscreen]: fullscreenParticipant,
const containerClasses = classNames(styles.inRoom, {
[styles.maximised]: maximisedParticipant,
});
return (
<div className={styles.inRoom} ref={elementRef}>
<div className={containerClasses} ref={containerRef}>
<audio ref={audioRef} />
{(!spatialAudio || fullscreenParticipant) && (
{(!spatialAudio || maximisedParticipant) && (
<AudioContainer
items={items}
audioContext={audioContext}
audioDestination={audioDestination}
/>
)}
{!hideHeader && !fullscreenParticipant && (
{!hideHeader && !maximisedParticipant && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
@ -302,16 +322,16 @@ export function InCallView({
</Header>
)}
{renderContent()}
<div className={footerClassNames}>
<div className={styles.footer}>
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
{canScreenshare && !isSafari && !fullscreenParticipant && (
{canScreenshare && !isSafari && !maximisedParticipant && (
<ScreenshareButton
enabled={isScreensharing}
onPress={toggleScreensharing}
/>
)}
{!fullscreenParticipant && (
{!maximisedParticipant && (
<OverflowMenu
inCall
roomIdOrAlias={roomIdOrAlias}

View file

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

View file

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

View file

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