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()) + } + } + } } + 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