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:
parent
658695dca5
commit
1843f7ac3a
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
217
package/example/src/DevicesPage.tsx
Normal file
217
package/example/src/DevicesPage.tsx
Normal 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,
|
||||
},
|
||||
})
|
@ -5,4 +5,5 @@ export type Routes = {
|
||||
path: string
|
||||
type: 'video' | 'photo'
|
||||
}
|
||||
Devices: undefined
|
||||
}
|
||||
|
20
package/example/src/hooks/usePreferredCameraDevice.ts
Normal file
20
package/example/src/hooks/usePreferredCameraDevice.ts
Normal 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]
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user