From e8ce6c64ec2adb4a56756457bd8aa9362afe7fc1 Mon Sep 17 00:00:00 2001 From: Milosz Filimowski Date: Tue, 24 Feb 2026 13:55:10 +0100 Subject: [PATCH 1/7] add permissions polyfill --- .../mobile-client/src/hooks/usePermissions.ts | 25 +++++++++++++++++++ packages/mobile-client/src/index.ts | 1 + .../src/overrides/getUserMedia.ts | 25 +++++++++++++++++++ packages/mobile-client/src/webrtc-polyfill.ts | 2 ++ 4 files changed, 53 insertions(+) create mode 100644 packages/mobile-client/src/hooks/usePermissions.ts create mode 100644 packages/mobile-client/src/overrides/getUserMedia.ts diff --git a/packages/mobile-client/src/hooks/usePermissions.ts b/packages/mobile-client/src/hooks/usePermissions.ts new file mode 100644 index 00000000..88d7761e --- /dev/null +++ b/packages/mobile-client/src/hooks/usePermissions.ts @@ -0,0 +1,25 @@ +import { permissions } from '@fishjam-cloud/react-native-webrtc'; +import { useCallback } from 'react'; + +type PermissionStatus = 'granted' | 'denied' | 'prompt'; + +function usePermission(name: 'camera' | 'microphone') { + const query = useCallback(async (): Promise => { + return (await permissions.query({ name })) as PermissionStatus; + }, [name]); + + const request = useCallback(async (): Promise => { + await permissions.request({ name }); + return (await permissions.query({ name })) as PermissionStatus; + }, [name]); + + return { query, request }; +} + +export function useCameraPermissions() { + return usePermission('camera'); +} + +export function useMicrophonePermissions() { + return usePermission('microphone'); +} diff --git a/packages/mobile-client/src/index.ts b/packages/mobile-client/src/index.ts index e6920ec9..41e8bc38 100644 --- a/packages/mobile-client/src/index.ts +++ b/packages/mobile-client/src/index.ts @@ -25,6 +25,7 @@ export { export type { CallKitAction, CallKitConfig, MediaStream } from '@fishjam-cloud/react-native-webrtc'; export { useForegroundService, type ForegroundServiceConfig } from './useForegroundService'; +export { useCameraPermissions, useMicrophonePermissions } from './hooks/usePermissions'; export { useCamera, diff --git a/packages/mobile-client/src/overrides/getUserMedia.ts b/packages/mobile-client/src/overrides/getUserMedia.ts new file mode 100644 index 00000000..4809aeb5 --- /dev/null +++ b/packages/mobile-client/src/overrides/getUserMedia.ts @@ -0,0 +1,25 @@ +import { permissions } from '@fishjam-cloud/react-native-webrtc'; + +export const patchGetUserMediaWithPermissionWarnings = () => { + const original = globalThis.navigator.mediaDevices.getUserMedia.bind(globalThis.navigator.mediaDevices); + + globalThis.navigator.mediaDevices.getUserMedia = async (constraints?: MediaStreamConstraints) => { + try { + const [cameraStatus, micStatus] = await Promise.all([ + constraints?.video ? permissions.query({ name: 'camera' }) : null, + constraints?.audio ? permissions.query({ name: 'microphone' }) : null, + ]); + + if (cameraStatus && cameraStatus !== 'granted') { + console.warn(`Attempting to access camera with permission status: "${cameraStatus}".`); + } + if (micStatus && micStatus !== 'granted') { + console.warn(`Attempting to access microphone with permission status: "${micStatus}".`); + } + } catch (error) { + console.warn('Failed to check permissions before getUserMedia', error); + } + + return original(constraints); + }; +}; diff --git a/packages/mobile-client/src/webrtc-polyfill.ts b/packages/mobile-client/src/webrtc-polyfill.ts index 4310fada..71196e0d 100644 --- a/packages/mobile-client/src/webrtc-polyfill.ts +++ b/packages/mobile-client/src/webrtc-polyfill.ts @@ -5,6 +5,7 @@ import { registerGlobals } from '@fishjam-cloud/react-native-webrtc'; // @ts-ignore - event-target-shim types not properly exported via package.json exports import { EventTarget } from 'event-target-shim'; +import { patchGetUserMediaWithPermissionWarnings } from './overrides/getUserMedia'; import { RTCPeerConnection } from './overrides/RTCPeerConnection'; import { LocalStoragePolyfill } from './polyfills/local-storage'; @@ -14,6 +15,7 @@ const registerGlobalsPolyfill = () => { registerGlobals(); // Custom overrides (globalThis.RTCPeerConnection as unknown as typeof RTCPeerConnection) = RTCPeerConnection; + patchGetUserMediaWithPermissionWarnings(); }; registerGlobalsPolyfill(); From 35f951a65e9d05f364c580e218c1ed973c48003b Mon Sep 17 00:00:00 2001 From: Milosz Filimowski Date: Tue, 24 Feb 2026 14:27:24 +0100 Subject: [PATCH 2/7] update example with the new hook --- .../fishjam-chat/app/room/preview.tsx | 31 ++++++++++++++----- .../fishjam-chat/hooks/useMediaPermissions.ts | 28 +++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 examples/mobile-client/fishjam-chat/hooks/useMediaPermissions.ts diff --git a/examples/mobile-client/fishjam-chat/app/room/preview.tsx b/examples/mobile-client/fishjam-chat/app/room/preview.tsx index 82b5c47c..38a7706a 100644 --- a/examples/mobile-client/fishjam-chat/app/room/preview.tsx +++ b/examples/mobile-client/fishjam-chat/app/room/preview.tsx @@ -12,6 +12,7 @@ import { } from "@fishjam-cloud/react-native-client"; import { Button, InCallButton, NoCameraView } from "../../components"; +import { useMediaPermissions } from "../../hooks/useMediaPermissions"; import { BrandColors } from "../../utils/Colors"; export default function PreviewScreen() { @@ -29,6 +30,8 @@ export default function PreviewScreen() { useMicrophone(); const { joinRoom, leaveRoom } = useConnection(); + const { granted: permissionsGranted, openSettings } = useMediaPermissions(); + const [isInitialized, setIsInitialized] = useState(false); const [isJoining, setIsJoining] = useState(false); const [error, setError] = useState(null); @@ -36,6 +39,8 @@ export default function PreviewScreen() { const hasJoinedRef = useRef(false); useEffect(() => { + if (!permissionsGranted) return; + const setup = async () => { try { await initializeDevices({ enableVideo: true, enableAudio: true }); @@ -61,7 +66,7 @@ export default function PreviewScreen() { } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [permissionsGranted]); const handleJoinRoom = useCallback(async () => { try { @@ -102,7 +107,11 @@ export default function PreviewScreen() { {!isInitialized ? ( - Initializing camera... + + {!permissionsGranted + ? "Requesting permissions..." + : "Initializing camera..."} + ) : cameraStream ? ( -