Skip to content

Instantly share code, notes, and snippets.

@jqn
Last active August 16, 2023 17:16
Show Gist options
  • Save jqn/747d22c86019406fd7438187a72ab82b to your computer and use it in GitHub Desktop.
Save jqn/747d22c86019406fd7438187a72ab82b to your computer and use it in GitHub Desktop.
React Native Camera Example with hooks
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",
},
});
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