Add support for screen-sharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
parent
9af122b96e
commit
305c2cb806
5 changed files with 127 additions and 54 deletions
|
|
@ -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%;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) && (
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue