Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 35 additions & 34 deletions README.md

Large diffs are not rendered by default.

98 changes: 93 additions & 5 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, ScrollView } from 'react-native';
import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Button, Alert, TextInput } from 'react-native';

import BarcodeScreenExample from './BarcodeScreenExample';
import CameraExample from './CameraExample';

const App = () => {
const [example, setExample] = useState<JSX.Element>();
const [example, setExample] = useState<any>(undefined);
const [testNo, setTestNo] = useState(0);
const [interval, setIntervalId] = useState<number | null>(null);
const [speed, setSpeed] = useState('1000');
const onBack = () => setExample(undefined);

if (example) {
return example;
}

const onBack = () => setExample(undefined);

return (
<ScrollView style={styles.scroll}>
<ScrollView style={styles.scroll} scrollEnabled={false}>
<View style={styles.container}>
<Text style={{ fontSize: 60 }}>🎈</Text>
<Text style={styles.headerText}>React Native Camera Kit</Text>
Expand All @@ -24,6 +26,67 @@ const App = () => {
<TouchableOpacity style={styles.button} onPress={() => setExample(<BarcodeScreenExample onBack={onBack} />)}>
<Text style={styles.buttonText}>Barcode Scanner</Text>
</TouchableOpacity>
<View>
<Text style={[styles.stressHeader, { marginTop: 12 }]}>Mount Stress Test</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{!testNo ? (
<>
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Speed (ms):</Text>
<TextInput
style={styles.input}
value={speed}
onChangeText={setSpeed}
keyboardType="number-pad"
placeholder="1000"
placeholderTextColor="#999"
/>
</View>

<Button
title="Start"
onPress={() => {
Alert.alert(
'2 min or more',
'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.',
[
{
text: 'OK',
onPress: () => {
setIntervalId(
setInterval(() => {
setTestNo((prev) => {
const newR = prev + 1;
if (newR % 2 === 0) {
setExample(<CameraExample key={String(Math.random())} stress onBack={onBack} />);
} else {
setExample(undefined);
}
return newR;
});
}, parseInt(speed, 10) || 1000),
);
},
},
],
);
}}
/>
</>
) : (
<Button
title="STOP STRESS TEST"
onPress={() => {
setTestNo(0);
if (interval) {
clearInterval(interval);
setIntervalId(null);
}
}}
/>
)}
</View>
</View>
</View>
</ScrollView>
);
Expand All @@ -49,6 +112,11 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginBlockEnd: 24,
},
stressHeader: {
color: 'white',
fontSize: 24,
fontWeight: 'bold',
},
button: {
height: 60,
borderRadius: 30,
Expand All @@ -62,4 +130,24 @@ const styles = StyleSheet.create({
textAlign: 'center',
fontSize: 20,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 12,
minWidth: 170,
},
inputLabel: {
color: 'white',
fontSize: 16,
marginRight: 12,
},
input: {
flex: 1,
height: 40,
borderRadius: 8,
backgroundColor: '#333',
color: 'white',
paddingHorizontal: 12,
fontSize: 16,
},
});
24 changes: 15 additions & 9 deletions example/src/CameraExample.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type React from 'react';
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Image, Animated, ScrollView } from 'react-native';
import Camera from '../../src/Camera';
import { type CameraApi, CameraType, type CaptureData } from '../../src/types';
Expand Down Expand Up @@ -33,7 +33,7 @@ function median(values: number[]): number {
return sortedValues.length % 2 ? sortedValues[half] : (sortedValues[half - 1] + sortedValues[half]) / 2;
}

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

// zoom to random positions every 10ms:
useEffect(() => {
if (stress !== true) return;
const interval = setInterval(() => {
setZoom(Math.random() * 10);
}, 500);
return () => clearInterval(interval);
}, [stress]);

// iOS will error out if capturing too fast,
// so block capturing until the current capture is done
// This also minimizes issues of delayed capturing
Expand Down Expand Up @@ -107,7 +116,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
if (!image) return;

