Skip to content

🐛(frontend) fix camera/microphone permission detection on mobile browsers#1157

Open
mmaudet wants to merge 2 commits intosuitenumerique:mainfrom
mmaudet:fix/775-769-mobile-permissions-detection
Open

🐛(frontend) fix camera/microphone permission detection on mobile browsers#1157
mmaudet wants to merge 2 commits intosuitenumerique:mainfrom
mmaudet:fix/775-769-mobile-permissions-detection

Conversation

@mmaudet
Copy link
Copy Markdown
Collaborator

@mmaudet mmaudet commented Mar 17, 2026

Summary

  • Fix camera/microphone permissions not being detected on Android Firefox and iOS Chrome
  • The existing permission polling workaround was restricted to Safari (isSafari() check), but iOS Chrome uses WebKit (same limitations) and Android Firefox has unreliable permission change events too
  • Add hasUnreliablePermissionsEvents() utility that detects Safari, all iOS browsers (WebKit-based), and Firefox
  • Add individual error handling for navigator.permissions.query() calls, which may throw on some mobile browsers
  • Also fixes a pre-existing bug in handleMicrophoneChange that checked microphonePermission.state instead of cameraPermission.state when deciding whether to stop polling

Root cause: On iOS, all browsers use WebKit (Apple's policy), so iOS Chrome has the same Permissions API limitations as Safari. Android Firefox also doesn't reliably fire permission change events. The polling mechanism that works around these limitations was only activated for Safari.

Closes #775
Closes #769

Test plan

  • Android + Firefox: Open Meet, grant camera/microphone permissions → verify they are detected and the join screen shows the preview
  • Android + Chrome: Same test → verify no regression
  • iOS + Chrome: Open Meet, grant permissions → verify they are detected
  • iOS + Safari: Same test → verify existing behavior still works (no regression)
  • Desktop + Chrome/Firefox/Safari: Verify no regression on desktop browsers
  • Deny permissions on mobile → verify the "permissions needed" state is shown correctly

@lebaudantoine
Copy link
Copy Markdown
Collaborator

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

Walkthrough

This pull request introduces a browser-agnostic approach to handling permission state changes in the permissions watching hook. A new utility function detects browsers with unreliable Permissions API change events (Safari, iOS browsers, and Firefox). The hook is refactored to conditionally enable polling only when events are unreliable or when permissions are in 'prompt' state. Individual permission queries are now wrapped in try/catch handlers with fallback behavior, while event listeners are guarded and conditionally removed during cleanup. The polling loop queries camera and microphone states separately with guards for unsupported queries and terminates once both permissions exit 'prompt' state.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly indicates a bug fix for camera/microphone permission detection on mobile browsers, which aligns with the main objective of the changeset.
Description check ✅ Passed The description is directly related to the changeset, explaining the root cause, solution, and test plan for the permission detection fix.
Linked Issues check ✅ Passed The PR addresses all coding requirements from linked issues #775 and #769: detects unreliable Permissions API on Firefox/iOS, adds error handling for navigator.permissions.query, fixes polling logic, and enables detection across mobile browsers.
Out of Scope Changes check ✅ Passed All changes are scoped to the permission detection fix: new hasUnreliablePermissionsEvents() utility, enhanced polling logic with error handling, and bug fix in handleMicrophoneChange. No unrelated modifications found.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/frontend/src/features/rooms/hooks/useWatchPermissions.ts`:
- Around line 30-39: The catch blocks in useWatchPermissions currently write to
permissionsStore (cameraPermission/microphonePermission) even if the hook has
been torn down; add a cancellation guard (e.g., an isActive/isMounted boolean or
AbortController flag stored in the hook scope) that is set false in the
useEffect cleanup and check that flag before mutating permissionsStore in both
catch paths (and after any awaited navigator.permissions.query calls); ensure
the cleanup toggles the flag so no writes occur after unmount.
- Around line 74-116: In useWatchPermissions, the polling loop can run forever
if navigator.permissions.query() always throws (updatedCameraState and
updatedMicrophoneState remain null); update the stop condition so the poll also
stops when both permission queries are unsupported (both states are null).
Concretely, inside the polling completion logic (the block using
updatedCameraState/updatedMicrophoneState, permissionsStore and the "Stop
polling when both permissions are resolved" check), add a branch that treats
{updatedCameraState === null && updatedMicrophoneState === null} as a terminal
condition and stop the interval / return early (preserving existing updates to
permissionsStore when one or the other is non-null). Ensure you reference
useWatchPermissions, updatedCameraState, updatedMicrophoneState,
navigator.permissions.query, permissionsStore and isCancelled when making the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eda8fcf9-705d-4436-9277-779ef00a33f6

📥 Commits

Reviewing files that changed from the base of the PR and between 0cb3fb8 and 99ecbfe.

📒 Files selected for processing (2)
  • src/frontend/src/features/rooms/hooks/useWatchPermissions.ts
  • src/frontend/src/utils/livekit.ts

Comment on lines +30 to +39
} catch {
permissionsStore.cameraPermission = 'prompt'
}

try {
microphonePermission = await navigator.permissions.query({
name: 'microphone' as PermissionName,
})
} catch {
permissionsStore.microphonePermission = 'prompt'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard catch-path store writes after cancellation.

Line 31 and Line 39 can still mutate global permissionsStore after teardown if unmount happens while queries are in flight.

Proposed fix
         } catch {
-          permissionsStore.cameraPermission = 'prompt'
+          if (!isCancelled) {
+            permissionsStore.cameraPermission = 'prompt'
+          }
         }
@@
         } catch {
-          permissionsStore.microphonePermission = 'prompt'
+          if (!isCancelled) {
+            permissionsStore.microphonePermission = 'prompt'
+          }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/src/features/rooms/hooks/useWatchPermissions.ts` around lines 30
- 39, The catch blocks in useWatchPermissions currently write to
permissionsStore (cameraPermission/microphonePermission) even if the hook has
been torn down; add a cancellation guard (e.g., an isActive/isMounted boolean or
AbortController flag stored in the hook scope) that is set false in the
useEffect cleanup and check that flag before mutating permissionsStore in both
catch paths (and after any awaited navigator.permissions.query calls); ensure
the cleanup toggles the flag so no writes occur after unmount.

Comment on lines 74 to 116
let updatedCameraState: PermissionState | null = null
let updatedMicrophoneState: PermissionState | null = null

try {
const updatedCamera = await navigator.permissions.query({
name: 'camera' as PermissionName,
})
updatedCameraState = updatedCamera.state
} catch {
// Permission query not supported, keep current state
}

if (isCancelled) return
try {
const updatedMicrophone = await navigator.permissions.query({
name: 'microphone' as PermissionName,
})
updatedMicrophoneState = updatedMicrophone.state
} catch {
// Permission query not supported, keep current state
}

const cameraChanged =
permissionsStore.cameraPermission !== updatedCamera.state
const microphoneChanged =
permissionsStore.microphonePermission !==
updatedMicrophone.state
if (isCancelled) return

if (cameraChanged) {
permissionsStore.cameraPermission = updatedCamera.state
if (
updatedCameraState &&
permissionsStore.cameraPermission !== updatedCameraState
) {
permissionsStore.cameraPermission = updatedCameraState
}

if (microphoneChanged) {
permissionsStore.microphonePermission =
updatedMicrophone.state
if (
updatedMicrophoneState &&
permissionsStore.microphonePermission !==
updatedMicrophoneState
) {
permissionsStore.microphonePermission = updatedMicrophoneState
}

// Stop polling when both permissions are resolved
if (
updatedCamera.state !== 'prompt' &&
updatedMicrophone.state !== 'prompt'
(updatedCameraState ?? 'prompt') !== 'prompt' &&
(updatedMicrophoneState ?? 'prompt') !== 'prompt'
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "useWatchPermissions.ts" 2>/dev/null

Repository: suitenumerique/meet

Length of output: 125


🏁 Script executed:

cat -n ./src/frontend/src/features/rooms/hooks/useWatchPermissions.ts

Repository: suitenumerique/meet

Length of output: 8538


Prevent unbounded polling when permission queries consistently fail.

On lines 114-115, null is coerced to 'prompt'. If navigator.permissions.query() keeps throwing (lines 78-84, 87-93), that side never resolves and the 500ms interval runs indefinitely for the session.

Proposed fix
-                // Stop polling when both permissions are resolved
-                if (
-                  (updatedCameraState ?? 'prompt') !== 'prompt' &&
-                  (updatedMicrophoneState ?? 'prompt') !== 'prompt'
-                ) {
+                // Stop polling when both permissions are resolved.
+                // If a query is unsupported (no PermissionStatus object), treat it as non-pollable.
+                const cameraStateForStop =
+                  updatedCameraState ?? cameraPermission?.state ?? 'unavailable'
+                const microphoneStateForStop =
+                  updatedMicrophoneState ??
+                  microphonePermission?.state ??
+                  'unavailable'
+
+                if (
+                  cameraStateForStop !== 'prompt' &&
+                  microphoneStateForStop !== 'prompt'
+                ) {
                   if (intervalId) {
                     clearInterval(intervalId)
                     intervalId = undefined
                   }
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/src/features/rooms/hooks/useWatchPermissions.ts` around lines 74
- 116, In useWatchPermissions, the polling loop can run forever if
navigator.permissions.query() always throws (updatedCameraState and
updatedMicrophoneState remain null); update the stop condition so the poll also
stops when both permission queries are unsupported (both states are null).
Concretely, inside the polling completion logic (the block using
updatedCameraState/updatedMicrophoneState, permissionsStore and the "Stop
polling when both permissions are resolved" check), add a branch that treats
{updatedCameraState === null && updatedMicrophoneState === null} as a terminal
condition and stop the interval / return early (preserving existing updates to
permissionsStore when one or the other is non-null). Ensure you reference
useWatchPermissions, updatedCameraState, updatedMicrophoneState,
navigator.permissions.query, permissionsStore and isCancelled when making the
change.

Fix camera/microphone permissions not being detected on Android
Firefox and iOS Chrome. Extend the polling workaround beyond
Safari to all browsers with unreliable permission change events.
Add individual error handling for navigator.permissions.query().

Closes suitenumerique#775 suitenumerique#769
@mmaudet mmaudet force-pushed the fix/775-769-mobile-permissions-detection branch from 99ecbfe to a7723d0 Compare March 20, 2026 18:14
…rmissions

- Add isCancelled guard in catch blocks to prevent store mutations after unmount
- Fix polling stop condition to handle unsupported permission queries (null state)
@sonarqubecloud
Copy link
Copy Markdown

@lebaudantoine lebaudantoine requested a review from Ovgodd April 8, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Microphone/Camera not detected on Android using Firefox iOS 15 using Chrome, camera and microphone permissions are not properly detected

2 participants