Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be94f3b374 | ||
|
|
b92e8fdf61 | ||
|
|
7f020cd892 | ||
|
|
8dd943d0ae | ||
|
|
08d6d3e4ce |
1
.gitignore
vendored
1
.gitignore
vendored
@ -85,7 +85,6 @@ example/.yarn/*
|
||||
ios/build/
|
||||
ios/DerivedData/
|
||||
dist/
|
||||
.codegen/
|
||||
### Xcode ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
node_modules/
|
||||
.codegen/
|
||||
old-example/
|
||||
example/
|
||||
example-js-code/
|
||||
|
||||
42
CLAUDE.md
42
CLAUDE.md
@ -173,45 +173,3 @@ The library is published to npm as `react-native-camera-kit` with the `files` ar
|
||||
- **Node**: Requires Node.js >= 18
|
||||
- **React Native**: Uses version 0.79.0, supports both legacy and new architecture (Fabric)
|
||||
- **Import resolution**: ESLint is configured to recognize `.ios.tsx`, `.android.tsx`, and `.js` platform variants
|
||||
|
||||
---
|
||||
|
||||
## Camera Kit Sync State
|
||||
|
||||
**Last synchronized upstream commit**: 8e5149a6e6d3902ae87dad50da0d06ec2c61d2b8
|
||||
**Upstream version**: 17.0.1
|
||||
**Fork version**: 17.0.3
|
||||
**Last sync date**: 2026-01-17
|
||||
**Sync status**: success
|
||||
|
||||
### Changes Synced (upstream v16.2.0 -> v17.0.1)
|
||||
|
||||
**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
|
||||
|
||||
**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` - 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)`
|
||||
|
||||
### Validation
|
||||
|
||||
- `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
|
||||
|
||||
102
README.md
102
README.md
@ -201,75 +201,39 @@ 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` |
|
||||
| `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) |
|
||||
| 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 |
|
||||
| **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)}` |
|
||||
|
||||
### 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.3'
|
||||
implementation 'com.github.limpbrains:qr:v0.0.1'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
@ -28,7 +28,6 @@ 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
|
||||
@ -109,7 +108,6 @@ 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!!
|
||||
@ -187,17 +185,7 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(getActivity())
|
||||
cameraProviderFuture.addListener({
|
||||
// Used to bind the lifecycle of cameras to the lifecycle owner
|
||||
try {
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
} catch (exc: Exception) {
|
||||
val rootCause = exc.cause?.cause?.message ?: exc.cause?.message ?: exc.message ?: "Camera initialization failed"
|
||||
Log.e(TAG, "Camera initialization failed: $rootCause", exc)
|
||||
val surfaceId = UIManagerHelper.getSurfaceId(currentContext)
|
||||
UIManagerHelper
|
||||
.getEventDispatcherForReactTag(currentContext, id)
|
||||
?.dispatchEvent(ErrorEvent(surfaceId, id, rootCause))
|
||||
return@addListener
|
||||
}
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
// Rotate the image according to device orientation, even when UI orientation is locked
|
||||
orientationListener = object : OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) {
|
||||
@ -340,8 +328,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)
|
||||
@ -466,8 +454,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)
|
||||
@ -686,26 +674,6 @@ 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()
|
||||
@ -726,10 +694,6 @@ 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"
|
||||
|
||||
@ -8,25 +8,10 @@ enum class CodeFormat(val code: String) {
|
||||
EAN_13("ean-13"),
|
||||
EAN_8("ean-8"),
|
||||
ITF("itf"),
|
||||
UPC_A("upc-a"),
|
||||
UPC_E("upc-e"),
|
||||
QR("qr"),
|
||||
PDF_417("pdf-417"),
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -149,14 +149,6 @@ class CKCameraManager(context: ReactApplicationContext) : SimpleViewManager<CKCa
|
||||
view?.setScanThrottleDelay(value)
|
||||
}
|
||||
|
||||
@ReactProp(name = "allowedBarcodeTypes")
|
||||
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 setRatioOverlay(view: CKCamera?, value: String?) = Unit
|
||||
|
||||
@ -168,7 +160,5 @@ 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,7 +2,6 @@ package com.rncamerakit
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.rncamerakit.NativeCameraKitModuleSpec
|
||||
|
||||
@ -39,8 +38,6 @@ 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 {
|
||||
@ -65,19 +62,6 @@ 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.
|
||||
*
|
||||
|
||||
@ -143,16 +143,7 @@ class CKCameraManager(var context: ReactApplicationContext) : SimpleViewManager<
|
||||
view?.setScanThrottleDelay(value)
|
||||
}
|
||||
|
||||
@ReactProp(name = "allowedBarcodeTypes")
|
||||
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.
|
||||
}
|
||||
|
||||
// Methods only available on iOS
|
||||
fun setIOsSleepBeforeStarting(view: CKCamera?, value: Int) = Unit
|
||||
|
||||
fun setRatioOverlay(view: CKCamera?, value: String?) = Unit
|
||||
|
||||
fun setRatioOverlayColor(view: CKCamera?, value: Int?) = Unit
|
||||
|
||||
@ -2,7 +2,6 @@ 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.
|
||||
@ -37,8 +36,6 @@ 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 {
|
||||
@ -63,19 +60,6 @@ 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,7 +10,6 @@
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
<string>3B52.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
|
||||
@ -1719,34 +1719,6 @@ 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
|
||||
@ -2251,7 +2223,7 @@ PODS:
|
||||
- React-perflogger (= 0.81.0)
|
||||
- React-utils (= 0.81.0)
|
||||
- SocketRocket
|
||||
- ReactNativeCameraKit (17.0.4):
|
||||
- ReactNativeCameraKit (16.1.1):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@ -2324,7 +2296,6 @@ 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`)
|
||||
@ -2445,8 +2416,6 @@ 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:
|
||||
@ -2553,7 +2522,6 @@ 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
|
||||
@ -2584,7 +2552,7 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: c91900fa724baee992f01c05eeb4c9e01a807f78
|
||||
ReactCodegen: a55799cae416c387aeaae3aabc1bc0289ac19cee
|
||||
ReactCommon: 116d6ee71679243698620d8cd9a9042541e44aa6
|
||||
ReactNativeCameraKit: 21c717ad3fc6f040b4293a07c4300aae3f1007d0
|
||||
ReactNativeCameraKit: b01e637c97fb6eefe43eff31917d1410fc77e1f8
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: 00013dd9cde63a2d98e8002fcc4f5ddb66c10782
|
||||
|
||||
|
||||
@ -15,8 +15,7 @@
|
||||
"dependencies": {
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.0",
|
||||
"react-native-camera-kit": "link:../",
|
||||
"react-native-image-picker": "^7.1.2"
|
||||
"react-native-camera-kit": "link:../"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
@ -1,101 +1,29 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Button, Alert, TextInput } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, Text, View, TouchableOpacity, ScrollView } from 'react-native';
|
||||
|
||||
import BarcodeScreenExample from './BarcodeScreenExample';
|
||||
import CameraExample from './CameraExample';
|
||||
import DetectQRExample from './DetectQRExample';
|
||||
|
||||
const App = () => {
|
||||
const [example, setExample] = useState<any>(undefined);
|
||||
const [testNo, setTestNo] = useState(0);
|
||||
const [interval, setIntervalId] = useState<number | null>(null);
|
||||
const [speed, setSpeed] = useState('1000');
|
||||
const onBack = () => setExample(undefined);
|
||||
const testStart = useRef(0);
|
||||
const [example, setExample] = useState<JSX.Element>();
|
||||
|
||||
if (example) {
|
||||
return example;
|
||||
}
|
||||
|
||||
const onBack = () => setExample(undefined);
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.scroll} scrollEnabled={false}>
|
||||
<ScrollView style={styles.scroll}>
|
||||
<View style={styles.container}>
|
||||
<Text style={{ fontSize: 60 }}>🎈</Text>
|
||||
<Text style={styles.headerText}>React Native Camera Kit</Text>
|
||||
<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' }}>
|
||||
{!testNo ? (
|
||||
<>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.inputLabel}>Speed (ms):</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={speed}
|
||||
onChangeText={setSpeed}
|
||||
keyboardType="number-pad"
|
||||
placeholder="1000"
|
||||
placeholderTextColor="#999"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title="Start"
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'2 min or more',
|
||||
'The mount stress test should run for at least 2 minutes on an iPhone 17 Pro before you can declare it a success. You need to press the stop button yourself.',
|
||||
[
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
testStart.current = Date.now();
|
||||
setTestNo(0);
|
||||
setIntervalId(
|
||||
setInterval(() => {
|
||||
setTestNo((prev) => {
|
||||
const newR = prev + 1;
|
||||
if (newR % 2 === 0) {
|
||||
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);
|
||||
}
|
||||
return newR;
|
||||
});
|
||||
}, parseInt(speed, 10) || 1000),
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
title="STOP STRESS TEST"
|
||||
onPress={() => {
|
||||
setTestNo(0);
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
setIntervalId(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
@ -121,11 +49,6 @@ const styles = StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
marginBlockEnd: 24,
|
||||
},
|
||||
stressHeader: {
|
||||
color: 'white',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
button: {
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
@ -139,24 +62,4 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
fontSize: 20,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginVertical: 12,
|
||||
minWidth: 170,
|
||||
},
|
||||
inputLabel: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
marginRight: 12,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#333',
|
||||
color: 'white',
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@ -193,13 +193,10 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => {
|
||||
frameColor="white"
|
||||
scanBarcode
|
||||
showFrame
|
||||
allowedBarcodeTypes={['qr', 'ean-13']}
|
||||
barcodeFrameSize={{ width: 300, height: 150 }}
|
||||
onReadCode={(event) => {
|
||||
setScanCount((prev) => prev + 1);
|
||||
setBarcode(event.nativeEvent.codeStringValue);
|
||||
console.log('barcode', event.nativeEvent.codeStringValue);
|
||||
console.log('codeFormat', event.nativeEvent.codeFormat);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@ -278,7 +275,8 @@ const styles = StyleSheet.create({
|
||||
backBtnContainer: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
captureButtonContainer: {},
|
||||
captureButtonContainer: {
|
||||
},
|
||||
textNumberContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type React from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { StyleSheet, Text, View, TouchableOpacity, Image, Animated, ScrollView } from 'react-native';
|
||||
import Camera from '../../src/Camera';
|
||||
import { type CameraApi, CameraType, type CaptureData } from '../../src/types';
|
||||
@ -33,7 +33,7 @@ function median(values: number[]): number {
|
||||
return sortedValues.length % 2 ? sortedValues[half] : (sortedValues[half - 1] + sortedValues[half]) / 2;
|
||||
}
|
||||
|
||||
const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolean }) => {
|
||||
const CameraExample = ({ onBack }: { onBack: () => void }) => {
|
||||
const cameraRef = useRef<CameraApi>(null);
|
||||
const [currentFlashArrayPosition, setCurrentFlashArrayPosition] = useState(0);
|
||||
const [captureImages, setCaptureImages] = useState<CaptureData[]>([]);
|
||||
@ -46,15 +46,6 @@ const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolea
|
||||
const [orientationAnim] = useState(new Animated.Value(3));
|
||||
const [resize, setResize] = useState<'contain' | 'cover'>('contain');
|
||||
|
||||
// zoom to random positions every 10ms:
|
||||
useEffect(() => {
|
||||
if (stress !== true) return;
|
||||
const interval = setInterval(() => {
|
||||
setZoom(Math.random() * 10);
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, [stress]);
|
||||
|
||||
// iOS will error out if capturing too fast,
|
||||
// so block capturing until the current capture is done
|
||||
// This also minimizes issues of delayed capturing
|
||||
@ -116,7 +107,7 @@ const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolea
|
||||
if (!image) return;
|
||||
|
||||
setCaptured(true);
|
||||
setCaptureImages((prev) => [...prev, image]);
|
||||
setCaptureImages(prev => [...prev, image]);
|
||||
console.log('image', image);
|
||||
times.push(Date.now() - start);
|
||||
}
|
||||
@ -224,7 +215,10 @@ const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolea
|
||||
|
||||
<View style={styles.cameraContainer}>
|
||||
{showImageUri ? (
|
||||
<ScrollView maximumZoomScale={10} contentContainerStyle={{ flexGrow: 1 }}>
|
||||
<ScrollView
|
||||
maximumZoomScale={10}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
>
|
||||
<Image source={{ uri: showImageUri }} style={styles.cameraPreview} />
|
||||
</ScrollView>
|
||||
) : (
|
||||
@ -243,7 +237,6 @@ const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolea
|
||||
}}
|
||||
torchMode={torchMode ? 'on' : 'off'}
|
||||
shutterPhotoSound
|
||||
iOsSleepBeforeStarting={100}
|
||||
maxPhotoQualityPrioritization="speed"
|
||||
onCaptureButtonPressIn={() => {
|
||||
console.log('capture button pressed in');
|
||||
@ -306,7 +299,8 @@ const CameraExample = ({ onBack, stress }: { onBack: () => void; stress?: boolea
|
||||
} else {
|
||||
setShowImageUri(captureImages[captureImages.length - 1].uri);
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Image source={{ uri: captureImages[captureImages.length - 1].uri }} style={styles.thumbnail} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
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,11 +4817,6 @@ 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,24 +6,22 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#if __has_include(<React/RCTBridge.h>)
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTViewManager.h>
|
||||
#import <React/RCTConvert.h>
|
||||
#else
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTViewManager.h"
|
||||
#import "RCTConvert.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)
|
||||
RCT_EXPORT_VIEW_PROPERTY(resizeMode, CKResizeMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(iOsSleepBeforeStarting, NSNumber)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(scanBarcode, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onReadCode, RCTDirectEventBlock)
|
||||
@ -32,7 +30,6 @@ RCT_EXPORT_VIEW_PROPERTY(scanThrottleDelay, NSInteger)
|
||||
RCT_EXPORT_VIEW_PROPERTY(laserColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(frameColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(barcodeFrameSize, NSDictionary)
|
||||
RCT_EXPORT_VIEW_PROPERTY(allowedBarcodeTypes, NSArray)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onCaptureButtonPressIn, RCTDirectEventBlock)
|
||||
|
||||
@ -16,47 +16,44 @@
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,270 +61,217 @@ 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];
|
||||
- (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;
|
||||
|
||||
// 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;
|
||||
[_view setOnReadCode:^(NSDictionary* event) {
|
||||
__typeof__(self) strongSelf = weakSelf;
|
||||
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
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;
|
||||
|
||||
[_view setOnReadCode:^(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::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) {
|
||||
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) {
|
||||
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)->onCaptureButtonPressIn({});
|
||||
}
|
||||
}];
|
||||
[_view setOnCaptureButtonPressOut:^(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::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;
|
||||
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];
|
||||
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"];
|
||||
|
||||
// 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.cameraType != newProps.cameraType) {
|
||||
_view.cameraType = newProps.cameraType == "back" ? CKCameraTypeBack : CKCameraTypeFront;
|
||||
[changedProps addObject:@"cameraType"];
|
||||
}
|
||||
[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);
|
||||
if (oldViewProps.resizeMode != newProps.resizeMode) {
|
||||
_view.resizeMode = newProps.resizeMode == "contain" ? CKResizeModeContain : CKResizeModeCover;
|
||||
[changedProps addObject:@"resizeMode"];
|
||||
}
|
||||
id allowedBarcodeTypes =
|
||||
CKConvertFollyDynamicToId(allowedBarcodeTypesDynamic);
|
||||
if (allowedBarcodeTypes != nil &&
|
||||
[allowedBarcodeTypes isKindOfClass:NSArray.class]) {
|
||||
_view.allowedBarcodeTypes = allowedBarcodeTypes;
|
||||
[changedProps addObject:@"allowedBarcodeTypes"];
|
||||
id flashMode = CKConvertFollyDynamicToId(newProps.flashMode);
|
||||
if (oldViewProps.flashMode != newProps.flashMode) {
|
||||
_view.flashMode = [flashMode isEqualToString:@"auto"] ? CKFlashModeAuto : [flashMode isEqualToString:@"on"] ? CKFlashModeOn : CKFlashModeOff;
|
||||
[changedProps addObject:@"flashMode"];
|
||||
}
|
||||
}
|
||||
|
||||
[super updateProps:props oldProps:oldProps];
|
||||
[_view didSetProps:changedProps];
|
||||
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"];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
|
||||
|
||||
[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,10 +4,8 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
import Foundation
|
||||
import React
|
||||
import Vision
|
||||
|
||||
/*
|
||||
* Class managing the communication between React Native and the native implementation
|
||||
@ -62,48 +60,4 @@ import Vision
|
||||
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,6 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
|
||||
func update(cameraType: CameraType)
|
||||
func update(onOrientationChange: RCTDirectEventBlock?)
|
||||
func update(onZoom: RCTDirectEventBlock?)
|
||||
func update(iOsDeferredStartEnabled: Bool?)
|
||||
func update(zoom: Double?)
|
||||
func update(maxZoom: Double?)
|
||||
func update(resizeMode: ResizeMode)
|
||||
@ -27,17 +26,13 @@ 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,10 @@ public class CameraView: UIView {
|
||||
// scanner
|
||||
private var lastBarcodeDetectedTime: TimeInterval = 0
|
||||
private var scannerInterfaceView: ScannerInterfaceView
|
||||
|
||||
private var supportedBarcodeType: [CodeFormat] = {
|
||||
return CodeFormat.allCases
|
||||
}()
|
||||
|
||||
// camera
|
||||
private var ratioOverlayView: RatioOverlayView?
|
||||
|
||||
@ -47,7 +50,6 @@ public class CameraView: UIView {
|
||||
@objc public var frameColor: UIColor?
|
||||
@objc public var laserColor: UIColor?
|
||||
@objc public var barcodeFrameSize: NSDictionary?
|
||||
@objc public var allowedBarcodeTypes: NSArray?
|
||||
|
||||
// other
|
||||
@objc public var onOrientationChange: RCTDirectEventBlock?
|
||||
@ -58,11 +60,10 @@ public class CameraView: UIView {
|
||||
@objc public var zoomMode: ZoomMode = .on
|
||||
@objc public var zoom: NSNumber?
|
||||
@objc public var maxZoom: NSNumber?
|
||||
@objc public var iOsDeferredStart: Bool = true
|
||||
|
||||
@objc public var onCaptureButtonPressIn: RCTDirectEventBlock?
|
||||
@objc public var onCaptureButtonPressOut: RCTDirectEventBlock?
|
||||
|
||||
|
||||
var eventInteraction: Any? = nil
|
||||
|
||||
// MARK: - Setup
|
||||
@ -81,22 +82,12 @@ public class CameraView: UIView {
|
||||
}
|
||||
private func setupCamera() {
|
||||
if hasPropBeenSetup && hasPermissionBeenGranted && !hasCameraBeenSetup {
|
||||
let convertedAllowedTypes = convertAllowedBarcodeTypes()
|
||||
|
||||
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 ? supportedBarcodeType : [])
|
||||
#else
|
||||
camera.setup(
|
||||
cameraType: cameraType,
|
||||
supportedBarcodeType: scanBarcode && onReadCode != nil
|
||||
? convertedAllowedTypes : [])
|
||||
camera.setup(cameraType: cameraType, supportedBarcodeType: scanBarcode && onReadCode != nil ? supportedBarcodeType : [])
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -109,7 +100,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)
|
||||
])
|
||||
}
|
||||
|
||||
@ -121,11 +112,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()
|
||||
@ -152,26 +143,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
|
||||
}
|
||||
|
||||
@ -187,7 +178,7 @@ public class CameraView: UIView {
|
||||
super.reactSetFrame(frame)
|
||||
self.updateSubviewsBounds(frame)
|
||||
}
|
||||
|
||||
|
||||
@objc public func updateSubviewsBounds(_ frame: CGRect) {
|
||||
camera.previewView.frame = bounds
|
||||
|
||||
@ -213,10 +204,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") {
|
||||
@ -236,7 +227,7 @@ public class CameraView: UIView {
|
||||
if changedProps.contains("onZoom") {
|
||||
camera.update(onZoom: onZoom)
|
||||
}
|
||||
|
||||
|
||||
if changedProps.contains("resizeMode") {
|
||||
camera.update(resizeMode: resizeMode)
|
||||
}
|
||||
@ -247,8 +238,7 @@ 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 {
|
||||
@ -262,32 +252,24 @@ public class CameraView: UIView {
|
||||
}
|
||||
|
||||
// Scanner
|
||||
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)
|
||||
})
|
||||
if changedProps.contains("scanBarcode") || changedProps.contains("onReadCode") {
|
||||
camera.isBarcodeScannerEnabled(scanBarcode,
|
||||
supportedBarcodeTypes: supportedBarcodeType,
|
||||
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)
|
||||
}
|
||||
@ -302,9 +284,6 @@ public class CameraView: UIView {
|
||||
}
|
||||
|
||||
// Others
|
||||
if changedProps.contains("iOsDeferredStart") {
|
||||
camera.update(iOsDeferredStartEnabled: iOsDeferredStart)
|
||||
}
|
||||
if changedProps.contains("focusMode") {
|
||||
focusInterfaceView.update(focusMode: focusMode)
|
||||
}
|
||||
@ -330,34 +309,27 @@ 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
|
||||
@ -365,8 +337,7 @@ 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
|
||||
}
|
||||
@ -380,50 +351,48 @@ public class CameraView: UIView {
|
||||
|
||||
private func handleCameraPermission() {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// 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
|
||||
// On macOS, camera permissions are handled differently
|
||||
if #available(macCatalyst 14.0, *) {
|
||||
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
|
||||
DispatchQueue.main.async {
|
||||
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)
|
||||
|
||||
@ -433,11 +402,10 @@ 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)
|
||||
}
|
||||
@ -449,13 +417,10 @@ 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)
|
||||
@ -463,7 +428,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 {
|
||||
@ -472,15 +437,7 @@ public class CameraView: UIView {
|
||||
|
||||
lastBarcodeDetectedTime = now
|
||||
|
||||
onReadCode?(["codeStringValue": barcode, "codeFormat": codeFormat.rawValue])
|
||||
}
|
||||
|
||||
private func convertAllowedBarcodeTypes() -> [CodeFormat] {
|
||||
guard let allowedTypes = allowedBarcodeTypes as? [String], !allowedTypes.isEmpty else {
|
||||
return CodeFormat.allCases
|
||||
}
|
||||
|
||||
return allowedTypes.compactMap { CodeFormat(rawValue: $0) }
|
||||
onReadCode?(["codeStringValue": barcode,"codeFormat":codeFormat.rawValue])
|
||||
}
|
||||
|
||||
// MARK: - Gesture selectors
|
||||
|
||||
@ -12,7 +12,6 @@ enum CodeFormat: String, CaseIterable {
|
||||
case code128 = "code-128"
|
||||
case code39 = "code-39"
|
||||
case code93 = "code-93"
|
||||
case codabar = "codabar"
|
||||
case ean13 = "ean-13"
|
||||
case ean8 = "ean-8"
|
||||
case itf14 = "itf-14"
|
||||
@ -21,21 +20,13 @@ enum CodeFormat: String, CaseIterable {
|
||||
case pdf417 = "pdf-417"
|
||||
case aztec = "aztec"
|
||||
case dataMatrix = "data-matrix"
|
||||
case code39Mod43 = "code-39-mod-43"
|
||||
case interleaved2of5 = "interleaved-2of5"
|
||||
case unknown = "unknown"
|
||||
|
||||
// Convert from AVMetadataObject.ObjectType to CodeFormat
|
||||
static func fromAVMetadataObjectType(_ type: AVMetadataObject.ObjectType) -> CodeFormat {
|
||||
if #available(iOS 15.4, *) {
|
||||
if (type == .codabar) {
|
||||
return .codabar
|
||||
}
|
||||
}
|
||||
switch type {
|
||||
case .code128: return .code128
|
||||
case .code39: return .code39
|
||||
case .code39Mod43: return .code39Mod43
|
||||
case .code93: return .code93
|
||||
case .ean13: return .ean13
|
||||
case .ean8: return .ean8
|
||||
@ -45,22 +36,15 @@ enum CodeFormat: String, CaseIterable {
|
||||
case .pdf417: return .pdf417
|
||||
case .aztec: return .aztec
|
||||
case .dataMatrix: return .dataMatrix
|
||||
case .interleaved2of5: return .interleaved2of5
|
||||
default: return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from CodeFormat to AVMetadataObject.ObjectType
|
||||
func toAVMetadataObjectType() -> AVMetadataObject.ObjectType {
|
||||
if #available(iOS 15.4, *) {
|
||||
if (self == .codabar) {
|
||||
return .codabar
|
||||
}
|
||||
}
|
||||
switch self {
|
||||
case .code128: return .code128
|
||||
case .code39: return .code39
|
||||
case .code39Mod43: return .code39Mod43
|
||||
case .code93: return .code93
|
||||
case .ean13: return .ean13
|
||||
case .ean8: return .ean8
|
||||
@ -70,9 +54,7 @@ enum CodeFormat: String, CaseIterable {
|
||||
case .pdf417: return .pdf417
|
||||
case .aztec: return .aztec
|
||||
case .dataMatrix: return .dataMatrix
|
||||
case .interleaved2of5: return .interleaved2of5
|
||||
case .unknown: fallthrough
|
||||
default: return .init(rawValue: "unknown")
|
||||
case .unknown: return .init(rawValue: "unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,10 +58,6 @@ 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
|
||||
@ -21,7 +21,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private let session = AVCaptureSession()
|
||||
// Communicate with the session and other session objects on this queue.
|
||||
private let sessionQueue = DispatchQueue(label: "com.tesla.react-native-camera-kit")
|
||||
|
||||
|
||||
// utilities
|
||||
private var setupResult: SetupResult = .notStarted
|
||||
private var isSessionRunning: Bool = false
|
||||
@ -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,6 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private var lastOnZoom: Double?
|
||||
private var zoom: Double?
|
||||
private var maxZoom: Double?
|
||||
private var deferredStartEnabled: Bool = true
|
||||
|
||||
// orientation
|
||||
private var deviceOrientation = UIDeviceOrientation.unknown
|
||||
@ -59,29 +58,28 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
private var inProgressPhotoCaptureDelegates = [Int64: PhotoCaptureDelegate]()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
override init() {
|
||||
super.init()
|
||||
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() })
|
||||
}
|
||||
// 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")
|
||||
@ -96,12 +94,11 @@ 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
|
||||
}
|
||||
|
||||
@ -112,35 +109,30 @@ 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.
|
||||
self.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.
|
||||
sessionQueue.async {
|
||||
self.setupResult = self.setupCaptureSession(cameraType: cameraType, supportedBarcodeType: supportedBarcodeType)
|
||||
|
||||
self.addObservers()
|
||||
self.addObservers()
|
||||
|
||||
if self.setupResult == .success {
|
||||
self.session.startRunning()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.setVideoOrientationToInterfaceOrientation()
|
||||
}
|
||||
if self.setupResult == .success {
|
||||
self.session.startRunning()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.setVideoOrientationToInterfaceOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,8 +150,7 @@ 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) {
|
||||
@ -216,26 +207,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
self.onZoomCallback = onZoom
|
||||
}
|
||||
|
||||
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 .customFocus(_, let resetFocus, let focusFinished) = focusBehavior {
|
||||
if case let .customFocus(_, resetFocus, focusFinished) = focusBehavior {
|
||||
self.resetFocus = resetFocus
|
||||
self.focusFinished = focusFinished
|
||||
} else {
|
||||
@ -247,22 +226,17 @@ 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 {
|
||||
@ -279,9 +253,7 @@ 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()
|
||||
@ -296,7 +268,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 }
|
||||
@ -304,8 +276,7 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,10 +288,9 @@ 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
|
||||
}
|
||||
|
||||
@ -357,13 +327,9 @@ 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
|
||||
@ -371,22 +337,16 @@ 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 {
|
||||
@ -398,7 +358,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
|
||||
@ -407,25 +367,22 @@ 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 = []
|
||||
}
|
||||
@ -451,8 +408,7 @@ 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 {
|
||||
@ -460,8 +416,7 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -469,29 +424,21 @@ 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 {
|
||||
@ -508,9 +455,7 @@ 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
|
||||
@ -527,37 +472,26 @@ 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
|
||||
}
|
||||
|
||||
@ -568,14 +502,13 @@ 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
|
||||
@ -600,33 +533,16 @@ 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 }
|
||||
@ -634,14 +550,11 @@ 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
|
||||
@ -658,7 +571,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 {
|
||||
@ -695,34 +608,27 @@ 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
|
||||
}
|
||||
|
||||
@ -748,36 +654,26 @@ 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) })
|
||||
|
||||
}
|
||||
|
||||
@ -786,7 +682,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
NotificationCenter.default.removeObserver(orientationObserver)
|
||||
self.orientationObserver = nil
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next notification_center_detachment
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
@ -806,25 +702,21 @@ 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)")
|
||||
|
||||
@ -850,13 +742,10 @@ 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 React
|
||||
import UIKit
|
||||
import React
|
||||
|
||||
/*
|
||||
* Fake camera implementation to be used on simulator
|
||||
@ -37,20 +37,16 @@ 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
|
||||
}
|
||||
|
||||
@ -58,8 +54,7 @@ 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)
|
||||
|
||||
}
|
||||
|
||||
@ -71,10 +66,6 @@ class SimulatorCamera: CameraProtocol {
|
||||
self.onZoom = onZoom
|
||||
}
|
||||
|
||||
func update(iOsDeferredStartEnabled: Bool?) {
|
||||
// Not applicable on simulator; deferred start only matters for real capture outputs.
|
||||
}
|
||||
|
||||
func setVideoDevice(zoomFactor: Double) {
|
||||
self.videoDeviceZoomFactor = zoomFactor
|
||||
self.mockPreview.zoomLabel.text = "Zoom: \(zoomFactor)"
|
||||
@ -112,15 +103,13 @@ 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 .customFocus(_, _, let focusFinished) = focusBehavior {
|
||||
fakeFocusFinishedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) {
|
||||
_ in
|
||||
if case let .customFocus(_, _, focusFinished) = focusBehavior {
|
||||
fakeFocusFinishedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
|
||||
focusFinished()
|
||||
}
|
||||
}
|
||||
@ -131,7 +120,7 @@ class SimulatorCamera: CameraProtocol {
|
||||
self.mockPreview.torchModeLabel.text = "Torch mode: \(torchMode)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
|
||||
}
|
||||
|
||||
@ -183,21 +172,16 @@ 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,13 +7,12 @@
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"version": "17.0.4",
|
||||
"version": "16.1.3",
|
||||
"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/ build/ .codegen/ android/app/build/",
|
||||
"prepare": "tsc --project tsconfig.json",
|
||||
"clean": "rm -rf dist/",
|
||||
"test": "jest",
|
||||
"lint": "yarn eslint -c .eslintrc.js",
|
||||
"check-ios": "scripts/check-ios.sh",
|
||||
@ -21,7 +20,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": "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",
|
||||
"codegen": "react-native codegen --verbose --path . --platform ios --source library && react-native codegen --verbose --path . --platform android --source library",
|
||||
"bootstrap": "cd example/ && bundle install && yarn && cd ios/ && bundle exec pod install",
|
||||
"bootstrap-linux": "cd example/ && yarn"
|
||||
},
|
||||
@ -31,11 +30,8 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"react-native": "src/index",
|
||||
"files": [
|
||||
"android/build.gradle",
|
||||
"android/gradle",
|
||||
"android/gradlew",
|
||||
"android/gradlew.bat",
|
||||
"android/src",
|
||||
"android",
|
||||
"build",
|
||||
"dist",
|
||||
"ios",
|
||||
"src",
|
||||
@ -54,11 +50,9 @@
|
||||
"@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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { findNodeHandle, processColor } from 'react-native';
|
||||
import { supportedCodeFormats, type CameraApi } from './types';
|
||||
import type { CameraApi } from './types';
|
||||
import type { CameraProps } from './CameraProps';
|
||||
import NativeCamera from './specs/CameraNativeComponent';
|
||||
import NativeCameraKitModule from './specs/NativeCameraKitModule';
|
||||
@ -15,8 +15,6 @@ const Camera = React.forwardRef<CameraApi, CameraProps>((props, ref) => {
|
||||
props.maxZoom = props.maxZoom ?? -1;
|
||||
props.scanThrottleDelay = props.scanThrottleDelay ?? -1;
|
||||
|
||||
props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats;
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
capture: async (options = {}) => {
|
||||
return await NativeCameraKitModule.capture(options, findNodeHandle(nativeRef.current) ?? undefined);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { findNodeHandle } from 'react-native';
|
||||
import { supportedCodeFormats, type CameraApi } from './types';
|
||||
import type { CameraApi } from './types';
|
||||
import type { CameraProps } from './CameraProps';
|
||||
import NativeCamera from './specs/CameraNativeComponent';
|
||||
import NativeCameraKitModule from './specs/NativeCameraKitModule';
|
||||
@ -14,9 +14,6 @@ const Camera = React.forwardRef<CameraApi, CameraProps>((props, ref) => {
|
||||
props.zoom = props.zoom ?? -1;
|
||||
props.maxZoom = props.maxZoom ?? -1;
|
||||
props.scanThrottleDelay = props.scanThrottleDelay ?? -1;
|
||||
props.iOsDeferredStart = props.iOsDeferredStart ?? true;
|
||||
|
||||
props.allowedBarcodeTypes = props.allowedBarcodeTypes ?? supportedCodeFormats;
|
||||
|
||||
props.resetFocusTimeout = props.resetFocusTimeout ?? 0;
|
||||
props.resetFocusWhenMotionDetected = props.resetFocusWhenMotionDetected ?? true;
|
||||
|
||||
@ -113,11 +113,8 @@ export interface CameraProps extends ViewProps {
|
||||
scanThrottleDelay?: number;
|
||||
/** **iOS Only**. 'speed' provides 60-80% faster image capturing */
|
||||
maxPhotoQualityPrioritization?: 'balanced' | 'quality' | 'speed';
|
||||
/** **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;
|
||||
onCaptureButtonPressOut?: ({ nativeEvent: {} }) => void;
|
||||
allowedBarcodeTypes?: CodeFormat[];
|
||||
}
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
17
src/index.ts
17
src/index.ts
@ -1,7 +1,6 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import Camera from './Camera';
|
||||
import NativeCameraKitModule from './specs/NativeCameraKitModule';
|
||||
import {
|
||||
CameraType,
|
||||
type CameraApi,
|
||||
@ -11,15 +10,10 @@ import {
|
||||
type TorchMode,
|
||||
type ZoomMode,
|
||||
type ResizeMode,
|
||||
type CodeFormat,
|
||||
} from './types';
|
||||
|
||||
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, // ⬆️
|
||||
@ -31,13 +25,4 @@ export const Orientation = {
|
||||
export default CameraKit;
|
||||
|
||||
export { Camera, CameraType };
|
||||
export type {
|
||||
TorchMode,
|
||||
FlashMode,
|
||||
FocusMode,
|
||||
ZoomMode,
|
||||
CameraApi,
|
||||
CaptureData,
|
||||
ResizeMode,
|
||||
CodeFormat,
|
||||
};
|
||||
export type { TorchMode, FlashMode, FocusMode, ZoomMode, CameraApi, CaptureData, ResizeMode };
|
||||
|
||||
@ -10,9 +10,6 @@ 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 = {
|
||||
@ -49,7 +46,6 @@ export interface NativeProps extends ViewProps {
|
||||
resetFocusWhenMotionDetected?: boolean;
|
||||
resizeMode?: string;
|
||||
scanThrottleDelay?: WithDefault<Int32, -1>;
|
||||
iOsDeferredStart?: boolean;
|
||||
barcodeFrameSize?: { width?: WithDefault<Float, 300>; height?: WithDefault<Float, 150> };
|
||||
shutterPhotoSound?: boolean;
|
||||
onOrientationChange?: DirectEventHandler<OnOrientationChangeData>;
|
||||
@ -58,7 +54,6 @@ export interface NativeProps extends ViewProps {
|
||||
onReadCode?: DirectEventHandler<OnReadCodeData>;
|
||||
onCaptureButtonPressIn?: DirectEventHandler<{}>;
|
||||
onCaptureButtonPressOut?: DirectEventHandler<{}>;
|
||||
allowedBarcodeTypes?: string[];
|
||||
|
||||
// not mentioned in props but available on the native side
|
||||
shutterAnimationDuration?: WithDefault<Int32, -1>;
|
||||
|
||||
@ -18,7 +18,6 @@ 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');
|
||||
|
||||
54
src/types.ts
54
src/types.ts
@ -3,46 +3,20 @@ export enum CameraType {
|
||||
Back = 'back',
|
||||
}
|
||||
|
||||
const codeFormatAndroid = [
|
||||
'code-128',
|
||||
'code-39',
|
||||
'code-93',
|
||||
'codabar',
|
||||
'ean-13',
|
||||
'ean-8',
|
||||
'itf',
|
||||
'upc-a',
|
||||
'upc-e',
|
||||
'qr',
|
||||
'pdf-417',
|
||||
'aztec',
|
||||
'data-matrix',
|
||||
'unknown',
|
||||
] as const;
|
||||
|
||||
const codeFormatIOS = [
|
||||
'code-128',
|
||||
'code-39',
|
||||
'code-93',
|
||||
'codabar', // only iOS 15.4+
|
||||
'ean-13',
|
||||
'ean-8',
|
||||
'itf-14',
|
||||
'upc-e',
|
||||
'qr',
|
||||
'pdf-417',
|
||||
'aztec',
|
||||
'data-matrix',
|
||||
'code-39-mod-43',
|
||||
'interleaved-2of5',
|
||||
] as const;
|
||||
|
||||
export const supportedCodeFormats = Array.from(new Set([...codeFormatAndroid, ...codeFormatIOS]));
|
||||
|
||||
type CodeFormatAndroid = (typeof codeFormatAndroid)[number];
|
||||
type CodeFormatIOS = (typeof codeFormatIOS)[number];
|
||||
|
||||
export type CodeFormat = CodeFormatAndroid | CodeFormatIOS | 'unknown';
|
||||
export type CodeFormat =
|
||||
| 'code-128'
|
||||
| 'code-39'
|
||||
| 'code-93'
|
||||
| 'codabar'
|
||||
| 'ean-13'
|
||||
| 'ean-8'
|
||||
| 'itf'
|
||||
| 'upc-e'
|
||||
| 'qr'
|
||||
| 'pdf-417'
|
||||
| 'aztec'
|
||||
| 'data-matrix'
|
||||
| 'unknown';
|
||||
|
||||
export type TorchMode = 'on' | 'off';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user