diff --git a/package/.gitignore b/package/.gitignore index a825567..98cd7d7 100644 --- a/package/.gitignore +++ b/package/.gitignore @@ -67,3 +67,6 @@ package-lock.json .cxx/ example/ios/vendor + +#.direnv +.direnv diff --git a/package/example/app.json b/package/example/app.json index 8367383..4d75e39 100644 --- a/package/example/app.json +++ b/package/example/app.json @@ -1,4 +1,4 @@ { "name": "VisionCameraExample", - "displayName": "VisionCamera Example" + "displayName": "Railbird VisionCamera" } diff --git a/package/example/src/App.tsx b/package/example/src/App.tsx index 8c9d239..66f9d75 100644 --- a/package/example/src/App.tsx +++ b/package/example/src/App.tsx @@ -1,55 +1,6 @@ -import { NavigationContainer } from '@react-navigation/native' import React from 'react' -import { createNativeStackNavigator } from '@react-navigation/native-stack' -import { PermissionsPage } from './PermissionsPage' -import { MediaPage } from './MediaPage' -import { CameraPage } from './CameraPage' -import { CodeScannerPage } from './CodeScannerPage' -import type { Routes } from './Routes' -import { Camera } from 'react-native-vision-camera' -import { GestureHandlerRootView } from 'react-native-gesture-handler' -import { StyleSheet } from 'react-native' -import { DevicesPage } from './DevicesPage' - -const Stack = createNativeStackNavigator() +import CameraScreen from './camera' export function App(): React.ReactElement | null { - const cameraPermission = Camera.getCameraPermissionStatus() - const microphonePermission = Camera.getMicrophonePermissionStatus() - - console.log(`Re-rendering Navigator. Camera: ${cameraPermission} | Microphone: ${microphonePermission}`) - - const showPermissionsPage = cameraPermission !== 'granted' || microphonePermission === 'not-determined' - return ( - - - - - - - - - - - - ) + return } - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, -}) diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx deleted file mode 100644 index 8b6fa2c..0000000 --- a/package/example/src/CameraPage.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import * as React from 'react' -import { useRef, useState, useCallback, useMemo } from 'react' -import { StyleSheet, Text, View } from 'react-native' -import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler' -import { CameraRuntimeError, PhotoFile, useCameraDevice, useCameraFormat, useFrameProcessor, VideoFile } from 'react-native-vision-camera' -import { Camera } from 'react-native-vision-camera' -import { CONTENT_SPACING, CONTROL_BUTTON_SIZE, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants' -import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated' -import { useEffect } from 'react' -import { useIsForeground } from './hooks/useIsForeground' -import { StatusBarBlurBackground } from './views/StatusBarBlurBackground' -import { CaptureButton } from './views/CaptureButton' -import { PressableOpacity } from 'react-native-pressable-opacity' -import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons' -import IonIcon from 'react-native-vector-icons/Ionicons' -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 { exampleKotlinSwiftPlugin } from './frame-processors/ExampleKotlinSwiftPlugin' -import { usePreferredCameraDevice } from './hooks/usePreferredCameraDevice' - -const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera) -Reanimated.addWhitelistedNativeProps({ - zoom: true, -}) - -const SCALE_FULL_ZOOM = 3 - -type Props = NativeStackScreenProps -export function CameraPage({ navigation }: Props): React.ReactElement { - const camera = useRef(null) - const [isCameraInitialized, setIsCameraInitialized] = useState(false) - const hasMicrophonePermission = useMemo(() => Camera.getMicrophonePermissionStatus() === 'granted', []) - const zoom = useSharedValue(0) - const isPressingButton = useSharedValue(false) - - // check if camera page is active - const isFocussed = useIsFocused() - const isForeground = useIsForeground() - const isActive = isFocussed && isForeground - - const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back') - const [enableHdr, setEnableHdr] = useState(false) - const [flash, setFlash] = useState<'off' | 'on'>('off') - const [enableNightMode, setEnableNightMode] = useState(false) - - // camera device settings - const [preferredDevice] = usePreferredCameraDevice() - let device = useCameraDevice(cameraPosition) - - if (preferredDevice != null && preferredDevice.position === cameraPosition) { - // override default device with the one selected by the user in settings - device = preferredDevice - } - - const [targetFps, setTargetFps] = useState(60) - - const screenAspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH - const format = useCameraFormat(device, [ - { fps: targetFps }, - { videoAspectRatio: screenAspectRatio }, - { videoResolution: 'max' }, - { photoAspectRatio: screenAspectRatio }, - { photoResolution: 'max' }, - ]) - - const fps = Math.min(format?.maxFps ?? 1, targetFps) - - const supportsFlash = device?.hasFlash ?? false - const supportsHdr = format?.supportsPhotoHdr - const supports60Fps = useMemo(() => device?.formats.some((f) => f.maxFps >= 60), [device?.formats]) - const canToggleNightMode = device?.supportsLowLightBoost ?? false - - //#region Animated Zoom - // This just maps the zoom factor to a percentage value. - // so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1] - const minZoom = device?.minZoom ?? 1 - const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR) - - const cameraAnimatedProps = useAnimatedProps(() => { - const z = Math.max(Math.min(zoom.value, maxZoom), minZoom) - return { - zoom: z, - } - }, [maxZoom, minZoom, zoom]) - //#endregion - - //#region Callbacks - const setIsPressingButton = useCallback( - (_isPressingButton: boolean) => { - isPressingButton.value = _isPressingButton - }, - [isPressingButton], - ) - // Camera callbacks - const onError = useCallback((error: CameraRuntimeError) => { - console.error(error) - }, []) - const onInitialized = useCallback(() => { - console.log('Camera initialized!') - setIsCameraInitialized(true) - }, []) - const onMediaCaptured = useCallback( - (media: PhotoFile | VideoFile, type: 'photo' | 'video') => { - console.log(`Media captured! ${JSON.stringify(media)}`) - navigation.navigate('MediaPage', { - path: media.path, - type: type, - }) - }, - [navigation], - ) - const onFlipCameraPressed = useCallback(() => { - setCameraPosition((p) => (p === 'back' ? 'front' : 'back')) - }, []) - const onFlashPressed = useCallback(() => { - setFlash((f) => (f === 'off' ? 'on' : 'off')) - }, []) - //#endregion - - //#region Tap Gesture - const onDoubleTap = useCallback(() => { - onFlipCameraPressed() - }, [onFlipCameraPressed]) - //#endregion - - //#region Effects - const neutralZoom = device?.neutralZoom ?? 1 - useEffect(() => { - // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes) - zoom.value = neutralZoom - }, [neutralZoom, zoom]) - //#endregion - - //#region Pinch to Zoom Gesture - // The gesture handler maps the linear pinch gesture (0 - 1) to an exponential curve since a camera's zoom - // function does not appear linear to the user. (aka zoom 0.1 -> 0.2 does not look equal in difference as 0.8 -> 0.9) - const onPinchGesture = useAnimatedGestureHandler({ - onStart: (_, context) => { - context.startZoom = zoom.value - }, - onActive: (event, context) => { - // we're trying to map the scale gesture to a linear zoom here - const startZoom = context.startZoom ?? 0 - const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP) - zoom.value = interpolate(scale, [-1, 0, 1], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP) - }, - }) - //#endregion - - useEffect(() => { - const f = - format != null - ? `(${format.photoWidth}x${format.photoHeight} photo / ${format.videoWidth}x${format.videoHeight}@${format.maxFps} video @ ${fps}fps)` - : undefined - console.log(`Camera: ${device?.name} | Format: ${f}`) - }, [device?.name, format, fps]) - - const frameProcessor = useFrameProcessor((frame) => { - 'worklet' - - // console.log(`${frame.timestamp}: ${frame.width}x${frame.height} ${frame.pixelFormat} Frame (${frame.orientation})`) - // examplePlugin(frame) - // exampleKotlinSwiftPlugin(frame) - }, []) - - return ( - - {device != null && ( - - - - - - - - )} - - - - - - - - - - {supportsFlash && ( - - - - )} - {supports60Fps && ( - setTargetFps((t) => (t === 30 ? 60 : 30))}> - {`${targetFps}\nFPS`} - - )} - {supportsHdr && ( - setEnableHdr((h) => !h)}> - - - )} - {canToggleNightMode && ( - setEnableNightMode(!enableNightMode)} disabledOpacity={0.4}> - - - )} - navigation.navigate('Devices')}> - - - navigation.navigate('CodeScannerPage')}> - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: 'black', - }, - captureButton: { - position: 'absolute', - alignSelf: 'center', - bottom: SAFE_AREA_PADDING.paddingBottom, - }, - button: { - marginBottom: CONTENT_SPACING, - width: CONTROL_BUTTON_SIZE, - height: CONTROL_BUTTON_SIZE, - borderRadius: CONTROL_BUTTON_SIZE / 2, - backgroundColor: 'rgba(140, 140, 140, 0.3)', - justifyContent: 'center', - alignItems: 'center', - }, - rightButtonRow: { - position: 'absolute', - right: SAFE_AREA_PADDING.paddingRight, - top: SAFE_AREA_PADDING.paddingTop, - }, - text: { - color: 'white', - fontSize: 11, - fontWeight: 'bold', - textAlign: 'center', - }, -}) diff --git a/package/example/src/CodeScannerPage.tsx b/package/example/src/CodeScannerPage.tsx deleted file mode 100644 index d846636..0000000 --- a/package/example/src/CodeScannerPage.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import * as React from 'react' -import { useCallback, useRef, useState } from 'react' -import { Alert, AlertButton, Linking, StyleSheet, View } from 'react-native' -import { Code, useCameraDevice, useCodeScanner } from 'react-native-vision-camera' -import { Camera } from 'react-native-vision-camera' -import { CONTENT_SPACING, CONTROL_BUTTON_SIZE, SAFE_AREA_PADDING } from './Constants' -import { useIsForeground } from './hooks/useIsForeground' -import { StatusBarBlurBackground } from './views/StatusBarBlurBackground' -import { PressableOpacity } from 'react-native-pressable-opacity' -import IonIcon from 'react-native-vector-icons/Ionicons' -import type { Routes } from './Routes' -import type { NativeStackScreenProps } from '@react-navigation/native-stack' -import { useIsFocused } from '@react-navigation/core' - -const showCodeAlert = (value: string, onDismissed: () => void): void => { - const buttons: AlertButton[] = [ - { - text: 'Close', - style: 'cancel', - onPress: onDismissed, - }, - ] - if (value.startsWith('http')) { - buttons.push({ - text: 'Open URL', - onPress: () => { - Linking.openURL(value) - onDismissed() - }, - }) - } - Alert.alert('Scanned Code', value, buttons) -} - -type Props = NativeStackScreenProps -export function CodeScannerPage({ navigation }: Props): React.ReactElement { - // 1. Use a simple default back camera - const device = useCameraDevice('back') - - // 2. Only activate Camera when the app is focused and this screen is currently opened - const isFocused = useIsFocused() - const isForeground = useIsForeground() - const isActive = isFocused && isForeground - - // 3. (Optional) enable a torch setting - const [torch, setTorch] = useState(false) - - // 4. On code scanned, we show an aler to the user - const isShowingAlert = useRef(false) - const onCodeScanned = useCallback((codes: Code[]) => { - console.log(`Scanned ${codes.length} codes:`, codes) - const value = codes[0]?.value - if (value == null) return - if (isShowingAlert.current) return - showCodeAlert(value, () => { - isShowingAlert.current = false - }) - isShowingAlert.current = true - }, []) - - // 5. Initialize the Code Scanner to scan QR codes and Barcodes - const codeScanner = useCodeScanner({ - codeTypes: ['qr', 'ean-13'], - onCodeScanned: onCodeScanned, - }) - - return ( - - {device != null && ( - - )} - - - - - setTorch(!torch)} disabledOpacity={0.4}> - - - - - {/* Back Button */} - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: 'black', - }, - button: { - marginBottom: CONTENT_SPACING, - width: CONTROL_BUTTON_SIZE, - height: CONTROL_BUTTON_SIZE, - borderRadius: CONTROL_BUTTON_SIZE / 2, - backgroundColor: 'rgba(140, 140, 140, 0.3)', - justifyContent: 'center', - alignItems: 'center', - }, - rightButtonRow: { - position: 'absolute', - right: SAFE_AREA_PADDING.paddingRight, - top: SAFE_AREA_PADDING.paddingTop, - }, - backButton: { - position: 'absolute', - left: SAFE_AREA_PADDING.paddingLeft, - top: SAFE_AREA_PADDING.paddingTop, - }, -}) diff --git a/package/example/src/DevicesPage.tsx b/package/example/src/DevicesPage.tsx deleted file mode 100644 index 1ba77d9..0000000 --- a/package/example/src/DevicesPage.tsx +++ /dev/null @@ -1,215 +0,0 @@ -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, - }, - resolutionText: { - marginLeft: 5, - fontSize: 12, - }, -}) diff --git a/package/example/src/MediaPage.tsx b/package/example/src/MediaPage.tsx deleted file mode 100644 index 6866d25..0000000 --- a/package/example/src/MediaPage.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react' -import { StyleSheet, View, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native' -import Video, { LoadError, OnLoadData } from 'react-native-video' -import { SAFE_AREA_PADDING } from './Constants' -import { useIsForeground } from './hooks/useIsForeground' -import { PressableOpacity } from 'react-native-pressable-opacity' -import IonIcon from 'react-native-vector-icons/Ionicons' -import { Alert } from 'react-native' -import { CameraRoll } from '@react-native-camera-roll/camera-roll' -import { StatusBarBlurBackground } from './views/StatusBarBlurBackground' -import type { NativeStackScreenProps } from '@react-navigation/native-stack' -import type { Routes } from './Routes' -import { useIsFocused } from '@react-navigation/core' -import FastImage, { OnLoadEvent } from 'react-native-fast-image' - -const requestSavePermission = async (): Promise => { - if (Platform.OS !== 'android') return true - - const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE - if (permission == null) return false - let hasPermission = await PermissionsAndroid.check(permission) - if (!hasPermission) { - const permissionRequestResult = await PermissionsAndroid.request(permission) - hasPermission = permissionRequestResult === 'granted' - } - return hasPermission -} - -const isVideoOnLoadEvent = (event: OnLoadData | OnLoadEvent): event is OnLoadData => 'duration' in event && 'naturalSize' in event - -type Props = NativeStackScreenProps -export function MediaPage({ navigation, route }: Props): React.ReactElement { - const { path, type } = route.params - const [hasMediaLoaded, setHasMediaLoaded] = useState(false) - const isForeground = useIsForeground() - const isScreenFocused = useIsFocused() - const isVideoPaused = !isForeground || !isScreenFocused - const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none') - - const onMediaLoad = useCallback((event: OnLoadData | OnLoadEvent) => { - if (isVideoOnLoadEvent(event)) { - console.log( - `Video loaded. Size: ${event.naturalSize.width}x${event.naturalSize.height} (${event.naturalSize.orientation}, ${event.duration} seconds)`, - ) - } else { - console.log(`Image loaded. Size: ${event.nativeEvent.width}x${event.nativeEvent.height}`) - } - }, []) - const onMediaLoadEnd = useCallback(() => { - console.log('media has loaded.') - setHasMediaLoaded(true) - }, []) - const onMediaLoadError = useCallback((error: LoadError) => { - console.log(`failed to load media: ${JSON.stringify(error)}`) - }, []) - - const onSavePressed = useCallback(async () => { - try { - setSavingState('saving') - - const hasPermission = await requestSavePermission() - if (!hasPermission) { - Alert.alert('Permission denied!', 'Vision Camera does not have permission to save the media to your camera roll.') - return - } - await CameraRoll.save(`file://${path}`, { - type: type, - }) - setSavingState('saved') - } catch (e) { - const message = e instanceof Error ? e.message : JSON.stringify(e) - setSavingState('none') - Alert.alert('Failed to save!', `An unexpected error occured while trying to save your ${type}. ${message}`) - } - }, [path, type]) - - const source = useMemo(() => ({ uri: `file://${path}/1.mp4` }), [path]) - - const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [hasMediaLoaded]) - - return ( - - {type === 'photo' && ( - - )} - {type === 'video' && ( - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'white', - }, - closeButton: { - position: 'absolute', - top: SAFE_AREA_PADDING.paddingTop, - left: SAFE_AREA_PADDING.paddingLeft, - width: 40, - height: 40, - }, - saveButton: { - position: 'absolute', - bottom: SAFE_AREA_PADDING.paddingBottom, - left: SAFE_AREA_PADDING.paddingLeft, - width: 40, - height: 40, - }, - icon: { - textShadowColor: 'black', - textShadowOffset: { - height: 0, - width: 0, - }, - textShadowRadius: 1, - }, -}) diff --git a/package/example/src/PermissionsPage.tsx b/package/example/src/PermissionsPage.tsx deleted file mode 100644 index 966930e..0000000 --- a/package/example/src/PermissionsPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { useCallback, useEffect, useState } from 'react' -import { ImageRequireSource, Linking } from 'react-native' - -import { StyleSheet, View, Text, Image } from 'react-native' -import { Camera, CameraPermissionStatus } from 'react-native-vision-camera' -import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants' -import type { Routes } from './Routes' - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const BANNER_IMAGE = require('./img/11.png') as ImageRequireSource - -type Props = NativeStackScreenProps -export function PermissionsPage({ navigation }: Props): React.ReactElement { - const [cameraPermissionStatus, setCameraPermissionStatus] = useState('not-determined') - const [microphonePermissionStatus, setMicrophonePermissionStatus] = useState('not-determined') - - const requestMicrophonePermission = useCallback(async () => { - console.log('Requesting microphone permission...') - const permission = await Camera.requestMicrophonePermission() - console.log(`Microphone permission status: ${permission}`) - - if (permission === 'denied') await Linking.openSettings() - setMicrophonePermissionStatus(permission) - }, []) - - const requestCameraPermission = useCallback(async () => { - console.log('Requesting camera permission...') - const permission = await Camera.requestCameraPermission() - console.log(`Camera permission status: ${permission}`) - - if (permission === 'denied') await Linking.openSettings() - setCameraPermissionStatus(permission) - }, []) - - useEffect(() => { - if (cameraPermissionStatus === 'granted' && microphonePermissionStatus === 'granted') navigation.replace('CameraPage') - }, [cameraPermissionStatus, microphonePermissionStatus, navigation]) - - return ( - - - Welcome to{'\n'}Vision Camera. - - {cameraPermissionStatus !== 'granted' && ( - - Vision Camera needs Camera permission.{' '} - - Grant - - - )} - {microphonePermissionStatus !== 'granted' && ( - - Vision Camera needs Microphone permission.{' '} - - Grant - - - )} - - - ) -} - -const styles = StyleSheet.create({ - welcome: { - fontSize: 38, - fontWeight: 'bold', - maxWidth: '80%', - }, - banner: { - position: 'absolute', - opacity: 0.4, - bottom: 0, - left: 0, - }, - container: { - flex: 1, - backgroundColor: 'white', - ...SAFE_AREA_PADDING, - }, - permissionsContainer: { - marginTop: CONTENT_SPACING * 2, - }, - permissionText: { - fontSize: 17, - }, - hyperlink: { - color: '#007aff', - fontWeight: 'bold', - }, - bold: { - fontWeight: 'bold', - }, -}) diff --git a/package/example/src/Routes.ts b/package/example/src/Routes.ts deleted file mode 100644 index 25c6078..0000000 --- a/package/example/src/Routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Routes = { - PermissionsPage: undefined - CameraPage: undefined - CodeScannerPage: undefined - MediaPage: { - path: string - type: 'video' | 'photo' - } - Devices: undefined -} diff --git a/package/example/src/camera.tsx b/package/example/src/camera.tsx new file mode 100644 index 0000000..36cddb9 --- /dev/null +++ b/package/example/src/camera.tsx @@ -0,0 +1,112 @@ +import React, { useCallback, useRef, useState } from 'react' +import { Button, StyleSheet, Text, View } from 'react-native' +import { + Camera, + useCameraPermission, + useCameraDevice, + useCameraFormat, + PhotoFile, + VideoFile, + CameraRuntimeError, + Orientation, + CameraDevice, +} from 'react-native-vision-camera' +import { RecordingButton } from './capture-button' +import { useIsForeground } from './is-foreground' + +export default function CameraScreen() { + const camera = useRef(null) + const { hasPermission, requestPermission } = useCameraPermission() + const [isCameraInitialized, setIsCameraInitialized] = useState(false) + + const isForeground: boolean = useIsForeground() + const isActive: boolean = isForeground // Should be combined with isFocused hook + + const onError = useCallback((error: CameraRuntimeError) => { + console.error(error) + }, []) + + const onInitialized = useCallback(() => { + console.log('Camera initialized!') + setIsCameraInitialized(true) + }, []) + + const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => { + console.log(`Media captured! ${JSON.stringify(media)}`) + }, []) + + if (!hasPermission) requestPermission() + // Error handling in case they refuse to give permission + + const device = useCameraDevice('back') + const format = useCameraFormat(device, [{ videoResolution: { width: 3048, height: 2160 } }, { fps: 60 }]) // this sets as a target + + //Orientation detection + const [orientation, setOrientation] = useState('portrait') + + const toggleOrientation = () => { + setOrientation( + (currentOrientation) => (currentOrientation === 'landscape-left' ? 'portrait' : 'landscape-left'), // Can adjust this and the type to match what we want + ) + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (device === null) return Camera not available. Does user have permissions: {hasPermission} + + return ( + hasPermission && ( + + + + +