You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Aug 19, 2024. It is now read-only.
I have a very similar issue to this one. However, the PR that seems to have solved it for the person raising the issue is not working for me as my version of the rectangle scanner is the latest one: 1.0.10. The device that I am using is iPhone 6s, iOS 14.1. However, other test users had the same issue with iPhone 11 and other devices. The navigation used in the app is react-native-navigation.
The initial time that the scanner is launched is usually alright. Then when I navigate away from the screen and relaunch the viewer freezes. I can still take the image blindly and it would accurately reflect where the camera is pointing to. I made a test repo, which exhibits a similar pattern of behavior. The code for my Rectangle Scanner in the test repo is:
import {PropTypes} from 'prop-types';
import React, {PureComponent} from 'react';
import {
ActivityIndicator,
Animated,
Dimensions,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import Scanner, {
Filters,
RectangleOverlay,
} from 'react-native-rectangle-scanner';
import ScannerFilters from './Filters';
import ImageCropper from './ImageCropper';
const styles = StyleSheet.create({
button: {
alignItems: 'center',
height: 70,
justifyContent: 'center',
width: 65,
},
buttonActionGroup: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
},
buttonBottomContainer: {
alignItems: 'flex-end',
bottom: 40,
flexDirection: 'row',
justifyContent: 'space-between',
left: 25,
position: 'absolute',
right: 25,
},
buttonContainer: {
alignItems: 'flex-end',
bottom: 25,
flexDirection: 'column',
justifyContent: 'space-between',
position: 'absolute',
right: 25,
top: 25,
},
buttonGroup: {
backgroundColor: '#00000080',
borderRadius: 17,
},
buttonIcon: {
color: 'white',
fontSize: 22,
marginBottom: 3,
textAlign: 'center',
},
buttonText: {
color: 'white',
fontSize: 13,
},
buttonTopContainer: {
alignItems: 'flex-start',
flexDirection: 'row',
justifyContent: 'space-between',
left: 25,
position: 'absolute',
right: 25,
top: 40,
},
cameraButton: {
backgroundColor: 'white',
borderRadius: 50,
flex: 1,
margin: 3,
},
cameraNotAvailableContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
marginHorizontal: 15,
},
cameraNotAvailableText: {
color: 'white',
fontSize: 25,
textAlign: 'center',
},
cameraOutline: {
borderColor: 'white',
borderRadius: 50,
borderWidth: 3,
height: 70,
width: 70,
},
container: {
backgroundColor: 'black',
flex: 1,
},
flashControl: {
alignItems: 'center',
borderRadius: 30,
height: 50,
justifyContent: 'center',
margin: 8,
paddingTop: 7,
width: 50,
},
loadingCameraMessage: {
color: 'white',
fontSize: 18,
marginTop: 10,
textAlign: 'center',
},
loadingContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
overlay: {
bottom: 0,
flex: 1,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
processingContainer: {
alignItems: 'center',
backgroundColor: 'rgba(220, 220, 220, 0.7)',
borderRadius: 16,
height: 140,
justifyContent: 'center',
width: 200,
},
scanner: {
flex: 1,
},
});
export default class RectangleScanner extends PureComponent {
static propTypes = {
cameraIsOn: PropTypes.bool,
onLayout: PropTypes.func,
onSkip: PropTypes.func,
onCancel: PropTypes.func,
onPictureTaken: PropTypes.func,
onPictureProcessed: PropTypes.func,
hideSkip: PropTypes.bool,
initialFilterId: PropTypes.number,
onFilterIdChange: PropTypes.func,
};
static defaultProps = {
cameraIsOn: undefined,
onLayout: () => {},
onSkip: () => {},
onCancel: () => {},
onPictureTaken: () => {},
onPictureProcessed: () => {},
onFilterIdChange: () => {},
hideSkip: false,
initialFilterId: Filters.PLATFORM_DEFAULT_FILTER_ID,
};
constructor(props) {
super(props);
this.state = {
flashEnabled: false,
showScannerView: false,
didLoadInitialLayout: false,
filterId: props.initialFilterId || Filters.PLATFORM_DEFAULT_FILTER_ID,
detectedRectangle: false,
isMultiTasking: false,
loadingCamera: true,
processingImage: false,
imageProcessed: false,
initialImage: {},
takingPicture: false,
overlayFlashOpacity: new Animated.Value(0),
device: {
initialized: false,
hasCamera: false,
permissionToUseCamera: false,
flashIsAvailable: false,
previewHeightPercent: 1,
previewWidthPercent: 1,
},
};
this.camera = React.createRef();
this.imageProcessorTimeout = null;
}
componentDidMount() {
if (this.state.didLoadInitialLayout && !this.state.isMultiTasking) {
this.turnOnCamera();
}
}
componentWillUnmount() {
this.setState({
detectedRectangle: false,
});
}
componentDidUpdate() {
if (this.state.didLoadInitialLayout) {
if (this.state.isMultiTasking) {
return this.turnOffCamera(true);
}
if (this.state.device.initialized) {
if (!this.state.device.hasCamera) {
return this.turnOffCamera();
}
if (!this.state.device.permissionToUseCamera) {
return this.turnOffCamera();
}
}
if (this.props.cameraIsOn === true && !this.state.showScannerView) {
return this.turnOnCamera();
}
if (this.props.cameraIsOn === false && this.state.showScannerView) {
return this.turnOffCamera(true);
}
if (this.props.cameraIsOn === undefined) {
return this.turnOnCamera();
}
}
return null;
}
componentWillUnmount() {
clearTimeout(this.imageProcessorTimeout);
}
// Called after the device gets setup. This lets you know some platform specifics
// like if the device has a camera or flash, or even if you have permission to use the
// camera. It also includes the aspect ratio correction of the preview
onDeviceSetup = (deviceDetails) => {
const {
hasCamera,
permissionToUseCamera,
flashIsAvailable,
previewHeightPercent,
previewWidthPercent,
} = deviceDetails;
this.setState({
loadingCamera: false,
device: {
initialized: true,
hasCamera,
permissionToUseCamera,
flashIsAvailable,
previewHeightPercent: previewHeightPercent || 1,
previewWidthPercent: previewWidthPercent || 1,
},
});
};
// Set the camera view filter
onFilterIdChange = (id) => {
this.setState({filterId: id});
this.props.onFilterIdChange(id);
};
onImageCropped = (imageCropped) => {
this.setState({
imageProcessed: imageCropped,
});
};
// Determine why the camera is disabled.
getCameraDisabledMessage() {
if (this.state.isMultiTasking) {
return 'Camera is not allowed in multi tasking mode.';
}
const {device} = this.state;
if (device.initialized) {
if (!device.hasCamera) {
return 'Could not find a camera on the device.';
}
if (!device.permissionToUseCamera) {
return 'Permission to use camera has not been granted.';
}
}
return 'Failed to set up the camera.';
}
// On some android devices, the aspect ratio of the preview is different than
// the screen size. This leads to distorted camera previews. This allows for correcting that.
getPreviewSize() {
const dimensions = Dimensions.get('window');
// We use set margin amounts because for some reasons the percentage values don't align the camera preview in the center correctly.
const heightMargin =
((1 - this.state.device.previewHeightPercent) * dimensions.height) / 2;
const widthMargin =
((1 - this.state.device.previewWidthPercent) * dimensions.width) / 2;
if (dimensions.height > dimensions.width) {
// Portrait
return {
height: this.state.device.previewHeightPercent,
width: this.state.device.previewWidthPercent,
marginTop: heightMargin,
marginLeft: widthMargin,
};
}
// Landscape
return {
width: this.state.device.previewHeightPercent,
height: this.state.device.previewWidthPercent,
marginTop: widthMargin,
marginLeft: heightMargin,
};
}
// Capture the current frame/rectangle. Triggers the flash animation and shows a
// loading/processing state. Will not take another picture if already taking a picture.
capture = () => {
if (this.state.takingPicture) {
return;
}
if (this.state.processingImage) {
return;
}
this.setState({takingPicture: true, processingImage: true});
this.camera.current.capture();
this.triggerSnapAnimation();
// If capture failed, allow for additional captures
this.imageProcessorTimeout = setTimeout(() => {
if (this.state.takingPicture) {
this.setState({takingPicture: false});
}
}, 100);
};
// The picture was captured but still needs to be processed.
onPictureTaken = (event) => {
console.log('onPictureTaken', event);
this.setState({takingPicture: false});
this.props.onPictureTaken(event);
};
// The picture was taken and cached. You can now go on to using it.
onPictureProcessed = (event) => {
const {navigation} = this.props;
const {detectedRectangle} = this.state;
if (event?.initialImage) {
this.setState({
initialImage: event?.initialImage,
});
}
console.log('initialImage', this.state.initialImage)
this.props.onPictureProcessed(event);
this.setState({
takingPicture: false,
processingImage: false,
showScannerView: this.props.cameraIsOn || false,
detectedRectangle: false,
imageProcessed: true,
});
};
onRectangleDetected = (detectedRectangle) => {
if (detectedRectangle !== false) {
console.log('detectedRectangle', detectedRectangle);
this.setState({
detectedRectangle: detectedRectangle,
});
}
};
// Flashes the screen on capture
triggerSnapAnimation() {
Animated.sequence([
Animated.timing(this.state.overlayFlashOpacity, {
toValue: 0.2,
duration: 100,
}),
Animated.timing(this.state.overlayFlashOpacity, {
toValue: 0,
duration: 50,
}),
Animated.timing(this.state.overlayFlashOpacity, {
toValue: 0.6,
delay: 100,
duration: 120,
}),
Animated.timing(this.state.overlayFlashOpacity, {
toValue: 0,
duration: 90,
}),
]).start();
}
// Hides the camera view. If the camera view was shown and onDeviceSetup was called,
// but no camera was found, it will not uninitialize the camera state.
turnOffCamera(shouldUninitializeCamera = false) {
if (shouldUninitializeCamera && this.state.device.initialized) {
this.setState(({device}) => ({
showScannerView: false,
device: {...device, initialized: false},
}));
} else if (this.state.showScannerView) {
this.setState({showScannerView: false});
}
}
// Will show the camera view which will setup the camera and start it.
// Expect the onDeviceSetup callback to be called
turnOnCamera() {
if (!this.state.showScannerView) {
this.setState({
showScannerView: true,
loadingCamera: true,
});
}
}
// Renders the flashlight button. Only shown if the device has a flashlight.
renderFlashControl() {
const {flashEnabled, device} = this.state;
if (!device.flashIsAvailable) {
return null;
}
return (
<TouchableOpacity
style={[
styles.flashControl,
{backgroundColor: flashEnabled ? '#FFFFFF80' : '#00000080'},
]}
activeOpacity={0.8}
onPress={() => this.setState({flashEnabled: !flashEnabled})}>
<Icon
name="ios-flashlight"
style={[
styles.buttonIcon,
{fontSize: 28, color: flashEnabled ? '#333' : '#FFF'},
]}
/>
</TouchableOpacity>
);
}
// Renders the camera controls. This will show controls on the side for large tablet screens
// or on the bottom for phones. (For small tablets it will adjust the view a little bit).
renderCameraControls() {
const dimensions = Dimensions.get('window');
const aspectRatio = dimensions.height / dimensions.width;
const isPhone = aspectRatio > 1.6;
const cameraIsDisabled =
this.state.takingPicture || this.state.processingImage;
const disabledStyle = {opacity: cameraIsDisabled ? 0.8 : 1};
if (!isPhone) {
if (dimensions.height < 500) {
return (
<View style={styles.buttonContainer}>
<View
style={[
styles.buttonActionGroup,
{
flexDirection: 'row',
alignItems: 'flex-end',
marginBottom: 28,
},
]}>
{this.renderFlashControl()}
<ScannerFilters
filterId={this.state.filterId}
onFilterIdChange={this.onFilterIdChange}
/>
{this.props.hideSkip ? null : (
<View style={[styles.buttonGroup, {marginLeft: 8}]}>
<TouchableOpacity
style={[styles.button, disabledStyle]}
onPress={cameraIsDisabled ? () => null : this.props.onSkip}
activeOpacity={0.8}>
<Icon
name="md-arrow-round-forward"
size={40}
color="white"
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Skip</Text>
</TouchableOpacity>
</View>
)}
</View>
<View style={[styles.cameraOutline, disabledStyle]}>
<TouchableOpacity
activeOpacity={0.8}
style={styles.cameraButton}
onPress={this.capture}
/>
</View>
<View style={[styles.buttonActionGroup, {marginTop: 28}]}>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.button}
onPress={this.props.onCancel}
activeOpacity={0.8}>
<Icon
name="ios-chevron-back-circle"
size={40}
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
}
return (
<View style={styles.buttonContainer}>
<View
style={[
styles.buttonActionGroup,
{justifyContent: 'flex-end', marginBottom: 20},
]}>
{this.renderFlashControl()}
<ScannerFilters
filterId={this.state.filterId}
onFilterIdChange={this.onFilterIdChange}
/>
</View>
<View style={[styles.cameraOutline, disabledStyle]}>
<TouchableOpacity
activeOpacity={0.8}
style={styles.cameraButton}
onPress={this.capture}
/>
</View>
<View style={[styles.buttonActionGroup, {marginTop: 28}]}>
<View style={styles.buttonGroup}>
{this.props.hideSkip ? null : (
<TouchableOpacity
style={[styles.button, disabledStyle]}
onPress={cameraIsDisabled ? () => null : this.props.onSkip}
activeOpacity={0.8}>
<Icon
name="md-arrow-round-forward"
size={40}
color="white"
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Skip</Text>
</TouchableOpacity>
)}
</View>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.button}
onPress={this.props.onCancel}
activeOpacity={0.8}>
<Icon
name="ios-close-circle"
size={40}
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
}
return (
<>
<View style={styles.buttonBottomContainer}>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.button}
onPress={this.props.onCancel}
activeOpacity={0.8}>
<Icon
name="ios-close-circle"
size={40}
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
</View>
<View style={[styles.cameraOutline, disabledStyle]}>
<TouchableOpacity
activeOpacity={0.8}
style={styles.cameraButton}
onPress={this.capture}
/>
</View>
<View>
<View
style={[
styles.buttonActionGroup,
{
justifyContent: 'flex-end',
marginBottom: this.props.hideSkip ? 0 : 16,
},
]}>
<ScannerFilters
filterId={this.state.filterId}
onFilterIdChange={this.onFilterIdChange}
/>
{this.renderFlashControl()}
</View>
<View style={styles.buttonGroup}>
{this.props.hideSkip ? null : (
<TouchableOpacity
style={[styles.button, disabledStyle]}
onPress={cameraIsDisabled ? () => null : this.props.onSkip}
activeOpacity={0.8}>
<Icon
name="md-arrow-round-forward"
size={40}
color="white"
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Skip</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
</>
);
}
// Renders the camera controls or a loading/processing state
renderCameraOverlay() {
let loadingState = null;
if (this.state.loadingCamera) {
loadingState = (
<View style={styles.overlay}>
<View style={styles.loadingContainer}>
<ActivityIndicator color="white" />
<Text style={styles.loadingCameraMessage}>Loading Camera</Text>
</View>
</View>
);
} else if (this.state.processingImage) {
loadingState = (
<View style={styles.overlay}>
<View style={styles.loadingContainer}>
<View style={styles.processingContainer}>
<ActivityIndicator color="#333333" size="large" />
<Text style={{color: '#333333', fontSize: 30, marginTop: 10}}>
Processing
</Text>
</View>
</View>
</View>
);
}
return (
<>
{loadingState}
<SafeAreaView style={[styles.overlay]}>
{this.renderCameraControls()}
</SafeAreaView>
</>
);
}
// Renders either the camera view, a loading state, or an error message
// letting the user know why camera use is not allowed
renderCameraView() {
if (this.state.showScannerView) {
const previewSize = this.getPreviewSize();
let rectangleOverlay = null;
if (!this.state.loadingCamera && !this.state.processingImage) {
rectangleOverlay = (
<RectangleOverlay
detectedRectangle={this.state.detectedRectangle}
previewRatio={previewSize}
backgroundColor="rgba(255,181,6, 0.2)"
borderColor="rgb(255,181,6)"
borderWidth={4}
// == These let you auto capture and change the overlay style on detection ==
// detectedBackgroundColor="rgba(255,181,6, 0.3)"
// detectedBorderWidth={6}
// detectedBorderColor="rgb(255,218,124)"
// onDetectedCapture={this.capture}
// allowDetection
/>
);
}
// NOTE: I set the background color on here because for some reason the view doesn't line up correctly otherwise. It's a weird quirk I noticed.
return (
<View
style={{
backgroundColor: 'rgba(0, 0, 0, 0)',
position: 'relative',
marginTop: previewSize.marginTop,
marginLeft: previewSize.marginLeft,
height: `${previewSize.height * 100}%`,
width: `${previewSize.width * 100}%`,
}}>
<Scanner
onPictureTaken={this.onPictureTaken}
onPictureProcessed={this.onPictureProcessed}
enableTorch={this.state.flashEnabled}
filterId={this.state.filterId}
ref={this.camera}
capturedQuality={0.6}
onRectangleDetected={({detectedRectangle}) =>
this.onRectangleDetected(detectedRectangle)
}
onDeviceSetup={this.onDeviceSetup}
onTorchChanged={({enabled}) =>
this.setState({flashEnabled: enabled})
}
style={styles.scanner}
/>
{rectangleOverlay}
<Animated.View
style={{
...styles.overlay,
backgroundColor: 'white',
opacity: this.state.overlayFlashOpacity,
}}
/>
{this.renderCameraOverlay()}
</View>
);
}
let message = null;
if (this.state.loadingCamera) {
message = (
<View style={styles.overlay}>
<View style={styles.loadingContainer}>
<ActivityIndicator color="white" />
<Text style={styles.loadingCameraMessage}>Loading Camera</Text>
</View>
</View>
);
} else {
message = (
<Text style={styles.cameraNotAvailableText}>
{this.getCameraDisabledMessage()}
</Text>
);
}
return (
<View style={styles.cameraNotAvailableContainer}>
{message}
<View style={styles.buttonBottomContainer}>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.button}
onPress={this.props.onCancel}
activeOpacity={0.8}>
<Icon
name="ios-close-circle"
size={40}
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
</View>
<View style={styles.buttonGroup}>
{this.props.hideSkip ? null : (
<TouchableOpacity
style={[styles.button, {marginTop: 8}]}
onPress={this.props.onSkip}
activeOpacity={0.8}>
<Icon
name="md-arrow-round-forward"
size={40}
color="white"
style={styles.buttonIcon}
/>
<Text style={styles.buttonText}>Skip</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
render() {
return (
<View
style={styles.container}
onLayout={(event) => {
// This is used to detect multi tasking mode on iOS/iPad
// Camera use is not allowed
this.props.onLayout(event);
if (this.state.didLoadInitialLayout && Platform.OS === 'ios') {
const screenWidth = Dimensions.get('screen').width;
const isMultiTasking =
Math.round(event.nativeEvent.layout.width) <
Math.round(screenWidth);
if (isMultiTasking) {
this.setState({isMultiTasking: true, loadingCamera: false});
} else {
this.setState({isMultiTasking: false});
}
} else {
this.setState({didLoadInitialLayout: true});
}
}}>
<StatusBar
backgroundColor="black"
barStyle="light-content"
hidden={Platform.OS !== 'android'}
/>
{this.state.imageProcessed ? (
<ImageCropper
imageParam={this.state.initialImage}
rectangleCoordinates={this.state.detectedRectangle}
navigation={this.props.navigation}
onImageCropped={this.onImageCropped}
/>
) : (
this.renderCameraView()
)}
</View>
);
}
}
The text was updated successfully, but these errors were encountered:
rosentoshev
changed the title
Camera Freezes a Few Seconds After Loading
Camera Freezes a Few Seconds After Loading [iOS]
Nov 6, 2020
@rosenexpend So the issue happens primarily on the second time the camera is opened correct? I've experienced some weirdness with react native navigation and mounting/unmounting components. Like when navigating away from the view with the camera, the camera doesn't always get unmounted. If you use the lifecycle hooks that react-native-navigation offers for navigating away from the camera, you can unmount the camera and it should work. I primarily use this package in modals so that when the modal/view is dismissed the camera gets unmounted fully (not just put into a background view waiting for the user to navigate back to it).
I have a very similar issue to this one. However, the PR that seems to have solved it for the person raising the issue is not working for me as my version of the rectangle scanner is the latest one:
1.0.10
. The device that I am using is iPhone 6s, iOS 14.1. However, other test users had the same issue with iPhone 11 and other devices. The navigation used in the app isreact-native-navigation
.The initial time that the scanner is launched is usually alright. Then when I navigate away from the screen and relaunch the viewer freezes. I can still take the image blindly and it would accurately reflect where the camera is pointing to. I made a test repo, which exhibits a similar pattern of behavior. The code for my Rectangle Scanner in the test repo is:
The text was updated successfully, but these errors were encountered: