Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ed049a62d | ||
|
|
58ff00300c | ||
|
|
e6dd85b4ea | ||
|
|
75117f1287 | ||
|
|
4323cb608d | ||
|
|
448cf1a9e0 | ||
|
|
a8535da0d3 | ||
|
|
edb054a8cb | ||
|
|
91cc291a62 | ||
|
|
202f9f6455 | ||
|
|
8e5149a6e6 | ||
|
|
3767ef668c | ||
|
|
611006999d | ||
|
|
b2b06b425b | ||
|
|
a73b84ef78 | ||
|
|
afeaac0996 | ||
|
|
dca0e421fc | ||
|
|
db93bcfc94 | ||
|
|
00b953263d | ||
|
|
a2f61a0cbb | ||
|
|
cdc1cced0e | ||
|
|
2c0a0e43f3 | ||
|
|
cc6515b914 | ||
|
|
03763d0470 | ||
|
|
f5e3bc98e9 | ||
|
|
6c7cfe44d5 | ||
|
|
ea894d96ad | ||
|
|
2a1f06aa12 | ||
|
|
5a709e03b0 | ||
|
|
1371773530 | ||
|
|
d42ef6290b | ||
|
|
4cbec39042 | ||
|
|
1ea413a099 | ||
|
|
f8be0f08e8 | ||
|
|
cc6d18ceaa | ||
|
|
b085a7801c | ||
|
|
6d0bed7ce0 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -85,6 +85,7 @@ example/.yarn/*
|
||||
ios/build/
|
||||
ios/DerivedData/
|
||||
dist/
|
||||
.codegen/
|
||||
### Xcode ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
.codegen/
|
||||
old-example/
|
||||
example/
|
||||
example-js-code/
|
||||
|
||||
98
CLAUDE.md
98
CLAUDE.md
@ -178,84 +178,40 @@ The library is published to npm as `react-native-camera-kit` with the `files` ar
|
||||
|
||||
## 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
|
||||
**Last synchronized upstream commit**: 8e5149a6e6d3902ae87dad50da0d06ec2c61d2b8
|
||||
**Upstream version**: 17.0.1
|
||||
**Fork version**: 17.0.3
|
||||
**Last sync date**: 2026-01-17
|
||||
**Sync status**: success
|
||||
**Fork point**: 5a709e0
|
||||
|
||||
### Changes Synced (commits 5a709e0..cc6515b)
|
||||
### Changes Synced (upstream v16.2.0 -> v17.0.1)
|
||||
|
||||
**Commit range**: 12 commits from upstream
|
||||
**Key changes from upstream**:
|
||||
- Replaced `iOsSleepBeforeStarting` (Int) with `iOsDeferredStart` (Bool) for iOS camera startup optimization
|
||||
- iOS 26+ deferred start support via `AVCaptureOutput.deferredStartEnabled`
|
||||
- Improved stress test logging with elapsed time tracking
|
||||
- Various iOS formatting and code organization improvements
|
||||
|
||||
**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
|
||||
**Files synced (16 files)**:
|
||||
- All iOS Swift files (CameraView.swift, RealCamera.swift, SimulatorCamera.swift, CameraProtocol.swift)
|
||||
- CKCameraViewComponentView.mm (Objective-C++ bridge)
|
||||
- TypeScript layer (Camera.ios.tsx, CameraProps.ts, CameraNativeComponent.ts)
|
||||
- Example app improvements (App.tsx, BarcodeScreenExample.tsx)
|
||||
- README.md with updated props documentation
|
||||
|
||||
### 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
|
||||
- `android/build.gradle` - Uses `implementation 'com.github.limpbrains:qr:v0.0.3'`
|
||||
- `android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt` - Uses `QRDecoder.decode()` from limpbrains/qr
|
||||
- `android/src/main/java/com/rncamerakit/CodeFormat.kt` - Simplified enum (no ML Kit conversions)
|
||||
- `android/src/main/java/com/rncamerakit/CKCamera.kt` - String callback `onQRCodeDetected(String)`
|
||||
|
||||
### Sync Statistics
|
||||
### Validation
|
||||
|
||||
- **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)
|
||||
- `yarn build` - PASSED
|
||||
- `yarn lint` - PASSED
|
||||
- `yarn test` - PASSED
|
||||
- No `google.mlkit` references in android/ - VERIFIED
|
||||
- `limpbrains/qr` dependency present - VERIFIED
|
||||
- `QRDecoder.decode()` preserved - VERIFIED
|
||||
|
||||
103
README.md
103
README.md
@ -201,40 +201,75 @@ Additionally, the Camera can be used for barcode scanning
|
||||
|
||||
### Camera Props (Optional)
|
||||
|
||||
| Props | Type | Description |
|
||||
| ------------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ref` | Ref | Reference on the camera view |
|
||||
| `style` | StyleProp\<ViewStyle> | Style to apply on the camera view |
|
||||
| `flashMode` | `'on'`/`'off'`/`'auto'` | Camera flash mode. Default: `auto` |
|
||||
| `focusMode` | `'on'`/`'off'` | Camera focus mode. Default: `on` |
|
||||
| `zoomMode` | `'on'`/`'off'` | Enable the pinch to zoom gesture. Default: `on`. If `on`, you must pass `zoom` as `undefined` or avoid setting `zoomMode` to allow pinch to zoom |
|
||||
| `zoom` | `number` | Control the zoom. Default: `1.0` |
|
||||
| `maxZoom` | `number` | Maximum zoom allowed (but not beyond what camera allows). Default: `undefined` (camera default max) |
|
||||
| `onZoom` | Function | Callback when user makes a pinch gesture, regardless of what the `zoom` prop was set to. Returned event contains `zoom`. Ex: `onZoom={(e) => console.log(e.nativeEvent.zoom)}`. |
|
||||
| `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` |
|
||||
| **iOS only** |
|
||||
| `ratioOverlay` | `'int:int'` | Show a guiding overlay in the camera preview for the selected ratio. Does not crop image as of v9.0. Example: `'16:9'` |
|
||||
| `ratioOverlayColor` | Color | Any color with alpha. Default: `'#ffffff77'` |
|
||||
| `resetFocusTimeout` | `number` | Dismiss tap to focus after this many milliseconds. Default `0` (disabled). Example: `5000` is 5 seconds. |
|
||||
| `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. |
|
||||
| `resizeMode` | `'cover' / 'contain'` | Determines the scaling and cropping behavior of content within the view. `cover` (resizeAspectFill on iOS) scales the content to fill the view completely, potentially cropping content if its aspect ratio differs from the view. `contain` (resizeAspect on iOS) scales the content to fit within the view's bounds without cropping, ensuring all content is visible but may introduce letterboxing. Default behavior depends on the specific use case. |
|
||||
| `scanThrottleDelay` | `number` | Duration between scan detection in milliseconds. Default 2000 (2s) |
|
||||
| `maxPhotoQualityPrioritization` | `'balanced'` / `'quality'` / `'speed'` | [iOS 13 and newer](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization). `'speed'` provides a 60-80% median capture time reduction vs 'quality' setting. Tested on iPhone 6S Max (66% faster) and iPhone 15 Pro Max (76% faster!). Default `balanced` |
|
||||
| `onCaptureButtonPressIn` | Function | Callback when iPhone capture button is pressed in or Android volume or camera button is pressed in. Ex: `onCaptureButtonPressIn={() => console.log("volume button pressed in")}` |
|
||||
| `onCaptureButtonPressOut` | Function | Callback when iPhone capture button is released or Android volume or camera button is released. Ex: `onCaptureButtonPressOut={() => console.log("volume button released")}` |
|
||||
| **Barcode only** |
|
||||
| `scanBarcode` | `boolean` | Enable barcode scanner. Default: `false` |
|
||||
| `showFrame` | `boolean` | Show frame in barcode scanner. Default: `false` |
|
||||
| `barcodeFrameSize` | `object` | Frame size of barcode scanner. Default: `{ width: 300, height: 150 }` |
|
||||
| `laserColor` | Color | Color of barcode scanner laser visualization. Default: `red` |
|
||||
| `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` |
|
||||
| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` |
|
||||
| Props | Type | Description |
|
||||
| ------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ref` | Ref | Reference on the camera view |
|
||||
| `style` | StyleProp\<ViewStyle> | Style to apply on the camera view |
|
||||
| `flashMode` | `'on'`/`'off'`/`'auto'` | Camera flash mode. Default: `auto` |
|
||||
| `focusMode` | `'on'`/`'off'` | Camera focus mode. Default: `on` |
|
||||
| `zoomMode` | `'on'`/`'off'` | Enable the pinch to zoom gesture. Default: `on`. If `on`, you must pass `zoom` as `undefined` or avoid setting `zoomMode` to allow pinch to zoom |
|
||||
| `zoom` | `number` | Control the zoom. Default: `1.0` |
|
||||
| `maxZoom` | `number` | Maximum zoom allowed (but not beyond what camera allows). Default: `undefined` (camera default max) |
|
||||
| `onZoom` | Function | Callback when user makes a pinch gesture, regardless of what the `zoom` prop was set to. Returned event contains `zoom`. Ex: `onZoom={(e) => console.log(e.nativeEvent.zoom)}`. |
|
||||
| `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` |
|
||||
| **iOS only** |
|
||||
| `ratioOverlay` | `'int:int'` | Show a guiding overlay in the camera preview for the selected ratio. Does not crop image as of v9.0. Example: `'16:9'` |
|
||||
| `ratioOverlayColor` | Color | Any color with alpha. Default: `'#ffffff77'` |
|
||||
| `resetFocusTimeout` | `number` | Dismiss tap to focus after this many milliseconds. Default `0` (disabled). Example: `5000` is 5 seconds. |
|
||||
| `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. |
|
||||
| `resizeMode` | `'cover' / 'contain'` | Determines the scaling and cropping behavior of content within the view. `cover` (resizeAspectFill on iOS) scales the content to fill the view completely, potentially cropping content if its aspect ratio differs from the view. `contain` (resizeAspect on iOS) scales the content to fit within the view's bounds without cropping, ensuring all content is visible but may introduce letterboxing. Default behavior depends on the specific use case. |
|
||||
| `scanThrottleDelay` | `number` | Duration between scan detection in milliseconds. Default 2000 (2s) |
|
||||
| `maxPhotoQualityPrioritization` | `'balanced'` / `'quality'` / `'speed'` | [iOS 13 and newer](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization). `'speed'` provides a 60-80% median capture time reduction vs 'quality' setting. Tested on iPhone 6S Max (66% faster) and iPhone 15 Pro Max (76% faster!). Default `balanced` |
|
||||
| `iOsDeferredStart` | `boolean` | iOS 26+ only. Enables `AVCaptureOutput.deferredStartEnabled` when supported to get the preview visible faster. Default `true`. When enabled, the first capture can be delayed by a few hundred milliseconds. Ignored on Android and on older iOS versions. |
|
||||
| `onCaptureButtonPressIn` | Function | Callback when iPhone capture button is pressed in or Android volume or camera button is pressed in. Ex: `onCaptureButtonPressIn={() => console.log("volume button pressed in")}` |
|
||||
| `onCaptureButtonPressOut` | Function | Callback when iPhone capture button is released or Android volume or camera button is released. Ex: `onCaptureButtonPressOut={() => console.log("volume button released")}` |
|
||||
| **Barcode only** |
|
||||
| `scanBarcode` | `boolean` | Enable barcode scanner. Default: `false` |
|
||||
| `showFrame` | `boolean` | Show frame in barcode scanner. Default: `false` |
|
||||
| `barcodeFrameSize` | `object` | Frame size of barcode scanner. Default: `{ width: 300, height: 150 }` |
|
||||
| `laserColor` | Color | Color of barcode scanner laser visualization. Default: `red` |
|
||||
| `frameColor` | Color | Color of barcode scanner frame visualization. Default: `yellow` |
|
||||
| `onReadCode` | Function | Callback when scanner successfully reads barcode. Returned event contains `codeStringValue`. Default: `null`. Ex: `onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}` |
|
||||
|
||||
### detectQRCodeInImage(base64)
|
||||
|
||||
Detect and decode a QR code from a static image. Takes a base64-encoded image string. Returns a promise that resolves with the decoded string, or `null` if the image is valid but contains no QR code. Rejects only on actual errors (invalid base64, undecodable image data, detector failure).
|
||||
|
||||
```ts
|
||||
import { detectQRCodeInImage } from 'react-native-camera-kit-no-google';
|
||||
|
||||
try {
|
||||
const decoded = await detectQRCodeInImage(base64ImageData);
|
||||
if (decoded === null) {
|
||||
// Image was valid but no QR code was found
|
||||
} else {
|
||||
console.log('QR code:', decoded);
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid image data or internal detection failure — e.message has details
|
||||
}
|
||||
```
|
||||
|
||||
Rejection error codes: `E_INVALID_IMAGE` (bad base64 or undecodable image) and `E_QR_DETECTION_FAILED` (Android: QR decoder error; iOS: Vision framework error or detector init failure).
|
||||
|
||||
This API is exclusive to this fork and is not available in the original react-native-camera-kit.
|
||||
|
||||
It does **not** require camera permissions — it works purely on image data. Useful for detecting QR codes from:
|
||||
- Images picked from the photo library
|
||||
- Clipboard images
|
||||
- Files received via deep links or share extensions
|
||||
|
||||
| Platform | Implementation |
|
||||
|----------|---------------|
|
||||
| iOS (device) | Vision framework (`VNDetectBarcodesRequest`) |
|
||||
| iOS (simulator) | CoreImage (`CIDetector`) |
|
||||
| Android | [limpbrains/qr](https://github.com/limpbrains/qr) |
|
||||
|
||||
### Imperative API
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ dependencies {
|
||||
// If you want to additionally use the CameraX Extensions library
|
||||
// implementation "androidx.camera:camera-extensions:${camerax_version}"
|
||||
|
||||
implementation 'com.github.limpbrains:qr:v0.0.2'
|
||||
implementation 'com.github.limpbrains:qr:v0.0.3'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
@ -28,6 +28,7 @@ import androidx.lifecycle.LifecycleObserver
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter
|
||||
import com.rncamerakit.barcode.BarcodeFrame
|
||||
@ -108,6 +109,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
|
||||
private var frameColor = Color.GREEN
|
||||
private var laserColor = Color.RED
|
||||
private var barcodeFrameSize: Size? = null
|
||||
private var allowedBarcodeTypes: Array<CodeFormat>? = null
|
||||
|
||||
private fun getActivity() : Activity {
|
||||
return currentContext.currentActivity!!
|
||||
@ -338,8 +340,8 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
|
||||
|
||||
val useCases = mutableListOf(preview, imageCapture)
|
||||
|
||||
if (scanBarcode) {
|
||||
val analyzer = QRCodeAnalyzer({ decodedValue ->
|
||||
if (scanBarcode) {
|
||||
val analyzer = QRCodeAnalyzer({ decodedValue ->
|
||||
onBarcodeRead(decodedValue)
|
||||
}, scanThrottleDelay)
|
||||
imageAnalyzer!!.setAnalyzer(cameraExecutor, analyzer)
|
||||
@ -464,8 +466,8 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
|
||||
|
||||
val imageFile = File(path)
|
||||
val imageSize = imageFile.length() // size in bytes
|
||||
imageInfo.putDouble("size", imageSize.toDouble())
|
||||
|
||||
imageInfo.putDouble("size", imageSize.toDouble())
|
||||
|
||||
promise.resolve(imageInfo)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Error while saving or decoding saved photo: ${ex.message}", ex)
|
||||
@ -684,6 +686,26 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
|
||||
}
|
||||
}
|
||||
|
||||
fun setAllowedBarcodeTypes(types: ReadableArray?) {
|
||||
if (types == null || types.size() == 0) {
|
||||
allowedBarcodeTypes = emptyArray()
|
||||
return
|
||||
}
|
||||
|
||||
// Convert only valid CodeFormat values
|
||||
val converted = mutableListOf<CodeFormat>()
|
||||
|
||||
for (i in 0 until types.size()) {
|
||||
val name = types.getString(i) ?: continue
|
||||
val format = CodeFormat.fromName(name)
|
||||
if (format != null) {
|
||||
converted.add(format)
|
||||
}
|
||||
}
|
||||
|
||||
allowedBarcodeTypes = converted.toTypedArray()
|
||||
}
|
||||
|
||||
private fun convertDeviceHeightToSupportedAspectRatio(actualWidth: Int, actualHeight: Int): Int {
|
||||
val maxScreenRatio = 16 / 9f
|
||||
return (if (actualHeight / actualWidth > maxScreenRatio) actualWidth * maxScreenRatio else actualHeight).toInt()
|
||||
@ -704,6 +726,10 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
|
||||
return false
|
||||
}
|
||||
|
||||
private fun convertAllowedBarcodeTypes(): Set<Int> {
|
||||
return allowedBarcodeTypes?.map { it.toBarcodeType() }?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "CameraKit"
|
||||
|
||||
@ -15,4 +15,18 @@ enum class CodeFormat(val code: String) {
|
||||
AZTEC("aztec"),
|
||||
DATA_MATRIX("data-matrix"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): CodeFormat? {
|
||||
val normalized = name.trim().lowercase()
|
||||
return values().firstOrNull { format ->
|
||||
format.code == normalized || format.name.lowercase() == normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeFormat.toBarcodeType(): Int {
|
||||
// QR-only Android implementation; no barcode type mapping required.
|
||||
return 0
|
||||
}
|
||||
|
||||
99
android/src/main/java/com/rncamerakit/ImageQRCodeDecoder.kt
Normal file
99
android/src/main/java/com/rncamerakit/ImageQRCodeDecoder.kt
Normal file
@ -0,0 +1,99 @@
|
||||
package com.rncamerakit
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import qr.QRDecoder
|
||||
|
||||
object ImageQRCodeDecoder {
|
||||
|
||||
// Center-crop retry removes a 10% border on each side. Borders often contain
|
||||
// background clutter (fingers, page edges, UI chrome) that confuses the
|
||||
// detector without contributing QR data.
|
||||
private const val CROP_FRACTION = 0.1
|
||||
|
||||
// The underlying QR decoder works best on images around ~600px on the longest
|
||||
// side. Larger inputs waste CPU on pixel scanning; much smaller inputs lose
|
||||
// the finder-pattern resolution. 600 is the empirical sweet spot.
|
||||
private const val RETRY_MAX_DIM = 600
|
||||
|
||||
/**
|
||||
* Decodes a QR code from a base64-encoded image.
|
||||
* Returns the decoded string, or null if no QR code could be found.
|
||||
* Throws IllegalArgumentException if the input is not a valid image.
|
||||
*/
|
||||
fun decode(base64: String): String? {
|
||||
val imageBytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
?: throw IllegalArgumentException("Could not decode base64 image data")
|
||||
|
||||
return try {
|
||||
decodeFromBitmap(bitmap)
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeFromBitmap(bitmap: Bitmap): String? {
|
||||
val rgba = bitmapToRgba(bitmap)
|
||||
return try {
|
||||
QRDecoder.decode(bitmap.width, bitmap.height, rgba)
|
||||
} catch (e: Exception) {
|
||||
// Retry with cropped-and-scaled image for hard-to-read QR codes
|
||||
decodeCroppedAndScaled(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeCroppedAndScaled(original: Bitmap): String? {
|
||||
val x = (original.width * CROP_FRACTION).toInt()
|
||||
val y = (original.height * CROP_FRACTION).toInt()
|
||||
val cropW = original.width - 2 * x
|
||||
val cropH = original.height - 2 * y
|
||||
|
||||
// Image is too small to retry with a center-crop — treat as "no QR found".
|
||||
if (cropW < 1 || cropH < 1) return null
|
||||
|
||||
val cropped = Bitmap.createBitmap(original, x, y, cropW, cropH)
|
||||
try {
|
||||
val longest = maxOf(cropW, cropH)
|
||||
val scale = if (longest > RETRY_MAX_DIM) RETRY_MAX_DIM.toFloat() / longest else 1f
|
||||
|
||||
val scaled = if (scale < 1f) {
|
||||
Bitmap.createScaledBitmap(cropped, (cropW * scale).toInt(), (cropH * scale).toInt(), true)
|
||||
} else {
|
||||
cropped
|
||||
}
|
||||
|
||||
try {
|
||||
val rgba = bitmapToRgba(scaled)
|
||||
return try {
|
||||
QRDecoder.decode(scaled.width, scaled.height, rgba)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
} finally {
|
||||
if (scaled !== cropped) scaled.recycle()
|
||||
}
|
||||
} finally {
|
||||
cropped.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToRgba(bitmap: Bitmap): ByteArray {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
|
||||
val rgba = ByteArray(width * height * 4)
|
||||
for (i in pixels.indices) {
|
||||
val pixel = pixels[i]
|
||||
val offset = i * 4
|
||||
rgba[offset] = ((pixel shr 16) and 0xFF).toByte() // R
|
||||
rgba[offset + 1] = ((pixel shr 8) and 0xFF).toByte() // G
|
||||
rgba[offset + 2] = (pixel and 0xFF).toByte() // B
|
||||
rgba[offset + 3] = ((pixel shr 24) and 0xFF).toByte() // A
|
||||
}
|
||||
return rgba
|
||||
}
|
||||
}
|
||||
@ -150,15 +150,14 @@ class CKCameraManager(context: ReactApplicationContext) : SimpleViewManager<CKCa
|
||||
}
|
||||
|
||||
@ReactProp(name = "allowedBarcodeTypes")
|
||||
override fun setAllowedBarcodeTypes(view: CKCamera?, types: ReadableArray?) {
|
||||
override fun setAllowedBarcodeTypes(view: CKCamera, types: ReadableArray?) {
|
||||
// Fork note: Android uses limpbrains/qr which is QR-only, no barcode filtering.
|
||||
// This prop is accepted for API compatibility but has no effect on Android.
|
||||
// iOS implementation supports this prop.
|
||||
view.setAllowedBarcodeTypes(types)
|
||||
}
|
||||
|
||||
// Methods only available on iOS
|
||||
override fun setIOsSleepBeforeStarting(view: CKCamera?, value: Int) = Unit
|
||||
|
||||
override fun setRatioOverlay(view: CKCamera?, value: String?) = Unit
|
||||
|
||||
override fun setRatioOverlayColor(view: CKCamera?, value: Int?) = Unit
|
||||
@ -169,5 +168,7 @@ class CKCameraManager(context: ReactApplicationContext) : SimpleViewManager<CKCa
|
||||
|
||||
override fun setResizeMode(view: CKCamera?, value: String?) = Unit
|
||||
|
||||
override fun setIOsDeferredStart(view: CKCamera?, value: Boolean) = Unit
|
||||
|
||||
override fun setMaxPhotoQualityPrioritization(view: CKCamera?, value: String?) = Unit
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.rncamerakit
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.rncamerakit.NativeCameraKitModuleSpec
|
||||
|
||||
@ -38,6 +39,8 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Nat
|
||||
const val LANDSCAPE_RIGHT = 3 // ➡️
|
||||
|
||||
const val REACT_CLASS = "RNCameraKitModule"
|
||||
|
||||
private val qrDecodeExecutor = Executors.newCachedThreadPool()
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
@ -62,6 +65,19 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Nat
|
||||
|
||||
override fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit
|
||||
|
||||
@ReactMethod
|
||||
override fun detectQRCodeInImage(base64: String, promise: Promise) {
|
||||
qrDecodeExecutor.execute {
|
||||
try {
|
||||
promise.resolve(ImageQRCodeDecoder.decode(base64))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
promise.reject("E_INVALID_IMAGE", e.message ?: "Invalid image data", e)
|
||||
} catch (e: Exception) {
|
||||
promise.reject("E_QR_DETECTION_FAILED", e.message ?: "QR detection failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a photo using the camera.
|
||||
*
|
||||
|
||||
@ -2,6 +2,7 @@ package com.rncamerakit
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Native module for interacting with the camera in React Native applications.
|
||||
@ -36,6 +37,8 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea
|
||||
const val LANDSCAPE_RIGHT = 3 // ➡️
|
||||
|
||||
const val REACT_CLASS = "RNCameraKitModule"
|
||||
|
||||
private val qrDecodeExecutor = Executors.newCachedThreadPool()
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
@ -60,6 +63,19 @@ class RNCameraKitModule(private val reactContext: ReactApplicationContext) : Rea
|
||||
|
||||
fun checkDeviceCameraAuthorizationStatus(promise: Promise?) = Unit
|
||||
|
||||
@ReactMethod
|
||||
fun detectQRCodeInImage(base64: String, promise: Promise) {
|
||||
qrDecodeExecutor.execute {
|
||||
try {
|
||||
promise.resolve(ImageQRCodeDecoder.decode(base64))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
promise.reject("E_INVALID_IMAGE", e.message ?: "Invalid image data", e)
|
||||
} catch (e: Exception) {
|
||||
promise.reject("E_QR_DETECTION_FAILED", e.message ?: "QR detection failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a photo using the camera.
|
||||
*
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
<string>3B52.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
|
||||
@ -1719,6 +1719,34 @@ PODS:
|
||||
- React-RCTFBReactNativeSpec
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- react-native-image-picker (7.2.3):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- React-NativeModulesApple (0.81.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@ -2223,7 +2251,7 @@ PODS:
|
||||
- React-perflogger (= 0.81.0)
|
||||
- React-utils (= 0.81.0)
|
||||
- SocketRocket
|
||||
- ReactNativeCameraKit (16.1.1):
|
||||
- ReactNativeCameraKit (17.0.4):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@ -2296,6 +2324,7 @@ DEPENDENCIES:
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
@ -2416,6 +2445,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-microtasksnativemodule:
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
react-native-image-picker:
|
||||
:path: "../node_modules/react-native-image-picker"
|
||||
React-NativeModulesApple:
|
||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||
React-oscompat:
|
||||
@ -2522,6 +2553,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: 04ce9229cb57db2c2a8164eaec1105f89da7fb22
|
||||
React-Mapbuffer: e402e7a0535b2213c50727553621480fe8cd8ade
|
||||
React-microtasksnativemodule: a63ce5595016996a9bac1f10c70a7a7fe6506649
|
||||
react-native-image-picker: b16541b587b275e81a12f9b82f215c5e9b0ccbf3
|
||||
React-NativeModulesApple: b3766e1f87b08064ebc459b9e1538da2447ca874
|
||||
React-oscompat: 34f3d3c06cadcbc470bc4509c717fb9b919eaa8b
|
||||
React-perflogger: a1edb025fd5d44f61bf09307e248f7608d7b2dcf
|
||||
@ -2552,7 +2584,7 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
|
||||
ReactCodegen: a55799cae416c387aeaae3aabc1bc0289ac19cee
|
||||
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
|
||||
ReactNativeCameraKit: b01e637c97fb6eefe43eff31917d1410fc77e1f8
|
||||
ReactNativeCameraKit: 21c717ad3fc6f040b4293a07c4300aae3f1007d0
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782
|
||||
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"dependencies": {
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.0",
|
||||
"react-native-camera-kit": "link:../"
|
||||
"react-native-camera-kit": "link:../",
|
||||
"react-native-image-picker": "^7.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Button, Alert, TextInput } from 'react-native';
|
||||
|
||||
import BarcodeScreenExample from './BarcodeScreenExample';
|
||||
import CameraExample from './CameraExample';
|
||||
import DetectQRExample from './DetectQRExample';
|
||||
|
||||
const App = () => {
|
||||
const [example, setExample] = useState<any>(undefined);
|
||||
@ -10,6 +11,7 @@ const App = () => {
|
||||
const [interval, setIntervalId] = useState<number | null>(null);
|
||||
const [speed, setSpeed] = useState('1000');
|
||||
const onBack = () => setExample(undefined);
|
||||
const testStart = useRef(0);
|
||||
|
||||
if (example) {
|
||||
return example;
|
||||
@ -20,12 +22,9 @@ const App = () => {
|
||||
<View style={styles.container}>
|
||||
<Text style={{ fontSize: 60 }}>🎈</Text>
|
||||
<Text style={styles.headerText}>React Native Camera Kit</Text>
|
||||
<TouchableOpacity style={styles.button} onPress={() => setExample(<CameraExample onBack={onBack} />)}>
|
||||
<Text style={styles.buttonText}>Camera</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.button} onPress={() => setExample(<BarcodeScreenExample onBack={onBack} />)}>
|
||||
<Text style={styles.buttonText}>Barcode Scanner</Text>
|
||||
</TouchableOpacity>
|
||||
<Button title="Camera" onPress={() => setExample(<CameraExample onBack={onBack} />)}></Button>
|
||||
<Button title="Barcode Scanner" onPress={() => setExample(<BarcodeScreenExample onBack={onBack} />)}></Button>
|
||||
<Button title="Detect QR from Image" onPress={() => setExample(<DetectQRExample onBack={onBack} />)}></Button>
|
||||
<View>
|
||||
<Text style={[styles.stressHeader, { marginTop: 12 }]}>Mount Stress Test</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
@ -53,12 +52,22 @@ const App = () => {
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
testStart.current = Date.now();
|
||||
setTestNo(0);
|
||||
setIntervalId(
|
||||
setInterval(() => {
|
||||
setTestNo((prev) => {
|
||||
const newR = prev + 1;
|
||||
if (newR % 2 === 0) {
|
||||
setExample(<CameraExample key={String(Math.random())} stress onBack={onBack} />);
|
||||
const elapsedMs = Date.now() - (testStart.current ?? Date.now());
|
||||
const minutes = Math.floor(elapsedMs / 60000);
|
||||
const seconds = Math.floor((elapsedMs % 60000) / 1000);
|
||||
console.log(
|
||||
`Stress test iteration ${newR / 2}${
|
||||
testStart.current ? `, elapsed time: ${minutes}m ${seconds}s` : ''
|
||||
}`,
|
||||
);
|
||||
setExample(<CameraExample key={`test-${Date.now()}`} stress onBack={onBack} />);
|
||||
} else {
|
||||
setExample(undefined);
|
||||
}
|
||||
|
||||
@ -198,6 +198,8 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => {
|
||||
onReadCode={(event) => {
|
||||
setScanCount((prev) => prev + 1);
|
||||
setBarcode(event.nativeEvent.codeStringValue);
|
||||
console.log('barcode', event.nativeEvent.codeStringValue);
|
||||
console.log('codeFormat', event.nativeEvent.codeFormat);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@ -276,8 +278,7 @@ const styles = StyleSheet.create({
|
||||
backBtnContainer: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
captureButtonContainer: {
|
||||
},
|
||||
captureButtonContainer: {},
|
||||
textNumberContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
152
example/src/DetectQRExample.tsx
Normal file
152
example/src/DetectQRExample.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import { useState } from 'react';
|
||||
import { StyleSheet, Text, View, TouchableOpacity, Image, ScrollView, ActivityIndicator } from 'react-native';
|
||||
import { launchImageLibrary } from 'react-native-image-picker';
|
||||
import { detectQRCodeInImage } from '../../src';
|
||||
import SafeAreaView from './SafeAreaView';
|
||||
|
||||
const DetectQRExample = ({ onBack }: { onBack: () => void }) => {
|
||||
const [imageUri, setImageUri] = useState<string | undefined>();
|
||||
const [result, setResult] = useState<string | undefined>();
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onPickImage = async () => {
|
||||
setResult(undefined);
|
||||
setError(undefined);
|
||||
|
||||
const response = await launchImageLibrary({
|
||||
mediaType: 'photo',
|
||||
selectionLimit: 1,
|
||||
includeBase64: true,
|
||||
});
|
||||
|
||||
if (response.didCancel || !response.assets?.length) return;
|
||||
|
||||
const asset = response.assets[0];
|
||||
setImageUri(asset.uri);
|
||||
|
||||
const base64 = asset.base64;
|
||||
if (!base64) {
|
||||
setError('No base64 data returned from image picker');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const decoded = await detectQRCodeInImage(base64);
|
||||
setResult(decoded === null ? '(no QR code found)' : decoded);
|
||||
} catch (e: any) {
|
||||
setError(e.message ?? 'Detection failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<SafeAreaView style={styles.header}>
|
||||
<TouchableOpacity onPress={onBack}>
|
||||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Detect QR from Image</Text>
|
||||
<View style={{ width: 50 }} />
|
||||
</SafeAreaView>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<TouchableOpacity style={styles.pickButton} onPress={onPickImage}>
|
||||
<Text style={styles.pickButtonText}>Pick Image</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{imageUri && <Image source={{ uri: imageUri }} style={styles.preview} resizeMode="contain" />}
|
||||
|
||||
{loading && <ActivityIndicator size="large" color="#ffffff" style={{ marginTop: 20 }} />}
|
||||
|
||||
{result !== undefined && (
|
||||
<View style={styles.resultBox}>
|
||||
<Text style={styles.resultLabel}>Decoded:</Text>
|
||||
<Text style={styles.resultValue} selectable>
|
||||
{result}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error !== undefined && (
|
||||
<View style={[styles.resultBox, styles.errorBox]}>
|
||||
<Text style={styles.resultLabel}>Error:</Text>
|
||||
<Text style={styles.errorValue}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetectQRExample;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
backText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
},
|
||||
title: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
content: {
|
||||
alignItems: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
pickButton: {
|
||||
backgroundColor: '#2196F3',
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 8,
|
||||
},
|
||||
pickButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
preview: {
|
||||
width: 280,
|
||||
height: 280,
|
||||
marginTop: 24,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#111',
|
||||
},
|
||||
resultBox: {
|
||||
marginTop: 20,
|
||||
backgroundColor: '#1a3a1a',
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
width: '100%',
|
||||
},
|
||||
errorBox: {
|
||||
backgroundColor: '#3a1a1a',
|
||||
},
|
||||
resultLabel: {
|
||||
color: '#aaa',
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
resultValue: {
|
||||
color: '#4caf50',
|
||||
fontSize: 16,
|
||||
},
|
||||
errorValue: {
|
||||
color: '#f44336',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
@ -4817,6 +4817,11 @@ react-is@^19.1.0:
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
react-native-image-picker@^7.1.2:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-7.2.3.tgz#9c402591462af256cdd9aed796c28083a48f90cd"
|
||||
integrity sha512-zKIZUlQNU3EtqizsXSH92zPeve4vpUrsqHu2kkpCxWE9TZhJFZBb+irDsBOY8J21k0+Edgt06TMQGJ+iPUIXyA==
|
||||
|
||||
react-native@0.81.0:
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.0.tgz#ebb645f3fb2fc2ffb222d2f294ca4e81e6568f15"
|
||||
|
||||
@ -6,18 +6,19 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#if __has_include(<React/RCTBridge.h>)
|
||||
#import <React/RCTViewManager.h>
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTViewManager.h>
|
||||
#else
|
||||
#import "RCTViewManager.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTViewManager.h"
|
||||
#endif
|
||||
|
||||
@interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager)
|
||||
@interface RCT_EXTERN_MODULE (CKCameraManager, RCTViewManager)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(cameraType, CKCameraType)
|
||||
RCT_EXPORT_VIEW_PROPERTY(flashMode, CKFlashMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxPhotoQualityPrioritization, CKMaxPhotoQualityPrioritization)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxPhotoQualityPrioritization,
|
||||
CKMaxPhotoQualityPrioritization)
|
||||
RCT_EXPORT_VIEW_PROPERTY(torchMode, CKTorchMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(ratioOverlay, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(ratioOverlayColor, UIColor)
|
||||
|
||||
@ -16,44 +16,47 @@
|
||||
|
||||
using namespace facebook::react;
|
||||
|
||||
static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
||||
{
|
||||
static id CKConvertFollyDynamicToId(const folly::dynamic &dyn) {
|
||||
// I could imagine an implementation which avoids copies by wrapping the
|
||||
// dynamic in a derived class of NSDictionary. We can do that if profiling
|
||||
// implies it will help.
|
||||
|
||||
switch (dyn.type()) {
|
||||
case folly::dynamic::NULLT:
|
||||
return nil;
|
||||
case folly::dynamic::BOOL:
|
||||
return dyn.getBool() ? @YES : @NO;
|
||||
case folly::dynamic::INT64:
|
||||
return @(dyn.getInt());
|
||||
case folly::dynamic::DOUBLE:
|
||||
return @(dyn.getDouble());
|
||||
case folly::dynamic::STRING:
|
||||
return [[NSString alloc] initWithBytes:dyn.c_str() length:dyn.size() encoding:NSUTF8StringEncoding];
|
||||
case folly::dynamic::ARRAY: {
|
||||
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()];
|
||||
for (const auto &elem : dyn) {
|
||||
id value = CKConvertFollyDynamicToId(elem);
|
||||
if (value) {
|
||||
[array addObject:value];
|
||||
}
|
||||
case folly::dynamic::NULLT:
|
||||
return nil;
|
||||
case folly::dynamic::BOOL:
|
||||
return dyn.getBool() ? @YES : @NO;
|
||||
case folly::dynamic::INT64:
|
||||
return @(dyn.getInt());
|
||||
case folly::dynamic::DOUBLE:
|
||||
return @(dyn.getDouble());
|
||||
case folly::dynamic::STRING:
|
||||
return [[NSString alloc] initWithBytes:dyn.c_str()
|
||||
length:dyn.size()
|
||||
encoding:NSUTF8StringEncoding];
|
||||
case folly::dynamic::ARRAY: {
|
||||
NSMutableArray *array =
|
||||
[[NSMutableArray alloc] initWithCapacity:dyn.size()];
|
||||
for (const auto &elem : dyn) {
|
||||
id value = CKConvertFollyDynamicToId(elem);
|
||||
if (value) {
|
||||
[array addObject:value];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
case folly::dynamic::OBJECT: {
|
||||
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
|
||||
for (const auto &elem : dyn.items()) {
|
||||
id key = CKConvertFollyDynamicToId(elem.first);
|
||||
id value = CKConvertFollyDynamicToId(elem.second);
|
||||
if (key && value) {
|
||||
dict[key] = value;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
case folly::dynamic::OBJECT: {
|
||||
NSMutableDictionary *dict =
|
||||
[[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
|
||||
for (const auto &elem : dyn.items()) {
|
||||
id key = CKConvertFollyDynamicToId(elem.first);
|
||||
id value = CKConvertFollyDynamicToId(elem.second);
|
||||
if (key && value) {
|
||||
dict[key] = value;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,234 +64,270 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
||||
@end
|
||||
|
||||
@implementation CKCameraViewComponentView {
|
||||
CKCameraView *_view;
|
||||
CKCameraView *_view;
|
||||
}
|
||||
|
||||
// Needed because of this: https://github.com/facebook/react-native/pull/37274
|
||||
+ (void)load
|
||||
{
|
||||
+ (void)load {
|
||||
[super load];
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
static const auto defaultProps = std::make_shared<const CKCameraProps>();
|
||||
_props = defaultProps;
|
||||
[self prepareView];
|
||||
}
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
static const auto defaultProps = std::make_shared<const CKCameraProps>();
|
||||
_props = defaultProps;
|
||||
[self prepareView];
|
||||
}
|
||||
|
||||
return self;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)prepareView
|
||||
{
|
||||
_view = [[CKCameraView alloc] init];
|
||||
|
||||
// just need to pass something, it won't really be used on fabric, but it's used to create events (it won't impact sending them)
|
||||
_view.reactTag = @-1;
|
||||
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
- (void)prepareView {
|
||||
_view = [[CKCameraView alloc] init];
|
||||
|
||||
[_view setOnReadCode:^(NSDictionary* event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
// just need to pass something, it won't really be used on fabric, but it's
|
||||
// used to create events (it won't impact sending them)
|
||||
_view.reactTag = @-1;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
std::string codeStringValue = [event valueForKey:@"codeStringValue"] == nil ? "" : std::string([[event valueForKey:@"codeStringValue"] UTF8String]);
|
||||
std::string codeFormat = [event valueForKey:@"codeFormat"] == nil ? "" : std::string([[event valueForKey:@"codeFormat"] UTF8String]);
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(strongSelf->_eventEmitter)->onReadCode({.codeStringValue = codeStringValue, .codeFormat = codeFormat});
|
||||
}
|
||||
}];
|
||||
[_view setOnOrientationChange:^(NSDictionary* event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
id orientation = [event valueForKey:@"orientation"] == nil ? 0 : [event valueForKey:@"orientation"];
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(strongSelf->_eventEmitter)->onOrientationChange({.orientation = [orientation intValue]});
|
||||
}
|
||||
}];
|
||||
[_view setOnZoom:^(NSDictionary* event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
[_view setOnReadCode:^(NSDictionary *event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
id zoom = [event valueForKey:@"zoom"] == nil ? 0 : [event valueForKey:@"zoom"];
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(strongSelf->_eventEmitter)->onZoom({.zoom = [zoom doubleValue]});
|
||||
}
|
||||
}];
|
||||
[_view setOnCaptureButtonPressIn:^(NSDictionary* event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
std::string codeStringValue =
|
||||
[event valueForKey:@"codeStringValue"] == nil
|
||||
? ""
|
||||
: std::string(
|
||||
[[event valueForKey:@"codeStringValue"] UTF8String]);
|
||||
std::string codeFormat =
|
||||
[event valueForKey:@"codeFormat"] == nil
|
||||
? ""
|
||||
: std::string([[event valueForKey:@"codeFormat"] UTF8String]);
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
|
||||
strongSelf->_eventEmitter)
|
||||
->onReadCode(
|
||||
{.codeStringValue = codeStringValue, .codeFormat = codeFormat});
|
||||
}
|
||||
}];
|
||||
[_view setOnOrientationChange:^(NSDictionary *event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(strongSelf->_eventEmitter)->onCaptureButtonPressIn({});
|
||||
}
|
||||
}];
|
||||
[_view setOnCaptureButtonPressOut:^(NSDictionary* event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
id orientation = [event valueForKey:@"orientation"] == nil
|
||||
? 0
|
||||
: [event valueForKey:@"orientation"];
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
|
||||
strongSelf->_eventEmitter)
|
||||
->onOrientationChange({.orientation = [orientation intValue]});
|
||||
}
|
||||
}];
|
||||
[_view setOnZoom:^(NSDictionary *event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(strongSelf->_eventEmitter)->onCaptureButtonPressOut({});
|
||||
}
|
||||
}];
|
||||
|
||||
self.contentView = _view;
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
id zoom =
|
||||
[event valueForKey:@"zoom"] == nil ? 0 : [event valueForKey:@"zoom"];
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
|
||||
strongSelf->_eventEmitter)
|
||||
->onZoom({.zoom = [zoom doubleValue]});
|
||||
}
|
||||
}];
|
||||
[_view setOnCaptureButtonPressIn:^(NSDictionary *event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
|
||||
strongSelf->_eventEmitter)
|
||||
->onCaptureButtonPressIn({});
|
||||
}
|
||||
}];
|
||||
[_view setOnCaptureButtonPressOut:^(NSDictionary *event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
|
||||
if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) {
|
||||
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(
|
||||
strongSelf->_eventEmitter)
|
||||
->onCaptureButtonPressOut({});
|
||||
}
|
||||
}];
|
||||
|
||||
self.contentView = _view;
|
||||
}
|
||||
|
||||
#pragma mark - RCTComponentViewProtocol
|
||||
|
||||
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
||||
{
|
||||
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
||||
return concreteComponentDescriptorProvider<CKCameraComponentDescriptor>();
|
||||
}
|
||||
|
||||
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
|
||||
{
|
||||
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
||||
[_view updateSubviewsBounds:RCTCGRectFromRect(layoutMetrics.frame)];
|
||||
- (void)updateLayoutMetrics:
|
||||
(const facebook::react::LayoutMetrics &)layoutMetrics
|
||||
oldLayoutMetrics:
|
||||
(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
|
||||
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
||||
[_view updateSubviewsBounds:RCTCGRectFromRect(layoutMetrics.frame)];
|
||||
}
|
||||
|
||||
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
|
||||
{
|
||||
const auto &oldViewProps = *std::static_pointer_cast<CKCameraProps const>(_props);
|
||||
const auto &newProps = *std::static_pointer_cast<CKCameraProps const>(props);
|
||||
- (void)updateProps:(const Props::Shared &)props
|
||||
oldProps:(const Props::Shared &)oldProps {
|
||||
const auto &oldViewProps =
|
||||
*std::static_pointer_cast<CKCameraProps const>(_props);
|
||||
const auto &newProps = *std::static_pointer_cast<CKCameraProps const>(props);
|
||||
|
||||
NSMutableArray<NSString *> *changedProps = [NSMutableArray new];
|
||||
|
||||
// Keep changedProps aligned with CameraView.swift didSetProps
|
||||
// Include event-related props so CameraView can update listeners/state
|
||||
[changedProps addObject:@"onOrientationChange"];
|
||||
[changedProps addObject:@"onZoom"];
|
||||
[changedProps addObject:@"onReadCode"];
|
||||
NSMutableArray<NSString *> *changedProps = [NSMutableArray new];
|
||||
|
||||
if (oldViewProps.cameraType != newProps.cameraType) {
|
||||
_view.cameraType = newProps.cameraType == "back" ? CKCameraTypeBack : CKCameraTypeFront;
|
||||
[changedProps addObject:@"cameraType"];
|
||||
// Keep changedProps aligned with CameraView.swift didSetProps
|
||||
// Include event-related props so CameraView can update listeners/state
|
||||
[changedProps addObject:@"onOrientationChange"];
|
||||
[changedProps addObject:@"onZoom"];
|
||||
[changedProps addObject:@"onReadCode"];
|
||||
|
||||
if (oldViewProps.cameraType != newProps.cameraType) {
|
||||
_view.cameraType =
|
||||
newProps.cameraType == "back" ? CKCameraTypeBack : CKCameraTypeFront;
|
||||
[changedProps addObject:@"cameraType"];
|
||||
}
|
||||
if (oldViewProps.resizeMode != newProps.resizeMode) {
|
||||
_view.resizeMode = newProps.resizeMode == "contain" ? CKResizeModeContain
|
||||
: CKResizeModeCover;
|
||||
[changedProps addObject:@"resizeMode"];
|
||||
}
|
||||
id flashMode = CKConvertFollyDynamicToId(newProps.flashMode);
|
||||
if (oldViewProps.flashMode != newProps.flashMode) {
|
||||
_view.flashMode = [flashMode isEqualToString:@"auto"] ? CKFlashModeAuto
|
||||
: [flashMode isEqualToString:@"on"] ? CKFlashModeOn
|
||||
: CKFlashModeOff;
|
||||
[changedProps addObject:@"flashMode"];
|
||||
}
|
||||
if (oldViewProps.maxPhotoQualityPrioritization !=
|
||||
newProps.maxPhotoQualityPrioritization) {
|
||||
if (newProps.maxPhotoQualityPrioritization == "balanced") {
|
||||
_view.maxPhotoQualityPrioritization =
|
||||
CKMaxPhotoQualityPrioritizationBalanced;
|
||||
} else if (newProps.maxPhotoQualityPrioritization == "quality") {
|
||||
_view.maxPhotoQualityPrioritization =
|
||||
CKMaxPhotoQualityPrioritizationQuality;
|
||||
} else {
|
||||
_view.maxPhotoQualityPrioritization =
|
||||
CKMaxPhotoQualityPrioritizationSpeed;
|
||||
}
|
||||
if (oldViewProps.resizeMode != newProps.resizeMode) {
|
||||
_view.resizeMode = newProps.resizeMode == "contain" ? CKResizeModeContain : CKResizeModeCover;
|
||||
[changedProps addObject:@"resizeMode"];
|
||||
[changedProps addObject:@"maxPhotoQualityPrioritization"];
|
||||
}
|
||||
if (oldViewProps.torchMode != newProps.torchMode) {
|
||||
_view.torchMode =
|
||||
newProps.torchMode == "on" ? CKTorchModeOn : CKTorchModeOff;
|
||||
[changedProps addObject:@"torchMode"];
|
||||
}
|
||||
id ratioOverlay = CKConvertFollyDynamicToId(newProps.ratioOverlay);
|
||||
if (ratioOverlay != nil) {
|
||||
_view.ratioOverlay = ratioOverlay;
|
||||
[changedProps addObject:@"ratioOverlay"];
|
||||
}
|
||||
if (oldViewProps.ratioOverlayColor != newProps.ratioOverlayColor) {
|
||||
_view.ratioOverlayColor =
|
||||
RCTUIColorFromSharedColor(newProps.ratioOverlayColor);
|
||||
[changedProps addObject:@"ratioOverlayColor"];
|
||||
}
|
||||
if (_view.scanBarcode != newProps.scanBarcode) {
|
||||
_view.scanBarcode = newProps.scanBarcode;
|
||||
[changedProps addObject:@"scanBarcode"];
|
||||
}
|
||||
if (_view.showFrame != newProps.showFrame) {
|
||||
_view.showFrame = newProps.showFrame;
|
||||
[changedProps addObject:@"showFrame"];
|
||||
}
|
||||
if (newProps.scanThrottleDelay > -1) {
|
||||
_view.scanThrottleDelay = newProps.scanThrottleDelay;
|
||||
[changedProps addObject:@"scanThrottleDelay"];
|
||||
}
|
||||
if (oldViewProps.frameColor != newProps.frameColor) {
|
||||
_view.frameColor = RCTUIColorFromSharedColor(newProps.frameColor);
|
||||
[changedProps addObject:@"frameColor"];
|
||||
}
|
||||
if (oldViewProps.laserColor != newProps.laserColor) {
|
||||
UIColor *laserColor = RCTUIColorFromSharedColor(newProps.laserColor);
|
||||
_view.laserColor = laserColor;
|
||||
[changedProps addObject:@"laserColor"];
|
||||
}
|
||||
if (oldViewProps.resetFocusTimeout != newProps.resetFocusTimeout) {
|
||||
_view.resetFocusTimeout = newProps.resetFocusTimeout;
|
||||
[changedProps addObject:@"resetFocusTimeout"];
|
||||
}
|
||||
if (_view.resetFocusWhenMotionDetected !=
|
||||
newProps.resetFocusWhenMotionDetected) {
|
||||
_view.resetFocusWhenMotionDetected = newProps.resetFocusWhenMotionDetected;
|
||||
[changedProps addObject:@"resetFocusWhenMotionDetected"];
|
||||
}
|
||||
if (oldViewProps.focusMode != newProps.focusMode) {
|
||||
id focusMode = CKConvertFollyDynamicToId(newProps.focusMode);
|
||||
_view.focusMode =
|
||||
[focusMode isEqualToString:@"on"] ? CKFocusModeOn : CKFocusModeOff;
|
||||
[changedProps addObject:@"focusMode"];
|
||||
}
|
||||
if (oldViewProps.zoomMode != newProps.zoomMode) {
|
||||
id zoomMode = CKConvertFollyDynamicToId(newProps.zoomMode);
|
||||
_view.zoomMode =
|
||||
[zoomMode isEqualToString:@"on"] ? CKZoomModeOn : CKZoomModeOff;
|
||||
[changedProps addObject:@"zoomMode"];
|
||||
}
|
||||
if (oldViewProps.zoom != newProps.zoom) {
|
||||
_view.zoom = newProps.zoom > -1 ? @(newProps.zoom) : nil;
|
||||
[changedProps addObject:@"zoom"];
|
||||
}
|
||||
if (oldViewProps.maxZoom != newProps.maxZoom) {
|
||||
_view.maxZoom = newProps.maxZoom > -1 ? @(newProps.maxZoom) : nil;
|
||||
[changedProps addObject:@"maxZoom"];
|
||||
}
|
||||
if (oldViewProps.iOsDeferredStart != newProps.iOsDeferredStart) {
|
||||
_view.iOsDeferredStart = newProps.iOsDeferredStart;
|
||||
[changedProps addObject:@"iOsDeferredStart"];
|
||||
}
|
||||
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 flashMode = CKConvertFollyDynamicToId(newProps.flashMode);
|
||||
if (oldViewProps.flashMode != newProps.flashMode) {
|
||||
_view.flashMode = [flashMode isEqualToString:@"auto"] ? CKFlashModeAuto : [flashMode isEqualToString:@"on"] ? CKFlashModeOn : CKFlashModeOff;
|
||||
[changedProps addObject:@"flashMode"];
|
||||
id allowedBarcodeTypes =
|
||||
CKConvertFollyDynamicToId(allowedBarcodeTypesDynamic);
|
||||
if (allowedBarcodeTypes != nil &&
|
||||
[allowedBarcodeTypes isKindOfClass:NSArray.class]) {
|
||||
_view.allowedBarcodeTypes = allowedBarcodeTypes;
|
||||
[changedProps addObject:@"allowedBarcodeTypes"];
|
||||
}
|
||||
if (oldViewProps.maxPhotoQualityPrioritization != newProps.maxPhotoQualityPrioritization) {
|
||||
if (newProps.maxPhotoQualityPrioritization == "balanced") {
|
||||
_view.maxPhotoQualityPrioritization = CKMaxPhotoQualityPrioritizationBalanced;
|
||||
} else if (newProps.maxPhotoQualityPrioritization == "quality") {
|
||||
_view.maxPhotoQualityPrioritization = CKMaxPhotoQualityPrioritizationQuality;
|
||||
} else {
|
||||
_view.maxPhotoQualityPrioritization = CKMaxPhotoQualityPrioritizationSpeed;
|
||||
}
|
||||
[changedProps addObject:@"maxPhotoQualityPrioritization"];
|
||||
}
|
||||
if (oldViewProps.torchMode != newProps.torchMode) {
|
||||
_view.torchMode = newProps.torchMode == "on" ? CKTorchModeOn : CKTorchModeOff;
|
||||
[changedProps addObject:@"torchMode"];
|
||||
}
|
||||
id ratioOverlay = CKConvertFollyDynamicToId(newProps.ratioOverlay);
|
||||
if (ratioOverlay != nil) {
|
||||
_view.ratioOverlay = ratioOverlay;
|
||||
[changedProps addObject:@"ratioOverlay"];
|
||||
}
|
||||
if (oldViewProps.ratioOverlayColor != newProps.ratioOverlayColor) {
|
||||
_view.ratioOverlayColor = RCTUIColorFromSharedColor(newProps.ratioOverlayColor);
|
||||
[changedProps addObject:@"ratioOverlayColor"];
|
||||
}
|
||||
if (_view.scanBarcode != newProps.scanBarcode) {
|
||||
_view.scanBarcode = newProps.scanBarcode;
|
||||
[changedProps addObject:@"scanBarcode"];
|
||||
}
|
||||
if (_view.showFrame != newProps.showFrame) {
|
||||
_view.showFrame = newProps.showFrame;
|
||||
[changedProps addObject:@"showFrame"];
|
||||
}
|
||||
if (newProps.scanThrottleDelay > -1) {
|
||||
_view.scanThrottleDelay = newProps.scanThrottleDelay;
|
||||
[changedProps addObject:@"scanThrottleDelay"];
|
||||
}
|
||||
if (oldViewProps.frameColor != newProps.frameColor) {
|
||||
_view.frameColor = RCTUIColorFromSharedColor(newProps.frameColor);
|
||||
[changedProps addObject:@"frameColor"];
|
||||
}
|
||||
if (oldViewProps.laserColor != newProps.laserColor) {
|
||||
UIColor *laserColor = RCTUIColorFromSharedColor(newProps.laserColor);
|
||||
_view.laserColor = laserColor;
|
||||
[changedProps addObject:@"laserColor"];
|
||||
}
|
||||
if (oldViewProps.resetFocusTimeout != newProps.resetFocusTimeout) {
|
||||
_view.resetFocusTimeout = newProps.resetFocusTimeout;
|
||||
[changedProps addObject:@"resetFocusTimeout"];
|
||||
}
|
||||
if (_view.resetFocusWhenMotionDetected != newProps.resetFocusWhenMotionDetected) {
|
||||
_view.resetFocusWhenMotionDetected = newProps.resetFocusWhenMotionDetected;
|
||||
[changedProps addObject:@"resetFocusWhenMotionDetected"];
|
||||
}
|
||||
if (oldViewProps.focusMode != newProps.focusMode) {
|
||||
id focusMode = CKConvertFollyDynamicToId(newProps.focusMode);
|
||||
_view.focusMode = [focusMode isEqualToString:@"on"] ? CKFocusModeOn : CKFocusModeOff;
|
||||
[changedProps addObject:@"focusMode"];
|
||||
}
|
||||
if (oldViewProps.zoomMode != newProps.zoomMode) {
|
||||
id zoomMode = CKConvertFollyDynamicToId(newProps.zoomMode);
|
||||
_view.zoomMode = [zoomMode isEqualToString:@"on"] ? CKZoomModeOn : CKZoomModeOff;
|
||||
[changedProps addObject:@"zoomMode"];
|
||||
}
|
||||
if (oldViewProps.zoom != newProps.zoom) {
|
||||
_view.zoom = newProps.zoom > -1 ? @(newProps.zoom) : nil;
|
||||
[changedProps addObject:@"zoom"];
|
||||
}
|
||||
if (oldViewProps.maxZoom != newProps.maxZoom) {
|
||||
_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];
|
||||
}
|
||||
|
||||
[super updateProps:props oldProps:oldProps];
|
||||
[_view didSetProps:changedProps];
|
||||
}
|
||||
|
||||
+ (BOOL)shouldBeRecycled
|
||||
{
|
||||
// Disable recycling as cameras are expensive to keep in memory and may cause unintended behaviors
|
||||
// (we need to reset the camera properly when recycling)
|
||||
// We can enable it later if find that the performance is needed
|
||||
return NO;
|
||||
+ (BOOL)shouldBeRecycled {
|
||||
// Disable recycling as cameras are expensive to keep in memory and may cause
|
||||
// unintended behaviors (we need to reset the camera properly when recycling)
|
||||
// We can enable it later if find that the performance is needed
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)prepareForRecycle
|
||||
{
|
||||
[super prepareForRecycle];
|
||||
[self prepareView];
|
||||
- (void)prepareForRecycle {
|
||||
[super prepareForRecycle];
|
||||
[self prepareView];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Class<RCTComponentViewProtocol> CKCameraCls(void)
|
||||
{
|
||||
Class<RCTComponentViewProtocol> CKCameraCls(void) {
|
||||
return CKCameraViewComponentView.class;
|
||||
}
|
||||
|
||||
|
||||
@ -4,8 +4,10 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
import Foundation
|
||||
import React
|
||||
import Vision
|
||||
|
||||
/*
|
||||
* Class managing the communication between React Native and the native implementation
|
||||
@ -60,4 +62,48 @@ import React
|
||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { resolve($0) })
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc public static func detectQRCodeInImage(_ base64: String,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
guard let data = Data(base64Encoded: base64, options: .ignoreUnknownCharacters) else {
|
||||
reject("E_INVALID_IMAGE", "Could not decode base64 image data", nil)
|
||||
return
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
guard let ciImage = CIImage(data: data) else {
|
||||
reject("E_INVALID_IMAGE", "Could not decode base64 image data", nil)
|
||||
return
|
||||
}
|
||||
guard let detector = CIDetector(ofType: CIDetectorTypeQRCode,
|
||||
context: nil,
|
||||
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else {
|
||||
reject("E_QR_DETECTION_FAILED", "Could not initialize QR detector", nil)
|
||||
return
|
||||
}
|
||||
let features = detector.features(in: ciImage) as? [CIQRCodeFeature]
|
||||
let value = features?.first?.messageString
|
||||
resolve(value?.isEmpty == false ? value : nil)
|
||||
#else
|
||||
guard let uiImage = UIImage(data: data),
|
||||
let cgImage = uiImage.cgImage else {
|
||||
reject("E_INVALID_IMAGE", "Could not decode base64 image data", nil)
|
||||
return
|
||||
}
|
||||
let request = VNDetectBarcodesRequest()
|
||||
request.symbologies = [.qr]
|
||||
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
reject("E_QR_DETECTION_FAILED", "Vision request failed: \(error.localizedDescription)", error)
|
||||
return
|
||||
}
|
||||
let value = request.results?.first?.payloadStringValue
|
||||
resolve(value?.isEmpty == false ? value : nil)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
|
||||
func update(cameraType: CameraType)
|
||||
func update(onOrientationChange: RCTDirectEventBlock?)
|
||||
func update(onZoom: RCTDirectEventBlock?)
|
||||
func update(iOsSleepBeforeStartingMs: Int?)
|
||||
func update(iOsDeferredStartEnabled: Bool?)
|
||||
func update(zoom: Double?)
|
||||
func update(maxZoom: Double?)
|
||||
func update(resizeMode: ResizeMode)
|
||||
@ -27,13 +27,17 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
|
||||
func zoomPinchStart()
|
||||
func zoomPinchChange(pinchScale: CGFloat)
|
||||
|
||||
func isBarcodeScannerEnabled(_ isEnabled: Bool,
|
||||
supportedBarcodeTypes: [CodeFormat],
|
||||
onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?)
|
||||
func isBarcodeScannerEnabled(
|
||||
_ isEnabled: Bool,
|
||||
supportedBarcodeTypes: [CodeFormat],
|
||||
onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?)
|
||||
|
||||
func update(scannerFrameSize: CGRect?)
|
||||
|
||||
func capturePicture(onWillCapture: @escaping () -> Void,
|
||||
onSuccess: @escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions) -> Void,
|
||||
onError: @escaping (_ message: String) -> Void)
|
||||
func capturePicture(
|
||||
onWillCapture: @escaping () -> Void,
|
||||
onSuccess:
|
||||
@escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions)
|
||||
-> Void,
|
||||
onError: @escaping (_ message: String) -> Void)
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import AVKit
|
||||
import React
|
||||
import UIKit
|
||||
|
||||
/*
|
||||
* View abtracting the logic unrelated to the actual camera
|
||||
@ -22,7 +22,7 @@ public class CameraView: UIView {
|
||||
// scanner
|
||||
private var lastBarcodeDetectedTime: TimeInterval = 0
|
||||
private var scannerInterfaceView: ScannerInterfaceView
|
||||
|
||||
|
||||
// camera
|
||||
private var ratioOverlayView: RatioOverlayView?
|
||||
|
||||
@ -58,11 +58,11 @@ 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 iOsDeferredStart: Bool = true
|
||||
|
||||
@objc public var onCaptureButtonPressIn: RCTDirectEventBlock?
|
||||
@objc public var onCaptureButtonPressOut: RCTDirectEventBlock?
|
||||
|
||||
|
||||
var eventInteraction: Any? = nil
|
||||
|
||||
// MARK: - Setup
|
||||
@ -81,16 +81,22 @@ public class CameraView: UIView {
|
||||
}
|
||||
private func setupCamera() {
|
||||
if hasPropBeenSetup && hasPermissionBeenGranted && !hasCameraBeenSetup {
|
||||
let convertedAllowedTypes = convertAllowedBarcodeTypes()
|
||||
let convertedAllowedTypes = convertAllowedBarcodeTypes()
|
||||
|
||||
camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)
|
||||
camera.update(iOsDeferredStartEnabled: iOsDeferredStart)
|
||||
|
||||
hasCameraBeenSetup = true
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// Force front camera on Mac Catalyst during initial setup
|
||||
camera.setup(cameraType: .front, supportedBarcodeType: scanBarcode && onReadCode != nil ? convertedAllowedTypes : [])
|
||||
// Force front camera on Mac Catalyst during initial setup
|
||||
camera.setup(
|
||||
cameraType: .front,
|
||||
supportedBarcodeType: scanBarcode && onReadCode != nil
|
||||
? convertedAllowedTypes : [])
|
||||
#else
|
||||
camera.setup(cameraType: cameraType, supportedBarcodeType: scanBarcode && onReadCode != nil ? convertedAllowedTypes : [])
|
||||
camera.setup(
|
||||
cameraType: cameraType,
|
||||
supportedBarcodeType: scanBarcode && onReadCode != nil
|
||||
? convertedAllowedTypes : [])
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -103,7 +109,7 @@ public class CameraView: UIView {
|
||||
subview.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
subview.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
subview.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
subview.trailingAnchor.constraint(equalTo: self.trailingAnchor)
|
||||
subview.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@ -115,11 +121,11 @@ public class CameraView: UIView {
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
#if targetEnvironment(simulator)
|
||||
camera = SimulatorCamera()
|
||||
#else
|
||||
camera = RealCamera()
|
||||
#endif
|
||||
#if targetEnvironment(simulator)
|
||||
camera = SimulatorCamera()
|
||||
#else
|
||||
camera = RealCamera()
|
||||
#endif
|
||||
|
||||
scannerInterfaceView = ScannerInterfaceView(frameColor: .white, laserColor: .red)
|
||||
focusInterfaceView = FocusInterfaceView()
|
||||
@ -146,26 +152,26 @@ public class CameraView: UIView {
|
||||
focusInterfaceView.delegate = camera
|
||||
|
||||
handleCameraPermission()
|
||||
|
||||
|
||||
configureHardwareInteraction()
|
||||
}
|
||||
|
||||
|
||||
private func configureHardwareInteraction() {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
// Create a new capture event interaction with a handler that captures a photo.
|
||||
if #available(iOS 17.2, *) {
|
||||
let interaction = AVCaptureEventInteraction { [weak self] event in
|
||||
// Capture a photo on "press up" of a hardware button.
|
||||
if event.phase == .began {
|
||||
self?.onCaptureButtonPressIn?(nil)
|
||||
} else if event.phase == .ended {
|
||||
self?.onCaptureButtonPressOut?(nil)
|
||||
// Create a new capture event interaction with a handler that captures a photo.
|
||||
if #available(iOS 17.2, *) {
|
||||
let interaction = AVCaptureEventInteraction { [weak self] event in
|
||||
// Capture a photo on "press up" of a hardware button.
|
||||
if event.phase == .began {
|
||||
self?.onCaptureButtonPressIn?(nil)
|
||||
} else if event.phase == .ended {
|
||||
self?.onCaptureButtonPressOut?(nil)
|
||||
}
|
||||
}
|
||||
// Add the interaction to the view controller's view.
|
||||
self.addInteraction(interaction)
|
||||
eventInteraction = interaction
|
||||
}
|
||||
// Add the interaction to the view controller's view.
|
||||
self.addInteraction(interaction)
|
||||
eventInteraction = interaction
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -181,7 +187,7 @@ public class CameraView: UIView {
|
||||
super.reactSetFrame(frame)
|
||||
self.updateSubviewsBounds(frame)
|
||||
}
|
||||
|
||||
|
||||
@objc public func updateSubviewsBounds(_ frame: CGRect) {
|
||||
camera.previewView.frame = bounds
|
||||
|
||||
@ -207,10 +213,10 @@ public class CameraView: UIView {
|
||||
// Camera settings
|
||||
if changedProps.contains("cameraType") {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// Force front camera on Mac Catalyst regardless of what's passed
|
||||
camera.update(cameraType: .front)
|
||||
// Force front camera on Mac Catalyst regardless of what's passed
|
||||
camera.update(cameraType: .front)
|
||||
#else
|
||||
camera.update(cameraType: cameraType)
|
||||
camera.update(cameraType: cameraType)
|
||||
#endif
|
||||
}
|
||||
if changedProps.contains("flashMode") {
|
||||
@ -230,7 +236,7 @@ public class CameraView: UIView {
|
||||
if changedProps.contains("onZoom") {
|
||||
camera.update(onZoom: onZoom)
|
||||
}
|
||||
|
||||
|
||||
if changedProps.contains("resizeMode") {
|
||||
camera.update(resizeMode: resizeMode)
|
||||
}
|
||||
@ -241,7 +247,8 @@ public class CameraView: UIView {
|
||||
if let ratioOverlayView {
|
||||
ratioOverlayView.setRatio(ratioOverlay)
|
||||
} else {
|
||||
ratioOverlayView = RatioOverlayView(frame: bounds, ratioString: ratioOverlay, overlayColor: ratioOverlayColor)
|
||||
ratioOverlayView = RatioOverlayView(
|
||||
frame: bounds, ratioString: ratioOverlay, overlayColor: ratioOverlayColor)
|
||||
addSubview(ratioOverlayView!)
|
||||
}
|
||||
} else {
|
||||
@ -255,26 +262,32 @@ public class CameraView: UIView {
|
||||
}
|
||||
|
||||
// Scanner
|
||||
if changedProps.contains("scanBarcode") || changedProps.contains("onReadCode") || changedProps.contains("allowedBarcodeTypes") {
|
||||
if changedProps.contains("scanBarcode") || changedProps.contains("onReadCode")
|
||||
|| changedProps.contains("allowedBarcodeTypes")
|
||||
{
|
||||
let convertedAllowedTypes: [CodeFormat] = convertAllowedBarcodeTypes()
|
||||
|
||||
camera.isBarcodeScannerEnabled(scanBarcode,
|
||||
supportedBarcodeTypes: convertedAllowedTypes,
|
||||
onBarcodeRead: { [weak self] (barcode, codeFormat) in
|
||||
self?.onBarcodeRead(barcode: barcode, codeFormat: codeFormat)
|
||||
})
|
||||
camera.isBarcodeScannerEnabled(
|
||||
scanBarcode,
|
||||
supportedBarcodeTypes: convertedAllowedTypes,
|
||||
onBarcodeRead: { [weak self] (barcode, codeFormat) in
|
||||
self?.onBarcodeRead(barcode: barcode, codeFormat: codeFormat)
|
||||
})
|
||||
}
|
||||
|
||||
if changedProps.contains("showFrame") || changedProps.contains("scanBarcode") {
|
||||
DispatchQueue.main.async {
|
||||
self.scannerInterfaceView.isHidden = !self.showFrame
|
||||
|
||||
self.camera.update(scannerFrameSize: self.showFrame ? self.scannerInterfaceView.frameSize : nil)
|
||||
self.camera.update(
|
||||
scannerFrameSize: self.showFrame ? self.scannerInterfaceView.frameSize : nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if changedProps.contains("barcodeFrameSize"), let barcodeFrameSize, showFrame, scanBarcode {
|
||||
if let width = barcodeFrameSize["width"] as? CGFloat, let height = barcodeFrameSize["height"] as? CGFloat {
|
||||
if let width = barcodeFrameSize["width"] as? CGFloat,
|
||||
let height = barcodeFrameSize["height"] as? CGFloat
|
||||
{
|
||||
scannerInterfaceView.update(frameSize: CGSize(width: width, height: height))
|
||||
camera.update(scannerFrameSize: showFrame ? scannerInterfaceView.frameSize : nil)
|
||||
}
|
||||
@ -289,8 +302,8 @@ public class CameraView: UIView {
|
||||
}
|
||||
|
||||
// Others
|
||||
if changedProps.contains("iOsSleepBeforeStarting") {
|
||||
camera.update(iOsSleepBeforeStartingMs: iOsSleepBeforeStarting?.intValue)
|
||||
if changedProps.contains("iOsDeferredStart") {
|
||||
camera.update(iOsDeferredStartEnabled: iOsDeferredStart)
|
||||
}
|
||||
if changedProps.contains("focusMode") {
|
||||
focusInterfaceView.update(focusMode: focusMode)
|
||||
@ -317,27 +330,34 @@ public class CameraView: UIView {
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@objc public func capture(onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
||||
onError: @escaping (_ error: String) -> Void) {
|
||||
camera.capturePicture(onWillCapture: { [weak self] in
|
||||
// Flash/dim preview to indicate shutter action
|
||||
DispatchQueue.main.async {
|
||||
self?.camera.previewView.alpha = 0
|
||||
UIView.animate(withDuration: 0.35, animations: {
|
||||
self?.camera.previewView.alpha = 1
|
||||
})
|
||||
}
|
||||
}, onSuccess: { [weak self] imageData, thumbnailData, dimensions in
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self?.writeCaptured(imageData: imageData,
|
||||
thumbnailData: thumbnailData,
|
||||
dimensions: dimensions,
|
||||
onSuccess: onSuccess,
|
||||
onError: onError)
|
||||
@objc public func capture(
|
||||
onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
||||
onError: @escaping (_ error: String) -> Void
|
||||
) {
|
||||
camera.capturePicture(
|
||||
onWillCapture: { [weak self] in
|
||||
// Flash/dim preview to indicate shutter action
|
||||
DispatchQueue.main.async {
|
||||
self?.camera.previewView.alpha = 0
|
||||
UIView.animate(
|
||||
withDuration: 0.35,
|
||||
animations: {
|
||||
self?.camera.previewView.alpha = 1
|
||||
})
|
||||
}
|
||||
},
|
||||
onSuccess: { [weak self] imageData, thumbnailData, dimensions in
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self?.writeCaptured(
|
||||
imageData: imageData,
|
||||
thumbnailData: thumbnailData,
|
||||
dimensions: dimensions,
|
||||
onSuccess: onSuccess,
|
||||
onError: onError)
|
||||
|
||||
self?.focusInterfaceView.resetFocus()
|
||||
}
|
||||
}, onError: onError)
|
||||
self?.focusInterfaceView.resetFocus()
|
||||
}
|
||||
}, onError: onError)
|
||||
}
|
||||
|
||||
// MARK: - Private Helper
|
||||
@ -345,7 +365,8 @@ public class CameraView: UIView {
|
||||
private func update(zoomMode: ZoomMode) {
|
||||
if zoomMode == .on {
|
||||
if zoomGestureRecognizer == nil {
|
||||
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchToZoomRecognizer(_:)))
|
||||
let pinchGesture = UIPinchGestureRecognizer(
|
||||
target: self, action: #selector(handlePinchToZoomRecognizer(_:)))
|
||||
addGestureRecognizer(pinchGesture)
|
||||
zoomGestureRecognizer = pinchGesture
|
||||
}
|
||||
@ -359,48 +380,50 @@ public class CameraView: UIView {
|
||||
|
||||
private func handleCameraPermission() {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// On macOS, camera permissions are handled differently
|
||||
if #available(macCatalyst 14.0, *) {
|
||||
// On macOS, camera permissions are handled differently
|
||||
if #available(macCatalyst 14.0, *) {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
hasPermissionBeenGranted = true
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
self?.hasPermissionBeenGranted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
#else
|
||||
// iOS permission handling
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
// The user has previously granted access to the camera.
|
||||
hasPermissionBeenGranted = true
|
||||
case .notDetermined:
|
||||
// The user has not yet been presented with the option to grant video access.
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
self?.hasPermissionBeenGranted = true
|
||||
}
|
||||
self?.hasPermissionBeenGranted = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
// The user has previously denied access.
|
||||
break
|
||||
}
|
||||
}
|
||||
#else
|
||||
// iOS permission handling
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
// The user has previously granted access to the camera.
|
||||
hasPermissionBeenGranted = true
|
||||
case .notDetermined:
|
||||
// The user has not yet been presented with the option to grant video access.
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
if granted {
|
||||
self?.hasPermissionBeenGranted = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
// The user has previously denied access.
|
||||
break
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func writeCaptured(imageData: Data,
|
||||
thumbnailData: Data?,
|
||||
dimensions: CMVideoDimensions,
|
||||
onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
||||
onError: @escaping (_ error: String) -> Void) {
|
||||
private func writeCaptured(
|
||||
imageData: Data,
|
||||
thumbnailData: Data?,
|
||||
dimensions: CMVideoDimensions,
|
||||
onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
|
||||
onError: @escaping (_ error: String) -> Void
|
||||
) {
|
||||
do {
|
||||
let temporaryImageFileURL = try saveToTmpFolder(imageData)
|
||||
|
||||
@ -410,10 +433,11 @@ public class CameraView: UIView {
|
||||
"name": temporaryImageFileURL.lastPathComponent,
|
||||
"thumb": "",
|
||||
"height": dimensions.height,
|
||||
"width": dimensions.width
|
||||
"width": dimensions.width,
|
||||
])
|
||||
} catch {
|
||||
let errorMessage = "Error occurred while writing image data to a temporary file: \(error)"
|
||||
let errorMessage =
|
||||
"Error occurred while writing image data to a temporary file: \(error)"
|
||||
print(errorMessage)
|
||||
onError(errorMessage)
|
||||
}
|
||||
@ -425,10 +449,13 @@ public class CameraView: UIView {
|
||||
let cachesUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
var temporaryFolderURL = cachesUrl
|
||||
if let bundleId = Bundle.main.bundleIdentifier {
|
||||
temporaryFolderURL = temporaryFolderURL.appendingPathComponent(bundleId, isDirectory: true)
|
||||
temporaryFolderURL = temporaryFolderURL.appendingPathComponent(
|
||||
bundleId, isDirectory: true)
|
||||
}
|
||||
temporaryFolderURL = temporaryFolderURL.appendingPathComponent("com.tesla.react-native-camera-kit", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: temporaryFolderURL, withIntermediateDirectories: true)
|
||||
temporaryFolderURL = temporaryFolderURL.appendingPathComponent(
|
||||
"com.tesla.react-native-camera-kit", isDirectory: true)
|
||||
try FileManager.default.createDirectory(
|
||||
at: temporaryFolderURL, withIntermediateDirectories: true)
|
||||
let temporaryFileURL = temporaryFolderURL.appendingPathComponent("\(temporaryFileName).jpg")
|
||||
|
||||
try data.write(to: temporaryFileURL, options: .atomic)
|
||||
@ -436,7 +463,7 @@ public class CameraView: UIView {
|
||||
return temporaryFileURL
|
||||
}
|
||||
|
||||
private func onBarcodeRead(barcode: String, codeFormat:CodeFormat) {
|
||||
private func onBarcodeRead(barcode: String, codeFormat: CodeFormat) {
|
||||
// Throttle barcode detection
|
||||
let now = Date.timeIntervalSinceReferenceDate
|
||||
guard lastBarcodeDetectedTime + Double(scanThrottleDelay) / 1000 < now else {
|
||||
@ -445,7 +472,7 @@ public class CameraView: UIView {
|
||||
|
||||
lastBarcodeDetectedTime = now
|
||||
|
||||
onReadCode?(["codeStringValue": barcode,"codeFormat":codeFormat.rawValue])
|
||||
onReadCode?(["codeStringValue": barcode, "codeFormat": codeFormat.rawValue])
|
||||
}
|
||||
|
||||
private func convertAllowedBarcodeTypes() -> [CodeFormat] {
|
||||
|
||||
@ -58,6 +58,10 @@ RCT_EXPORT_METHOD(capture:(NSDictionary *)options tag:(nonnull NSNumber *)tag re
|
||||
[CKCameraManager requestDeviceCameraAuthorization:resolve reject:reject];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(detectQRCodeInImage:(NSString *)base64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
||||
[CKCameraManager detectQRCodeInImage:base64 resolve:resolve reject:reject];
|
||||
}
|
||||
|
||||
// Thanks to this guard, we won't compile this code when we build for the old architecture.
|
||||
#ifdef RCT_NEW_ARCH_ENABLED
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import CoreMotion
|
||||
import React
|
||||
import UIKit
|
||||
|
||||
/*
|
||||
* Real camera implementation that uses AVFoundation
|
||||
@ -37,7 +37,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?
|
||||
private var resetFocus: (() -> Void)?
|
||||
private var focusFinished: (() -> Void)?
|
||||
private var onBarcodeRead: ((_ barcode: String,_ codeFormat : CodeFormat) -> Void)?
|
||||
private var onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?
|
||||
private var scannerFrameSize: CGRect? = nil
|
||||
private var barcodeFrameSize: CGSize? = nil
|
||||
private var onOrientationChange: RCTDirectEventBlock?
|
||||
@ -45,7 +45,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private var lastOnZoom: Double?
|
||||
private var zoom: Double?
|
||||
private var maxZoom: Double?
|
||||
private var sleepBeforeStartingMs: Int = 100
|
||||
private var deferredStartEnabled: Bool = true
|
||||
|
||||
// orientation
|
||||
private var deviceOrientation = UIDeviceOrientation.unknown
|
||||
@ -59,28 +59,29 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private var inProgressPhotoCaptureDelegates = [Int64: PhotoCaptureDelegate]()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// In addition to using accelerometer to determine REAL orientation
|
||||
// we also listen to UI orientation changes (UIDevice does not report rotation if orientation lock is on, so photos aren't rotated correctly)
|
||||
// When UIDevice reports rotation to the left, UI is rotated right to compensate, but that means we need to re-rotate left
|
||||
// to make camera appear correctly (see self.uiOrientationChanged)
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
orientationObserver = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification,
|
||||
object: UIDevice.current,
|
||||
queue: nil,
|
||||
using: { _ in self.setVideoOrientationToInterfaceOrientation() })
|
||||
}
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// In addition to using accelerometer to determine REAL orientation
|
||||
// we also listen to UI orientation changes (UIDevice does not report rotation if orientation lock is on, so photos aren't rotated correctly)
|
||||
// When UIDevice reports rotation to the left, UI is rotated right to compensate, but that means we need to re-rotate left
|
||||
// to make camera appear correctly (see self.uiOrientationChanged)
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
orientationObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.orientationDidChangeNotification,
|
||||
object: UIDevice.current,
|
||||
queue: nil,
|
||||
using: { _ in self.setVideoOrientationToInterfaceOrientation() })
|
||||
}
|
||||
#else
|
||||
override init() {
|
||||
super.init()
|
||||
// Mac Catalyst doesn't support device orientation notifications
|
||||
}
|
||||
override init() {
|
||||
super.init()
|
||||
// Mac Catalyst doesn't support device orientation notifications
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
@ -95,11 +96,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
motionManager?.stopAccelerometerUpdates()
|
||||
motionManager?.stopAccelerometerUpdates()
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
|
||||
NotificationCenter.default.removeObserver(
|
||||
self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
|
||||
|
||||
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
||||
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -110,36 +112,35 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
// MARK: - Public
|
||||
|
||||
func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
|
||||
// cameraPreview.session = ... causes begin/commitConfiguration on the current thread
|
||||
// so it must be on the same thread as setupCaptureSession to avoid startRunning executing inbetween
|
||||
// Observe this with a breakpoint on `.session = X` and look in Console.app for
|
||||
// "AVCaptureSession beginConfiguration" from the com.apple.cameracapture subsystem
|
||||
DispatchQueue.main.async {
|
||||
self.cameraPreview.session = self.session
|
||||
self.cameraPreview.previewLayer.videoGravity = .resizeAspect
|
||||
}
|
||||
|
||||
self.initializeMotionManager()
|
||||
self.initializeMotionManager()
|
||||
|
||||
// Setup the capture session.
|
||||
// In general, it is not safe to mutate an AVCaptureSession or any of its inputs, outputs, or connections from multiple threads at the same time.
|
||||
// Why not do all of this on the main queue?
|
||||
// Because -[AVCaptureSession startRunning] is a blocking call which can take a long time. We dispatch session setup to the sessionQueue
|
||||
// so that the main queue isn't blocked, which keeps the UI responsive.
|
||||
sessionQueue.async {
|
||||
self.setupResult = self.setupCaptureSession(cameraType: cameraType, supportedBarcodeType: supportedBarcodeType)
|
||||
// Setup the capture session.
|
||||
// In general, it is not safe to mutate an AVCaptureSession or any of its inputs, outputs, or connections from multiple threads at the same time.
|
||||
// Why not do all of this on the main queue?
|
||||
// Because -[AVCaptureSession startRunning] is a blocking call which can take a long time. We dispatch session setup to the sessionQueue
|
||||
// so that the main queue isn't blocked, which keeps the UI responsive.
|
||||
self.sessionQueue.async {
|
||||
self.setupResult = self.setupCaptureSession(
|
||||
cameraType: cameraType, supportedBarcodeType: supportedBarcodeType)
|
||||
|
||||
self.addObservers()
|
||||
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)
|
||||
if self.setupResult == .success {
|
||||
self.session.startRunning()
|
||||
}
|
||||
self.session.startRunning()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.setVideoOrientationToInterfaceOrientation()
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.setVideoOrientationToInterfaceOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +158,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
sessionQueue.async {
|
||||
guard let videoDevice = self.videoDeviceInput?.device else { return }
|
||||
|
||||
let desiredZoomFactor = (self.zoomStartedAt / self.defaultZoomFactor(for: videoDevice)) * pinchScale
|
||||
let desiredZoomFactor =
|
||||
(self.zoomStartedAt / self.defaultZoomFactor(for: videoDevice)) * pinchScale
|
||||
let zoomForDevice = self.getValidZoom(forDevice: videoDevice, zoom: desiredZoomFactor)
|
||||
|
||||
if zoomForDevice != self.normalizedZoom(for: videoDevice) {
|
||||
@ -214,20 +216,26 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
self.onZoomCallback = onZoom
|
||||
}
|
||||
|
||||
func update(iOsSleepBeforeStartingMs: Int?) {
|
||||
let defaultDelayMs = 100
|
||||
let providedDelay = iOsSleepBeforeStartingMs ?? defaultDelayMs
|
||||
sleepBeforeStartingMs = max(0, providedDelay)
|
||||
func update(iOsDeferredStartEnabled: Bool?) {
|
||||
let defaultDeferredStart = true
|
||||
let shouldEnableDeferredStart = iOsDeferredStartEnabled ?? defaultDeferredStart
|
||||
|
||||
sessionQueue.async {
|
||||
guard shouldEnableDeferredStart != self.deferredStartEnabled else { return }
|
||||
self.deferredStartEnabled = shouldEnableDeferredStart
|
||||
self.applyDeferredStartConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
func focus(at touchPoint: CGPoint, focusBehavior: FocusBehavior) {
|
||||
DispatchQueue.main.async {
|
||||
let devicePoint = self.cameraPreview.previewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)
|
||||
let devicePoint = self.cameraPreview.previewLayer.captureDevicePointConverted(
|
||||
fromLayerPoint: touchPoint)
|
||||
|
||||
self.sessionQueue.async {
|
||||
guard let videoDevice = self.videoDeviceInput?.device else { return }
|
||||
|
||||
if case let .customFocus(_, resetFocus, focusFinished) = focusBehavior {
|
||||
if case .customFocus(_, let resetFocus, let focusFinished) = focusBehavior {
|
||||
self.resetFocus = resetFocus
|
||||
self.focusFinished = focusFinished
|
||||
} else {
|
||||
@ -239,17 +247,22 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
try videoDevice.lockForConfiguration()
|
||||
self.reconfigureLockedVideoDevice(videoDevice)
|
||||
|
||||
if videoDevice.isFocusPointOfInterestSupported && videoDevice.isFocusModeSupported(focusBehavior.avFocusMode) {
|
||||
if videoDevice.isFocusPointOfInterestSupported
|
||||
&& videoDevice.isFocusModeSupported(focusBehavior.avFocusMode)
|
||||
{
|
||||
videoDevice.focusPointOfInterest = devicePoint
|
||||
videoDevice.focusMode = focusBehavior.avFocusMode
|
||||
}
|
||||
|
||||
if videoDevice.isExposurePointOfInterestSupported && videoDevice.isExposureModeSupported(focusBehavior.exposureMode) {
|
||||
if videoDevice.isExposurePointOfInterestSupported
|
||||
&& videoDevice.isExposureModeSupported(focusBehavior.exposureMode)
|
||||
{
|
||||
videoDevice.exposurePointOfInterest = devicePoint
|
||||
videoDevice.exposureMode = focusBehavior.exposureMode
|
||||
}
|
||||
|
||||
videoDevice.isSubjectAreaChangeMonitoringEnabled = focusBehavior.isSubjectAreaChangeMonitoringEnabled
|
||||
videoDevice.isSubjectAreaChangeMonitoringEnabled =
|
||||
focusBehavior.isSubjectAreaChangeMonitoringEnabled
|
||||
|
||||
videoDevice.unlockForConfiguration()
|
||||
} catch {
|
||||
@ -266,7 +279,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
func update(torchMode: TorchMode) {
|
||||
sessionQueue.async {
|
||||
self.torchMode = torchMode
|
||||
guard let videoDevice = self.videoDeviceInput?.device, videoDevice.torchMode != torchMode.avTorchMode else { return }
|
||||
guard let videoDevice = self.videoDeviceInput?.device,
|
||||
videoDevice.torchMode != torchMode.avTorchMode
|
||||
else { return }
|
||||
|
||||
do {
|
||||
try videoDevice.lockForConfiguration()
|
||||
@ -281,7 +296,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
func update(flashMode: FlashMode) {
|
||||
self.flashMode = flashMode
|
||||
}
|
||||
|
||||
|
||||
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
|
||||
guard #available(iOS 13.0, *) else { return }
|
||||
guard maxPhotoQualityPrioritization != self.maxPhotoQualityPrioritization else { return }
|
||||
@ -289,7 +304,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
self.session.beginConfiguration()
|
||||
defer { self.session.commitConfiguration() }
|
||||
self.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization
|
||||
self.photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization?.avQualityPrioritization ?? .balanced
|
||||
self.photoOutput.maxPhotoQualityPrioritization =
|
||||
maxPhotoQualityPrioritization?.avQualityPrioritization ?? .balanced
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,9 +317,10 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
|
||||
// Avoid chaining device inputs when camera input is denied by the user, since both front and rear vido input devices will be nil
|
||||
guard self.setupResult == .success,
|
||||
let currentViewDeviceInput = self.videoDeviceInput,
|
||||
let videoDevice = self.getBestDevice(for: cameraType),
|
||||
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
|
||||
let currentViewDeviceInput = self.videoDeviceInput,
|
||||
let videoDevice = self.getBestDevice(for: cameraType),
|
||||
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -340,9 +357,13 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
}
|
||||
}
|
||||
|
||||
func capturePicture(onWillCapture: @escaping () -> Void,
|
||||
onSuccess: @escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions) -> Void,
|
||||
onError: @escaping (_ message: String) -> Void) {
|
||||
func capturePicture(
|
||||
onWillCapture: @escaping () -> Void,
|
||||
onSuccess:
|
||||
@escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions)
|
||||
-> Void,
|
||||
onError: @escaping (_ message: String) -> Void
|
||||
) {
|
||||
/*
|
||||
Retrieve the video preview layer's video orientation on the main queue before
|
||||
entering the session queue. Do this to ensure that UI elements are accessed on
|
||||
@ -350,16 +371,22 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
*/
|
||||
DispatchQueue.main.async {
|
||||
let videoPreviewLayerOrientation =
|
||||
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation
|
||||
self.videoOrientation(from: self.deviceOrientation)
|
||||
?? self.cameraPreview.previewLayer.connection?.videoOrientation
|
||||
|
||||
self.sessionQueue.async {
|
||||
if let photoOutputConnection = self.photoOutput.connection(with: .video), let videoPreviewLayerOrientation {
|
||||
if let photoOutputConnection = self.photoOutput.connection(with: .video),
|
||||
let videoPreviewLayerOrientation
|
||||
{
|
||||
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
||||
}
|
||||
|
||||
let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
|
||||
let settings = AVCapturePhotoSettings(format: [
|
||||
AVVideoCodecKey: AVVideoCodecType.jpeg
|
||||
])
|
||||
if #available(iOS 13.0, *) {
|
||||
settings.photoQualityPrioritization = self.photoOutput.maxPhotoQualityPrioritization
|
||||
settings.photoQualityPrioritization =
|
||||
self.photoOutput.maxPhotoQualityPrioritization
|
||||
}
|
||||
|
||||
if self.videoDeviceInput?.device.isFlashAvailable == true {
|
||||
@ -371,7 +398,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
onWillCapture: onWillCapture,
|
||||
onCaptureSuccess: { uniqueID, imageData, thumbnailData, dimensions in
|
||||
self.inProgressPhotoCaptureDelegates[uniqueID] = nil
|
||||
|
||||
|
||||
onSuccess(imageData, thumbnailData, dimensions)
|
||||
},
|
||||
onCaptureError: { uniqueID, errorMessage in
|
||||
@ -380,22 +407,25 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
}
|
||||
)
|
||||
|
||||
self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = photoCaptureDelegate
|
||||
self.inProgressPhotoCaptureDelegates[
|
||||
photoCaptureDelegate.requestedPhotoSettings.uniqueID] = photoCaptureDelegate
|
||||
self.photoOutput.capturePhoto(with: settings, delegate: photoCaptureDelegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isBarcodeScannerEnabled(_ isEnabled: Bool,
|
||||
supportedBarcodeTypes supportedBarcodeType: [CodeFormat],
|
||||
onBarcodeRead: ((_ barcode: String,_ codeFormat:CodeFormat) -> Void)?) {
|
||||
func isBarcodeScannerEnabled(
|
||||
_ isEnabled: Bool,
|
||||
supportedBarcodeTypes supportedBarcodeType: [CodeFormat],
|
||||
onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?
|
||||
) {
|
||||
sessionQueue.async {
|
||||
self.onBarcodeRead = onBarcodeRead
|
||||
let newTypes: [AVMetadataObject.ObjectType]
|
||||
if isEnabled && onBarcodeRead != nil {
|
||||
let availableTypes = self.metadataOutput.availableMetadataObjectTypes
|
||||
newTypes = supportedBarcodeType.map { $0.toAVMetadataObjectType() }
|
||||
.filter { availableTypes.contains($0) }
|
||||
.filter { availableTypes.contains($0) }
|
||||
} else {
|
||||
newTypes = []
|
||||
}
|
||||
@ -421,7 +451,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
DispatchQueue.main.async {
|
||||
var visibleRect: CGRect?
|
||||
if scannerFrameSize != nil && scannerFrameSize != .zero {
|
||||
visibleRect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scannerFrameSize!)
|
||||
visibleRect = self.cameraPreview.previewLayer.metadataOutputRectConverted(
|
||||
fromLayerRect: scannerFrameSize!)
|
||||
}
|
||||
|
||||
self.sessionQueue.async {
|
||||
@ -429,7 +460,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
return
|
||||
}
|
||||
|
||||
self.metadataOutput.rectOfInterest = visibleRect ?? CGRect(x: 0, y: 0, width: 1, height: 1)
|
||||
self.metadataOutput.rectOfInterest =
|
||||
visibleRect ?? CGRect(x: 0, y: 0, width: 1, height: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -437,21 +469,29 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
|
||||
// MARK: - AVCaptureMetadataOutputObjectsDelegate
|
||||
|
||||
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
||||
func metadataOutput(
|
||||
_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection
|
||||
) {
|
||||
// Try to retrieve the barcode from the metadata extracted
|
||||
guard let machineReadableCodeObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
||||
let codeStringValue = machineReadableCodeObject.stringValue else {
|
||||
guard
|
||||
let machineReadableCodeObject = metadataObjects.first
|
||||
as? AVMetadataMachineReadableCodeObject,
|
||||
let codeStringValue = machineReadableCodeObject.stringValue
|
||||
else {
|
||||
return
|
||||
}
|
||||
// Determine the barcode type and convert it to CodeFormat
|
||||
let barcodeType = CodeFormat.fromAVMetadataObjectType(machineReadableCodeObject.type)
|
||||
let barcodeType = CodeFormat.fromAVMetadataObjectType(machineReadableCodeObject.type)
|
||||
|
||||
onBarcodeRead?(codeStringValue,barcodeType)
|
||||
onBarcodeRead?(codeStringValue, barcodeType)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func videoOrientation(from deviceOrientation: UIDeviceOrientation) -> AVCaptureVideoOrientation? {
|
||||
private func videoOrientation(from deviceOrientation: UIDeviceOrientation)
|
||||
-> AVCaptureVideoOrientation?
|
||||
{
|
||||
// Device orientation counter-rotate interface when in landscapeLeft/Right so it appears level
|
||||
// (note how landscapeLeft sets landscapeRight)
|
||||
switch deviceOrientation {
|
||||
@ -468,7 +508,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
}
|
||||
}
|
||||
|
||||
private func videoOrientation(from interfaceOrientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation {
|
||||
private func videoOrientation(from interfaceOrientation: UIInterfaceOrientation)
|
||||
-> AVCaptureVideoOrientation
|
||||
{
|
||||
switch interfaceOrientation {
|
||||
case .portrait:
|
||||
return .portrait
|
||||
@ -485,26 +527,37 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
|
||||
private func getBestDevice(for cameraType: CameraType) -> AVCaptureDevice? {
|
||||
if #available(iOS 13.0, *) {
|
||||
if let device = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: cameraType.avPosition) {
|
||||
return device // multi-lens/logical device, ultra-wide & wide & tele
|
||||
if let device = AVCaptureDevice.default(
|
||||
.builtInTripleCamera, for: .video, position: cameraType.avPosition)
|
||||
{
|
||||
return device // multi-lens/logical device, ultra-wide & wide & tele
|
||||
}
|
||||
if let device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: cameraType.avPosition) {
|
||||
return device // multi-lens/logical device, ultra-wide & wide
|
||||
if let device = AVCaptureDevice.default(
|
||||
.builtInDualWideCamera, for: .video, position: cameraType.avPosition)
|
||||
{
|
||||
return device // multi-lens/logical device, ultra-wide & wide
|
||||
}
|
||||
}
|
||||
if let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: cameraType.avPosition) {
|
||||
return device // multi-lens/logical device, wide & tele (no ultra-wide)
|
||||
if let device = AVCaptureDevice.default(
|
||||
.builtInDualCamera, for: .video, position: cameraType.avPosition)
|
||||
{
|
||||
return device // multi-lens/logical device, wide & tele (no ultra-wide)
|
||||
}
|
||||
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraType.avPosition) {
|
||||
return device // single-lens/physical device
|
||||
if let device = AVCaptureDevice.default(
|
||||
.builtInWideAngleCamera, for: .video, position: cameraType.avPosition)
|
||||
{
|
||||
return device // single-lens/physical device
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func setupCaptureSession(cameraType: CameraType,
|
||||
supportedBarcodeType: [CodeFormat]) -> SetupResult {
|
||||
private func setupCaptureSession(
|
||||
cameraType: CameraType,
|
||||
supportedBarcodeType: [CodeFormat]
|
||||
) -> SetupResult {
|
||||
guard let videoDevice = self.getBestDevice(for: cameraType),
|
||||
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
|
||||
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice)
|
||||
else {
|
||||
return .sessionConfigurationFailed
|
||||
}
|
||||
|
||||
@ -515,13 +568,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
}
|
||||
|
||||
session.sessionPreset = .photo
|
||||
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if let maxPhotoQualityPrioritization {
|
||||
photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
|
||||
photoOutput.maxPhotoQualityPrioritization =
|
||||
maxPhotoQualityPrioritization.avQualityPrioritization
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if session.canAddInput(videoDeviceInput) {
|
||||
session.addInput(videoDeviceInput)
|
||||
self.videoDeviceInput = videoDeviceInput
|
||||
@ -546,16 +600,33 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||
|
||||
let availableTypes = self.metadataOutput.availableMetadataObjectTypes
|
||||
let filteredTypes = supportedBarcodeType
|
||||
.map { $0.toAVMetadataObjectType() }
|
||||
.filter { availableTypes.contains($0) }
|
||||
let filteredTypes =
|
||||
supportedBarcodeType
|
||||
.map { $0.toAVMetadataObjectType() }
|
||||
.filter { availableTypes.contains($0) }
|
||||
|
||||
metadataOutput.metadataObjectTypes = filteredTypes
|
||||
}
|
||||
|
||||
|
||||
applyDeferredStartConfiguration()
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
private func applyDeferredStartConfiguration() {
|
||||
guard #available(iOS 26.0, *) else { return }
|
||||
|
||||
let enableDeferredStart = deferredStartEnabled
|
||||
|
||||
if photoOutput.isDeferredStartSupported {
|
||||
photoOutput.isDeferredStartEnabled = enableDeferredStart
|
||||
}
|
||||
|
||||
if metadataOutput.isDeferredStartSupported {
|
||||
metadataOutput.isDeferredStartEnabled = enableDeferredStart
|
||||
}
|
||||
}
|
||||
|
||||
private func defaultZoomFactor(for videoDevice: AVCaptureDevice) -> CGFloat {
|
||||
let fallback = 1.0
|
||||
guard #available(iOS 13.0, *) else { return fallback }
|
||||
@ -563,11 +634,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
// Devices that have multiple physical cameras are hidden behind one virtual camera input
|
||||
// The zoom factor defines what physical camera it actually uses
|
||||
// The default lens on the native camera app is the wide angle
|
||||
if let wideAngleIndex = videoDevice.constituentDevices.firstIndex(where: { $0.deviceType == .builtInWideAngleCamera }) {
|
||||
if let wideAngleIndex = videoDevice.constituentDevices.firstIndex(where: {
|
||||
$0.deviceType == .builtInWideAngleCamera
|
||||
}) {
|
||||
// .virtualDeviceSwitchOverVideoZoomFactors has the .constituentDevices zoom factor which borders the NEXT device
|
||||
// so we grab the one PRIOR to the wide angle to get the wide angle's zoom factor
|
||||
guard wideAngleIndex >= 1 else { return fallback }
|
||||
return videoDevice.virtualDeviceSwitchOverVideoZoomFactors[wideAngleIndex - 1].doubleValue
|
||||
return videoDevice.virtualDeviceSwitchOverVideoZoomFactors[wideAngleIndex - 1]
|
||||
.doubleValue
|
||||
}
|
||||
|
||||
return fallback
|
||||
@ -584,7 +658,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
print("CKCameraKit: setZoomFor error: \(error))")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Torch mode will turn off unless set again when the videoDevice is locked and unlocked
|
||||
private func reconfigureLockedVideoDevice(_ videoDevice: AVCaptureDevice) {
|
||||
if videoDevice.isTorchModeSupported(torchMode.avTorchMode) && videoDevice.hasTorch {
|
||||
@ -621,27 +695,34 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
|
||||
private func initializeMotionManager() {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
motionManager = CMMotionManager()
|
||||
motionManager?.accelerometerUpdateInterval = 0.2
|
||||
motionManager?.gyroUpdateInterval = 0.2
|
||||
motionManager?.startAccelerometerUpdates(to: OperationQueue(), withHandler: { (accelerometerData, error) -> Void in
|
||||
guard error == nil else {
|
||||
print("\(error!)")
|
||||
return
|
||||
}
|
||||
guard let accelerometerData else {
|
||||
print("no acceleration data")
|
||||
return
|
||||
}
|
||||
motionManager = CMMotionManager()
|
||||
motionManager?.accelerometerUpdateInterval = 0.2
|
||||
motionManager?.gyroUpdateInterval = 0.2
|
||||
motionManager?.startAccelerometerUpdates(
|
||||
to: OperationQueue(),
|
||||
withHandler: { (accelerometerData, error) -> Void in
|
||||
guard error == nil else {
|
||||
print("\(error!)")
|
||||
return
|
||||
}
|
||||
guard let accelerometerData else {
|
||||
print("no acceleration data")
|
||||
return
|
||||
}
|
||||
|
||||
guard let newOrientation = self.deviceOrientation(from: accelerometerData.acceleration),
|
||||
newOrientation != self.deviceOrientation else {
|
||||
return
|
||||
}
|
||||
guard
|
||||
let newOrientation = self.deviceOrientation(
|
||||
from: accelerometerData.acceleration),
|
||||
newOrientation != self.deviceOrientation
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
self.deviceOrientation = newOrientation
|
||||
self.onOrientationChange?(["orientation": Orientation.init(from: newOrientation)!.rawValue])
|
||||
})
|
||||
self.deviceOrientation = newOrientation
|
||||
self.onOrientationChange?([
|
||||
"orientation": Orientation.init(from: newOrientation)!.rawValue
|
||||
])
|
||||
})
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -667,26 +748,36 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private func addObservers() {
|
||||
guard adjustingFocusObservation == nil else { return }
|
||||
|
||||
adjustingFocusObservation = videoDeviceInput?.device.observe(\.isAdjustingFocus,
|
||||
options: .new,
|
||||
changeHandler: { [weak self] _, change in
|
||||
guard let self, let isFocusing = change.newValue else { return }
|
||||
adjustingFocusObservation = videoDeviceInput?.device.observe(
|
||||
\.isAdjustingFocus,
|
||||
options: .new,
|
||||
changeHandler: { [weak self] _, change in
|
||||
guard let self, let isFocusing = change.newValue else { return }
|
||||
|
||||
self.isAdjustingFocus(isFocusing: isFocusing)
|
||||
})
|
||||
self.isAdjustingFocus(isFocusing: isFocusing)
|
||||
})
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .AVCaptureDeviceSubjectAreaDidChange,
|
||||
object: videoDeviceInput?.device,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in self?.subjectAreaDidChange(notification: notification) })
|
||||
NotificationCenter.default.addObserver(forName: .AVCaptureSessionRuntimeError,
|
||||
object: session,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in self?.sessionRuntimeError(notification: notification) })
|
||||
NotificationCenter.default.addObserver(forName: .AVCaptureSessionWasInterrupted,
|
||||
object: session,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in self?.sessionWasInterrupted(notification: notification) })
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .AVCaptureDeviceSubjectAreaDidChange,
|
||||
object: videoDeviceInput?.device,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in
|
||||
self?.subjectAreaDidChange(notification: notification)
|
||||
})
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .AVCaptureSessionRuntimeError,
|
||||
object: session,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in
|
||||
self?.sessionRuntimeError(notification: notification)
|
||||
})
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .AVCaptureSessionWasInterrupted,
|
||||
object: session,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in
|
||||
self?.sessionWasInterrupted(notification: notification)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -695,7 +786,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
NotificationCenter.default.removeObserver(orientationObserver)
|
||||
self.orientationObserver = nil
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next notification_center_detachment
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
@ -715,21 +806,25 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
|
||||
private func setVideoOrientationToInterfaceOrientation() {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
var interfaceOrientation: UIInterfaceOrientation
|
||||
if #available(iOS 13.0, *) {
|
||||
interfaceOrientation = self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
|
||||
} else {
|
||||
interfaceOrientation = UIApplication.shared.statusBarOrientation
|
||||
}
|
||||
self.cameraPreview.previewLayer.connection?.videoOrientation = self.videoOrientation(from: interfaceOrientation)
|
||||
var interfaceOrientation: UIInterfaceOrientation
|
||||
if #available(iOS 13.0, *) {
|
||||
interfaceOrientation =
|
||||
self.previewView.window?.windowScene?.interfaceOrientation ?? .portrait
|
||||
} else {
|
||||
interfaceOrientation = UIApplication.shared.statusBarOrientation
|
||||
}
|
||||
self.cameraPreview.previewLayer.connection?.videoOrientation = self.videoOrientation(
|
||||
from: interfaceOrientation)
|
||||
#else
|
||||
// Mac Catalyst always uses portrait orientation
|
||||
self.cameraPreview.previewLayer.connection?.videoOrientation = .portrait
|
||||
// Mac Catalyst always uses portrait orientation
|
||||
self.cameraPreview.previewLayer.connection?.videoOrientation = .portrait
|
||||
#endif
|
||||
}
|
||||
|
||||
private func sessionRuntimeError(notification: Notification) {
|
||||
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }
|
||||
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
|
||||
return
|
||||
}
|
||||
|
||||
print("Capture session runtime error: \(error)")
|
||||
|
||||
@ -755,10 +850,13 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
var showResumeButton = false
|
||||
|
||||
if let reasonValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int,
|
||||
let reason = AVCaptureSession.InterruptionReason(rawValue: reasonValue) {
|
||||
let reason = AVCaptureSession.InterruptionReason(rawValue: reasonValue)
|
||||
{
|
||||
print("Capture session was interrupted with reason \(reason)")
|
||||
|
||||
if reason == .audioDeviceInUseByAnotherClient || reason == .videoDeviceInUseByAnotherClient {
|
||||
if reason == .audioDeviceInUseByAnotherClient
|
||||
|| reason == .videoDeviceInUseByAnotherClient
|
||||
{
|
||||
showResumeButton = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import React
|
||||
import UIKit
|
||||
|
||||
/*
|
||||
* Fake camera implementation to be used on simulator
|
||||
@ -37,16 +37,20 @@ class SimulatorCamera: CameraProtocol {
|
||||
|
||||
// Listen to orientation changes
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification,
|
||||
object: UIDevice.current,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in self?.orientationChanged(notification: notification) })
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.orientationDidChangeNotification,
|
||||
object: UIDevice.current,
|
||||
queue: nil,
|
||||
using: { [weak self] notification in
|
||||
self?.orientationChanged(notification: notification)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private func orientationChanged(notification: Notification) {
|
||||
guard let device = notification.object as? UIDevice,
|
||||
let orientation = Orientation(from: device.orientation) else {
|
||||
let orientation = Orientation(from: device.orientation)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -54,7 +58,8 @@ class SimulatorCamera: CameraProtocol {
|
||||
}
|
||||
|
||||
func cameraRemovedFromSuperview() {
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
|
||||
NotificationCenter.default.removeObserver(
|
||||
self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
|
||||
|
||||
}
|
||||
|
||||
@ -66,8 +71,8 @@ class SimulatorCamera: CameraProtocol {
|
||||
self.onZoom = onZoom
|
||||
}
|
||||
|
||||
func update(iOsSleepBeforeStartingMs: Int?) {
|
||||
// No-op on simulator; startup delay only applies to real devices.
|
||||
func update(iOsDeferredStartEnabled: Bool?) {
|
||||
// Not applicable on simulator; deferred start only matters for real capture outputs.
|
||||
}
|
||||
|
||||
func setVideoDevice(zoomFactor: Double) {
|
||||
@ -107,13 +112,15 @@ class SimulatorCamera: CameraProtocol {
|
||||
|
||||
func focus(at: CGPoint, focusBehavior: FocusBehavior) {
|
||||
DispatchQueue.main.async {
|
||||
self.mockPreview.focusAtLabel.text = "Focus at: (\(Int(at.x)), \(Int(at.y))), focusMode: \(focusBehavior.avFocusMode)"
|
||||
self.mockPreview.focusAtLabel.text =
|
||||
"Focus at: (\(Int(at.x)), \(Int(at.y))), focusMode: \(focusBehavior.avFocusMode)"
|
||||
}
|
||||
|
||||
// Fake focus finish after a second
|
||||
fakeFocusFinishedTimer?.invalidate()
|
||||
if case let .customFocus(_, _, focusFinished) = focusBehavior {
|
||||
fakeFocusFinishedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
|
||||
if case .customFocus(_, _, let focusFinished) = focusBehavior {
|
||||
fakeFocusFinishedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) {
|
||||
_ in
|
||||
focusFinished()
|
||||
}
|
||||
}
|
||||
@ -124,7 +131,7 @@ class SimulatorCamera: CameraProtocol {
|
||||
self.mockPreview.torchModeLabel.text = "Torch mode: \(torchMode)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
|
||||
}
|
||||
|
||||
@ -176,16 +183,21 @@ class SimulatorCamera: CameraProtocol {
|
||||
self.mockPreview.randomize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func isBarcodeScannerEnabled(_ isEnabled: Bool,
|
||||
supportedBarcodeTypes: [CodeFormat],
|
||||
onBarcodeRead: ((_ barcode: String,_ codeFormat:CodeFormat) -> Void)?) {}
|
||||
func isBarcodeScannerEnabled(
|
||||
_ isEnabled: Bool,
|
||||
supportedBarcodeTypes: [CodeFormat],
|
||||
onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?
|
||||
) {}
|
||||
func update(scannerFrameSize: CGRect?) {}
|
||||
|
||||
func capturePicture(onWillCapture: @escaping () -> Void,
|
||||
onSuccess: @escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions) -> Void,
|
||||
onError: @escaping (_ message: String) -> Void) {
|
||||
func capturePicture(
|
||||
onWillCapture: @escaping () -> Void,
|
||||
onSuccess:
|
||||
@escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions)
|
||||
-> Void,
|
||||
onError: @escaping (_ message: String) -> Void
|
||||
) {
|
||||
onWillCapture()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
16
package.json
16
package.json
@ -7,12 +7,13 @@
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"version": "16.2.0",
|
||||
"version": "17.0.4",
|
||||
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
||||
"nativePackage": true,
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"clean": "rm -rf dist/",
|
||||
"clean": "rm -rf dist/ build/ .codegen/ android/app/build/",
|
||||
"prepare": "tsc --project tsconfig.json",
|
||||
"test": "jest",
|
||||
"lint": "yarn eslint -c .eslintrc.js",
|
||||
"check-ios": "scripts/check-ios.sh",
|
||||
@ -20,7 +21,7 @@
|
||||
"release:beta": "yarn clean && yarn build && yarn publish --tag beta --verbose",
|
||||
"release:local": "yarn clean && yarn build && tmp=$(mktemp) && yarn pack --filename $tmp.tar.gz && open -R $tmp.tar.gz",
|
||||
"start": "watchman watch-del-all && node node_modules/react-native/local-cli/cli.js start",
|
||||
"codegen": "react-native codegen --verbose --path . --platform ios --source library && react-native codegen --verbose --path . --platform android --source library",
|
||||
"codegen": "rm -rf .codegen/ && react-native codegen --verbose --path . --platform ios --outputPath .codegen --source library && react-native codegen --verbose --path . --platform android --outputPath .codegen --source library",
|
||||
"bootstrap": "cd example/ && bundle install && yarn && cd ios/ && bundle exec pod install",
|
||||
"bootstrap-linux": "cd example/ && yarn"
|
||||
},
|
||||
@ -30,8 +31,11 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"react-native": "src/index",
|
||||
"files": [
|
||||
"android",
|
||||
"build",
|
||||
"android/build.gradle",
|
||||
"android/gradle",
|
||||
"android/gradlew",
|
||||
"android/gradlew.bat",
|
||||
"android/src",
|
||||
"dist",
|
||||
"ios",
|
||||
"src",
|
||||
@ -50,9 +54,11 @@
|
||||
"@react-native/eslint-config": "0.79.0",
|
||||
"@react-native/metro-config": "0.79.0",
|
||||
"@react-native/typescript-config": "0.79.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-test-renderer": "^19.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^30.3.0",
|
||||
"prettier": "2.8.8",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.0",
|
||||
|
||||
@ -14,7 +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.iOsDeferredStart = props.iOsDeferredStart ?? true;
|
||||
|
||||
props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats;
|
||||
|
||||
|
||||
@ -113,8 +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;
|
||||
/** **iOS Only (iOS 26+)**. Enables `AVCaptureOutput.deferredStartEnabled` when supported to prioritize getting the preview visible faster. Default: `true`. When enabled, the first capture may be delayed by a few hundred milliseconds. Not supported on Android. */
|
||||
iOsDeferredStart?: boolean;
|
||||
/** **Android only**. Play a shutter capture sound when capturing a photo */
|
||||
shutterPhotoSound?: boolean;
|
||||
onCaptureButtonPressIn?: ({ nativeEvent: {} }) => void;
|
||||
|
||||
51
src/__tests__/detectQRCodeInImage.test.tsx
Normal file
51
src/__tests__/detectQRCodeInImage.test.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
jest.mock('react-native', () => ({
|
||||
NativeModules: { CameraKit: {} },
|
||||
Platform: { OS: 'ios' },
|
||||
TurboModuleRegistry: { getEnforcing: jest.fn(() => ({})) },
|
||||
}));
|
||||
|
||||
jest.mock('../specs/NativeCameraKitModule', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
detectQRCodeInImage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import NativeCameraKitModule from '../specs/NativeCameraKitModule';
|
||||
import { detectQRCodeInImage } from '../index';
|
||||
|
||||
const mockedDetect = NativeCameraKitModule.detectQRCodeInImage as jest.Mock;
|
||||
|
||||
describe('detectQRCodeInImage', () => {
|
||||
beforeEach(() => {
|
||||
mockedDetect.mockReset();
|
||||
});
|
||||
|
||||
it('forwards the base64 argument to the native module', async () => {
|
||||
mockedDetect.mockResolvedValueOnce('decoded-value');
|
||||
|
||||
await detectQRCodeInImage('base64-data');
|
||||
|
||||
expect(mockedDetect).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDetect).toHaveBeenCalledWith('base64-data');
|
||||
});
|
||||
|
||||
it('resolves with the decoded string from the native module', async () => {
|
||||
mockedDetect.mockResolvedValueOnce('https://example.com');
|
||||
|
||||
await expect(detectQRCodeInImage('xyz')).resolves.toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('resolves with null when no QR code was found', async () => {
|
||||
mockedDetect.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(detectQRCodeInImage('xyz')).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('propagates native rejections (e.g. invalid image)', async () => {
|
||||
const err = new Error('Could not decode base64 image data');
|
||||
mockedDetect.mockRejectedValueOnce(err);
|
||||
|
||||
await expect(detectQRCodeInImage('xyz')).rejects.toBe(err);
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import Camera from './Camera';
|
||||
import NativeCameraKitModule from './specs/NativeCameraKitModule';
|
||||
import {
|
||||
CameraType,
|
||||
type CameraApi,
|
||||
@ -15,6 +16,10 @@ import {
|
||||
|
||||
const { CameraKit } = NativeModules;
|
||||
|
||||
export const detectQRCodeInImage = (base64: string): Promise<string | null> => {
|
||||
return NativeCameraKitModule.detectQRCodeInImage(base64);
|
||||
};
|
||||
|
||||
// Start with portrait/pointing up, increment while moving counter-clockwise
|
||||
export const Orientation = {
|
||||
PORTRAIT: 0, // ⬆️
|
||||
|
||||
@ -10,6 +10,9 @@ import type {
|
||||
Int32,
|
||||
WithDefault
|
||||
} from 'react-native/Libraries/Types/CodegenTypes';
|
||||
|
||||
// While this import is deprecated, official docs still shows this as valid
|
||||
// and the alternative doesn't work (import doesn't exist for this RN version)
|
||||
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
||||
|
||||
type OnReadCodeData = {
|
||||
@ -46,7 +49,7 @@ export interface NativeProps extends ViewProps {
|
||||
resetFocusWhenMotionDetected?: boolean;
|
||||
resizeMode?: string;
|
||||
scanThrottleDelay?: WithDefault<Int32, -1>;
|
||||
iOsSleepBeforeStarting?: WithDefault<Int32, -1>;
|
||||
iOsDeferredStart?: boolean;
|
||||
barcodeFrameSize?: { width?: WithDefault<Float, 300>; height?: WithDefault<Float, 150> };
|
||||
shutterPhotoSound?: boolean;
|
||||
onOrientationChange?: DirectEventHandler<OnOrientationChangeData>;
|
||||
|
||||
@ -18,6 +18,7 @@ export interface Spec extends TurboModule {
|
||||
capture(options?: UnsafeObject, tag?: Double): Promise<CaptureData>;
|
||||
requestDeviceCameraAuthorization: () => Promise<boolean>;
|
||||
checkDeviceCameraAuthorizationStatus: () => Promise<boolean>;
|
||||
detectQRCodeInImage(base64: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('RNCameraKitModule');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user