feat: system/tab audio on supported systems

This commit is contained in:
Jannis Mattheis
2025-05-07 23:44:20 +02:00
parent 12a3e5c3d4
commit 3f33538816
2 changed files with 120 additions and 48 deletions

View File

@@ -1,9 +1,11 @@
import React, {useCallback} from 'react'; import React, {useCallback} from 'react';
import {Badge, IconButton, Paper, Tooltip, Typography} from '@mui/material'; import {Badge, Box, IconButton, Paper, Tooltip, Typography, Slider, Stack} from '@mui/material';
import CancelPresentationIcon from '@mui/icons-material/CancelPresentation'; import CancelPresentationIcon from '@mui/icons-material/CancelPresentation';
import PresentToAllIcon from '@mui/icons-material/PresentToAll'; import PresentToAllIcon from '@mui/icons-material/PresentToAll';
import FullScreenIcon from '@mui/icons-material/Fullscreen'; import FullScreenIcon from '@mui/icons-material/Fullscreen';
import PeopleIcon from '@mui/icons-material/People'; import PeopleIcon from '@mui/icons-material/People';
import VolumeMuteIcon from '@mui/icons-material/VolumeOff';
import VolumeIcon from '@mui/icons-material/VolumeUp';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import {useHotkeys} from 'react-hotkeys-hook'; import {useHotkeys} from 'react-hotkeys-hook';
import {Video} from './Video'; import {Video} from './Video';
@@ -97,7 +99,17 @@ export const Room = ({
React.useEffect(() => { React.useEffect(() => {
if (videoElement && stream) { if (videoElement && stream) {
videoElement.srcObject = stream; videoElement.srcObject = stream;
videoElement.play().catch((e) => console.log('Could not play main video', e)); videoElement.play().catch((err) => {
console.log('Could not play main video', err);
if (err.name === 'NotAllowedError') {
videoElement.muted = true;
videoElement
.play()
.catch((retryErr) =>
console.log('Could not play main video with mute', retryErr)
);
}
});
} }
}, [videoElement, stream]); }, [videoElement, stream]);
@@ -161,6 +173,15 @@ export const Room = ({
}, },
[state.clientStreams, selectedStream] [state.clientStreams, selectedStream]
); );
useHotkeys(
'm',
() => {
if (videoElement) {
videoElement.muted = !videoElement.muted;
}
},
[videoElement]
);
const videoClasses = () => { const videoClasses = () => {
switch (settings.displayMode) { switch (settings.displayMode) {
@@ -194,7 +215,6 @@ export const Room = ({
{stream ? ( {stream ? (
<video <video
muted
ref={setVideoElement} ref={setVideoElement}
className={videoClasses()} className={videoClasses()}
onDoubleClick={handleFullscreen} onDoubleClick={handleFullscreen}
@@ -217,53 +237,58 @@ export const Room = ({
{controlVisible && ( {controlVisible && (
<Paper className={classes.control} elevation={10} {...setHoverState}> <Paper className={classes.control} elevation={10} {...setHoverState}>
{state.hostStream ? ( {(stream?.getAudioTracks().length ?? 0) > 0 && videoElement && (
<Tooltip title="Cancel Presentation" arrow> <AudioControl video={videoElement} />
<IconButton onClick={stopShare} size="large">
<CancelPresentationIcon fontSize="large" />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Start Presentation" arrow>
<IconButton onClick={share} size="large">
<PresentToAllIcon fontSize="large" />
</IconButton>
</Tooltip>
)} )}
<Box whiteSpace="nowrap">
{state.hostStream ? (
<Tooltip title="Cancel Presentation" arrow>
<IconButton onClick={stopShare} size="large">
<CancelPresentationIcon fontSize="large" />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Start Presentation" arrow>
<IconButton onClick={share} size="large">
<PresentToAllIcon fontSize="large" />
</IconButton>
</Tooltip>
)}
<Tooltip <Tooltip
classes={{tooltip: classes.noMaxWidth}} classes={{tooltip: classes.noMaxWidth}}
title={ title={
<div> <div>
<Typography variant="h5">Member List</Typography> <Typography variant="h5">Member List</Typography>
{state.users.map((user) => ( {state.users.map((user) => (
<Typography key={user.id}> <Typography key={user.id}>
{user.name} {flags(user)} {user.name} {flags(user)}
</Typography> </Typography>
))} ))}
</div> </div>
} }
arrow arrow
>
<Badge badgeContent={state.users.length} color="primary">
<PeopleIcon fontSize="large" />
</Badge>
</Tooltip>
<Tooltip title="Fullscreen" arrow>
<IconButton
onClick={() => handleFullscreen()}
disabled={!selectedStream}
size="large"
> >
<FullScreenIcon fontSize="large" /> <Badge badgeContent={state.users.length} color="primary">
</IconButton> <PeopleIcon fontSize="large" />
</Tooltip> </Badge>
</Tooltip>
<Tooltip title="Fullscreen" arrow>
<IconButton
onClick={() => handleFullscreen()}
disabled={!selectedStream}
size="large"
>
<FullScreenIcon fontSize="large" />
</IconButton>
</Tooltip>
<Tooltip title="Settings" arrow> <Tooltip title="Settings" arrow>
<IconButton onClick={() => setOpen(true)} size="large"> <IconButton onClick={() => setOpen(true)} size="large">
<SettingsIcon fontSize="large" /> <SettingsIcon fontSize="large" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box>
</Paper> </Paper>
)} )}
@@ -353,6 +378,40 @@ const useShowOnMouseMovement = (doShow: (s: boolean) => void) => {
); );
}; };
const AudioControl = ({video}: {video: FullScreenHTMLVideoElement}) => {
// this is used to force a rerender
const [, setMuted] = React.useState<boolean>();
React.useEffect(() => {
const handler = () => setMuted(video.muted);
video.addEventListener('volumechange', handler);
setMuted(video.muted);
return () => video.removeEventListener('volumechange', handler);
});
return (
<Stack spacing={0.5} pr={2} direction="row" sx={{alignItems: 'center', my: 1, height: 35}}>
<IconButton size="large" onClick={() => (video.muted = !video.muted)}>
{video.muted ? (
<VolumeMuteIcon fontSize="large" />
) : (
<VolumeIcon fontSize="large" />
)}
</IconButton>
<Slider
min={0}
max={1}
step={0.01}
defaultValue={video.volume}
onChange={(_, newVolume) => {
video.muted = false;
video.volume = newVolume;
}}
/>
</Stack>
);
};
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
title: { title: {
padding: 15, padding: 15,

View File

@@ -143,10 +143,15 @@ const clientSession = async ({
done(); done();
} }
}; };
let notified = false;
const stream = new MediaStream();
peer.ontrack = (event) => { peer.ontrack = (event) => {
const stream = new MediaStream();
stream.addTrack(event.track); stream.addTrack(event.track);
onTrack(stream); if (!notified) {
notified = true;
onTrack(stream);
}
}; };
return peer; return peer;
@@ -332,6 +337,14 @@ export const useRoom = (config: UIConfig): UseRoom => {
try { try {
stream.current = await navigator.mediaDevices.getDisplayMedia({ stream.current = await navigator.mediaDevices.getDisplayMedia({
video: {frameRate: loadSettings().framerate}, video: {frameRate: loadSettings().framerate},
audio: {
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false,
// https://medium.com/@trystonperry/why-is-getdisplaymedias-audio-quality-so-bad-b49ba9cfaa83
// @ts-expect-error
googAutoGainControl: false,
},
}); });
} catch (e) { } catch (e) {
console.log('Could not getDisplayMedia', e); console.log('Could not getDisplayMedia', e);