Ezmary commited on
Commit
233e6fa
·
verified ·
1 Parent(s): ebcc2a0

Update src/components/control-tray/ControlTray.tsx

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -1,11 +1,10 @@
1
  // src/components/control-tray/ControlTray.tsx
2
 
3
  import cn from "classnames";
4
- import React, { memo, RefObject, useEffect, useState, useCallback } from "react";
5
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
6
  import { AudioRecorder } from "../../lib/audio-recorder";
7
- import Logo from '../logo/Logo';
8
- import { pauseIcon, microphoneIcon, cameraIcon, stopCamIcon } from '../icons';
9
 
10
  const SvgSwitchCameraIcon = () => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-[22px] h-[22px]"><path d="M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/><path d="M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"/><path d="m17 12-3-3 3-3"/><path d="m7 12 3 3-3 3"/></svg>;
11
 
@@ -19,40 +18,59 @@ export type ControlTrayProps = {
19
  onAppCamToggle: (active: boolean) => void;
20
  currentFacingMode: 'user' | 'environment';
21
  onFacingModeChange: (mode: 'user' | 'environment') => void;
 
22
  };
23
 
24
- const ControlTray: React.FC<ControlTrayProps> = ({
25
- videoRef,
26
- onVideoStreamChange,
27
- supportsVideo,
28
- isAppMicActive,
29
- onAppMicToggle,
30
- isAppCamActive,
31
- onAppCamToggle,
32
- currentFacingMode,
33
- onFacingModeChange,
34
- }) => {
35
- const { client, connected, connect, volume } = useLiveAPIContext();
36
- const [audioRecorder] = useState(() => new AudioRecorder());
37
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
38
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
39
  const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  useEffect(() => {
42
  if (videoRef.current) {
43
  if (videoRef.current.srcObject !== activeLocalVideoStream) {
44
  videoRef.current.srcObject = activeLocalVideoStream;
45
- if (activeLocalVideoStream) {
46
- videoRef.current.play().catch(e => console.warn("Video play failed:", e));
47
- }
48
  }
49
  }
50
  }, [activeLocalVideoStream, videoRef]);
51
 
52
  const stopWebcam = useCallback(() => {
53
- if (activeLocalVideoStream) {
54
- activeLocalVideoStream.getTracks().forEach(track => track.stop());
55
- }
56
  setActiveLocalVideoStream(null);
57
  onVideoStreamChange(null);
58
  }, [activeLocalVideoStream, onVideoStreamChange]);
@@ -60,11 +78,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({
60
  const startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment') => {
61
  if (isSwitchingCamera) return;
62
  setIsSwitchingCamera(true);
63
-
64
- if (activeLocalVideoStream) {
65
- activeLocalVideoStream.getTracks().forEach(track => track.stop());
66
- }
67
-
68
  try {
69
  const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
70
  setActiveLocalVideoStream(mediaStream);
@@ -78,13 +92,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({
78
  } finally {
79
  setIsSwitchingCamera(false);
80
  }
81
- }, [
82
- isSwitchingCamera,
83
- activeLocalVideoStream,
84
- onVideoStreamChange,
85
- onFacingModeChange,
86
- onAppCamToggle,
87
- ]);
88
 
89
  useEffect(() => {
90
  if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
@@ -94,26 +102,6 @@ const ControlTray: React.FC<ControlTrayProps> = ({
94
  }
95
  }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, stopWebcam, currentFacingMode]);
96
 
97
-
98
- useEffect(() => {
99
- const onData = (base64: string) => {
100
- if (client && connected && isAppMicActive) {
101
- client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
102
- }
103
- };
104
- if (connected && isAppMicActive && audioRecorder) {
105
- audioRecorder.on("data", onData).start();
106
- } else if (audioRecorder && audioRecorder.recording) {
107
- audioRecorder.stop();
108
- }
109
- return () => {
110
- if (audioRecorder) {
111
- audioRecorder.off("data", onData);
112
- if (audioRecorder.recording) audioRecorder.stop();
113
- }
114
- };
115
- }, [connected, client, isAppMicActive, audioRecorder]);
116
-
117
  useEffect(() => {
118
  let timeoutId = -1;
119
  function sendVideoFrame() {
@@ -138,13 +126,9 @@ const ControlTray: React.FC<ControlTrayProps> = ({
138
  }
139
  } catch (error) { console.error("❌ Error frame:", error); }
140
  }
141
- if (connected && activeLocalVideoStream) {
142
- timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
143
- }
144
- }
145
- if (connected && activeLocalVideoStream && videoRef.current) {
146
- timeoutId = window.setTimeout(sendVideoFrame, 200);
147
  }
 
148
  return () => clearTimeout(timeoutId);
149
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
150
 
@@ -168,15 +152,9 @@ const ControlTray: React.FC<ControlTrayProps> = ({
168
  const handleCamToggle = async () => {
169
  if (isSwitchingCamera) return;
170
  const newCamState = !isAppCamActive;
171
-
172
  if (newCamState) {
173
- if (!(await ensureConnectedAndReady())) {
174
- onAppCamToggle(false);
175
- return;
176
- }
177
- if (!isAppMicActive) {
178
- onAppMicToggle(true);
179
- }
180
  onAppCamToggle(true);
181
  } else {
182
  onAppCamToggle(false);
@@ -186,13 +164,10 @@ const ControlTray: React.FC<ControlTrayProps> = ({
186
  const handleSwitchCamera = async () => {
187
  if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
188
  setIsSwitchingCamera(true);
189
-
190
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
191
-
192
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
193
  setActiveLocalVideoStream(null);
194
  onVideoStreamChange(null);
195
-
196
  try {
197
  const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
198
  setActiveLocalVideoStream(newStream);
@@ -201,7 +176,6 @@ const ControlTray: React.FC<ControlTrayProps> = ({
201
  } catch (error) {
202
  console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error);
203
  try {
204
- console.log(`Attempting to restore to ${currentFacingMode}`);
205
  const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: {exact: currentFacingMode } }, audio: false });
206
  setActiveLocalVideoStream(restoredStream);
207
  onVideoStreamChange(restoredStream);
@@ -219,50 +193,15 @@ const ControlTray: React.FC<ControlTrayProps> = ({
219
  return (
220
  <footer id="footer-controls" className="footer-controls-html-like">
221
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
222
-
223
- <div
224
- id="mic-button"
225
- className="control-button mic-button-color"
226
- onClick={handleMicToggle}
227
- >
228
- {isAppMicActive ? pauseIcon : microphoneIcon}
229
  </div>
230
-
231
- {(isAppMicActive || isAppCamActive) && (
232
- <div
233
- id="small-logo-footer-container"
234
- className={cn("small-logo-footer-html-like", {
235
- 'user-is-speaking-pulse': isAppMicActive,
236
- })}
237
- >
238
- <Logo
239
- isMini={true}
240
- isActive={true}
241
- isAi={false}
242
- speakingVolume={volume}
243
- />
244
- </div>
245
- )}
246
-
247
  <div id="cam-button-wrapper" className="control-button-wrapper cam-wrapper-html-like">
248
- <div
249
- id="cam-button"
250
- className="control-button cam-button-color"
251
- onClick={handleCamToggle}
252
- >
253
  {isAppCamActive ? stopCamIcon : cameraIcon}
254
  </div>
255
- <div
256
- id="switch-camera-button-container"
257
- className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}
258
- >
259
- <button
260
- id="switch-camera-button"
261
- aria-label="Switch Camera"
262
- className="switch-camera-button-content group"
263
- onClick={handleSwitchCamera}
264
- disabled={!isAppCamActive || isSwitchingCamera}
265
- >
266
  <SvgSwitchCameraIcon/>
267
  </button>
268
  </div>
 
1
  // src/components/control-tray/ControlTray.tsx
2
 
3
  import cn from "classnames";
4
+ import React, { memo, RefObject, useEffect, useState, useCallback, useRef } from "react";
5
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
6
  import { AudioRecorder } from "../../lib/audio-recorder";
7
+ import { PauseIconWithPulse, microphoneIcon, cameraIcon, stopCamIcon } from '../icons';
 
8
 
9
  const SvgSwitchCameraIcon = () => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-[22px] h-[22px]"><path d="M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/><path d="M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"/><path d="m17 12-3-3 3-3"/><path d="m7 12 3 3-3 3"/></svg>;
10
 
 
18
  onAppCamToggle: (active: boolean) => void;
19
  currentFacingMode: 'user' | 'environment';
20
  onFacingModeChange: (mode: 'user' | 'environment') => void;
21
+ onUserSpeakingChange: (isSpeaking: boolean) => void;
22
  };
23
 
24
+ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onVideoStreamChange, supportsVideo, isAppMicActive, onAppMicToggle, isAppCamActive, onAppCamToggle, currentFacingMode, onFacingModeChange, onUserSpeakingChange }) => {
25
+ const { client, connected, connect } = useLiveAPIContext();
26
+ const audioRecorderRef = useRef<AudioRecorder | null>(null);
 
 
 
 
 
 
 
 
 
 
27
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
28
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
29
  const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
30
+ const [userVolume, setUserVolume] = useState(0);
31
 
32
+ useEffect(() => {
33
+ if (!audioRecorderRef.current) {
34
+ audioRecorderRef.current = new AudioRecorder();
35
+ }
36
+ const audioRecorder = audioRecorderRef.current;
37
+
38
+ const onData = (base64: string, volume: number) => {
39
+ if (client && connected && isAppMicActive) {
40
+ client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
41
+ setUserVolume(volume);
42
+ onUserSpeakingChange(volume > 0.01);
43
+ }
44
+ };
45
+ const onStop = () => {
46
+ setUserVolume(0);
47
+ onUserSpeakingChange(false);
48
+ }
49
+
50
+ if (connected && isAppMicActive) {
51
+ audioRecorder.on("data", onData).on("stop", onStop).start();
52
+ } else {
53
+ if (audioRecorder.recording) audioRecorder.stop();
54
+ }
55
+ return () => {
56
+ if (audioRecorder) {
57
+ audioRecorder.off("data", onData).off("stop", onStop);
58
+ if (audioRecorder.recording) audioRecorder.stop();
59
+ }
60
+ };
61
+ }, [connected, client, isAppMicActive, onUserSpeakingChange]);
62
+
63
  useEffect(() => {
64
  if (videoRef.current) {
65
  if (videoRef.current.srcObject !== activeLocalVideoStream) {
66
  videoRef.current.srcObject = activeLocalVideoStream;
67
+ if (activeLocalVideoStream) videoRef.current.play().catch(e => console.warn("Video play failed:", e));
 
 
68
  }
69
  }
70
  }, [activeLocalVideoStream, videoRef]);
71
 
72
  const stopWebcam = useCallback(() => {
73
+ if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop());
 
 
74
  setActiveLocalVideoStream(null);
75
  onVideoStreamChange(null);
76
  }, [activeLocalVideoStream, onVideoStreamChange]);
 
78
  const startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment') => {
79
  if (isSwitchingCamera) return;
80
  setIsSwitchingCamera(true);
81
+ if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop());
 
 
 
 
82
  try {
83
  const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
84
  setActiveLocalVideoStream(mediaStream);
 
92
  } finally {
93
  setIsSwitchingCamera(false);
94
  }
95
+ }, [isSwitchingCamera, activeLocalVideoStream, onVideoStreamChange, onFacingModeChange, onAppCamToggle]);
 
 
 
 
 
 
96
 
97
  useEffect(() => {
98
  if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
 
102
  }
103
  }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, stopWebcam, currentFacingMode]);
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  useEffect(() => {
106
  let timeoutId = -1;
107
  function sendVideoFrame() {
 
126
  }
127
  } catch (error) { console.error("❌ Error frame:", error); }
128
  }
129
+ if (connected && activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
 
 
 
 
 
130
  }
131
+ if (connected && activeLocalVideoStream && videoRef.current) timeoutId = window.setTimeout(sendVideoFrame, 200);
132
  return () => clearTimeout(timeoutId);
133
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
134
 
 
152
  const handleCamToggle = async () => {
153
  if (isSwitchingCamera) return;
154
  const newCamState = !isAppCamActive;
 
155
  if (newCamState) {
156
+ if (!(await ensureConnectedAndReady())) { onAppCamToggle(false); return; }
157
+ if (!isAppMicActive) onAppMicToggle(true);
 
 
 
 
 
158
  onAppCamToggle(true);
159
  } else {
160
  onAppCamToggle(false);
 
164
  const handleSwitchCamera = async () => {
165
  if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
166
  setIsSwitchingCamera(true);
 
167
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
 
168
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
169
  setActiveLocalVideoStream(null);
170
  onVideoStreamChange(null);
 
171
  try {
172
  const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
173
  setActiveLocalVideoStream(newStream);
 
176
  } catch (error) {
177
  console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error);
178
  try {
 
179
  const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: {exact: currentFacingMode } }, audio: false });
180
  setActiveLocalVideoStream(restoredStream);
181
  onVideoStreamChange(restoredStream);
 
193
  return (
194
  <footer id="footer-controls" className="footer-controls-html-like">
195
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
196
+ <div id="mic-button" className="control-button mic-button-color" onClick={handleMicToggle}>
197
+ {isAppMicActive ? <PauseIconWithPulse userVolume={userVolume} /> : microphoneIcon}
 
 
 
 
 
198
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  <div id="cam-button-wrapper" className="control-button-wrapper cam-wrapper-html-like">
200
+ <div id="cam-button" className="control-button cam-button-color" onClick={handleCamToggle}>
 
 
 
 
201
  {isAppCamActive ? stopCamIcon : cameraIcon}
202
  </div>
203
+ <div id="switch-camera-button-container" className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}>
204
+ <button id="switch-camera-button" aria-label="Switch Camera" className="switch-camera-button-content group" onClick={handleSwitchCamera} disabled={!isAppCamActive || isSwitchingCamera}>
 
 
 
 
 
 
 
 
 
205
  <SvgSwitchCameraIcon/>
206
  </button>
207
  </div>