Skip to content

Commit f5e3bc9

Browse files
committed
Add mount stress test to CameraKit example app
Introduces a mount stress test feature in the example app, allowing repeated mounting and unmounting of the CameraExample component at a configurable interval. Updates CameraExample to support a 'stress' mode that triggers image capture and random zoom changes on mount.
1 parent 6c7cfe4 commit f5e3bc9

File tree

12 files changed

+177
-49
lines changed

12 files changed

+177
-49
lines changed

README.md

Lines changed: 35 additions & 34 deletions
Large diffs are not rendered by default.

example/src/App.tsx

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import React, { useState } from 'react';
2-
import { StyleSheet, Text, View, TouchableOpacity, ScrollView } from 'react-native';
2+
import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Button, Alert, TextInput } from 'react-native';
33

44
import BarcodeScreenExample from './BarcodeScreenExample';
55
import CameraExample from './CameraExample';
66

77
const App = () => {
8-
const [example, setExample] = useState<JSX.Element>();
8+
const [example, setExample] = useState<any>(undefined);
9+
const [testNo, setTestNo] = useState(0);
10+
const [interval, setIntervalId] = useState<number | null>(null);
11+
const [speed, setSpeed] = useState('1000');
12+
const onBack = () => setExample(undefined);
913

1014
if (example) {
1115
return example;
1216
}
1317

14-
const onBack = () => setExample(undefined);
15-
1618
return (
17-
<ScrollView style={styles.scroll}>
19+
<ScrollView style={styles.scroll} scrollEnabled={false}>
1820
<View style={styles.container}>
1921
<Text style={{ fontSize: 60 }}>🎈</Text>
2022
<Text style={styles.headerText}>React Native Camera Kit</Text>
@@ -24,6 +26,67 @@ const App = () => {
2426
<TouchableOpacity style={styles.button} onPress={() => setExample(<BarcodeScreenExample onBack={onBack} />)}>
2527
<Text style={styles.buttonText}>Barcode Scanner</Text>
2628
</TouchableOpacity>
29+
<View>
30+
<Text style={[styles.stressHeader, { marginTop: 12 }]}>Mount Stress Test</Text>
31+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
32+
{!testNo ? (
33+
<>
34+
<View style={styles.inputContainer}>
35+
<Text style={styles.inputLabel}>Speed (ms):</Text>
36+
<TextInput
37+
style={styles.input}
38+
value={speed}
39+
onChangeText={setSpeed}
40+
keyboardType="number-pad"
41+
placeholder="1000"
42+
placeholderTextColor="#999"
43+
/>
44+
</View>
45+
46+
<Button
47+
title="Start"
48+
onPress={() => {
49+
Alert.alert(
50+
'2 min or more',
51+
'The mount stress test should run for at least 2 minutes on an iPhone 17 Pro before you can declare it a success. You need to press the stop button yourself.',
52+
[
53+
{
54+
text: 'OK',
55+
onPress: () => {
56+
setIntervalId(
57+
setInterval(() => {
58+
setTestNo((prev) => {
59+
const newR = prev + 1;
60+
if (newR % 2 === 0) {
61+
setExample(<CameraExample key={String(Math.random())} stress onBack={onBack} />);
62+
} else {
63+
setExample(undefined);
64+
}
65+
return newR;
66+
});
67+
}, parseInt(speed, 10) || 1000),
68+
);
69+
},
70+
},
71+
],
72+
);
73+
}}
74+
/>
75+
</>
76+
) : (
77+
<Button
78+
title="STOP STRESS TEST"
79+
onPress={() => {
80+
setTestNo(0);
81+
if (interval) {
82+
clearInterval(interval);
83+
setIntervalId(null);
84+
}
85+
}}
86+
/>
87+
)}
88+
</View>
89+
</View>
2790
</View>
2891
</ScrollView>
2992
);
@@ -49,6 +112,11 @@ const styles = StyleSheet.create({
49112
fontWeight: 'bold',
50113
marginBlockEnd: 24,
51114
},
115+
stressHeader: {
116+
color: 'white',
117+
fontSize: 24,
118+
fontWeight: 'bold',
119+
},
52120
button: {
53121
height: 60,
54122
borderRadius: 30,
@@ -62,4 +130,24 @@ const styles = StyleSheet.create({
62130
textAlign: 'center',
63131
fontSize: 20,
64132
},
133+
inputContainer: {
134+
flexDirection: 'row',
135+
alignItems: 'center',
136+
marginVertical: 12,
137+
minWidth: 170,
138+
},
139+
inputLabel: {
140+
color: 'white',
141+
fontSize: 16,
142+
marginRight: 12,
143+
},
144+
input: {
145+
flex: 1,
146+
height: 40,
147+
borderRadius: 8,
148+
backgroundColor: '#333',
149+
color: 'white',
150+
paddingHorizontal: 12,
151+
fontSize: 16,
152+
},
65153
});

example/src/CameraExample.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type React from 'react';
2-
import { useState, useRef } from 'react';
2+
import { useState, useRef, useEffect } from 'react';
33
import { StyleSheet, Text, View, TouchableOpacity, Image, Animated, ScrollView } from 'react-native';
44
import Camera from '../../src/Camera';
55
import { type CameraApi, CameraType, type CaptureData } from '../../src/types';
@@ -33,7 +33,7 @@ function median(values: number[]): number {
3333
return sortedValues.length % 2 ? sortedValues[half] : (sortedValues[half - 1] + sortedValues[half]) / 2;
3434
}
3535

36-
const CameraExample = ({ onBack }: { onBack: () => void }) => {
36+
const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolean }) => {
3737
const cameraRef = useRef<CameraApi>(null);
3838
const [currentFlashArrayPosition, setCurrentFlashArrayPosition] = useState(0);
3939
const [captureImages, setCaptureImages] = useState<CaptureData[]>([]);
@@ -46,6 +46,15 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
4646
const [orientationAnim] = useState(new Animated.Value(3));
4747
const [resize, setResize] = useState<'contain' | 'cover'>('contain');
4848

49+
// zoom to random positions every 10ms:
50+
useEffect(() => {
51+
if (stress !== true) return;
52+
const interval = setInterval(() => {
53+
setZoom(Math.random() * 10);
54+
}, 500);
55+
return () => clearInterval(interval);
56+
}, [stress]);
57+
4958
// iOS will error out if capturing too fast,
5059
// so block capturing until the current capture is done
5160
// This also minimizes issues of delayed capturing
@@ -107,7 +116,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
107116
if (!image) return;
108117

109118
setCaptured(true);
110-
setCaptureImages(prev => [...prev, image]);
119+
setCaptureImages((prev) => [...prev, image]);
111120
console.log('image', image);
112121
times.push(Date.now() - start);
113122
}
@@ -215,10 +224,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
215224

216225
<View style={styles.cameraContainer}>
217226
{showImageUri ? (
218-
<ScrollView
219-
maximumZoomScale={10}
220-
contentContainerStyle={{ flexGrow: 1 }}
221-
>
227+
<ScrollView maximumZoomScale={10} contentContainerStyle={{ flexGrow: 1 }}>
222228
<Image source={{ uri: showImageUri }} style={styles.cameraPreview} />
223229
</ScrollView>
224230
) : (
@@ -237,6 +243,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
237243
}}
238244
torchMode={torchMode ? 'on' : 'off'}
239245
shutterPhotoSound
246+
iOsSleepBeforeStarting={100}
240247
maxPhotoQualityPrioritization="speed"
241248
onCaptureButtonPressIn={() => {
242249
console.log('capture button pressed in');
@@ -299,8 +306,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
299306
} else {
300307
setShowImageUri(captureImages[captureImages.length - 1].uri);
301308
}
302-
}}
303-
>
309+
}}>
304310
<Image source={{ uri: captureImages[captureImages.length - 1].uri }} style={styles.thumbnail} />
305311
</TouchableOpacity>
306312
)}

