fix(huddle): guard mediaDevices access when the API is unavailable - #3817
fix(huddle): guard mediaDevices access when the API is unavailable#3817dpetrakov wants to merge 3 commits into
Conversation
|
sensible guard. a one-line log when mediaDevices is missing would help support spot locked-down environments |
Review feedback on block#3817: the guard left the mount-time path silent, so a locked-down environment produced an empty device list and no diagnostic at all. Support only had a signal once someone tried to join, and even then only the user-facing copy. Emit one `console.warn` from `availableMediaDevices()`, latched so it fires at most once per process. Three call sites reach it and mount-time effects run twice under `React.StrictMode`, so an unguarded log would emit five identical lines and bury the signal. The latch is environment state, not community state, so it is deliberately not wired into `resetCommunityState()`. Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
|
Good call – the mount path was silent, so support had nothing to go on until someone actually tried to join, and even then only the user-facing copy. Added in 47518fb. One deliberate wrinkle: the warning is latched to fire at most once per process rather than once per call. Two notes for whoever reviews it:
|
`navigator.mediaDevices` is undefined in a non-secure context, and huddle touched it unguarded from two mount-time effects. The whole React tree crashed to the error boundary with "undefined is not an object (evaluating 'navigator.mediaDevices.enumerateDevices')" before the user ever started a huddle. Route the five call sites through `availableMediaDevices()`, mirroring the guard already used in `profile/lib/animatedAvatarCapture.ts`. The input device list degrades to empty and the hot-plug listeners are skipped; the output device list is unaffected because it comes from Rust. The `getUserMedia` join path now throws a recognizable sentinel that `formatHuddleActionError` maps to actionable copy instead of crashing. Fixes block#3118 Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
Review feedback on block#3817: the guard left the mount-time path silent, so a locked-down environment produced an empty device list and no diagnostic at all. Support only had a signal once someone tried to join, and even then only the user-facing copy. Emit one `console.warn` from `availableMediaDevices()`, latched so it fires at most once per process. Three call sites reach it and mount-time effects run twice under `React.StrictMode`, so an unguarded log would emit five identical lines and bury the signal. The latch is environment state, not community state, so it is deliberately not wired into `resetCommunityState()`. Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
The guards themselves had no coverage: the helper was unit-tested, but nothing exercised the actual regression, which is a component throwing during its mount effect. jsdom and @testing-library/react landed in the desktop dev deps since this branch was opened, so that gap can now be closed. jsdom implements no `navigator.mediaDevices`, which is precisely the non-secure-context state from block#3118. Rendering a harness around `useAudioDevices` in that environment reproduces the crash: reverting the guard fails this test with `TypeError: Cannot read properties of undefined (reading 'enumerateDevices')`, the same error the issue reports. Signed-off-by: Dmitry Petrakov <dpetrakov@hotmail.com>
47518fb to
8130da5
Compare
|
Rebased onto The bug is still there on current The conflict itself was an import collision, nothing structural, and every guard reapplied cleanly. Two things worth calling out. The redesign left And
Sorry about the force-push. Nothing had been reviewed yet, so no comment threads were harmed, and a merge commit seemed worse given the repo squash-merges. |
Summary
navigator.mediaDevicesisundefinedin a non-secure context, for example a WKWebView that never got a secure origin. Huddle touched it unguarded from two mount-time effects, so the whole React tree crashed to the "Something went wrong" boundary withundefined is not an object (evaluating 'navigator.mediaDevices.enumerateDevices'). That happened before the user ever started a huddle.All five call sites now go through
lib/mediaDevices.ts, which mirrors the guard this codebase already uses atdesktop/src/features/profile/lib/animatedAvatarCapture.ts:131.lib/useAudioDevices.ts,enumerateDevices+devicechangeHuddleContext.tsx, output-devicedevicechangelist_audio_output_devicesHuddleContext.tsx,getUserMediaon joinformatHuddleActionErrormaps to "Microphone access isn't available in this window. Try restarting Buzz."Two shapes worth explaining.
availableMediaDevices()returnsMediaDevices | nullfor the paths that can degrade, andrequireMediaDevices()throws the sentinel for the join path, which cannot. And the missing-API warning is latched to fire once per process: three call sites reach it and mount effects run twice underReact.StrictMode, so an unguarded log would print five identical lines and bury the signal. The latch is environment state rather than community state, so it is deliberately not registered inresetCommunityState().Related issue
Fixes #3118.
Duplicate search on 2026-08-09: no open PR touches huddle plus
mediaDevices/getUserMedia. Closest existing PR: none found.Testing
just cipasses end to end on macOS arm64, rebased on5bf78671f. Desktop suite is 4545 tests, 0 failures.The regression test is the one that matters.
useAudioDevices.test.mjsrenders a harness around the hook under jsdom, which implements nonavigator.mediaDevicesand is therefore exactly the state this issue describes. I checked that it can actually fail: reverting the guard makes it throwTypeError: Cannot read properties of undefined (reading 'enumerateDevices'), the same error in the report.mediaDevices.test.mjscovers the helper directly, including each way the API can be missing and the once-per-process latch. That last case lives inmediaDevicesWarning.test.mjsbecause the node runner gives every test file its own process, and it is the only place the latch is observable unset.huddleError.test.mjscovers the new copy. The pre-existing case that passes the literal string"Microphone unavailable"still returns it verbatim, because the sentinel matches onmicrophone_unavailablewith an underscore.No screenshots: this replaces a crash with a degradation path. The only new visible string is the error copy quoted above.
Rebase note
Rebased onto current
mainafter the Huddle redesign (#4281). The conflict was an import collision, nothing structural, and all guard sites reapplied cleanly.One thing the redesign forced:
HuddleContext.tsxcame out of it at exactly the 1000-line ratchet limit, and this change pushed it to 1007. Rather than touch the limit, I moved the throw-or-return logic intorequireMediaDevices(). The file is now 997 lines, two below wheremainhas it.Follow-up, deliberately not here
profile/lib/animatedAvatarCapture.ts:157has the same unguardedgetUserMedia, noted in the issue. Different feature, so it is a separate change. Say the word and I will send it.