setCaptured(true);
setCaptureImages(prev => [...prev, image]);
setCaptureImages((prev) => [...prev, image]);
console.log('image', image);
times.push(Date.now() - start);
}
Expand Down Expand Up @@ -215,10 +224,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {

<View style={styles.cameraContainer}>
{showImageUri ? (
<ScrollView
maximumZoomScale={10}
contentContainerStyle={{ flexGrow: 1 }}
>
<ScrollView maximumZoomScale={10} contentContainerStyle={{ flexGrow: 1 }}>
<Image source={{ uri: showImageUri }} style={styles.cameraPreview} />
</ScrollView>
) : (
Expand All @@ -237,6 +243,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
}}
torchMode={torchMode ? 'on' : 'off'}
shutterPhotoSound
iOsSleepBeforeStarting={100}
maxPhotoQualityPrioritization="speed"
onCaptureButtonPressIn={() => {
console.log('capture button pressed in');
Expand Down Expand Up @@ -299,8 +306,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
} else {
setShowImageUri(captureImages[captureImages.length - 1].uri);
}
}}
>
}}>
<Image source={{ uri: captureImages[captureImages.length - 1].uri }} style={styles.thumbnail} />
</TouchableOpacity>
)}
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CKCameraManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(ratioOverlay, NSString)
RCT_EXPORT_VIEW_PROPERTY(ratioOverlayColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(resizeMode, CKResizeMode)
RCT_EXPORT_VIEW_PROPERTY(iOsSleepBeforeStarting, NSNumber)

RCT_EXPORT_VIEW_PROPERTY(scanBarcode, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onReadCode, RCTDirectEventBlock)
Expand Down
4 changes: 4 additions & 0 deletions ios/ReactNativeCameraKit/CKCameraViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
_view.maxZoom = newProps.maxZoom > -1 ? @(newProps.maxZoom) : nil;
[changedProps addObject:@"maxZoom"];
}
if (oldViewProps.iOsSleepBeforeStarting != newProps.iOsSleepBeforeStarting) {
_view.iOsSleepBeforeStarting = newProps.iOsSleepBeforeStarting >= 0 ? @(newProps.iOsSleepBeforeStarting) : nil;
[changedProps addObject:@"iOsSleepBeforeStarting"];
}
float barcodeWidth = newProps.barcodeFrameSize.width;
float barcodeHeight = newProps.barcodeFrameSize.height;
if (barcodeWidth != [_view.barcodeFrameSize[@"width"] floatValue] || barcodeHeight != [_view.barcodeFrameSize[@"height"] floatValue]) {
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CameraProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
func update(cameraType: CameraType)
func update(onOrientationChange: RCTDirectEventBlock?)
func update(onZoom: RCTDirectEventBlock?)
func update(iOsSleepBeforeStartingMs: Int?)
func update(zoom: Double?)
func update(maxZoom: Double?)
func update(resizeMode: ResizeMode)
Expand Down
6 changes: 6 additions & 0 deletions ios/ReactNativeCameraKit/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// scanner
private var lastBarcodeDetectedTime: TimeInterval = 0
private var scannerInterfaceView: ScannerInterfaceView

Check warning on line 25 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Lines should not have trailing whitespace (trailing_whitespace)
// camera
private var ratioOverlayView: RatioOverlayView?

Expand Down Expand Up @@ -58,11 +58,12 @@
@objc public var zoomMode: ZoomMode = .on
@objc public var zoom: NSNumber?
@objc public var maxZoom: NSNumber?
@objc public var iOsSleepBeforeStarting: NSNumber?

@objc public var onCaptureButtonPressIn: RCTDirectEventBlock?
@objc public var onCaptureButtonPressOut: RCTDirectEventBlock?

Check warning on line 65 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Lines should not have trailing whitespace (trailing_whitespace)
var eventInteraction: Any? = nil

Check warning on line 66 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Optional should be implicitly initialized without nil (implicit_optional_initialization)

// MARK: - Setup

Expand All @@ -82,6 +83,8 @@
if hasPropBeenSetup && hasPermissionBeenGranted && !hasCameraBeenSetup {
let convertedAllowedTypes = convertAllowedBarcodeTypes()

camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)

hasCameraBeenSetup = true
#if targetEnvironment(macCatalyst)
// Force front camera on Mac Catalyst during initial setup
Expand Down Expand Up @@ -143,10 +146,10 @@
focusInterfaceView.delegate = camera

handleCameraPermission()

Check warning on line 149 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Lines should not have trailing whitespace (trailing_whitespace)
configureHardwareInteraction()
}

Check warning on line 152 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Lines should not have trailing whitespace (trailing_whitespace)
private func configureHardwareInteraction() {
#if !targetEnvironment(macCatalyst)
// Create a new capture event interaction with a handler that captures a photo.
Expand Down Expand Up @@ -178,7 +181,7 @@
super.reactSetFrame(frame)
self.updateSubviewsBounds(frame)
}

Check warning on line 184 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Lines should not have trailing whitespace (trailing_whitespace)
@objc public func updateSubviewsBounds(_ frame: CGRect) {
camera.previewView.frame = bounds

Expand Down Expand Up @@ -286,6 +289,9 @@
}

// Others
if changedProps.contains("iOsSleepBeforeStarting") {
camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)
}
if changedProps.contains("focusMode") {
focusInterfaceView.update(focusMode: focusMode)
}
Expand Down Expand Up @@ -430,7 +436,7 @@
return temporaryFileURL
}

private func onBarcodeRead(barcode: String, codeFormat:CodeFormat) {

Check warning on line 439 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)
// Throttle barcode detection
let now = Date.timeIntervalSinceReferenceDate
guard lastBarcodeDetectedTime + Double(scanThrottleDelay) / 1000 < now else {
Expand All @@ -439,7 +445,7 @@

lastBarcodeDetectedTime = now

onReadCode?(["codeStringValue": barcode,"codeFormat":codeFormat.rawValue])

Check warning on line 448 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

There should be no space before and one after any comma (comma)

Check warning on line 448 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)
}

