Last active
August 16, 2023 17:16
-
-
Save jqn/747d22c86019406fd7438187a72ab82b to your computer and use it in GitHub Desktop.
React Native Camera Example with hooks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useRef, useEffect } from "react"; | |
| import { | |
| StyleSheet, | |
| Dimensions, | |
| View, | |
| Text, | |
| TouchableOpacity, | |
| SafeAreaView, | |
| } from "react-native"; | |
| import { Camera } from "expo-camera"; | |
| import { Video } from "expo-av"; | |
| const WINDOW_HEIGHT = Dimensions.get("window").height; | |
| const closeButtonSize = Math.floor(WINDOW_HEIGHT * 0.032); | |
| const captureSize = Math.floor(WINDOW_HEIGHT * 0.09); | |
| export default function App() { | |
| const [hasPermission, setHasPermission] = useState(null); | |
| const [cameraType, setCameraType] = useState(Camera.Constants.Type.back); | |
| const [isPreview, setIsPreview] = useState(false); | |
| const [isCameraReady, setIsCameraReady] = useState(false); | |
| const [isVideoRecording, setIsVideoRecording] = useState(false); | |
| const [videoSource, setVideoSource] = useState(null); | |
| const cameraRef = useRef(); | |
| useEffect(() => { | |
| (async () => { | |
| const { status } = await Camera.requestPermissionsAsync(); | |
| setHasPermission(status === "granted"); | |
| })(); | |
| }, []); | |
| const onCameraReady = () => { | |
| setIsCameraReady(true); | |
| }; | |
| const takePicture = async () => { | |
| if (cameraRef.current) { | |
| const options = { quality: 0.5, base64: true, skipProcessing: true }; | |
| const data = await cameraRef.current.takePictureAsync(options); | |
| const source = data.uri; | |
| if (source) { | |
| await cameraRef.current.pausePreview(); | |
| setIsPreview(true); | |
| console.log("picture source", source); | |
| } | |
| } | |
| }; | |
| const recordVideo = async () => { | |
| if (cameraRef.current) { | |
| try { | |
| const videoRecordPromise = cameraRef.current.recordAsync(); | |
| if (videoRecordPromise) { | |
| setIsVideoRecording(true); | |
| const data = await videoRecordPromise; | |
| const source = data.uri; | |
| if (source) { | |
| setIsPreview(true); | |
| console.log("video source", source); | |
| setVideoSource(source); | |
| } | |
| } | |
| } catch (error) { | |
| console.warn(error); | |
| } | |
| } | |
| }; | |
| const stopVideoRecording = () => { | |
| if (cameraRef.current) { | |
| setIsPreview(false); | |
| setIsVideoRecording(false); | |
| cameraRef.current.stopRecording(); | |
| } | |
| }; | |
| const switchCamera = () => { | |
| if (isPreview) { | |
| return; | |
| } | |
| setCameraType((prevCameraType) => | |
| prevCameraType === Camera.Constants.Type.back | |
| ? Camera.Constants.Type.front | |
| : Camera.Constants.Type.back | |
| ); | |
| }; | |
| const cancelPreview = async () => { | |
| await cameraRef.current.resumePreview(); | |
| setIsPreview(false); | |
| setVideoSource(null); | |
| }; | |
| const renderCancelPreviewButton = () => ( | |
| <TouchableOpacity onPress={cancelPreview} style={styles.closeButton}> | |
| <View style={[styles.closeCross, { transform: [{ rotate: "45deg" }] }]} /> | |
| <View | |
| style={[styles.closeCross, { transform: [{ rotate: "-45deg" }] }]} | |
| /> | |
| </TouchableOpacity> | |
| ); | |
| const renderVideoPlayer = () => ( | |
| <Video | |
| source={{ uri: videoSource }} | |
| shouldPlay={true} | |
| style={styles.media} | |
| /> | |
| ); | |
| const renderVideoRecordIndicator = () => ( | |
| <View style={styles.recordIndicatorContainer}> | |
| <View style={styles.recordDot} /> | |
| <Text style={styles.recordTitle}>{"Recording..."}</Text> | |
| </View> | |
| ); | |
| const renderCaptureControl = () => ( | |
| <View style={styles.control}> | |
| <TouchableOpacity disabled={!isCameraReady} onPress={switchCamera}> | |
| <Text style={styles.text}>{"Flip"}</Text> | |
| </TouchableOpacity> | |
| <TouchableOpacity | |
| activeOpacity={0.7} | |
| disabled={!isCameraReady} | |
| onLongPress={recordVideo} | |
| onPressOut={stopVideoRecording} | |
| onPress={takePicture} | |
| style={styles.capture} | |
| /> | |
| </View> | |
| ); | |
| if (hasPermission === null) { | |
| return <View />; | |
| } | |
| if (hasPermission === false) { | |
| return <Text style={styles.text}>No access to camera</Text>; | |
| } | |
| return ( | |
| <SafeAreaView style={styles.container}> | |
| <Camera | |
| ref={cameraRef} | |
| style={styles.container} | |
| type={cameraType} | |
| flashMode={Camera.Constants.FlashMode.on} | |
| onCameraReady={onCameraReady} | |
| onMountError={(error) => { | |
| console.log("cammera error", error); | |
| }} | |
| /> | |
| <View style={styles.container}> | |
| {isVideoRecording && renderVideoRecordIndicator()} | |
| {videoSource && renderVideoPlayer()} | |
| {isPreview && renderCancelPreviewButton()} | |
| {!videoSource && !isPreview && renderCaptureControl()} | |
| </View> | |
| </SafeAreaView> | |
| ); | |
| } | |
| const styles = StyleSheet.create({ | |
| container: { | |
| ...StyleSheet.absoluteFillObject, | |
| }, | |
| closeButton: { | |
| position: "absolute", | |
| top: 35, | |
| left: 15, | |
| height: closeButtonSize, | |
| width: closeButtonSize, | |
| borderRadius: Math.floor(closeButtonSize / 2), | |
| justifyContent: "center", | |
| alignItems: "center", | |
| backgroundColor: "#c4c5c4", | |
| opacity: 0.7, | |
| zIndex: 2, | |
| }, | |
| media: { | |
| ...StyleSheet.absoluteFillObject, | |
| }, | |
| closeCross: { | |
| width: "68%", | |
| height: 1, | |
| backgroundColor: "black", | |
| }, | |
| control: { | |
| position: "absolute", | |
| flexDirection: "row", | |
| bottom: 38, | |
| width: "100%", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| }, | |
| capture: { | |
| backgroundColor: "#f5f6f5", | |
| borderRadius: 5, | |
| height: captureSize, | |
| width: captureSize, | |
| borderRadius: Math.floor(captureSize / 2), | |
| marginHorizontal: 31, | |
| }, | |
| recordIndicatorContainer: { | |
| flexDirection: "row", | |
| position: "absolute", | |
| top: 25, | |
| alignSelf: "center", | |
| justifyContent: "center", | |
| alignItems: "center", | |
| backgroundColor: "transparent", | |
| opacity: 0.7, | |
| }, | |
| recordTitle: { | |
| fontSize: 14, | |
| color: "#ffffff", | |
| textAlign: "center", | |
| }, | |
| recordDot: { | |
| borderRadius: 3, | |
| height: 6, | |
| width: 6, | |
| backgroundColor: "#ff0000", | |
| marginHorizontal: 5, | |
| }, | |
| text: { | |
| color: "#fff", | |
| }, | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, {Component} from 'react'; | |
| import { | |
| Platform, | |
| StyleSheet, | |
| Dimensions, | |
| View, | |
| Alert, | |
| SafeAreaView, | |
| AppState, | |
| TouchableOpacity | |
| } from 'react-native'; | |
| import Slider from '@react-native-community/slider'; | |
| import _ from 'underscore'; | |
| import { Container, Button, Text, Icon, Footer, FooterTab, Spinner, H2, connectStyle, Toast } from 'native-base'; | |
| import { RNCamera } from 'react-native-camera'; | |
| import {NavigationEvents} from 'react-navigation'; | |
| import conf from 'src/conf'; | |
| import {getOrientation} from 'src/baseComponents/orientation'; | |
| import KeyboardShiftView from 'src/baseComponents/KeyboardShiftView'; | |
| import ZoomView from 'src/baseComponents/ZoomView'; | |
| import {runAfterInteractions} from 'src/baseComponents/utils'; | |
| import MainHeader from 'src/baseComponents/MainHeader'; | |
| const IS_IOS = Platform.OS == 'ios'; | |
| const touchCoordsSize = 100 * conf.theme.variables.sizeScaling; | |
| const flashIcons = { | |
| 'on': <Icon transparent name='flash' type='MaterialCommunityIcons'></Icon>, | |
| 'auto': <Icon transparent name='flash-auto' type='MaterialCommunityIcons'></Icon>, | |
| 'off': <Icon transparent name='flash-off' type='MaterialCommunityIcons'></Icon>, | |
| 'torch': <Icon transparent name='flashlight' type='MaterialCommunityIcons'></Icon>, | |
| } | |
| const MAX_ZOOM = 8; // iOS only | |
| const ZOOM_F = IS_IOS ? 0.01 : 0.1; | |
| const BACK_TYPE = RNCamera.Constants.Type.back; | |
| const FRONT_TYPE = RNCamera.Constants.Type.front; | |
| const WB_OPTIONS = [ | |
| RNCamera.Constants.WhiteBalance.auto, | |
| RNCamera.Constants.WhiteBalance.sunny, | |
| RNCamera.Constants.WhiteBalance.cloudy, | |
| RNCamera.Constants.WhiteBalance.shadow, | |
| RNCamera.Constants.WhiteBalance.incandescent, | |
| RNCamera.Constants.WhiteBalance.fluorescent | |
| ]; | |
| const WB_OPTIONS_MAP = { | |
| 0: 'WB', | |
| 1: "SU", | |
| 2: "CL", | |
| 3: "SH", | |
| 4: "IN", | |
| 5: "FL", | |
| 6: "CW" | |
| } | |
| const CUSTOM_WB_OPTIONS_MAP = { | |
| temperature: {label: "Temp.", min: 1000, max: 10000, steps: 500}, | |
| tint: {label: "Tint", min: -20, max: 20, steps: 0.5}, | |
| redGainOffset: {label: "Red", min: -1.0, max: 1.0, steps: 0.05}, | |
| greenGainOffset: {label: "Green", min: -1.0, max: 1.0, steps: 0.05}, | |
| blueGainOffset: {label: "Blue", min: -1.0, max: 1.0, steps: 0.05}, | |
| }; | |
| const getCameraType = (type) => { | |
| if(type == 'AVCaptureDeviceTypeBuiltInTelephotoCamera'){ | |
| return 'zoomed'; | |
| } | |
| if(type == 'AVCaptureDeviceTypeBuiltInUltraWideCamera'){ | |
| return 'wide'; | |
| } | |
| return 'normal'; | |
| } | |
| const flex1 = {flex: 1}; | |
| const styles = StyleSheet.create({ | |
| content: {flex: 1}, | |
| actionStyles: { | |
| position: 'absolute', | |
| bottom: 0, | |
| width: '100%', | |
| backgroundColor: 'transparent' | |
| }, | |
| capturingStyle: { | |
| position: 'absolute', | |
| bottom: 0, | |
| width: '100%', | |
| backgroundColor: 'rgba(0,0,0,0.4)', | |
| padding: conf.theme.variables.contentPadding, | |
| }, | |
| cameraLoading: {flex: 1, alignSelf: 'center'}, | |
| cameraNotAuthorized: { | |
| padding: 20 * conf.theme.variables.sizeScaling, | |
| paddingTop: 35 * conf.theme.variables.sizeScaling | |
| }, | |
| cameraButton: { | |
| flex: 1 | |
| }, | |
| buttonsView: { | |
| flex: 1, | |
| backgroundColor: 'black', | |
| width: '100%', | |
| flexDirection: 'row', | |
| alignItems: 'center', | |
| justifyContent: 'center' | |
| }, | |
| cameraSelectionRow: { | |
| flexDirection: 'row', | |
| flex: 1, | |
| alignItems: 'center', | |
| justifyContent: 'center' | |
| }, | |
| ratioButton: { | |
| width: 100 * conf.theme.variables.sizeScaling | |
| }, | |
| customWBView: { | |
| backgroundColor: '#00000080', | |
| flex: 1, | |
| width: '100%', | |
| height: 50, | |
| flexDirection: 'row', | |
| alignItems: 'center', | |
| paddingHorizontal: 8, | |
| }, | |
| customWBViewButton: { | |
| backgroundColor: 'transparent', | |
| alignSelf: 'center', | |
| width: '25%', | |
| }, | |
| customWBViewText: { | |
| color: 'white', | |
| }, | |
| customWBViewSlider: { | |
| flex: 2, | |
| marginRight: 6, | |
| }, | |
| }) | |
| const cameraNotAuthorized = <Text transparent style={styles.cameraNotAuthorized}>Camera access was not granted. Please go to your phone's settings and allow camera access.</Text>; | |
| const defaultCameraOptions = { | |
| flashMode: 'off', // on, auto, off, torch | |
| wb: 0, | |
| zoom: 0, // 0-1 | |
| focusCoords: undefined, | |
| currentCustomWBOption: "temperature", | |
| customWhiteBalance: { | |
| temperature: 6000, | |
| tint: 0.0, | |
| redGainOffset: 0.0, | |
| greenGainOffset: 0.0, | |
| blueGainOffset: 0.0, | |
| }, | |
| }; | |
| function parseRatio(str){ | |
| let [p1, p2] = str.split(":"); | |
| p1 = parseInt(p1); | |
| p2 = parseInt(p2); | |
| return p1 / p2; | |
| } | |
| class CameraSelectorButton extends React.PureComponent{ | |
| onChange = () => { | |
| if(!this.props.isSelected){ | |
| this.props.onChange(this.props.camera.id); | |
| } | |
| } | |
| render(){ | |
| let {camera, isSelected} = this.props; | |
| let cameraType = camera.cameraType; | |
| let IconComp; | |
| if(camera.type == BACK_TYPE){ | |
| if(cameraType == 'wide'){ | |
| IconComp = (props) => <Icon {...props} name='zoom-out' type='Feather'/>; | |
| } | |
| else if(cameraType == 'zoomed'){ | |
| IconComp = (props) => <Icon {...props} name='zoom-in' type='Feather'/>; | |
| } | |
| else{ | |
| IconComp = (props) => <Icon {...props} name='camera-rear' type='MaterialIcons'/>; | |
| } | |
| } | |
| else if(camera.type == FRONT_TYPE){ | |
| IconComp = (props) => <Icon {...props} name='camera-front' type='MaterialIcons'/>; | |
| } | |
| // should never happen | |
| else{ | |
| IconComp = (props) => <Icon {...props} normal name='ios-reverse-camera' type='Ionicons'/>; | |
| } | |
| return ( | |
| <Button | |
| transparent | |
| rounded | |
| onPress={this.onChange} | |
| selfCenter | |
| > | |
| <IconComp transparent={!isSelected} warning={isSelected} /> | |
| </Button> | |
| ) | |
| } | |
| } | |
| class CameraSelector extends React.PureComponent{ | |
| loopCamera = () => { | |
| let {cameraId, cameraIds, onChange} = this.props; | |
| if(cameraId != null && cameraIds.length){ | |
| let newIdx = (cameraIds.findIndex(i => i.id == cameraId) + 1) % cameraIds.length; | |
| onChange(cameraIds[newIdx].id); | |
| } | |
| else{ | |
| // if no available camera ids, always call with null | |
| onChange(null); | |
| } | |
| } | |
| render(){ | |
| let {cameraId, cameraIds} = this.props; | |
| if(!cameraIds){return null;} | |
| // camera ID is null, means we have no info about the camera. | |
| // fallback to regular switch | |
| if(cameraId == null){ | |
| return ( | |
| <Button | |
| transparent | |
| onPress={this.loopCamera} | |
| selfCenter | |
| > | |
| <Icon transparent normal name='ios-reverse-camera' type='Ionicons'></Icon> | |
| </Button> | |
| ) | |
| } | |
| // 0 or 1 cameras, no button | |
| if(cameraIds.length <= 1){ | |
| return null; | |
| } | |
| // 2 cameras, 1 button, no set default option | |
| if(cameraIds.length == 2){ | |
| return ( | |
| <Button | |
| transparent | |
| onPress={this.loopCamera} | |
| selfCenter | |
| > | |
| <Icon transparent normal name='ios-reverse-camera' type='Ionicons'></Icon> | |
| </Button> | |
| ) | |
| } | |
| // 3 or more cameras, multiple buttons | |
| return ( | |
| <React.Fragment> | |
| {cameraIds.map((v, i) => { | |
| return ( | |
| <CameraSelectorButton | |
| key={`${i}`} | |
| camera={v} | |
| isSelected={cameraId == v.id} | |
| onChange={this.props.onChange} | |
| /> | |
| ) | |
| })} | |
| </React.Fragment> | |
| ) | |
| } | |
| } | |
| class Camera extends Component{ | |
| constructor(props) { | |
| super(props); | |
| this.state = { | |
| ...defaultCameraOptions, | |
| orientation: getOrientation(), | |
| takingPic: false, | |
| recording: false, | |
| audioDisabled: false, | |
| elapsed: 0, | |
| cameraReady: false, | |
| cameraIds: null, // null means not checked, empty list means no results | |
| cameraType: BACK_TYPE, | |
| cameraId: null, | |
| aspectRatioStr: "4:3", | |
| aspectRatio: parseRatio("4:3") | |
| }; | |
| this._prevPinch = 1; | |
| } | |
| componentDidMount(){ | |
| this.mounted = true; | |
| AppState.addEventListener('change', this.handleAppStateChange); | |
| Dimensions.addEventListener('change', this.adjustOrientation); | |
| } | |
| componentWillUnmount(){ | |
| this.mounted = false; | |
| AppState.removeEventListener('change', this.handleAppStateChange); | |
| this.stopVideo(); | |
| } | |
| adjustOrientation = () => { | |
| setTimeout(()=>{ | |
| if(this.mounted){ | |
| this.setState({orientation: getOrientation()}); | |
| } | |
| }, 50); | |
| } | |
| // audio permission will be android only | |
| onCameraStatusChange = (s) => { | |
| if(s.cameraStatus == 'READY'){ | |
| let audioDisabled = s.recordAudioPermissionStatus == 'NOT_AUTHORIZED'; | |
| this.setState({audioDisabled: audioDisabled}, async () => { | |
| let ids = []; | |
| // dummy for simulator test | |
| // uncomment above and below | |
| // let ids = [ | |
| // {id: '1', type: BACK_TYPE, deviceType: 'AVCaptureDeviceTypeBuiltInWideAngleCamera'}, | |
| // {id: '2', type: BACK_TYPE, deviceType: 'AVCaptureDeviceTypeBuiltInTelephotoCamera'}, | |
| // {id: '3', type: BACK_TYPE, deviceType: 'AVCaptureDeviceTypeBuiltInUltraWideCamera'}, | |
| // {id: '4', type: FRONT_TYPE, deviceType: 'AVCaptureDeviceTypeBuiltInWideAngleCamera'}, | |
| // ] | |
| let cameraId = null; | |
| try{ | |
| ids = await this.camera.getCameraIdsAsync(); | |
| // map deviceType to our types | |
| ids = ids.map(d => { | |
| d.cameraType = getCameraType(d.deviceType); | |
| return d; | |
| }); | |
| if(ids.length){ | |
| // select the first back camera found | |
| cameraId = ids[0].id; | |
| for(let c of ids){ | |
| if(c.type == BACK_TYPE){ | |
| cameraId = c.id; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| catch(err){ | |
| console.error("Failed to get camera ids", err.message || err); | |
| } | |
| // sort ids so front cameras are first | |
| ids = _.sortBy(ids, v => v.type == FRONT_TYPE ? 0 : 1); | |
| this.setState({cameraIds: ids, cameraId: cameraId}); | |
| }); | |
| } | |
| else{ | |
| if(this.state.cameraReady){ | |
| this.setState({cameraReady: false}); | |
| } | |
| } | |
| } | |
| onCameraReady = () => { | |
| if(!this.state.cameraReady){ | |
| this.setState({cameraReady: true}); | |
| } | |
| } | |
| onCameraMountError = () => { | |
| setTimeout(()=>{ | |
| Alert.alert("Error", "Camera start failed."); | |
| }, 150); | |
| } | |
| handleAppStateChange = (nextAppState) => { | |
| } | |
| onDidFocus = () => { | |
| this.focused = true; | |
| } | |
| onDidBlur = async () => { | |
| this.focused = false; | |
| this.stopVideo(); | |
| } | |
| onPinchProgress = (p) => { | |
| let p2 = p - this._prevPinch; | |
| if(p2 > 0 && p2 > ZOOM_F){ | |
| this._prevPinch = p; | |
| this.setState({zoom: Math.min(this.state.zoom + ZOOM_F, 1)}) | |
| } | |
| else if (p2 < 0 && p2 < -ZOOM_F){ | |
| this._prevPinch = p; | |
| this.setState({zoom: Math.max(this.state.zoom - ZOOM_F, 0)}) | |
| } | |
| } | |
| onTapToFocus = (touchOrigin) => { | |
| if(!this.cameraStyle || this.state.takingPic){ | |
| return; | |
| } | |
| const {x, y} = touchOrigin; | |
| let {width, height, top, left} = this.cameraStyle; | |
| // compensate for top/left changes | |
| let pageX2 = x - left; | |
| let pageY2 = y - top; | |
| // normalize coords as described by https://gist.github.com/Craigtut/6632a9ac7cfff55e74fb561862bc4edb | |
| const x0 = pageX2 / width; | |
| const y0 = pageY2 / height; | |
| let computedX = x0; | |
| let computedY = y0; | |
| // if portrait, need to apply a transform because RNCamera always measures coords in landscape mode | |
| // with the home button on the right. If the phone is rotated with the home button to the left | |
| // we will have issues here, and we have no way to detect that orientation! | |
| // TODO: Fix this, however, that orientation should never be used due to camera positon | |
| if(this.state.orientation.isPortrait){ | |
| computedX = y0; | |
| computedY = -x0 + 1; | |
| } | |
| this.setState({ | |
| focusCoords: { | |
| x: computedX, | |
| y: computedY, | |
| autoExposure: true | |
| }, | |
| touchCoords: { | |
| x: pageX2 - 50, | |
| y: pageY2 - 50 | |
| } | |
| },this.onSetFocus); | |
| // remove focus rectangle | |
| if(this.focusTimeout){ | |
| clearTimeout(this.focusTimeout); | |
| this.focusTimeout = null; | |
| } | |
| } | |
| onSetFocus = () => { | |
| this.focusTimeout = setTimeout(() => { | |
| if (this.mounted) { | |
| this.setState({touchCoords: null}); | |
| } | |
| }, 1500); | |
| } | |
| onPinchStart = () => { | |
| this._prevPinch = 1; | |
| } | |
| onPinchEnd = () => { | |
| this._prevPinch = 1; | |
| } | |
| onAudioInterrupted = () => { | |
| this.setState({audioDisabled: true}); | |
| } | |
| onAudioConnected = () => { | |
| this.setState({audioDisabled: false}); | |
| } | |
| onPictureTaken = () => { | |
| this.setState({takingPic: false}); | |
| } | |
| onRecordingStart = () => { | |
| this.reportRequestPrompt = true; | |
| if(this._recordingTimer){ | |
| clearInterval(this._recordingTimer); | |
| this._recordingTimer = null; | |
| } | |
| if(this.state.recording){ | |
| this.setState({elapsed: 0}) | |
| this._recordingTimer = setInterval(()=>{ | |
| this.setState({elapsed: this.state.elapsed + 1}) | |
| }, 1000); | |
| } | |
| } | |
| onRecordingEnd = () => { | |
| this.reportRequestPrompt = true; | |
| if(this._recordingTimer){ | |
| clearInterval(this._recordingTimer); | |
| this._recordingTimer = null; | |
| } | |
| } | |
| goBack = () => { | |
| this.props.navigation.goBack(); | |
| } | |
| render() { | |
| let {orientation, takingPic, cameraReady, recording, audioDisabled, zoom, wb, cameraType, cameraId, cameraIds, flashMode, elapsed} = this.state; | |
| let {style} = this.props; | |
| let isPortrait = orientation.isPortrait; | |
| let disable = takingPic || !cameraReady; | |
| let disableOrRecording = disable || recording; | |
| // flag to decide how to layout camera buttons | |
| let cameraCount = 0; | |
| // we have queried the list of cameras | |
| if(cameraIds != null){ | |
| if(cameraId == null){ | |
| cameraCount = 2; // no camera id info, assume 2 cameras to switch from back and front | |
| } | |
| else{ | |
| cameraCount = cameraIds.length; | |
| } | |
| } | |
| let buttons = ( | |
| <React.Fragment> | |
| <Button | |
| transparent | |
| rounded | |
| onPress={this.takePicture} | |
| disabled={disableOrRecording} | |
| style={styles.cameraButton} | |
| > | |
| <Icon name={disableOrRecording ? 'camera-off' :'camera'} type='MaterialCommunityIcons'></Icon> | |
| </Button> | |
| {recording ? | |
| <Button | |
| transparent | |
| rounded | |
| onPress={this.stopVideo} | |
| danger | |
| > | |
| <Icon name='video-slash' type='FontAwesome5'></Icon> | |
| </Button> | |
| : | |
| <Button | |
| transparent | |
| rounded | |
| onPress={this.startVideo} | |
| disabled={disable} | |
| > | |
| <Icon name='video' type='FontAwesome5'></Icon> | |
| </Button> | |
| } | |
| </React.Fragment>); | |
| let cameraStyle; | |
| // style to cover all the screen exactly | |
| // leaving footer and extra heights | |
| let mainViewStyle = { | |
| flex: 1, | |
| width: isPortrait ? orientation.width : orientation.width - style.footerWidth, | |
| height: orientation.height - (style.footerHeight * isPortrait) - orientation.minusHeight - orientation.insetBottom | |
| } | |
| if(isPortrait){ | |
| let height = orientation.width * this.state.aspectRatio; | |
| cameraStyle = { | |
| position: 'absolute', | |
| top: Math.max(0, (mainViewStyle.height - height) / 2), | |
| left: 0, | |
| width: orientation.width, | |
| height: height | |
| } | |
| } | |
| else{ | |
| let height = orientation.height - orientation.minusHeight; | |
| let width = height * this.state.aspectRatio; | |
| cameraStyle = { | |
| position: 'absolute', | |
| top: 0, | |
| left: Math.max(0, (mainViewStyle.width - width) / 2), | |
| width: width, | |
| height: height | |
| } | |
| } | |
| this.cameraStyle = cameraStyle; | |
| let isCustomWhiteBalance = wb >= WB_OPTIONS.length; | |
| let whiteBalance = isCustomWhiteBalance ? this.state.customWhiteBalance : WB_OPTIONS[wb]; | |
| const { currentCustomWBOption } = this.state; | |
| let customWhiteBalanceValue = this.state.customWhiteBalance[currentCustomWBOption]; | |
| let customWhiteBalanceOption = CUSTOM_WB_OPTIONS_MAP[currentCustomWBOption] | |
| return ( | |
| <Container fullBlack> | |
| <KeyboardShiftView style={styles.content} keyboardShouldPersistTaps={'never'} extraHeight={0} bounces={false}> | |
| <NavigationEvents | |
| onDidFocus={this.onDidFocus} | |
| onDidBlur={this.onDidBlur} | |
| /> | |
| <View style={mainViewStyle}> | |
| <MainHeader | |
| transparent | |
| back={true} | |
| title={"Camera"} | |
| navigation={this.props.navigation} | |
| /> | |
| <RNCamera | |
| ref={ref => { | |
| this.camera = ref; | |
| }} | |
| style={cameraStyle} | |
| type={cameraType} | |
| cameraId={cameraId} | |
| //useCamera2Api={true} | |
| onAudioInterrupted={this.onAudioInterrupted} | |
| onAudioConnected={this.onAudioConnected} | |
| onPictureTaken={this.onPictureTaken} | |
| onRecordingStart={this.onRecordingStart} | |
| onRecordingEnd={this.onRecordingEnd} | |
| ratio={this.state.aspectRatioStr} | |
| flashMode={flashMode} | |
| zoom={zoom} | |
| maxZoom={MAX_ZOOM} | |
| useNativeZoom={true} | |
| onTap={this.onTapToFocus} | |
| whiteBalance={whiteBalance} | |
| autoFocusPointOfInterest={this.state.focusCoords} | |
| androidCameraPermissionOptions={{ | |
| title: 'Permission to use camera', | |
| message: 'We need your permission to use your camera', | |
| buttonPositive: 'Ok', | |
| buttonNegative: 'Cancel', | |
| }} | |
| androidRecordAudioPermissionOptions={{ | |
| title: 'Permission to use audio recording', | |
| message: 'We need your permission to use your audio', | |
| buttonPositive: 'Ok', | |
| buttonNegative: 'Cancel', | |
| }} | |
| onStatusChange={this.onCameraStatusChange} | |
| onCameraReady={this.onCameraReady} | |
| onMountError={this.onCameraMountError} | |
| pendingAuthorizationView={ | |
| <SafeAreaView style={styles.cameraLoading}> | |
| <Spinner color={style.brandLight}/> | |
| </SafeAreaView> | |
| } | |
| notAuthorizedView={ | |
| <View> | |
| {cameraNotAuthorized} | |
| </View> | |
| } | |
| > | |
| {this.state.touchCoords ? | |
| <View style={{ | |
| borderWidth: 2, | |
| borderColor: takingPic ? 'red' : 'gray', | |
| position: 'absolute', | |
| top: this.state.touchCoords.y, | |
| left: this.state.touchCoords.x, | |
| width: touchCoordsSize, | |
| height: touchCoordsSize | |
| }}> | |
| </View> | |
| : null} | |
| </RNCamera> | |
| {!takingPic && !recording && !this.state.spinnerVisible && cameraReady ? | |
| <SafeAreaView | |
| style={styles.actionStyles} | |
| > | |
| <React.Fragment> | |
| {cameraCount > 2 ? | |
| <View style={styles.cameraSelectionRow}> | |
| <Button | |
| transparent | |
| onPress={this.changeWB} | |
| selfCenter | |
| > | |
| <Text transparent>{WB_OPTIONS_MAP[wb]}</Text> | |
| </Button> | |
| <CameraSelector | |
| cameraId={cameraId} | |
| cameraIds={this.state.cameraIds} | |
| onChange={this.onCameraChange} | |
| /> | |
| </View> | |
| : null} | |
| {isCustomWhiteBalance && ( | |
| <View style={styles.customWBView}> | |
| <Button style={styles.customWBViewButton} onPress={this.changeCustomWBOption}> | |
| <Text style={styles.customWBViewText}> | |
| {customWhiteBalanceOption.label} | |
| </Text> | |
| </Button> | |
| <Slider | |
| style={styles.customWBViewSlider} | |
| value={customWhiteBalanceValue} | |
| step={customWhiteBalanceOption.steps} | |
| minimumValue={customWhiteBalanceOption.min} | |
| maximumValue={customWhiteBalanceOption.max} | |
| minimumTrackTintColor="#FFFFFF" | |
| maximumTrackTintColor="#000000" | |
| onValueChange={this.changeCustomWBOptionValue} | |
| /> | |
| <Text style={[styles.customWBViewText, {minWidth: '15%'}]}> | |
| {customWhiteBalanceValue.toFixed(1)} | |
| </Text> | |
| </View> | |
| )} | |
| <View style={styles.buttonsView}> | |
| <Button | |
| transparent | |
| onPress={this.resetZoom} | |
| selfCenter | |
| > | |
| <Text transparent>{`${(zoom * 100).toFixed(0)}%`}</Text> | |
| </Button> | |
| {cameraCount <= 2 ? | |
| <Button | |
| transparent | |
| onPress={this.changeWB} | |
| selfCenter | |
| > | |
| <Text transparent>{WB_OPTIONS_MAP[wb]}</Text> | |
| </Button> | |
| : null} | |
| <Button | |
| transparent | |
| onPress={this.toggleRatio} | |
| style={styles.ratioButton} | |
| selfCenter | |
| > | |
| <Text transparent>{this.state.aspectRatioStr}</Text> | |
| </Button> | |
| <Button | |
| transparent | |
| onPress={this.toggleFlash} | |
| selfCenter | |
| > | |
| {flashIcons[flashMode]} | |
| </Button> | |
| {(cameraCount > 1 && cameraCount <= 2) ? | |
| <CameraSelector | |
| cameraId={cameraId} | |
| cameraIds={this.state.cameraIds} | |
| onChange={this.onCameraChange} | |
| /> | |
| : null} | |
| </View> | |
| </React.Fragment> | |
| </SafeAreaView> | |
| : null } | |
| {(takingPic || recording)? | |
| <View | |
| style={styles.capturingStyle} | |
| > | |
| {takingPic ? <H2 transparent>Capturing Picture...</H2> : <H2 transparent>{`Capturing Video${audioDisabled ? ' (muted)' : ''}... (${elapsed != -1 ? elapsed : "Preparing Camera..."})`}</H2>} | |
| </View> | |
| : null} | |
| </View> | |
| </KeyboardShiftView> | |
| {isPortrait ? | |
| <Footer> | |
| <FooterTab> | |
| {buttons} | |
| </FooterTab> | |
| </Footer> | |
| : | |
| <Footer landscape> | |
| <FooterTab landscape> | |
| {buttons} | |
| </FooterTab> | |
| </Footer> | |
| } | |
| </Container> | |
| ); | |
| } | |
| takePicture = async () => { | |
| if (this.camera) { | |
| if(this.state.takingPic || this.state.recording || !this.state.cameraReady){ | |
| return; | |
| } | |
| // if we have a non original quality, skip processing and compression. | |
| // we will use JPEG compression on resize. | |
| let options = { | |
| quality: 0.85, | |
| fixOrientation: true, | |
| forceUpOrientation: true, | |
| writeExif: true | |
| }; | |
| this.setState({takingPic: true}); | |
| let data = null; | |
| try{ | |
| data = await this.camera.takePictureAsync(options); | |
| } | |
| catch(err){ | |
| Alert.alert("Error", "Failed to take picture: " + (err.message || err)); | |
| return; | |
| } | |
| Alert.alert("Picture Taken!", JSON.stringify(data, null, 2)); | |
| } | |
| } | |
| startVideo = async () => { | |
| if (this.camera && !this.state.recording) { | |
| // need to do this in order to avoid race conditions | |
| this.state.recording = true; | |
| const options = { | |
| quality: '480p', | |
| maxDuration: 60, | |
| maxFileSize: 100 * 1024 * 1024 | |
| }; | |
| this.setState({recording: true, elapsed: -1}, async () => { | |
| let result = null; | |
| try { | |
| result = await this.camera.recordAsync(options); | |
| } | |
| catch(err){ | |
| console.warn("VIDEO RECORD FAIL", err.message, err); | |
| Alert.alert("Error", "Failed to store recorded video: " + err.message); | |
| } | |
| if(result){ | |
| Alert.alert("Video recorded!", JSON.stringify(result)); | |
| } | |
| // give time for the camera to recover | |
| setTimeout(()=>{ | |
| this.setState({recording: false}); | |
| }, 500); | |
| // might be cleared on recording stop or | |
| // here if we had errors | |
| if(this._recordingTimer){ | |
| clearInterval(this._recordingTimer); | |
| this._recordingTimer = null; | |
| } | |
| }); | |
| } | |
| } | |
| stopVideo = () => { | |
| if(this.camera && this.state.recording){ | |
| this.camera.stopRecording(); | |
| } | |
| } | |
| toggleFlash = () => { | |
| if (this.state.flashMode === 'torch') { | |
| this.setState({ flashMode: 'off' }); | |
| } else if (this.state.flashMode === 'off') { | |
| this.setState({ flashMode: 'auto' }); | |
| } else if (this.state.flashMode === 'auto') { | |
| this.setState({ flashMode: 'on' }); | |
| } else if (this.state.flashMode === 'on') { | |
| this.setState({ flashMode: 'torch' }); | |
| } | |
| } | |
| changeWB = () => { | |
| // The custom white balance feature is only available on iOS (#2774) | |
| const numberOfOptions = IS_IOS ? Object.keys(WB_OPTIONS_MAP).length : WB_OPTIONS.length; | |
| this.setState({ | |
| wb: (this.state.wb + 1) % numberOfOptions | |
| }); | |
| } | |
| changeCustomWBOption = () => { | |
| const optionKeys = Object.keys(CUSTOM_WB_OPTIONS_MAP); | |
| let currentOptionIndex = optionKeys.indexOf(this.state.currentCustomWBOption); | |
| let nextOptionIndex = (currentOptionIndex + 1) % optionKeys.length; | |
| this.setState({ | |
| currentCustomWBOption: optionKeys[nextOptionIndex] | |
| }); | |
| } | |
| changeCustomWBOptionValue = (value) => { | |
| this.setState((state) => ({ | |
| customWhiteBalance: { | |
| ...state.customWhiteBalance, | |
| [state.currentCustomWBOption]: value, | |
| }, | |
| })); | |
| } | |
| toggleRatio = () => { | |
| if(this.state.aspectRatioStr == "4:3"){ | |
| this.setState({ | |
| aspectRatioStr: "1:1", | |
| aspectRatio: parseRatio("1:1") | |
| }); | |
| } | |
| else if(this.state.aspectRatioStr == "1:1"){ | |
| this.setState({ | |
| aspectRatioStr: "16:9", | |
| aspectRatio: parseRatio("16:9") | |
| }); | |
| } | |
| else{ | |
| this.setState({ | |
| aspectRatioStr: "4:3", | |
| aspectRatio: parseRatio("4:3") | |
| }); | |
| } | |
| } | |
| onCameraChange = (cameraId) => { | |
| this.setState({cameraReady: false}, () => { | |
| runAfterInteractions(() => { | |
| // cameraId will be null if we failed to get a camera by ID or | |
| // our id list is empty. Fallback to back/front setting | |
| if(cameraId == null){ | |
| let cameraType = this.state.cameraType; | |
| if(cameraType == FRONT_TYPE){ | |
| this.setState({cameraType: BACK_TYPE, cameraId: null, ...defaultCameraOptions}); | |
| } | |
| else{ | |
| this.setState({cameraType: FRONT_TYPE, cameraId: null, ...defaultCameraOptions}); | |
| } | |
| } | |
| else{ | |
| this.setState({cameraId: cameraId, ...defaultCameraOptions}); | |
| } | |
| }); | |
| }); | |
| } | |
| resetZoom = () => { | |
| this._prevPinch = 1; | |
| this.setState({zoom: 0}); | |
| } | |
| } | |
| Camera.navigationOptions = ({ navigation }) => { | |
| return { | |
| header: props => null | |
| } | |
| } | |
| Camera = connectStyle("Branding")(Camera); | |
| export default Camera; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment