Add support for screen-sharing

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2022-08-07 19:09:45 +02:00
parent 9af122b96e
commit 305c2cb806
No known key found for this signature in database
GPG Key ID: 4F68B9EC0536B5CC
5 changed files with 127 additions and 54 deletions

View File

@ -54,6 +54,12 @@ limitations under the License.
margin-right: 0px;
}
.footerFullscreen {
position: absolute;
width: 100%;
bottom: 0;
}
.avatar {
position: absolute;
top: 50%;

View File

@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useMemo } from "react";
import React, { useCallback, useMemo, useRef } from "react";
import { usePreventScroll } from "@react-aria/overlays";
import { GroupCall, MatrixClient } from "matrix-js-sdk";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames";
import styles from "./InCallView.module.css";
import {
@ -46,6 +47,7 @@ import { useMediaHandler } from "../settings/useMediaHandler";
import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { useAudioContext } from "../video-grid/useMediaStream";
import { useFullscreen } from "../video-grid/useFullscreen";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -72,7 +74,7 @@ interface Props {
roomId: string;
unencryptedEventsFromUsers: Set<string>;
}
interface Participant {
export interface Participant {
id: string;
callFeed: CallFeed;
focused: boolean;
@ -100,7 +102,9 @@ export function InCallView({
unencryptedEventsFromUsers,
}: Props) {
usePreventScroll();
const elementRef = useRef<HTMLDivElement>();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
const { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef);
const [audioContext, audioDestination, audioRef] = useAudioContext();
const { audioOutput } = useMediaHandler();
@ -161,65 +165,107 @@ export function InCallView({
);
}, []);
const renderContent = useCallback(() => {
if (items.length === 0) {
return (
<div className={styles.centerMessage}>
<p>Waiting for other participants...</p>
</div>
);
}
if (fullscreenParticipant) {
return (
<VideoTileContainer
key={fullscreenParticipant.id}
item={fullscreenParticipant}
getAvatar={renderAvatar}
audioOutputDevice={audioOutput}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={true}
isFullscreen={fullscreenParticipant}
onFullscreen={toggleFullscreen}
/>
);
}
return (
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
{({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
<VideoTileContainer
key={item.id}
item={item}
getAvatar={renderAvatar}
showName={items.length > 2 || item.focused}
audioOutputDevice={audioOutput}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={items.length < 3}
isFullscreen={fullscreenParticipant}
onFullscreen={toggleFullscreen}
{...rest}
/>
)}
</VideoGrid>
);
}, [
fullscreenParticipant,
items,
audioContext,
audioDestination,
audioOutput,
layout,
renderAvatar,
toggleFullscreen,
]);
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
const footerClassNames = classNames(styles.footer, {
[styles.footerFullscreen]: fullscreenParticipant,
});
return (
<div className={styles.inRoom}>
<div className={styles.inRoom} ref={elementRef}>
<audio ref={audioRef} />
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
<VersionMismatchWarning
users={unencryptedEventsFromUsers}
room={groupCall.room}
/>
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
<UserMenuContainer preventNavigation />
</RightNav>
</Header>
{items.length === 0 ? (
<div className={styles.centerMessage}>
<p>Waiting for other participants...</p>
</div>
) : (
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
{({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
<VideoTileContainer
key={item.id}
item={item}
getAvatar={renderAvatar}
showName={items.length > 2 || item.focused}
audioOutputDevice={audioOutput}
audioContext={audioContext}
audioDestination={audioDestination}
disableSpeakingIndicator={items.length < 3}
{...rest}
{!fullscreenParticipant && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
<VersionMismatchWarning
users={unencryptedEventsFromUsers}
room={groupCall.room}
/>
)}
</VideoGrid>
</LeftNav>
<RightNav>
<GridLayoutMenu layout={layout} setLayout={setLayout} />
<UserMenuContainer preventNavigation />
</RightNav>
</Header>
)}
<div className={styles.footer}>
{renderContent()}
<div className={footerClassNames}>
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
{canScreenshare && !isSafari && (
{canScreenshare && !isSafari && !fullscreenParticipant && (
<ScreenshareButton
enabled={isScreensharing}
onPress={toggleScreensharing}
/>
)}
<OverflowMenu
inCall
roomId={roomId}
groupCall={groupCall}
showInvite={true}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>
{!fullscreenParticipant && (
<OverflowMenu
inCall
roomId={roomId}
groupCall={groupCall}
showInvite={true}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>
)}
<HangupButton onPress={onLeave} />
</div>
<GroupCallInspector

View File

@ -20,7 +20,7 @@ import classNames from "classnames";
import styles from "./VideoTile.module.css";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
import { AudioButton } from "../button/Button";
import { AudioButton, FullscreenButton } from "../button/Button";
export const VideoTile = forwardRef(
(
@ -39,6 +39,8 @@ export const VideoTile = forwardRef(
onOptionsPress,
showOptions,
localVolume,
isFullscreen,
onFullscreen,
...rest
},
ref
@ -50,17 +52,27 @@ export const VideoTile = forwardRef(
[styles.speaking]: speaking,
[styles.muted]: audioMuted,
[styles.screenshare]: screenshare,
[styles.fullscreen]: isFullscreen,
})}
ref={ref}
{...rest}
>
{showOptions && (
{(!isLocal || screenshare) && (
<div className={classNames(styles.toolbar)}>
<AudioButton
className={styles.button}
volume={localVolume}
onPress={onOptionsPress}
/>
{!isLocal && (
<AudioButton
className={styles.button}
volume={localVolume}
onPress={onOptionsPress}
/>
)}
{screenshare && (
<FullscreenButton
className={styles.button}
fullscreen={isFullscreen}
onPress={onFullscreen}
/>
)}
</div>
)}
{(videoMuted || noVideo) && (

View File

@ -40,6 +40,11 @@
box-shadow: inset 0 0 0 4px var(--accent) !important;
}
.videoTile.fullscreen {
position: relative;
border-radius: 0;
}
.videoTile.screenshare > video {
object-fit: contain;
}
@ -79,10 +84,11 @@
z-index: 1;
}
.videoTile:not(.isLocal):not(:hover) .toolbar {
.videoTile:not(:hover) .toolbar {
display: none;
}
.videoTile:not(.fullscreen):hover .presenterLabel,
.videoTile:not(.isLocal):hover .presenterLabel {
top: calc(42px + 20px); /* toolbar + margin */
}

View File

@ -33,6 +33,8 @@ export function VideoTileContainer({
audioContext,
audioDestination,
disableSpeakingIndicator,
isFullscreen,
onFullscreen,
...rest
}) {
const {
@ -81,8 +83,9 @@ export function VideoTileContainer({
mediaRef={mediaRef}
avatar={getAvatar && getAvatar(member, width, height)}
onOptionsPress={onOptionsPress}
showOptions={!item.callFeed.isLocal()}
localVolume={localVolume}
isFullscreen={isFullscreen}
onFullscreen={() => onFullscreen(item)}
{...rest}
/>
{videoTileSettingsModalState.isOpen && (