ios/ReactNativeCameraKit/CKCameraManager.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager)
2222
RCT_EXPORT_VIEW_PROPERTY(ratioOverlay, NSString)
2323
RCT_EXPORT_VIEW_PROPERTY(ratioOverlayColor, UIColor)
2424
RCT_EXPORT_VIEW_PROPERTY(resizeMode, CKResizeMode)
25+
RCT_EXPORT_VIEW_PROPERTY(iOsSleepBeforeStarting, NSNumber)
2526

2627
RCT_EXPORT_VIEW_PROPERTY(scanBarcode, BOOL)
2728
RCT_EXPORT_VIEW_PROPERTY(onReadCode, RCTDirectEventBlock)

ios/ReactNativeCameraKit/CKCameraViewComponentView.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
242242
_view.maxZoom = newProps.maxZoom > -1 ? @(newProps.maxZoom) : nil;
243243
[changedProps addObject:@"maxZoom"];
244244
}
245+
if (oldViewProps.iOsSleepBeforeStarting != newProps.iOsSleepBeforeStarting) {
246+
_view.iOsSleepBeforeStarting = newProps.iOsSleepBeforeStarting >= 0 ? @(newProps.iOsSleepBeforeStarting) : nil;
247+
[changedProps addObject:@"iOsSleepBeforeStarting"];
248+
}
245249
float barcodeWidth = newProps.barcodeFrameSize.width;
246250
float barcodeHeight = newProps.barcodeFrameSize.height;
247251
if (barcodeWidth != [_view.barcodeFrameSize[@"width"] floatValue] || barcodeHeight != [_view.barcodeFrameSize[@"height"] floatValue]) {

ios/ReactNativeCameraKit/CameraProtocol.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
1717
func update(cameraType: CameraType)
1818
func update(onOrientationChange: RCTDirectEventBlock?)
1919
func update(onZoom: RCTDirectEventBlock?)
20+
func update(iOsSleepBeforeStartingMs: Int?)
2021
func update(zoom: Double?)
2122
func update(maxZoom: Double?)
2223
func update(resizeMode: ResizeMode)

ios/ReactNativeCameraKit/CameraView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public class CameraView: UIView {
5858
@objc public var zoomMode: ZoomMode = .on
5959
@objc public var zoom: NSNumber?
6060
@objc public var maxZoom: NSNumber?
61+
@objc public var iOsSleepBeforeStarting: NSNumber?
6162

6263
@objc public var onCaptureButtonPressIn: RCTDirectEventBlock?
6364
@objc public var onCaptureButtonPressOut: RCTDirectEventBlock?
@@ -82,6 +83,8 @@ public class CameraView: UIView {
8283
if hasPropBeenSetup && hasPermissionBeenGranted && !hasCameraBeenSetup {
8384
let convertedAllowedTypes = convertAllowedBarcodeTypes()
8485

86+
camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)
87+
8588
hasCameraBeenSetup = true
8689
#if targetEnvironment(macCatalyst)
8790
// Force front camera on Mac Catalyst during initial setup
@@ -286,6 +289,9 @@ public class CameraView: UIView {
286289
}
287290

288291
// Others
292+
if changedProps.contains("iOsSleepBeforeStarting") {
293+
camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)
294+
}
289295
if changedProps.contains("focusMode") {
290296
focusInterfaceView.update(focusMode: focusMode)
291297
}

ios/ReactNativeCameraKit/RealCamera.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
2121
private let session = AVCaptureSession()
2222
// Communicate with the session and other session objects on this queue.
2323
private let sessionQueue = DispatchQueue(label: "com.tesla.react-native-camera-kit")
24-
24+
2525
// utilities
2626
private var setupResult: SetupResult = .notStarted
2727
private var isSessionRunning: Bool = false
@@ -45,6 +45,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
4545
private var lastOnZoom: Double?
4646
private var zoom: Double?
4747
private var maxZoom: Double?
48+
private var sleepBeforeStartingMs: Int = 100
4849

4950
// orientation
5051
private var deviceOrientation = UIDeviceOrientation.unknown
@@ -127,6 +128,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
127128
self.addObservers()
128129

129130
if self.setupResult == .success {
131+
let delay = self.sleepBeforeStartingMs
132+
// Guard against calling startRunning while commitConfiguration is still finishing.
133+
// See README iOsSleepBeforeStarting for details about preventing occasional crashes.
134+
if delay > 0 {
135+
Thread.sleep(forTimeInterval: Double(delay) / 1000.0)
136+
}
130137
self.session.startRunning()
131138
}
132139

@@ -207,6 +214,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
207214
self.onZoomCallback = onZoom
208215
}
209216

217+
func update(iOsSleepBeforeStartingMs: Int?) {
218+
let defaultDelayMs = 100
219+
let providedDelay = iOsSleepBeforeStartingMs ?? defaultDelayMs
220+
sleepBeforeStartingMs = max(0, providedDelay)
221+
}
222+
210223
func focus(at touchPoint: CGPoint, focusBehavior: FocusBehavior) {
211224
DispatchQueue.main.async {
212225
let devicePoint = self.cameraPreview.previewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)

ios/ReactNativeCameraKit/SimulatorCamera.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ class SimulatorCamera: CameraProtocol {
6666
self.onZoom = onZoom
6767
}
6868

69+
func update(iOsSleepBeforeStartingMs: Int?) {
70+
// No-op on simulator; startup delay only applies to real devices.
71+
}
72+
6973
func setVideoDevice(zoomFactor: Double) {
7074
self.videoDeviceZoomFactor = zoomFactor
7175
self.mockPreview.zoomLabel.text = "Zoom: \(zoomFactor)"

src/Camera.ios.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const Camera = React.forwardRef<CameraApi, CameraProps>((props, ref) => {
1414
props.zoom = props.zoom ?? -1;
1515
props.maxZoom = props.maxZoom ?? -1;
1616
props.scanThrottleDelay = props.scanThrottleDelay ?? -1;
17+
props.iOsSleepBeforeStarting = props.iOsSleepBeforeStarting ?? -1;
1718

1819
props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats;
1920

0 commit comments

Comments
 (0)