private func convertAllowedBarcodeTypes() -> [CodeFormat] {
Expand All @@ -460,4 +466,4 @@
camera.zoomPinchChange(pinchScale: pinchRecognizer.scale)
}
}
}

Check warning on line 469 in ios/ReactNativeCameraKit/CameraView.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

File should contain 400 lines or less: currently contains 469 (file_length)
15 changes: 14 additions & 1 deletion ios/ReactNativeCameraKit/RealCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
private let session = AVCaptureSession()
// Communicate with the session and other session objects on this queue.
private let sessionQueue = DispatchQueue(label: "com.tesla.react-native-camera-kit")

// utilities
private var setupResult: SetupResult = .notStarted
private var isSessionRunning: Bool = false
Expand All @@ -45,6 +45,7 @@
private var lastOnZoom: Double?
private var zoom: Double?
private var maxZoom: Double?
private var sleepBeforeStartingMs: Int = 100

// orientation
private var deviceOrientation = UIDeviceOrientation.unknown
Expand Down Expand Up @@ -127,6 +128,12 @@
self.addObservers()

if self.setupResult == .success {
let delay = self.sleepBeforeStartingMs
// Guard against calling startRunning while commitConfiguration is still finishing.
// See README iOsSleepBeforeStarting for details about preventing occasional crashes.
if delay > 0 {
Thread.sleep(forTimeInterval: Double(delay) / 1000.0)
}
self.session.startRunning()
}

Expand Down Expand Up @@ -171,7 +178,7 @@
self.update(zoom: self.zoom)
}

func update(zoom z: Double?) {

Check failure on line 181 in ios/ReactNativeCameraKit/RealCamera.swift

View workflow job for this annotation

GitHub Actions / Lint iOS

Variable name 'z' should be between 3 and 40 characters long (identifier_name)
sessionQueue.async {
self.zoom = z == -1 ? nil : z
guard let videoDevice = self.videoDeviceInput?.device else { return }
Expand Down Expand Up @@ -207,6 +214,12 @@
self.onZoomCallback = onZoom
}

func update(iOsSleepBeforeStartingMs: Int?) {
let defaultDelayMs = 100
let providedDelay = iOsSleepBeforeStartingMs ?? defaultDelayMs
sleepBeforeStartingMs = max(0, providedDelay)
}

func focus(at touchPoint: CGPoint, focusBehavior: FocusBehavior) {
DispatchQueue.main.async {
let devicePoint = self.cameraPreview.previewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)
Expand Down
4 changes: 4 additions & 0 deletions ios/ReactNativeCameraKit/SimulatorCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class SimulatorCamera: CameraProtocol {
self.onZoom = onZoom
}

func update(iOsSleepBeforeStartingMs: Int?) {
// No-op on simulator; startup delay only applies to real devices.
}

func setVideoDevice(zoomFactor: Double) {
self.videoDeviceZoomFactor = zoomFactor
self.mockPreview.zoomLabel.text = "Zoom: \(zoomFactor)"
Expand Down
1 change: 1 addition & 0 deletions src/Camera.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Camera = React.forwardRef<CameraApi, CameraProps>((props, ref) => {
props.zoom = props.zoom ?? -1;
props.maxZoom = props.maxZoom ?? -1;
props.scanThrottleDelay = props.scanThrottleDelay ?? -1;
props.iOsSleepBeforeStarting = props.iOsSleepBeforeStarting ?? -1;

props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats;

Expand Down
2 changes: 2 additions & 0 deletions src/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export interface CameraProps extends ViewProps {
scanThrottleDelay?: number;
/** **iOS Only**. 'speed' provides 60-80% faster image capturing */
maxPhotoQualityPrioritization?: 'balanced' | 'quality' | 'speed';
/** **iOS Only**. Delay in milliseconds before the camera session starts; default `100`. Set to `0` to skip. Helpful to ensure `session.commitConfiguration()` finishes before `session.startRunning()`, reducing occasional startup crashes when toggling cameras repeatedly. */
iOsSleepBeforeStarting?: number;
/** **Android only**. Play a shutter capture sound when capturing a photo */
shutterPhotoSound?: boolean;
onCaptureButtonPressIn?: ({ nativeEvent: {} }) => void;
Expand Down
1 change: 1 addition & 0 deletions src/specs/CameraNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface NativeProps extends ViewProps {
resetFocusWhenMotionDetected?: boolean;
resizeMode?: string;
scanThrottleDelay?: WithDefault<Int32, -1>;
iOsSleepBeforeStarting?: WithDefault<Int32, -1>;
barcodeFrameSize?: { width?: WithDefault<Float, 300>; height?: WithDefault<Float, 150> };
shutterPhotoSound?: boolean;
onOrientationChange?: DirectEventHandler<OnOrientationChangeData>;
Expand Down
Loading