1
- import { useRef , useState , useCallback , useMemo } from "react" ;
1
+ import { useRef , useState , useCallback , useMemo , useEffect } from "react" ;
2
2
3
3
import { CallsManager , CallState } from "~/lib/calls" ;
4
4
@@ -7,36 +7,108 @@ import FullscreenVideo from "~/components/FullscreenVideo";
7
7
import EndCallButton from "~/components/EndCallButton" ;
8
8
import AvatarPlaceholder from "~/components/AvatarPlaceholder" ;
9
9
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" ;
10
15
11
16
import "./App.css" ;
12
17
13
18
export default function App ( ) {
14
19
const [ state , setState ] = useState < CallState > ( CallsManager . initialState ) ;
15
20
const outVidRef = useRef < HTMLVideoElement | null > ( null ) ;
16
21
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 ) ;
31
61
outStreamPromise . then ( ( s ) => {
32
- outVidRef . current ! . srcObject = s ;
62
+ if ( ! outdated ) {
63
+ setOutStream ( s ) ;
64
+ }
33
65
} ) ;
34
66
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 ( ( ) => {
35
81
const onIncStream = ( incStream : MediaStream ) => {
36
82
incVidRef . current ! . srcObject = incStream ;
37
83
} ;
38
84
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 ;
40
112
41
113
const endCall = useCallback ( ( ) => {
42
114
manager . endCall ( ) ;
@@ -55,6 +127,18 @@ export default function App() {
55
127
height : "100%" ,
56
128
} ;
57
129
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
+
58
142
return (
59
143
< div style = { { height : "100vh" , overflow : "hidden" } } >
60
144
< div style = { containerStyle } >
@@ -97,7 +181,33 @@ export default function App() {
97
181
textAlign : "center" ,
98
182
} }
99
183
>
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 } />
101
211
</ div >
102
212
</ div >
103
213
) ;
0 commit comments