From 5b1e66589726d79b771ad33e1fd6e696b62db686 Mon Sep 17 00:00:00 2001 From: Khoa Luu Date: Thu, 8 Jan 2026 11:25:00 +0700 Subject: [PATCH 1/2] fix: Add get devices manually --- .../camera/react/CameraDevicesManager.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt index 03864788d8..01a1b7ff48 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt @@ -6,6 +6,7 @@ import android.util.Log import androidx.camera.extensions.ExtensionsManager import androidx.camera.lifecycle.ProcessCameraProvider import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod @@ -122,4 +123,36 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : @Suppress("unused", "UNUSED_PARAMETER") @ReactMethod fun removeListeners(count: Int) {} + + private suspend fun ensureInitialized() { + if (cameraProvider != null && extensionsManager != null) return + + // Try init again (idempotent enough for this use-case) + if (cameraProvider == null) { + cameraProvider = ProcessCameraProvider.getInstance(reactContext).await(executor) + } + if (extensionsManager == null && cameraProvider != null) { + extensionsManager = + ExtensionsManager.getInstanceAsync(reactContext, cameraProvider!!).await(executor) + } + } + + /** + * Exposed to JS: returns current available camera devices at call time. + * JS usage: + * const devices = await NativeModules.CameraDevices.getAvailableDeviceManually() + */ + @ReactMethod + fun getAvailableDeviceManually(promise: Promise) { + coroutineScope.launch { + try { + ensureInitialized() + val devices = getDevicesJson() + promise.resolve(devices) + } catch (t: Throwable) { + promise.resolve(Arguments.createArray()) + } + } + } } + From 73e561014e1619b4d88d2cb2e6f91f4a3942b145 Mon Sep 17 00:00:00 2001 From: Khoa Luu Date: Thu, 8 Jan 2026 12:00:24 +0700 Subject: [PATCH 2/2] fix(Android): Fix useCameraDevices hooks --- package/src/CameraDevices.ts | 27 ++++++++++++++++++++++++++- package/src/hooks/useCameraDevices.ts | 18 ++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/package/src/CameraDevices.ts b/package/src/CameraDevices.ts index 5866143858..52d61c24e2 100644 --- a/package/src/CameraDevices.ts +++ b/package/src/CameraDevices.ts @@ -1,4 +1,4 @@ -import { NativeModules, NativeEventEmitter } from 'react-native' +import { NativeModules, NativeEventEmitter, Platform } from 'react-native' import type { CameraDevice } from './types/CameraDevice' const CameraDevicesManager = NativeModules.CameraDevices as { @@ -6,8 +6,11 @@ const CameraDevicesManager = NativeModules.CameraDevices as { availableCameraDevices: CameraDevice[] userPreferredCameraDevice: CameraDevice | undefined } + getAvailableDeviceManually: () => Promise } +const isAndroid = Platform.OS === 'android' + const constants = CameraDevicesManager.getConstants() let devices = constants.availableCameraDevices @@ -18,9 +21,31 @@ eventEmitter.addListener(DEVICES_CHANGED_NAME, (newDevices: CameraDevice[]) => { devices = newDevices }) +// On Android, sometimes the devices are not ready when the module is initialized. +// So we try to fetch them again after a delay if none are available. +if (isAndroid) { + if ((devices?.length || 0) === 0) { + setTimeout(() => { + CameraDevicesManager.getAvailableDeviceManually().then((newDevices) => { + devices = newDevices + }) + }, 5000) + } +} + export const CameraDevices = { userPreferredCameraDevice: constants.userPreferredCameraDevice, getAvailableCameraDevices: () => devices, + getAvailableCameraDevicesManually: async () => { + if (isAndroid) { + const newDevices = await CameraDevicesManager.getAvailableDeviceManually() + if ((newDevices?.length || 0) > 0) { + devices = newDevices + } + return newDevices + } + return Promise.resolve([]) + }, addCameraDevicesChangedListener: (callback: (newDevices: CameraDevice[]) => void) => { return eventEmitter.addListener(DEVICES_CHANGED_NAME, callback) }, diff --git a/package/src/hooks/useCameraDevices.ts b/package/src/hooks/useCameraDevices.ts index 6dbd038321..866d442c79 100644 --- a/package/src/hooks/useCameraDevices.ts +++ b/package/src/hooks/useCameraDevices.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import type { CameraDevice } from '../types/CameraDevice' import { CameraDevices } from '../CameraDevices' @@ -11,12 +11,26 @@ import { CameraDevices } from '../CameraDevices' */ export function useCameraDevices(): CameraDevice[] { const [devices, setDevices] = useState(() => CameraDevices.getAvailableCameraDevices()) + const numberOfDevicesRef = useRef(devices.length) useEffect(() => { + let isMounted = true const listener = CameraDevices.addCameraDevicesChangedListener((newDevices) => { setDevices(newDevices) }) - return () => listener.remove() + // Only update if we got new devices and the component is still mounted + // This happens with Android only + if (numberOfDevicesRef.current === 0) { + CameraDevices.getAvailableCameraDevicesManually().then((newDevices) => { + if (isMounted && newDevices.length > 0) { + setDevices(newDevices) + } + }) + } + return () => { + isMounted = false + listener.remove() + } }, []) return devices