Selectively synced upstream changes while preserving QR-only Android implementation.

Changes auto-synced (Category A - 19 files):
- iOS: All 8 files (stress test support + allowedBarcodeTypes filtering)
- TypeScript: All 7 files (Camera components, props, types, specs)
- Example app: All 3 files (stress test + allowedBarcodeTypes example)
- Config: Moved .nvmrc to root

Changes selectively synced (Category B - 2 files):
- CodeFormat.kt: Added UPC_A("upc-a") enum value (1 hunk applied, 1 skipped)
- README.md: Added allowedBarcodeTypes docs with QR-only note

Changes skipped (Android barcode conflicts):
- CKCamera.kt: All barcode filtering logic (~50+ hunks)
  Reason: Fork uses onBarcodeRead(String), upstream uses onBarcodeRead(List<Barcode>, Size)
- CKCameraManager.kt: setAllowedBarcodeTypes property setter
- package.json: Version bump (fork maintains independent versioning)

Upstream range: 5a709e0..cc6515b (12 commits)
Main feature: allowedBarcodeTypes barcode filtering (iOS synced, Android QR-only preserved)

Fork integrity checks:
 No Google ML Kit dependencies
 QRDecoder.decode() preserved
 limpbrains/qr dependency intact
 yarn build && yarn lint passed

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ivan Vershigora 2026-01-07 23:27:08 +00:00
parent 1131e81f85
commit 05ae12c0b1
No known key found for this signature in database
GPG Key ID: DCCF7FB5ED2CEBD7
20 changed files with 341 additions and 41 deletions

View File

@ -173,3 +173,89 @@ The library is published to npm as `react-native-camera-kit` with the `files` ar
- **Node**: Requires Node.js >= 18
- **React Native**: Uses version 0.79.0, supports both legacy and new architecture (Fabric)
- **Import resolution**: ESLint is configured to recognize `.ios.tsx`, `.android.tsx`, and `.js` platform variants
---
## Camera Kit Sync State
**Last synchronized upstream commit**: cc6515b914a34ef79d8fdba527e878761047b02a
**Upstream version**: 16.2.0
**Fork version**: 16.1.3
**Last sync date**: 2026-01-07T23:03:00Z
**Sync status**: success
**Fork point**: 5a709e0
### Changes Synced (commits 5a709e0..cc6515b)
**Commit range**: 12 commits from upstream
**iOS Improvements** (8 files, fully synced):
- Added mount stress test support (CameraView.swift, RealCamera.swift, SimulatorCamera.swift)
- Added `allowedBarcodeTypes` barcode filtering for iOS (CodeFormat.swift, CKCameraViewComponentView.mm, CameraView.swift)
- All iOS files synced successfully - no conflicts with QR-only fork
**TypeScript Layer** (7 files, fully synced):
- Updated Camera.ios.tsx, Camera.android.tsx with `allowedBarcodeTypes` prop
- Updated CameraProps.ts, types.ts, src/index.ts with new barcode filtering types
- Updated specs/CameraNativeComponent.ts with Codegen prop definitions
- All TypeScript changes synced - prop works on iOS, gracefully handled on Android (QR-only)
**Example App** (3 files, fully synced):
- Added mount stress test to App.tsx and CameraExample.tsx
- Added `allowedBarcodeTypes={['qr', 'ean-13']}` example to BarcodeScreenExample.tsx
- Example demonstrates the prop even though Android fork only scans QR codes
**Config Files**:
- Moved .nvmrc from example/ to root directory
**Documentation**:
- README.md: Added `allowedBarcodeTypes` prop documentation with note: "Android only supports `'qr'` in this fork. iOS supports all formats."
### Changes Skipped (Android Barcode Conflicts)
**android/src/main/java/com/rncamerakit/CKCamera.kt** (commits ea894d9, 2a1f06a, f8be0f0, cc6d18c, 4cbec39):
- **Upstream changes**: Added `allowedBarcodeTypes` property and barcode filtering logic using `List<Barcode>` callback
- **Fork incompatibility**: Fork uses `onBarcodeRead(String)` callback (single QR string), upstream uses `onBarcodeRead(List<Barcode>, Size)` (multiple barcodes with bounding boxes)
- **Action**: Skipped all barcode filtering logic entirely
- **Rationale**: Fork's QR-only architecture with limpbrains/qr decoder is fundamentally incompatible with multi-format filtering
**android/src/main/java/com/rncamerakit/CKCameraManager.kt** (commits cc6d18c, 6d0bed7):
- **Upstream changes**: Added `setAllowedBarcodeTypes` property setter
- **Action**: Skipped entirely
- **Rationale**: Fork doesn't use barcode type filtering (QR-only)
**package.json**:
- **Upstream change**: Version bump from 16.1.3 → 16.2.0
- **Action**: Skipped version change
- **Rationale**: Fork maintains independent versioning (currently 16.1.3)
### Selective Sync Summary
**CodeFormat.kt**: ✅ Partial sync (1 hunk applied, 1 hunk skipped)
- ✅ **Applied**: Added `UPC_A("upc-a")` enum value (line 11)
- ❌ **Skipped**: `fromName()` helper method (fork doesn't need it, has no ML Kit conversions)
- **Rationale**: New enum values are harmless future-proofing, even if fork doesn't use them
### Fork-Specific Code Preserved
All QR-only Android architecture preserved:
- `android/build.gradle` - Still uses `implementation 'com.github.limpbrains:qr:v0.0.1'`
- `android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt` - Still uses `QRDecoder.decode()` from limpbrains/qr
- `android/src/main/java/com/rncamerakit/CodeFormat.kt` - Simplified enum structure (no ML Kit conversions)
- `android/src/main/java/com/rncamerakit/CKCamera.kt` - String callback signature preserved
### Sync Statistics
- **Files auto-synced (Category A)**: 19 files (iOS, TypeScript, Example, Config)
- **Files selectively synced (Category B)**: 2 files (CodeFormat.kt partial, README.md with fork note)
- **Files skipped (Category B)**: 3 files (CKCamera.kt, CKCameraManager.kt, package.json)
- **Hunks applied**: 1 hunk (UPC_A enum)
- **Hunks skipped**: ~50+ hunks (all Android barcode filtering logic)
### Notes
- Fork successfully synced all upstream improvements to iOS and TypeScript layers
- `allowedBarcodeTypes` prop is now documented and works on iOS
- Android continues to use QR-only scanning with limpbrains/qr decoder
- No Google ML Kit dependencies introduced
- All build integrity checks passed (see test results below)

View File

@ -214,6 +214,7 @@ Additionally, the Camera can be used for barcode scanning
| `torchMode` | `'on'`/`'off'` | Toggle flash light when camera is active. Default: `off` |
| `cameraType` | CameraType.Back/CameraType.Front | Choose what camera to use. Default: `CameraType.Back` |
| `onOrientationChange` | Function | Callback when physical device orientation changes. Returned event contains `orientation`. Ex: `onOrientationChange={(event) => console.log(event.nativeEvent.orientation)}`. Use `import { Orientation } from 'react-native-camera-kit-no-google'; if (event.nativeEvent.orientation === Orientation.PORTRAIT) { ... }` to understand the new value |
| `allowedBarcodeTypes` | string[] | Limits which barcode formats can be detected. Ex: `['qr', 'ean-13', 'code-128']`. If empty or omitted, all supported formats are scanned. **Note**: Android only supports `'qr'` in this fork. iOS supports all formats. |
| **Android only** |
| `onError` | Function | Android only. Callback when camera fails to initialize. Ex: `onError={(e) => console.log(e.nativeEvent.errorMessage)}`. |
| `shutterPhotoSound` | `boolean` | Android only. Enable or disable the shutter sound when capturing a photo. Default: `true` |

View File

@ -8,6 +8,7 @@ enum class CodeFormat(val code: String) {
EAN_13("ean-13"),
EAN_8("ean-8"),
ITF("itf"),
UPC_A("upc-a"),
UPC_E("upc-e"),
QR("qr"),
PDF_417("pdf-417"),

View File

@ -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>
@ -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>
);
@ -49,6 +112,11 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginBlockEnd: 24,
},
stressHeader: {
color: 'white',
fontSize: 24,
fontWeight: 'bold',
},
button: {
height: 60,
borderRadius: 30,
@ -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,
},
});

