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
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; margin-right: 0px;
} }
.footerFullscreen {
position: absolute;
width: 100%;
bottom: 0;
}
.avatar { .avatar {
position: absolute; position: absolute;
top: 50%; top: 50%;

View file

@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo, useRef } from "react";
import { usePreventScroll } from "@react-aria/overlays"; import { usePreventScroll } from "@react-aria/overlays";
import { GroupCall, MatrixClient } from "matrix-js-sdk"; import { GroupCall, MatrixClient } from "matrix-js-sdk";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames";
import styles from "./InCallView.module.css"; import styles from "./InCallView.module.css";
import { import {
@ -46,6 +47,7 @@ import { useMediaHandler } from "../settings/useMediaHandler";
import { useShowInspector } from "../settings/useSetting"; import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
import { useAudioContext } from "../video-grid/useMediaStream"; import { useAudioContext } from "../video-grid/useMediaStream";
import { useFullscreen } from "../video-grid/useFullscreen";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -72,7 +74,7 @@ interface Props {
roomId: string; roomId: string;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
} }
interface Participant { export interface Participant {
id: string; id: string;
callFeed: CallFeed; callFeed: CallFeed;
focused: boolean; focused: boolean;
@ -100,7 +102,9 @@ export function InCallView({
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
}: Props) { }: Props) {
usePreventScroll(); usePreventScroll();
const elementRef = useRef<HTMLDivElement>();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0); const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
const { toggleFullscreen, fullscreenParticipant } = useFullscreen(elementRef);
const [audioContext, audioDestination, audioRef] = useAudioContext(); const [audioContext, audioDestination, audioRef] = useAudioContext();
const { audioOutput } = useMediaHandler(); const { audioOutput } = useMediaHandler();
@ -161,14 +165,73 @@ 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 { const {
modalState: rageshakeRequestModalState, modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps, modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId); } = useRageshakeRequestModal(groupCall.room.roomId);
const footerClassNames = classNames(styles.footer, {
[styles.footerFullscreen]: fullscreenParticipant,
});
return ( return (
<div className={styles.inRoom}> <div className={styles.inRoom} ref={elementRef}>
<audio ref={audioRef} /> <audio ref={audioRef} />
{!fullscreenParticipant && (
<Header> <Header>
<LeftNav> <LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} /> <RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
@ -182,36 +245,18 @@ export function InCallView({
<UserMenuContainer preventNavigation /> <UserMenuContainer preventNavigation />
</RightNav> </RightNav>
</Header> </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}
/>
)} )}
</VideoGrid> {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 && ( {canScreenshare && !isSafari && !fullscreenParticipant && (
<ScreenshareButton <ScreenshareButton
enabled={isScreensharing} enabled={isScreensharing}
onPress={toggleScreensharing} onPress={toggleScreensharing}
/> />
)} )}
{!fullscreenParticipant && (
<OverflowMenu <OverflowMenu
inCall inCall
roomId={roomId} roomId={roomId}
@ -220,6 +265,7 @@ export function InCallView({
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps} feedbackModalProps={feedbackModalProps}
/> />
)}
<HangupButton onPress={onLeave} /> <HangupButton onPress={onLeave} />
</div> </div>
<GroupCallInspector <GroupCallInspector

View file

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

View file

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

View file

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