Skip to content

Commit f5bb323

Browse files
Log device-change events and optimize banners
Wire up stage event logging for device changes and improve device banner behavior. - Import and use useStageEventLogger in FixAV and VideoCall, and call logEvent on user device selections. - Propagate logEvent through useDeviceAlignment and add detailed device-changed events for user-select, auto-fallback, alignment, and recovery flows. - Clean up speaker label handling and remove noisy console logs. - Optimize useDeviceBanners.clearBannersForDevice to return the same array reference when nothing is removed (prevents unnecessary re-renders) and ensure timers for removed banners are cleared. Files updated: FixAV.jsx, VideoCall.jsx, useDeviceAlignment.js, useDeviceBanners.js.
1 parent 7bfc303 commit f5bb323

4 files changed

Lines changed: 89 additions & 31 deletions

File tree

client/src/call/FixAV.jsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
generateRecoverySummary,
2222
} from "./utils/avRecovery";
2323
import { findMatchingDevice } from "./utils/deviceAlignment";
24+
import { useStageEventLogger } from "./hooks/eventLogger";
2425

2526
/**
2627
* Collects comprehensive diagnostic data for A/V troubleshooting.
@@ -285,6 +286,7 @@ export function useFixAV(
285286
const [audioLevel, setAudioLevel] = useState(0);
286287
const [speakerError, setSpeakerError] = useState(null);
287288
const audioRef = useRef(null);
289+
const logEvent = useStageEventLogger();
288290

289291
// ------------------- audio level monitoring for mic picker ---------------------
290292
useAudioLevelObserver(
@@ -313,10 +315,11 @@ export function useFixAV(
313315
message: `Camera changed to "${label || selectedId}"`,
314316
level: "info",
315317
});
318+
logEvent("device-changed", { deviceType: "camera", reason: "user-select-fixav", toId: selectedId, toLabel: label });
316319
} catch (err) {
317320
console.warn("[FixAV] Failed to set camera:", err);
318321
}
319-
}, [devices, player]);
322+
}, [devices, player, logEvent]);
320323

321324
const handleMicChange = useCallback(async (e) => {
322325
const selectedId = e.target.value;
@@ -336,10 +339,11 @@ export function useFixAV(
336339
message: `Microphone changed to "${label || selectedId}"`,
337340
level: "info",
338341
});
342+
logEvent("device-changed", { deviceType: "microphone", reason: "user-select-fixav", toId: selectedId, toLabel: label });
339343
} catch (err) {
340344
console.warn("[FixAV] Failed to set microphone:", err);
341345
}
342-
}, [devices, player]);
346+
}, [devices, player, logEvent]);
343347

344348
const handleSpeakerChange = useCallback(async (e) => {
345349
const selectedId = e.target.value;
@@ -360,6 +364,7 @@ export function useFixAV(
360364
message: `Speaker changed to "${label || selectedId}"`,
361365
level: "info",
362366
});
367+
logEvent("device-changed", { deviceType: "speaker", reason: "user-select-fixav", toId: selectedId, toLabel: label });
363368
} catch (err) {
364369
const isGestureGated =
365370
err?.name === "NotAllowedError" ||
@@ -371,7 +376,7 @@ export function useFixAV(
371376
console.warn("[FixAV] Failed to set speaker:", err);
372377
}
373378
}
374-
}, [devices, player]);
379+
}, [devices, player, logEvent]);
375380

376381
const handleTestSound = useCallback(() => {
377382
if (audioRef.current) {

client/src/call/VideoCall.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414

1515
import { Tray } from "./Tray";
1616
import { Call } from "./Call";
17-
import { useDailyEventLogger } from "./hooks/eventLogger";
17+
import { useDailyEventLogger, useStageEventLogger } from "./hooks/eventLogger";
1818
import { useAutoDiagnostics } from "./hooks/useAutoDiagnostics";
1919
import { useAudioContextMonitor } from "./hooks/useAudioContextMonitor";
2020
import { useDisplayNameSync } from "./hooks/useDisplayNameSync";
@@ -155,6 +155,8 @@ export function VideoCall({
155155
deviceBanners, addDeviceBanner, clearDeviceBanner, clearBannersForDevice,
156156
} = useDeviceBanners();
157157

158+
const logEvent = useStageEventLogger();
159+
158160
// ------------------- device errors + Daily event listeners ---------------------
159161
const {
160162
cameraError, setCameraError,
@@ -208,7 +210,7 @@ export function VideoCall({
208210
{ setCameraError, setMicError, setSpeakerError },
209211
{ handleSetupFailure, setPendingGestureOperations, setPendingOperationDetails },
210212
{ cameraError, micError, speakerError },
211-
{ addDeviceBanner, clearBannersForDevice }
213+
{ addDeviceBanner, clearBannersForDevice, logEvent }
212214
);
213215

214216
// ------------------- Fix A/V ref for banner → modal link ---------------------

client/src/call/hooks/useDeviceAlignment.js

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function useDeviceAlignment(
3636
handleSetupFailure, setPendingGestureOperations, setPendingOperationDetails,
3737
} = gestureHandlers;
3838
const { cameraError, micError, speakerError } = errorValues;
39-
const { addDeviceBanner, clearBannersForDevice } = bannerCallbacks;
39+
const { addDeviceBanner, clearBannersForDevice, logEvent } = bannerCallbacks;
4040

4141
const preferredCameraId = player?.get("cameraId") ?? "waiting";
4242
const preferredMicId = player?.get("micId") ?? "waiting";
@@ -72,23 +72,29 @@ export function useDeviceAlignment(
7272
try {
7373
if (deviceType === "camera") {
7474
await callObject.setInputDevicesAsync({ videoDeviceId: deviceId });
75+
const toLabel = devices?.cameras?.find((c) => c.device.deviceId === deviceId)?.device?.label || null;
7576
player?.set("cameraId", deviceId);
7677
loggedUnavailableCameraRef.current = null;
78+
if (logEvent) logEvent("device-changed", { deviceType, reason: "user-select", toId: deviceId, toLabel });
7779
} else if (deviceType === "microphone") {
7880
await callObject.setInputDevicesAsync({ audioDeviceId: deviceId });
81+
const toLabel = devices?.microphones?.find((m) => m.device.deviceId === deviceId)?.device?.label || null;
7982
player?.set("micId", deviceId);
8083
loggedUnavailableMicRef.current = null;
84+
if (logEvent) logEvent("device-changed", { deviceType, reason: "user-select", toId: deviceId, toLabel });
8185
} else if (deviceType === "speaker") {
8286
await devices.setSpeaker(deviceId);
83-
player?.set("speakerId", deviceId);
84-
// Also save the label so future Safari ID-rotation fallback works
8587
const selectedSpeaker = devices?.speakers?.find(
8688
(s) => s.device.deviceId === deviceId
8789
);
90+
const toLabel = selectedSpeaker?.device?.label || null;
91+
player?.set("speakerId", deviceId);
92+
// Also save the label so future Safari ID-rotation fallback works
8893
if (selectedSpeaker) {
89-
player?.set("speakerLabel", selectedSpeaker.device.label);
94+
player?.set("speakerLabel", toLabel);
9095
}
9196
loggedUnavailableSpeakerRef.current = null;
97+
if (logEvent) logEvent("device-changed", { deviceType, reason: "user-select", toId: deviceId, toLabel });
9298
}
9399
// Clear only the error for the device that was just switched
94100
if (deviceType === "camera") setCameraError(null);
@@ -111,7 +117,7 @@ export function useDeviceAlignment(
111117
}
112118
}
113119
},
114-
[callObject, player, devices, handleSetupFailure, setCameraError, setMicError, setSpeakerError]
120+
[callObject, player, devices, handleSetupFailure, setCameraError, setMicError, setSpeakerError, logEvent]
115121
);
116122

117123
// ------------------- alignment effect ---------------------
@@ -183,6 +189,13 @@ export function useDeviceAlignment(
183189
message: `"${prefName}" disconnected — switched to "${fallbackLabel}"`,
184190
});
185191
}
192+
if (logEvent) {
193+
logEvent("device-changed", {
194+
deviceType, reason: "auto-fallback",
195+
toId: targetId, toLabel: fallbackLabel,
196+
preferredId, preferredLabel,
197+
});
198+
}
186199
} catch (err) {
187200
console.error(
188201
`Failed to auto-switch ${deviceType} to fallback`,
@@ -197,19 +210,25 @@ export function useDeviceAlignment(
197210

198211
// Skip if we're already using this device
199212
if (currentDeviceId === targetId) {
200-
// Clear any stale banner if the device is now matched
201-
if (clearBannersForDevice) clearBannersForDevice(deviceType);
202-
// If we previously fell back, clear the ref and show recovery banner
213+
// If we previously fell back, show recovery banner.
214+
// addDeviceBanner internally clears existing banners for this device
215+
// type first, so no separate clearBannersForDevice call is needed.
203216
if (loggedUnavailableRef.current === preferredId) {
204217
// eslint-disable-next-line no-param-reassign
205218
loggedUnavailableRef.current = null;
219+
const deviceName = targetDevice.device.label || preferredLabel || preferredId;
206220
if (addDeviceBanner) {
207-
const deviceName = targetDevice.device.label || preferredLabel || preferredId;
208221
addDeviceBanner({
209222
deviceType,
210223
message: `"${deviceName}" reconnected — switched back`,
211224
});
212225
}
226+
if (logEvent) {
227+
logEvent("device-changed", {
228+
deviceType, reason: "recovery-already-active",
229+
toId: targetId, toLabel: deviceName, preferredId, preferredLabel,
230+
});
231+
}
213232
}
214233
return;
215234
}
@@ -224,9 +243,6 @@ export function useDeviceAlignment(
224243
},
225244
});
226245

227-
console.log(`Setting ${deviceType} via ${matchType} match`, {
228-
preferredId, preferredLabel, targetId, targetLabel: targetDevice.device.label, matchType,
229-
});
230246
// Check if this is a recovery (we previously fell back for this device)
231247
const isRecovery = loggedUnavailableRef.current === preferredId;
232248
// eslint-disable-next-line no-param-reassign
@@ -249,6 +265,16 @@ export function useDeviceAlignment(
249265
message: `"${deviceName}" reconnected — switched back`,
250266
});
251267
}
268+
if (logEvent) {
269+
logEvent("device-changed", {
270+
deviceType,
271+
reason: isRecovery ? "recovery" : "alignment",
272+
matchType,
273+
fromId: currentDeviceId,
274+
toId: targetId,
275+
toLabel: targetDevice.device.label,
276+
});
277+
}
252278
}
253279
} catch (err) {
254280
console.error(`Failed to set ${deviceType} via ${matchType} match`, err);
@@ -327,6 +353,13 @@ export function useDeviceAlignment(
327353
message: `"${prefName}" disconnected — switched to "${fallbackLabel}"`,
328354
});
329355
}
356+
if (logEvent) {
357+
logEvent("device-changed", {
358+
deviceType: "speaker", reason: "auto-fallback",
359+
toId: targetId, toLabel: fallbackLabel,
360+
preferredId: preferredSpeakerId, preferredLabel: preferredSpeakerLabel,
361+
});
362+
}
330363
} catch (err) {
331364
console.error(
332365
"Failed to auto-switch speaker to fallback",
@@ -351,17 +384,24 @@ export function useDeviceAlignment(
351384
}
352385

353386
if (devices?.currentSpeaker?.device?.deviceId === targetId) {
354-
if (clearBannersForDevice) clearBannersForDevice("speaker");
355-
// If we previously fell back, clear the ref and show recovery banner
387+
// If we previously fell back, show recovery banner.
388+
// addDeviceBanner internally clears existing banners for speaker first.
356389
if (loggedUnavailableSpeakerRef.current === preferredSpeakerId) {
357390
loggedUnavailableSpeakerRef.current = null;
391+
const deviceName = targetSpeaker.device.label || preferredSpeakerLabel || preferredSpeakerId;
358392
if (addDeviceBanner) {
359-
const deviceName = targetSpeaker.device.label || preferredSpeakerLabel || preferredSpeakerId;
360393
addDeviceBanner({
361394
deviceType: "speaker",
362395
message: `"${deviceName}" reconnected — switched back`,
363396
});
364397
}
398+
if (logEvent) {
399+
logEvent("device-changed", {
400+
deviceType: "speaker", reason: "recovery-already-active",
401+
toId: targetId, toLabel: deviceName,
402+
preferredId: preferredSpeakerId, preferredLabel: preferredSpeakerLabel,
403+
});
404+
}
365405
}
366406
return;
367407
}
@@ -379,10 +419,6 @@ export function useDeviceAlignment(
379419
},
380420
});
381421

382-
console.log(`Setting speaker via ${matchType} match`, {
383-
preferredSpeakerId, preferredSpeakerLabel,
384-
targetId, targetLabel: targetSpeaker.device.label, matchType,
385-
});
386422
const isSpeakerRecovery = loggedUnavailableSpeakerRef.current === preferredSpeakerId;
387423
loggedUnavailableSpeakerRef.current = null;
388424
updatingSpeakerRef.current = true;
@@ -402,6 +438,15 @@ export function useDeviceAlignment(
402438
message: `"${deviceName}" reconnected — switched back`,
403439
});
404440
}
441+
if (logEvent) {
442+
logEvent("device-changed", {
443+
deviceType: "speaker",
444+
reason: isSpeakerRecovery ? "recovery" : "alignment",
445+
matchType,
446+
toId: targetId,
447+
toLabel: targetSpeaker.device.label,
448+
});
449+
}
405450
setPendingGestureOperations((prev) => ({ ...prev, speaker: false }));
406451
setPendingOperationDetails((prev) => ({ ...prev, speaker: null }));
407452
}
@@ -515,6 +560,7 @@ export function useDeviceAlignment(
515560
setPendingOperationDetails,
516561
addDeviceBanner,
517562
clearBannersForDevice,
563+
logEvent,
518564
]);
519565

520566
return { handleSwitchDevice };

client/src/call/hooks/useDeviceBanners.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,20 @@ export function useDeviceBanners() {
3131

3232
const clearBannersForDevice = useCallback((deviceType) => {
3333
setBanners((prev) => {
34-
const removed = prev.filter((b) => b.deviceType === deviceType);
35-
removed.forEach((b) => {
36-
const timer = timerRefs.current.get(b.id);
37-
if (timer) {
38-
clearTimeout(timer);
39-
timerRefs.current.delete(b.id);
34+
const remaining = prev.filter((b) => b.deviceType !== deviceType);
35+
// Bail out with same reference if nothing was removed — prevents
36+
// unnecessary re-renders that can cause infinite loops in effects.
37+
if (remaining.length === prev.length) return prev;
38+
prev.forEach((b) => {
39+
if (b.deviceType === deviceType) {
40+
const timer = timerRefs.current.get(b.id);
41+
if (timer) {
42+
clearTimeout(timer);
43+
timerRefs.current.delete(b.id);
44+
}
4045
}
4146
});
42-
return prev.filter((b) => b.deviceType !== deviceType);
47+
return remaining;
4348
});
4449
}, []);
4550

0 commit comments

Comments
 (0)