Skip to content

Commit b5b41ee

Browse files
committed
feat: buttons to mute mic and toggle video
1 parent fc0461c commit b5b41ee

File tree

2 files changed

+128
-21
lines changed

2 files changed

+128
-21
lines changed

src/App.tsx

Lines changed: 128 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef, useState, useCallback, useMemo } from "react";
1+
import { useRef, useState, useCallback, useMemo, useEffect } from "react";
22

33
import { CallsManager, CallState } from "~/lib/calls";
44

@@ -7,36 +7,108 @@ import FullscreenVideo from "~/components/FullscreenVideo";
77
import EndCallButton from "~/components/EndCallButton";
88
import AvatarPlaceholder from "~/components/AvatarPlaceholder";
99
import AvatarImage from "~/components/AvatarImage";
10+
import Button from "~/components/Button";
11+
import MaterialSymbolsVideocam from "~icons/material-symbols/videocam";
12+
import MaterialSymbolsVideocamOff from "~icons/material-symbols/videocam-off";
13+
import MaterialSymbolsMic from "~icons/material-symbols/mic";
14+
import MaterialSymbolsMicOff from "~icons/material-symbols/mic-off";
1015

1116
import "./App.css";
1217

1318
export default function App() {
1419
const [state, setState] = useState<CallState>(CallsManager.initialState);
1520
const outVidRef = useRef<HTMLVideoElement | null>(null);
1621
const incVidRef = useRef<HTMLVideoElement | null>(null);
17-
const manager = useMemo(() => {
18-
const outStreamPromise = (async () => {
19-
try {
20-
return await navigator.mediaDevices.getUserMedia({
21-
video: true,
22-
audio: true,
23-
});
24-
} catch (error) {
25-
console.warn("Failed to getUserMedia with video, will try just audio");
26-
return await navigator.mediaDevices.getUserMedia({
27-
audio: true,
28-
});
29-
}
30-
})();
22+
23+
const [isOutAudioEnabled, setIsOutAudioEnabled] = useState(true);
24+
const [isOutVideoEnabled, setIsOutVideoEnabled] = useState(true);
25+
const isOutAudioEnabledRef = useRef(isOutAudioEnabled);
26+
isOutAudioEnabledRef.current = isOutAudioEnabled;
27+
const isOutVideoEnabledRef = useRef(isOutVideoEnabled);
28+
isOutVideoEnabledRef.current = isOutVideoEnabled;
29+
30+
const outStreamPromise = useMemo(async () => {
31+
let stream: MediaStream;
32+
try {
33+
stream = await navigator.mediaDevices.getUserMedia({
34+
video: true,
35+
audio: true,
36+
});
37+
} catch (error) {
38+
console.warn("Failed to getUserMedia with video, will try just audio");
39+
stream = await navigator.mediaDevices.getUserMedia({
40+
audio: true,
41+
});
42+
}
43+
44+
// Make sure to set the initial `enabled` values
45+
// before we even from this async function,
46+
// to make sure that we don't accidentally send a frame or two.
47+
stream
48+
.getAudioTracks()
49+
.forEach((t) => (t.enabled = isOutAudioEnabledRef.current));
50+
stream
51+
.getVideoTracks()
52+
.forEach((t) => (t.enabled = isOutVideoEnabledRef.current));
53+
54+
return stream;
55+
}, []);
56+
const [outStream, setOutStream] = useState<MediaStream | null>(null);
57+
useEffect(() => {
58+
let outdated = false;
59+
60+
setOutStream(null);
3161
outStreamPromise.then((s) => {
32-
outVidRef.current!.srcObject = s;
62+
if (!outdated) {
63+
setOutStream(s);
64+
}
3365
});
3466

67+
return () => {
68+
outdated = true;
69+
};
70+
}, [outStreamPromise]);
71+
72+
if (
73+
outStream &&
74+
outVidRef.current &&
75+
outVidRef.current.srcObject !== outStream
76+
) {
77+
outVidRef.current.srcObject = outStream;
78+
}
79+
80+
const manager = useMemo(() => {
3581
const onIncStream = (incStream: MediaStream) => {
3682
incVidRef.current!.srcObject = incStream;
3783
};
3884
return new CallsManager(outStreamPromise, onIncStream, setState);
39-
}, []);
85+
}, [outStreamPromise]);
86+
87+
useEffect(() => {
88+
if (outStream == undefined) {
89+
return;
90+
}
91+
92+
const enableOrDisableTracks = () => {
93+
outStream
94+
.getAudioTracks()
95+
.forEach((t) => (t.enabled = isOutAudioEnabled));
96+
outStream
97+
.getVideoTracks()
98+
.forEach((t) => (t.enabled = isOutVideoEnabled));
99+
};
100+
enableOrDisableTracks();
101+
102+
outStream.addEventListener("addtrack", enableOrDisableTracks);
103+
outStream.addEventListener("removetrack", enableOrDisableTracks);
104+
return () => {
105+
outStream.removeEventListener("addtrack", enableOrDisableTracks);
106+
outStream.removeEventListener("removetrack", enableOrDisableTracks);
107+
};
108+
}, [outStream, isOutAudioEnabled, isOutVideoEnabled]);
109+
110+
const outStreamHasVideoTrack =
111+
outStream == undefined || outStream.getVideoTracks().length >= 1;
40112

41113
const endCall = useCallback(() => {
42114
manager.endCall();
@@ -55,6 +127,18 @@ export default function App() {
55127
height: "100%",
56128
};
57129

130+
const toggleAudioLabel = isOutAudioEnabled
131+
? "Mute microphone"
132+
: "Unmute microphone";
133+
const toggleVideoLabel = isOutVideoEnabled ? "Stop camera" : "Start camera";
134+
135+
const buttonsStyle = {
136+
color: "white",
137+
borderRadius: "50%",
138+
fontSize: "1.5em",
139+
margin: "0 1rem",
140+
};
141+
58142
return (
59143
<div style={{ height: "100vh", overflow: "hidden" }}>
60144
<div style={containerStyle}>
@@ -97,7 +181,33 @@ export default function App() {
97181
textAlign: "center",
98182
}}
99183
>
100-
<EndCallButton onClick={endCall} />
184+
<Button
185+
aria-label={toggleAudioLabel}
186+
title={toggleAudioLabel}
187+
onClick={() => setIsOutAudioEnabled((v) => !v)}
188+
style={buttonsStyle}
189+
>
190+
{isOutAudioEnabled ? (
191+
<MaterialSymbolsMic />
192+
) : (
193+
<MaterialSymbolsMicOff />
194+
)}
195+
</Button>
196+
{outStreamHasVideoTrack && (
197+
<Button
198+
aria-label={toggleVideoLabel}
199+
title={toggleVideoLabel}
200+
onClick={() => setIsOutVideoEnabled((v) => !v)}
201+
style={buttonsStyle}
202+
>
203+
{isOutVideoEnabled ? (
204+
<MaterialSymbolsVideocam />
205+
) : (
206+
<MaterialSymbolsVideocamOff />
207+
)}
208+
</Button>
209+
)}
210+
<EndCallButton onClick={endCall} style={buttonsStyle} />
101211
</div>
102212
</div>
103213
);

src/components/EndCallButton.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import MaterialSymbolsCallEnd from "~icons/material-symbols/call-end";
33
import Button from "~/components/Button";
44

55
const containerStyle = {
6-
color: "white",
76
background: "#cb2233",
8-
borderRadius: "50%",
9-
fontSize: "1.5em",
107
};
118

129
interface Props {

0 commit comments

Comments
 (0)