diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index 1c74f1f..7e5322b 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -27,6 +27,9 @@ PODS: - libwebp/sharpyuv (1.3.2) - libwebp/webp (1.3.2): - libwebp/sharpyuv + - MMKV (1.3.1): + - MMKVCore (~> 1.3.1) + - MMKVCore (1.3.1) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -331,6 +334,9 @@ PODS: - React-Core - react-native-cameraroll (5.7.2): - React-Core + - react-native-mmkv (2.10.2): + - MMKV (>= 1.2.13) + - React-Core - react-native-safe-area-context (4.7.1): - React-Core - react-native-video (5.2.1): @@ -338,7 +344,7 @@ PODS: - react-native-video/Video (= 5.2.1) - react-native-video/Video (5.2.1): - React-Core - - react-native-worklets-core (0.2.0): + - react-native-worklets-core (0.2.1): - React - React-callinvoker - React-Core @@ -458,7 +464,7 @@ PODS: - SDWebImageWebPCoder (~> 0.8.4) - RNGestureHandler (2.12.1): - React-Core - - RNReanimated (3.4.2): + - RNReanimated (3.5.4): - DoubleConversion - FBLazyVector - glog @@ -534,6 +540,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" + - react-native-mmkv (from `../node_modules/react-native-mmkv`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-video (from `../node_modules/react-native-video`) - react-native-worklets-core (from `../node_modules/react-native-worklets-core`) @@ -568,6 +575,8 @@ SPEC REPOS: - fmt - libevent - libwebp + - MMKV + - MMKVCore - SDWebImage - SDWebImageWebPCoder - SocketRocket @@ -620,6 +629,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/blur" react-native-cameraroll: :path: "../node_modules/@react-native-camera-roll/camera-roll" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-video: @@ -687,6 +698,8 @@ SPEC CHECKSUMS: hermes-engine: 10fbd3f62405c41ea07e71973ea61e1878d07322 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 + MMKV: 5a07930c70c70b86cd87761a42c8f3836fb681d7 + MMKVCore: e50135dbd33235b6ab390635991bab437ab873c0 RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18 RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3 @@ -704,9 +717,10 @@ SPEC CHECKSUMS: React-logger: c5b527272d5f22eaa09bb3c3a690fee8f237ae95 react-native-blur: cfdad7b3c01d725ab62a8a729f42ea463998afa2 react-native-cameraroll: 134805127580aed23403b8c2cb1548920dd77b3a + react-native-mmkv: 9ae7ca3977e8ef48dbf7f066974eb844c20b5fd7 react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2 react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 - react-native-worklets-core: 7ad416a8965086b98b07964f7f6932560a54a14c + react-native-worklets-core: 7de763135ed696ba16e8d5471e41f595ba9802bb React-NativeModulesApple: c57f3efe0df288a6532b726ad2d0322a9bf38472 React-perflogger: 6bd153e776e6beed54c56b0847e1220a3ff92ba5 React-RCTActionSheet: c0b62af44e610e69d9a2049a682f5dba4e9dff17 @@ -726,7 +740,7 @@ SPEC CHECKSUMS: ReactCommon: 3ccb8fb14e6b3277e38c73b0ff5e4a1b8db017a9 RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNGestureHandler: c0d04458598fcb26052494ae23dda8f8f5162b13 - RNReanimated: 726395a2fa2f04cea340274ba57a4e659bc0d9c1 + RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87 RNScreens: b21dc57dfa2b710c30ec600786a3fc223b1b92e7 RNStaticSafeAreaInsets: 055ddbf5e476321720457cdaeec0ff2ba40ec1b8 RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9 diff --git a/package/example/package.json b/package/example/package.json index 59eeef8..6df04f5 100644 --- a/package/example/package.json +++ b/package/example/package.json @@ -21,8 +21,9 @@ "react-native": "^0.72.3", "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "^2.12.1", + "react-native-mmkv": "^2.10.2", "react-native-pressable-opacity": "^1.0.10", - "react-native-reanimated": "^3.4.2", + "react-native-reanimated": "^3.5.4", "react-native-safe-area-context": "^4.7.1", "react-native-screens": "^3.24.0", "react-native-static-safe-area-insets": "^2.2.0", diff --git a/package/example/src/App.tsx b/package/example/src/App.tsx index 07d0072..77e0a85 100644 --- a/package/example/src/App.tsx +++ b/package/example/src/App.tsx @@ -8,6 +8,7 @@ import type { Routes } from './Routes' import { Camera, CameraPermissionStatus } from 'react-native-vision-camera' import { GestureHandlerRootView } from 'react-native-gesture-handler' import { StyleSheet } from 'react-native' +import { DevicesPage } from './DevicesPage' const Stack = createNativeStackNavigator() @@ -48,6 +49,7 @@ export function App(): React.ReactElement | null { presentation: 'transparentModal', }} /> + diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index cc44fee..2ac1189 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -17,6 +17,7 @@ import type { Routes } from './Routes' import type { NativeStackScreenProps } from '@react-navigation/native-stack' import { useIsFocused } from '@react-navigation/core' import { examplePlugin } from './frame-processors/ExamplePlugin' +import { usePreferredCameraDevice } from './hooks/usePreferredCameraDevice' const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera) Reanimated.addWhitelistedNativeProps({ @@ -44,7 +45,8 @@ export function CameraPage({ navigation }: Props): React.ReactElement { const [flash, setFlash] = useState<'off' | 'on'>('off') const [enableNightMode, setEnableNightMode] = useState(false) - // camera format settings + // camera device settings + const [preferredDevice] = usePreferredCameraDevice() const device = useCameraDevice(cameraPosition) const [targetFps, setTargetFps] = useState(60) @@ -170,7 +172,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { )} + navigation.navigate('Devices')}> + + ) diff --git a/package/example/src/DevicesPage.tsx b/package/example/src/DevicesPage.tsx new file mode 100644 index 0000000..7742c79 --- /dev/null +++ b/package/example/src/DevicesPage.tsx @@ -0,0 +1,217 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useMemo } from 'react' +import IonIcon from 'react-native-vector-icons/Ionicons' +import { StyleSheet, View, Text, ListRenderItemInfo, SectionList, SectionListData } from 'react-native' +import { CameraDevice, useCameraDevices } from 'react-native-vision-camera' +import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants' +import type { Routes } from './Routes' +import { PressableOpacity } from 'react-native-pressable-opacity' +import { usePreferredCameraDevice } from './hooks/usePreferredCameraDevice' + +const keyExtractor = (item: CameraDevice): string => item.id + +interface SectionType { + position: CameraDevice['position'] | 'preferred' +} +type SectionData = SectionListData + +interface DeviceProps { + device: CameraDevice + onPress: () => void +} + +function Device({ device, onPress }: DeviceProps): React.ReactElement { + const maxPhotoRes = useMemo( + () => + device.formats.reduce((prev, curr) => { + if (curr.photoWidth * curr.photoHeight > prev.photoWidth * prev.photoHeight) return curr + return prev + }), + [device.formats], + ) + const maxVideoRes = useMemo( + () => + device.formats.reduce((prev, curr) => { + if (curr.videoWidth * curr.videoHeight > prev.videoWidth * prev.videoHeight) return curr + return prev + }), + [device.formats], + ) + const deviceTypes = useMemo(() => device.physicalDevices.map((t) => t.replace('-camera', '')).join(' + '), [device.physicalDevices]) + + return ( + + + + + {device.name} ({device.position}) + + + {deviceTypes} + + + + {maxPhotoRes.photoWidth}x{maxPhotoRes.photoHeight} + + + + + + {maxVideoRes.videoWidth}x{maxVideoRes.videoHeight} @ {maxVideoRes.maxFps} FPS + + + + {device.id} + + + ) +} + +type Props = NativeStackScreenProps +export function DevicesPage({ navigation }: Props): React.ReactElement { + const devices = useCameraDevices() + const [preferredDevice, setPreferredDevice] = usePreferredCameraDevice() + + const sections = useMemo((): SectionData[] => { + return [ + { + position: 'preferred', + data: preferredDevice != null ? [preferredDevice] : [], + }, + { + position: 'back', + data: devices.filter((d) => d.position === 'back'), + }, + { + position: 'front', + data: devices.filter((d) => d.position === 'front'), + }, + { + position: 'external', + data: devices.filter((d) => d.position === 'external'), + }, + ] + }, [devices, preferredDevice]) + + const onDevicePressed = useCallback( + (device: CameraDevice) => { + setPreferredDevice(device) + navigation.navigate('CameraPage') + }, + [navigation, setPreferredDevice], + ) + + const renderItem = useCallback( + ({ item }: ListRenderItemInfo) => { + return onDevicePressed(item)} /> + }, + [onDevicePressed], + ) + + const renderSectionHeader = useCallback(({ section }: { section: SectionData }) => { + if (section.data.length === 0) return null + return ( + + {section.position.toUpperCase()} + + ) + }, []) + + return ( + + + + + + + Camera Devices + + + These are all detected Camera devices on your phone. This list will automatically update as you plug devices in or out. + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + headerContainer: { + paddingTop: SAFE_AREA_PADDING.paddingTop, + paddingLeft: SAFE_AREA_PADDING.paddingLeft, + paddingRight: SAFE_AREA_PADDING.paddingRight, + }, + header: { + fontSize: 38, + fontWeight: 'bold', + maxWidth: '80%', + }, + subHeader: { + marginTop: 10, + fontSize: 18, + maxWidth: '80%', + }, + list: { + marginTop: CONTENT_SPACING, + }, + listContent: { + paddingBottom: SAFE_AREA_PADDING.paddingBottom, + }, + sectionHeader: { + paddingHorizontal: CONTENT_SPACING / 2, + paddingVertical: 5, + }, + sectionHeaderText: { + opacity: 0.4, + fontSize: 16, + }, + itemContainer: { + paddingHorizontal: CONTENT_SPACING, + paddingVertical: 7, + }, + deviceName: { + fontSize: 17, + marginLeft: 5, + flexShrink: 1, + fontWeight: 'bold', + }, + devicePosition: { + opacity: 0.4, + }, + deviceId: { + fontSize: 12, + opacity: 0.4, + }, + deviceTypes: { + fontSize: 12, + opacity: 0.4, + }, + horizontal: { + flexDirection: 'row', + alignItems: 'center', + }, + backButton: { + width: 40, + height: 40, + marginTop: 7, + }, + icon: {}, + inlineIcon: {}, + resolutionText: { + marginLeft: 5, + fontSize: 12, + }, +}) diff --git a/package/example/src/Routes.ts b/package/example/src/Routes.ts index e0a1ec8..f79e0d3 100644 --- a/package/example/src/Routes.ts +++ b/package/example/src/Routes.ts @@ -5,4 +5,5 @@ export type Routes = { path: string type: 'video' | 'photo' } + Devices: undefined } diff --git a/package/example/src/hooks/usePreferredCameraDevice.ts b/package/example/src/hooks/usePreferredCameraDevice.ts new file mode 100644 index 0000000..f5a7a27 --- /dev/null +++ b/package/example/src/hooks/usePreferredCameraDevice.ts @@ -0,0 +1,20 @@ +import { useMMKVString } from 'react-native-mmkv' +import { CameraDevice } from '../../../src/CameraDevice' +import { useCallback, useMemo } from 'react' +import { useCameraDevices } from '../../../src/hooks/useCameraDevices' + +export function usePreferredCameraDevice(): [CameraDevice | undefined, (device: CameraDevice) => void] { + const [preferredDeviceId, setPreferredDeviceId] = useMMKVString('camera.preferredDeviceId') + + const set = useCallback( + (device: CameraDevice) => { + setPreferredDeviceId(device.id) + }, + [setPreferredDeviceId], + ) + + const devices = useCameraDevices() + const device = useMemo(() => devices.find((d) => d.id === preferredDeviceId), [devices, preferredDeviceId]) + + return [device, set] +} diff --git a/package/example/yarn.lock b/package/example/yarn.lock index 740e555..ff32580 100644 --- a/package/example/yarn.lock +++ b/package/example/yarn.lock @@ -5746,15 +5746,20 @@ react-native-gesture-handler@^2.12.1: lodash "^4.17.21" prop-types "^15.7.2" +react-native-mmkv@^2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.10.2.tgz#73f06bb710388f67bade031e7b8e42a6d2358e40" + integrity sha512-hNrZzwvIFyogJkqf//rVSw7EwceYqkx/jl3hb5tzct6qqwEmS1L9ybvnDjzDkaMyDeouQIqAnsdnb6AuDSrgQQ== + react-native-pressable-opacity@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/react-native-pressable-opacity/-/react-native-pressable-opacity-1.0.10.tgz#799df1a913d3b28f42ada765465fe7723eb7166d" integrity sha512-Py9YH9TlS3Lv1so5JCj6bgiqkeYYGupF4ZImlpoyhhId/t/RiSqR68LlASOHgdctqQuqVJObQiFfzX8oZI9+6w== -react-native-reanimated@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.4.2.tgz#744154fead6d8d31d5bd9ac617d8c84d74a6f697" - integrity sha512-FbtG+f1PB005vDTJSv4zAnTK7nNXi+FjFgbAM5gOzIZDajfph2BFMSUstzIsN8T77+OKuugUBmcTqLnQ24EBVg== +react-native-reanimated@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz#a6c2b0c43b6dad246f5d276213974afedb8e3fc7" + integrity sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg== dependencies: "@babel/plugin-transform-object-assign" "^7.16.7" "@babel/preset-typescript" "^7.16.7"