feat: Add "Camera Devices" screen to Example (#1927)

* feat: Add "Camera Devices" screen to Example

* feat: Store device in MMKV
This commit is contained in:
Marc Rousavy 2023-10-05 14:52:03 +02:00 committed by GitHub
parent 658695dca5
commit 1843f7ac3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 276 additions and 11 deletions

View File

@ -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

View File

@ -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",

View File

@ -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<Routes>()
@ -48,6 +49,7 @@ export function App(): React.ReactElement | null {
presentation: 'transparentModal',
}}
/>
<Stack.Screen name="Devices" component={DevicesPage} />
</Stack.Navigator>
</GestureHandlerRootView>
</NavigationContainer>

View File

@ -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 {
<ReanimatedCamera
ref={camera}
style={StyleSheet.absoluteFill}
device={device}
device={preferredDevice ?? device}
format={format}
fps={fps}
hdr={enableHdr}
@ -230,6 +232,9 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
<IonIcon name={enableNightMode ? 'moon' : 'moon-outline'} color="white" size={24} />
</PressableOpacity>
)}
<PressableOpacity style={styles.button} onPress={() => navigation.navigate('Devices')}>
<IonIcon name="settings-outline" color="white" size={24} />
</PressableOpacity>
</View>
</View>
)

View File

@ -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<CameraDevice, SectionType>
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 (
<PressableOpacity style={styles.itemContainer} onPress={onPress}>
<View style={styles.horizontal}>
<IonIcon name="camera" size={18} color="black" style={styles.icon} />
<Text style={styles.deviceName} numberOfLines={3}>
{device.name} <Text style={styles.devicePosition}>({device.position})</Text>
</Text>
</View>
<Text style={styles.deviceTypes}>{deviceTypes}</Text>
<View style={styles.horizontal}>
<IonIcon name="camera" size={12} color="black" style={styles.inlineIcon} />
<Text style={styles.resolutionText}>
{maxPhotoRes.photoWidth}x{maxPhotoRes.photoHeight}
</Text>
</View>
<View style={styles.horizontal}>
<IonIcon name="videocam" size={12} color="black" style={styles.inlineIcon} />
<Text style={styles.resolutionText}>
{maxVideoRes.videoWidth}x{maxVideoRes.videoHeight} @ {maxVideoRes.maxFps} FPS
</Text>
</View>
<Text style={styles.deviceId} numberOfLines={2} ellipsizeMode="middle">
{device.id}
</Text>
</PressableOpacity>
)
}
type Props = NativeStackScreenProps<Routes, 'Devices'>
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<CameraDevice>) => {
return <Device device={item} onPress={() => onDevicePressed(item)} />
},
[onDevicePressed],
)
const renderSectionHeader = useCallback(({ section }: { section: SectionData }) => {
if (section.data.length === 0) return null
return (
<View style={styles.sectionHeader}>
<Text style={styles.sectionHeaderText}>{section.position.toUpperCase()}</Text>
</View>
)
}, [])
return (
<View style={styles.container}>
<View style={styles.headerContainer}>
<View style={styles.horizontal}>
<PressableOpacity style={styles.backButton} onPress={navigation.goBack}>
<IonIcon name="chevron-back" size={35} color="black" style={styles.icon} />
</PressableOpacity>
<Text style={styles.header}>Camera Devices</Text>
</View>
<Text style={styles.subHeader}>
These are all detected Camera devices on your phone. This list will automatically update as you plug devices in or out.
</Text>
</View>
<SectionList
style={styles.list}
contentContainerStyle={styles.listContent}
sections={sections}
keyExtractor={keyExtractor}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
stickySectionHeadersEnabled={false}
/>
</View>
)
}
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,
},
})

View File

@ -5,4 +5,5 @@ export type Routes = {
path: string
type: 'video' | 'photo'
}
Devices: undefined
}

View File

@ -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]
}

View File

@ -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"