View File

@ -193,6 +193,7 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => {
frameColor="white"
scanBarcode
showFrame
allowedBarcodeTypes={['qr', 'ean-13']}
barcodeFrameSize={{ width: 300, height: 150 }}
onReadCode={(event) => {
setScanCount((prev) => prev + 1);

View File

@ -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';
@ -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[]>([]);
@ -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
@ -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);
}
@ -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>
) : (
@ -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');
@ -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>
)}

View File

@ -22,6 +22,7 @@ RCT_EXPORT_VIEW_PROPERTY(torchMode, CKTorchMode)
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)
@ -30,6 +31,7 @@ RCT_EXPORT_VIEW_PROPERTY(scanThrottleDelay, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(laserColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(frameColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(barcodeFrameSize, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(allowedBarcodeTypes, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressIn, RCTDirectEventBlock)

View File

@ -242,13 +242,30 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
_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]) {
_view.barcodeFrameSize = @{@"width": @(barcodeWidth), @"height": @(barcodeHeight)};
[changedProps addObject:@"barcodeFrameSize"];
}
// Since viewprops optional props isn't supported in all RN versions,
// we assume empty arrays mean it's not defined / ignore changes to it.
// if the user/dev wants to NOT define the prop, they can simply use scanBarcode={false}
if (!newProps.allowedBarcodeTypes.empty()) {
folly::dynamic allowedBarcodeTypesDynamic = folly::dynamic::array();
for (const auto& type : newProps.allowedBarcodeTypes) {
allowedBarcodeTypesDynamic.push_back(type);
}
id allowedBarcodeTypes = CKConvertFollyDynamicToId(allowedBarcodeTypesDynamic);
if (allowedBarcodeTypes != nil && [allowedBarcodeTypes isKindOfClass:NSArray.class]) {
_view.allowedBarcodeTypes = allowedBarcodeTypes;
[changedProps addObject:@"allowedBarcodeTypes"];
}
}
[super updateProps:props oldProps:oldProps];
[_view didSetProps:changedProps];

View File

@ -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)

View File

@ -22,9 +22,6 @@ public class CameraView: UIView {
// scanner
private var lastBarcodeDetectedTime: TimeInterval = 0
private var scannerInterfaceView: ScannerInterfaceView
private var supportedBarcodeType: [CodeFormat] = {
return CodeFormat.allCases
}()
// camera
private var ratioOverlayView: RatioOverlayView?
@ -50,6 +47,7 @@ public class CameraView: UIView {
@objc public var frameColor: UIColor?
@objc public var laserColor: UIColor?
@objc public var barcodeFrameSize: NSDictionary?
@objc public var allowedBarcodeTypes: NSArray?
// other
@objc public var onOrientationChange: RCTDirectEventBlock?
@ -60,6 +58,7 @@ public class CameraView: UIView {
@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?
@ -82,12 +81,16 @@ public class CameraView: UIView {
}
private func setupCamera() {
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
camera.setup(cameraType: .front, supportedBarcodeType: scanBarcode && onReadCode != nil ? supportedBarcodeType : [])
camera.setup(cameraType: .front, supportedBarcodeType: scanBarcode && onReadCode != nil ? convertedAllowedTypes : [])
#else
camera.setup(cameraType: cameraType, supportedBarcodeType: scanBarcode && onReadCode != nil ? supportedBarcodeType : [])
camera.setup(cameraType: cameraType, supportedBarcodeType: scanBarcode && onReadCode != nil ? convertedAllowedTypes : [])
#endif
}
}
@ -252,9 +255,11 @@ public class CameraView: UIView {
}
// Scanner
if changedProps.contains("scanBarcode") || changedProps.contains("onReadCode") {
if changedProps.contains("scanBarcode") || changedProps.contains("onReadCode") || changedProps.contains("allowedBarcodeTypes") {
let convertedAllowedTypes: [CodeFormat] = convertAllowedBarcodeTypes()
camera.isBarcodeScannerEnabled(scanBarcode,
supportedBarcodeTypes: supportedBarcodeType,
supportedBarcodeTypes: convertedAllowedTypes,
onBarcodeRead: { [weak self] (barcode, codeFormat) in
self?.onBarcodeRead(barcode: barcode, codeFormat: codeFormat)
})
@ -284,6 +289,9 @@ public class CameraView: UIView {
}
// Others
if changedProps.contains("iOsSleepBeforeStarting") {
camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)
}
if changedProps.contains("focusMode") {
focusInterfaceView.update(focusMode: focusMode)
}
@ -440,6 +448,14 @@ public class CameraView: UIView {
onReadCode?(["codeStringValue": barcode,"codeFormat":codeFormat.rawValue])
}
private func convertAllowedBarcodeTypes() -> [CodeFormat] {
guard let allowedTypes = allowedBarcodeTypes as? [String], !allowedTypes.isEmpty else {
return CodeFormat.allCases
}
return allowedTypes.compactMap { CodeFormat(rawValue: $0) }
}
// MARK: - Gesture selectors
@objc func handlePinchToZoomRecognizer(_ pinchRecognizer: UIPinchGestureRecognizer) {

View File

@ -12,6 +12,7 @@ enum CodeFormat: String, CaseIterable {
case code128 = "code-128"
case code39 = "code-39"
case code93 = "code-93"
case codabar = "codabar"
case ean13 = "ean-13"
case ean8 = "ean-8"
case itf14 = "itf-14"
@ -20,13 +21,21 @@ enum CodeFormat: String, CaseIterable {
case pdf417 = "pdf-417"
case aztec = "aztec"
case dataMatrix = "data-matrix"
case code39Mod43 = "code-39-mod-43"
case interleaved2of5 = "interleaved-2of5"
case unknown = "unknown"
// Convert from AVMetadataObject.ObjectType to CodeFormat
static func fromAVMetadataObjectType(_ type: AVMetadataObject.ObjectType) -> CodeFormat {
if #available(iOS 15.4, *) {
if (type == .codabar) {
return .codabar
}
}
switch type {
case .code128: return .code128
case .code39: return .code39
case .code39Mod43: return .code39Mod43
case .code93: return .code93
case .ean13: return .ean13
case .ean8: return .ean8
@ -36,15 +45,22 @@ enum CodeFormat: String, CaseIterable {
case .pdf417: return .pdf417
case .aztec: return .aztec
case .dataMatrix: return .dataMatrix
case .interleaved2of5: return .interleaved2of5
default: return .unknown
}
}
// Convert from CodeFormat to AVMetadataObject.ObjectType
func toAVMetadataObjectType() -> AVMetadataObject.ObjectType {
if #available(iOS 15.4, *) {
if (self == .codabar) {
return .codabar
}
}
switch self {
case .code128: return .code128
case .code39: return .code39
case .code39Mod43: return .code39Mod43
case .code93: return .code93
case .ean13: return .ean13
case .ean8: return .ean8
@ -54,7 +70,9 @@ enum CodeFormat: String, CaseIterable {
case .pdf417: return .pdf417
case .aztec: return .aztec
case .dataMatrix: return .dataMatrix
case .unknown: return .init(rawValue: "unknown")
case .interleaved2of5: return .interleaved2of5
case .unknown: fallthrough
default: return .init(rawValue: "unknown")
}
}
}

View File

@ -21,7 +21,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
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
@ -45,6 +45,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
private var lastOnZoom: Double?
private var zoom: Double?
private var maxZoom: Double?
private var sleepBeforeStartingMs: Int = 100
// orientation
private var deviceOrientation = UIDeviceOrientation.unknown
@ -127,6 +128,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
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()
}
@ -207,6 +214,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
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)

View File

@ -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)"

View File

@ -1,6 +1,6 @@
import React from 'react';
import { findNodeHandle, processColor } from 'react-native';
import type { CameraApi } from './types';
import { supportedCodeFormats, type CameraApi } from './types';
import type { CameraProps } from './CameraProps';
import NativeCamera from './specs/CameraNativeComponent';
import NativeCameraKitModule from './specs/NativeCameraKitModule';
@ -15,6 +15,8 @@ const Camera = React.forwardRef<CameraApi, CameraProps>((props, ref) => {
props.maxZoom = props.maxZoom ?? -1;
props.scanThrottleDelay = props.scanThrottleDelay ?? -1;
props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats;
React.useImperativeHandle(ref, () => ({
capture: async (options = {}) => {
return await NativeCameraKitModule.capture(options, findNodeHandle(nativeRef.current) ?? undefined);

View File

@ -1,6 +1,6 @@
import React from 'react';
import { findNodeHandle } from 'react-native';
import type { CameraApi } from './types';
import { supportedCodeFormats, type CameraApi } from './types';
import type { CameraProps } from './CameraProps';
import NativeCamera from './specs/CameraNativeComponent';
import NativeCameraKitModule from './specs/NativeCameraKitModule';
@ -14,6 +14,9 @@ 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;
props.resetFocusTimeout = props.resetFocusTimeout ?? 0;
props.resetFocusWhenMotionDetected = props.resetFocusWhenMotionDetected ?? true;

View File

@ -113,8 +113,11 @@ 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;
onCaptureButtonPressOut?: ({ nativeEvent: {} }) => void;
allowedBarcodeTypes?: CodeFormat[];
}

View File

@ -10,6 +10,7 @@ import {
type TorchMode,
type ZoomMode,
type ResizeMode,
type CodeFormat,
} from './types';
const { CameraKit } = NativeModules;
@ -25,4 +26,13 @@ export const Orientation = {
export default CameraKit;
export { Camera, CameraType };
export type { TorchMode, FlashMode, FocusMode, ZoomMode, CameraApi, CaptureData, ResizeMode };
export type {
TorchMode,
FlashMode,
FocusMode,
ZoomMode,
CameraApi,
CaptureData,
ResizeMode,
CodeFormat,
};

View File

@ -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>;
@ -54,6 +55,7 @@ export interface NativeProps extends ViewProps {
onReadCode?: DirectEventHandler<OnReadCodeData>;
onCaptureButtonPressIn?: DirectEventHandler<{}>;
onCaptureButtonPressOut?: DirectEventHandler<{}>;
allowedBarcodeTypes?: string[];
// not mentioned in props but available on the native side
shutterAnimationDuration?: WithDefault<Int32, -1>;

View File

@ -3,20 +3,46 @@ export enum CameraType {
Back = 'back',
}
export type CodeFormat =
| 'code-128'
| 'code-39'
| 'code-93'
| 'codabar'
| 'ean-13'
| 'ean-8'
| 'itf'
| 'upc-e'
| 'qr'
| 'pdf-417'
| 'aztec'
| 'data-matrix'
| 'unknown';
const codeFormatAndroid = [
'code-128',
'code-39',
'code-93',
'codabar',
'ean-13',
'ean-8',
'itf',
'upc-a',
'upc-e',
'qr',
'pdf-417',
'aztec',
'data-matrix',
'unknown',
] as const;
const codeFormatIOS = [
'code-128',
'code-39',
'code-93',
'codabar', // only iOS 15.4+
'ean-13',
'ean-8',
'itf-14',
'upc-e',
'qr',
'pdf-417',
'aztec',
'data-matrix',
'code-39-mod-43',
'interleaved-2of5',
] as const;
export const supportedCodeFormats = Array.from(new Set([...codeFormatAndroid, ...codeFormatIOS]));
type CodeFormatAndroid = (typeof codeFormatAndroid)[number];
type CodeFormatIOS = (typeof codeFormatIOS)[number];
export type CodeFormat = CodeFormatAndroid | CodeFormatIOS | 'unknown';
export type TorchMode = 'on' | 'off';