diff --git a/.gitignore b/.gitignore index 703f792..7ae6ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,5 @@ dist/ android/generated/ReactCodegen.podspec example/.rock/cache/ + +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e09d122 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,175 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +React Native Camera Kit is a high-performance, cross-platform camera library for React Native applications (iOS and Android). It provides: +- Photo capture with high performance optimization +- Barcode/QR code scanning capabilities +- Camera preview support (including iOS simulator) +- Extensive camera control options (flash, focus, zoom, torch) +- Device orientation detection + +This is a native library that uses React Native Codegen for cross-platform native module bindings. + +## Architecture + +### JavaScript/TypeScript Layer (`src/`) + +The TypeScript codebase provides the React wrapper and type definitions: + +- **Entry point** (`src/index.ts`): Exports the Camera component, types, and Orientation constants +- **Camera component** (`src/Camera.tsx`): Platform-agnostic wrapper that lazy-loads platform-specific implementations + - `src/Camera.ios.tsx`: iOS-specific camera component + - `src/Camera.android.tsx`: Android-specific camera component +- **Types and props**: + - `src/types.ts`: Core type definitions (CameraType, CodeFormat, TorchMode, FlashMode, FocusMode, ZoomMode, ResizeMode, CameraApi, CaptureData) + - `src/CameraProps.ts`: Complete Camera component props interface (both platform-specific and shared) +- **Native specs** (`src/specs/`): + - `CameraNativeComponent.ts`: React Native Codegen definition for the camera view component (NativeProps) + - `NativeCameraKitModule.ts`: TurboModule specification for native camera functions (capture, authorization) + +**Key Pattern**: Optional numeric props are represented as `-1` or `undefined` until React Native Fabric supports optional values. Both platform-specific implementations handle this conversion. + +### Native Layer + +- **iOS** (`ios/ReactNativeCameraKit/`): Swift implementation + - `RealCamera.swift` / `SimulatorCamera.swift`: Core camera implementation (real and simulator) + - `CameraManager.swift`: Manages camera state and configuration + - `PhotoCaptureDelegate.swift`: Handles photo capture logic + - `ScannerInterfaceView.swift` / `ScannerFrameView.swift`: Barcode scanning UI + - `RatioOverlayView.swift`: Aspect ratio guide overlay + +- **Android** (`android/src/main/java/com/rncamerakit/`): Kotlin implementation + - `CKCamera.kt`: Main camera view component + - `QRCodeAnalyzer.kt`: Barcode scanning using CameraX + - Event classes in `events/`: Handle camera callbacks (zoom, orientation, errors, etc.) + - Platform-specific code split between `newarch/` (React Native 0.73+, Fabric) and `oldarch/` (legacy) + +## Development Commands + +### Build and Compilation + +```bash +# Build TypeScript to JavaScript (outputs to dist/) +yarn build + +# Clean build artifacts +yarn clean + +# Run both clean and build +yarn clean && yarn build +``` + +### Linting and Code Quality + +```bash +# Run ESLint +yarn lint + +# ESLint rules are configured in .eslintrc.js with: +# - Max line length: 120 characters +# - Required semicolons, proper indentation (2 spaces) +# - No console.log or debugger statements allowed +# - Strict import resolution checking +``` + +### Testing + +```bash +# Run all tests +yarn test + +# Run tests for a specific file +yarn test -- src/__tests__/index.test.tsx + +# The project uses Jest with minimal test configuration +# Tests should be placed in __tests__ directories +``` + +### Example Project + +```bash +# Bootstrap the example app (installs dependencies and pods) +yarn bootstrap + +# For Linux: +yarn bootstrap-linux +``` + +## Key Development Notes + +### Native Component Integration + +The camera component uses **React Native Codegen** to auto-generate native binding code: +- Props are defined in `src/specs/CameraNativeComponent.ts` (NativeProps interface) +- Changes to props require running: `yarn codegen` (generates `build/` directory) +- The codegen config is in `package.json` under `codegenConfig` + +### Platform-Specific Code + +The library uses React Native's platform module for loading platform-specific implementations: +```typescript +// In src/Camera.tsx +const Camera = lazy(() => + Platform.OS === 'ios' + ? import('./Camera.ios') + : import('./Camera.android'), +); +``` + +Both implementations handle color props differently: +- **Android**: Uses `processColor()` to convert color values +- **iOS**: Passes colors as-is + +### Optional Props Pattern + +React Native Codegen doesn't support optional numeric props, so: +- Numeric props default to `-1` to indicate "undefined" +- The native layer interprets `-1` as "no value provided" +- This affects: `zoom`, `maxZoom`, `scanThrottleDelay`, `resetFocusTimeout`, `shutterAnimationDuration` + +### Type System + +The project uses strict TypeScript (`strict: true` in tsconfig.json): +- `@ts-expect-error` comments are used for Codegen type mismatches (see Camera.ios.tsx line 33) +- Type definitions must be accurate between user-facing types and native specs +- All numeric types from Codegen props must be converted in both platform files + +## Testing and Releases + +### Running Tests + +```bash +# Run Jest tests +yarn test + +# Build TypeScript (validates code) +yarn build + +# Test files follow Jest conventions and are excluded from build (tsconfig.json excludes *.test.tsx) +``` + +### Release Process + +```bash +# Standard npm release +yarn release + +# Beta release +yarn release:beta + +# Local testing (creates and opens tar.gz) +yarn release:local +``` + +The library is published to npm as `react-native-camera-kit` with the `files` array in package.json controlling what gets included in the published bundle. + +## Important Configuration Details + +- **TypeScript**: Strict mode enabled, targets ESNext, outputs to `dist/` with declaration files +- **Package managers**: Yarn 1.22.22 required (specified in package.json engines) +- **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 diff --git a/README.md b/README.md index 9c5a2eb..f35d075 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,48 @@

- 🎈 React Native Camera Kit + 🎈 React Native Camera Kit (No Google)

A high performance, easy to use, rock solid
- camera library for React Native apps. + camera library for React Native apps.
+ No Google ML Kit dependency.

- + React Native Camera Kit is released under the MIT license. - - Current npm package version. + + Current npm package version.

+ +> **⚠️ Fork Notice** +> +> This is a fork of [teslamotors/react-native-camera-kit](https://github.com/teslamotors/react-native-camera-kit) with **Google ML Kit removed** from Android. +> +> **Why?** Google ML Kit is closed source. This fork uses a completely open source pure Kotlin QR decoder instead. + +## Key Differences from Original + +| Feature | Original | This Fork | +|---------|----------|-----------| +| Android barcode library | Google ML Kit (closed source) | Pure Kotlin ([limpbrains/qr](https://github.com/limpbrains/qr)) | +| Source code | Closed source (ML Kit) | Fully open source | +| Google Play Services | Required | Not required | +| Barcode formats | Multiple formats | **QR codes only** | + +### Limitations + +- **Android barcode scanning supports QR codes only** (no EAN, UPC, Code128, etc.) +- iOS barcode scanning is unchanged (uses native AVFoundation, supports all formats) + +### QR Decoder Attribution + +The Android QR decoder is based on: +- [limpbrains/qr](https://github.com/limpbrains/qr) - Kotlin QR code reader library +- [paulmillr/qr](https://github.com/paulmillr/qr) - Original JavaScript implementation by Paul Miller + @@ -34,7 +63,7 @@ ## Installation (RN > 0.60) ```bash -yarn add react-native-camera-kit +yarn add react-native-camera-kit-no-google ``` ```bash @@ -143,7 +172,7 @@ Add the following usage descriptions to your `Info.plist` (usually found at: `io Barebones camera component if you need advanced/customized interface ```ts -import { Camera, CameraType } from 'react-native-camera-kit'; +import { Camera, CameraType } from 'react-native-camera-kit-no-google'; ``` ```tsx @@ -184,7 +213,7 @@ Additionally, the Camera can be used for barcode scanning | `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'; if (event.nativeEvent.orientation === Orientation.PORTRAIT) { ... }` to understand the new value | +| `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` | diff --git a/ReactNativeCameraKit.podspec b/ReactNativeCameraKit.podspec index 36ad9a2..5d820a3 100644 --- a/ReactNativeCameraKit.podspec +++ b/ReactNativeCameraKit.podspec @@ -9,10 +9,10 @@ Pod::Spec.new do |s| s.license = "MIT" s.authors = "CameraKit" - s.homepage = "https://github.com/teslamotors/react-native-camera-kit" + s.homepage = "https://github.com/limpbrains/react-native-camera-kit-no-google" s.platform = :ios, "15.0" - s.source = { :git => "https://github.com/teslamotors/react-native-camera-kit.git", :tag => "v#{s.version}" } + s.source = { :git => "https://github.com/limpbrains/react-native-camera-kit-no-google.git", :tag => "v#{s.version}" } s.source_files = [ # Exclude .h files as they cause Swift compiler to treat them as C files, but they are C++ # See https://github.com/facebook/react-native/issues/45424#issuecomment-2354737063 diff --git a/android/build.gradle b/android/build.gradle index 6cc0733..08aed5d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -66,8 +66,9 @@ dependencies { // If you want to additionally use the CameraX Extensions library // implementation "androidx.camera:camera-extensions:${camerax_version}" - implementation 'com.google.mlkit:barcode-scanning:17.3.0' + implementation 'com.github.limpbrains:qr:v0.0.1' } repositories { mavenCentral() + maven { url 'https://jitpack.io' } } diff --git a/android/src/main/java/com/rncamerakit/CKCamera.kt b/android/src/main/java/com/rncamerakit/CKCamera.kt index 4bb3c85..cdae5b1 100644 --- a/android/src/main/java/com/rncamerakit/CKCamera.kt +++ b/android/src/main/java/com/rncamerakit/CKCamera.kt @@ -44,7 +44,6 @@ import android.graphics.Rect import android.graphics.RectF import android.util.Size import com.facebook.react.uimanager.UIManagerHelper -import com.google.mlkit.vision.barcode.common.Barcode import com.rncamerakit.events.* class RectOverlay constructor(context: Context) : @@ -330,35 +329,8 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs val useCases = mutableListOf(preview, imageCapture) if (scanBarcode) { - val analyzer = QRCodeAnalyzer(analyzerBlock@{ barcodes, imageSize -> - if (barcodes.isEmpty()) { - return@analyzerBlock - } - - val barcodeFrame = barcodeFrame - if (barcodeFrame == null) { - onBarcodeRead(barcodes) - return@analyzerBlock - } - - // Calculate scaling factors (image is always rotated by 90 degrees) - val scaleX = viewFinder.width.toFloat() / imageSize.height - val scaleY = viewFinder.height.toFloat() / imageSize.width - - val filteredBarcodes = barcodes.filter { barcode -> - val barcodeBoundingBox = barcode.boundingBox ?: return@filter false; - val scaledBarcodeBoundingBox = Rect( - (barcodeBoundingBox.left * scaleX).toInt(), - (barcodeBoundingBox.top * scaleY).toInt(), - (barcodeBoundingBox.right * scaleX).toInt(), - (barcodeBoundingBox.bottom * scaleY).toInt() - ) - barcodeFrame.frameRect.contains(scaledBarcodeBoundingBox) - } - - if (filteredBarcodes.isNotEmpty()) { - onBarcodeRead(filteredBarcodes) - } + val analyzer = QRCodeAnalyzer({ decodedValue -> + onBarcodeRead(decodedValue) }, scanThrottleDelay) imageAnalyzer!!.setAnalyzer(cameraExecutor, analyzer) useCases.add(imageAnalyzer) @@ -509,12 +481,11 @@ class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObs rectOverlay.drawRectBounds(focusRects) } - private fun onBarcodeRead(barcodes: List) { - val codeFormat = CodeFormat.fromBarcodeType(barcodes.first().format); + private fun onBarcodeRead(decodedValue: String) { val surfaceId = UIManagerHelper.getSurfaceId(currentContext) UIManagerHelper .getEventDispatcherForReactTag(currentContext, id) - ?.dispatchEvent(ReadCodeEvent(surfaceId, id, barcodes.first().rawValue, codeFormat.code)) + ?.dispatchEvent(ReadCodeEvent(surfaceId, id, decodedValue, CodeFormat.QR.code)) } private fun onOrientationChange(orientation: Int) { diff --git a/android/src/main/java/com/rncamerakit/CodeFormat.kt b/android/src/main/java/com/rncamerakit/CodeFormat.kt index 207c715..d34d3ad 100644 --- a/android/src/main/java/com/rncamerakit/CodeFormat.kt +++ b/android/src/main/java/com/rncamerakit/CodeFormat.kt @@ -1,7 +1,5 @@ package com.rncamerakit -import com.google.mlkit.vision.barcode.common.Barcode - enum class CodeFormat(val code: String) { CODE_128("code-128"), CODE_39("code-39"), @@ -16,41 +14,4 @@ enum class CodeFormat(val code: String) { AZTEC("aztec"), DATA_MATRIX("data-matrix"), UNKNOWN("unknown"); - - fun toBarcodeType(): Int { - return when (this) { - CODE_128 -> Barcode.FORMAT_CODE_128 - CODE_39 -> Barcode.FORMAT_CODE_39 - CODE_93 -> Barcode.FORMAT_CODE_93 - CODABAR -> Barcode.FORMAT_CODABAR - EAN_13 -> Barcode.FORMAT_EAN_13 - EAN_8 -> Barcode.FORMAT_EAN_8 - ITF -> Barcode.FORMAT_ITF - UPC_E -> Barcode.FORMAT_UPC_E - QR -> Barcode.FORMAT_QR_CODE - PDF_417 -> Barcode.FORMAT_PDF417 - AZTEC -> Barcode.FORMAT_AZTEC - DATA_MATRIX -> Barcode.FORMAT_DATA_MATRIX - UNKNOWN -> -1 // Or any other default value you prefer - } - } - - companion object { - fun fromBarcodeType(@Barcode.BarcodeFormat barcodeType: Int): CodeFormat = - when (barcodeType) { - Barcode.FORMAT_CODE_128 -> CODE_128 - Barcode.FORMAT_CODE_39 -> CODE_39 - Barcode.FORMAT_CODE_93 -> CODE_93 - Barcode.FORMAT_CODABAR -> CODABAR - Barcode.FORMAT_EAN_13 -> EAN_13 - Barcode.FORMAT_EAN_8 -> EAN_8 - Barcode.FORMAT_ITF -> ITF - Barcode.FORMAT_UPC_E -> UPC_E - Barcode.FORMAT_QR_CODE -> QR - Barcode.FORMAT_PDF417 -> PDF_417 - Barcode.FORMAT_AZTEC -> AZTEC - Barcode.FORMAT_DATA_MATRIX -> DATA_MATRIX - else -> UNKNOWN - } - } } diff --git a/android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt b/android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt index dd3bfc6..a5bf694 100644 --- a/android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt +++ b/android/src/main/java/com/rncamerakit/QRCodeAnalyzer.kt @@ -1,48 +1,65 @@ package com.rncamerakit -import android.annotation.SuppressLint -import android.util.Size import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.common.Barcode -import com.google.mlkit.vision.common.InputImage +import qr.QRDecoder +import qr.QRDecodingException -class QRCodeAnalyzer ( - private val onQRCodesDetected: (qrCodes: List, imageSize: Size) -> Unit, +class QRCodeAnalyzer( + private val onQRCodeDetected: (decodedValue: String) -> Unit, private val scanThrottleDelay: Long = 0L ) : ImageAnalysis.Analyzer { - // Time in milliseconds of the last time we dispatched detected barcodes - private var lastBarcodeDetectedTime: Long = 0L - @SuppressLint("UnsafeExperimentalUsageError") + // Time in milliseconds of the last time we dispatched detected QR code + private var lastQRDetectedTime: Long = 0L + @ExperimentalGetImage override fun analyze(image: ImageProxy) { - val mediaImage = image.image ?: return + try { + val grayscaleData = extractYPlane(image) + val decoded = QRDecoder.decode(image.width, image.height, grayscaleData) - val inputImage = InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees) - - val scanner = BarcodeScanning.getClient() - scanner.process(inputImage) - .addOnSuccessListener { barcodes -> - // Throttle callback invocations based on scanThrottleDelay (ms) - val now = System.currentTimeMillis() - if (scanThrottleDelay > 0 && (now - lastBarcodeDetectedTime) < scanThrottleDelay) { - return@addOnSuccessListener - } - - val strBarcodes = mutableListOf() - barcodes.forEach { barcode -> - strBarcodes.add(barcode ?: return@forEach) - } - - if (strBarcodes.isNotEmpty()) { - lastBarcodeDetectedTime = now - onQRCodesDetected(strBarcodes, Size(image.width, image.height)) - } + // Throttle callback invocations based on scanThrottleDelay (ms) + val now = System.currentTimeMillis() + if (scanThrottleDelay > 0 && (now - lastQRDetectedTime) < scanThrottleDelay) { + return } - .addOnCompleteListener { - image.close() + + lastQRDetectedTime = now + onQRCodeDetected(decoded) + } catch (e: QRDecodingException) { + // No QR code found or decoding error - this is expected for most frames + } finally { + image.close() + } + } + + /** + * Extracts the Y (luminance) plane from a YUV_420_888 ImageProxy. + * The Y plane is already grayscale data (1 byte per pixel). + * Handles row stride padding when rowStride > width. + */ + private fun extractYPlane(image: ImageProxy): ByteArray { + val yPlane = image.planes[0] + val yBuffer = yPlane.buffer + val rowStride = yPlane.rowStride + val width = image.width + val height = image.height + val yBytes = ByteArray(width * height) + + if (rowStride == width) { + // Fast path: contiguous data, no padding + yBuffer.rewind() + yBuffer.get(yBytes, 0, width * height) + } else { + // Slow path: handle row stride padding + yBuffer.rewind() + for (row in 0 until height) { + yBuffer.position(row * rowStride) + yBuffer.get(yBytes, row * width, width) } + } + + return yBytes } } diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 65fd3e1..6ef2db2 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -4,3 +4,4 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli rootProject.name = 'CameraKitExample' include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') +// includeBuild('../../../qr') diff --git a/example/src/BarcodeScreenExample.tsx b/example/src/BarcodeScreenExample.tsx index f125091..e40ecbe 100644 --- a/example/src/BarcodeScreenExample.tsx +++ b/example/src/BarcodeScreenExample.tsx @@ -46,6 +46,46 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => { const [cameraType, setCameraType] = useState(CameraType.Back); const [barcode, setBarcode] = useState(''); + const [fps, setFps] = useState(0); + const [scanCount, setScanCount] = useState(0); + const [scansPerSec, setScansPerSec] = useState(0); + + // FPS counter using requestAnimationFrame + useEffect(() => { + let frameCount = 0; + let lastTime = performance.now(); + let animationId: number; + + const measureFps = () => { + frameCount++; + const now = performance.now(); + const elapsed = now - lastTime; + + if (elapsed >= 1000) { + setFps(Math.round((frameCount * 1000) / elapsed)); + frameCount = 0; + lastTime = now; + } + + animationId = requestAnimationFrame(measureFps); + }; + + animationId = requestAnimationFrame(measureFps); + + return () => { + cancelAnimationFrame(animationId); + }; + }, []); + + // Scans per second counter + useEffect(() => { + const interval = setInterval(() => { + setScansPerSec(scanCount); + setScanCount(0); + }, 1000); + + return () => clearInterval(interval); + }, [scanCount]); useEffect(() => { const t = setTimeout(() => { @@ -109,6 +149,11 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => { resizeMode="contain" /> + + + {fps} FPS + {scansPerSec} scans/s + @@ -119,7 +164,7 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => { flashMode={flashData?.mode} zoomMode="on" focusMode="on" - scanThrottleDelay={2000} + scanThrottleDelay={0} torchMode={torchMode ? 'on' : 'off'} onOrientationChange={(e) => { // We recommend locking the camera UI to portrait (using a different library) @@ -150,11 +195,8 @@ const BarcodeExample = ({ onBack }: { onBack: () => void }) => { showFrame barcodeFrameSize={{ width: 300, height: 150 }} onReadCode={(event) => { - Vibration.vibrate(100); + setScanCount((prev) => prev + 1); setBarcode(event.nativeEvent.codeStringValue); - console.log('barcode', event.nativeEvent.codeStringValue); - console.log('codeFormat', event.nativeEvent.codeFormat); - }} /> @@ -254,4 +296,17 @@ const styles = StyleSheet.create({ color: 'white', fontSize: 20, }, + fpsContainer: { + backgroundColor: '#222', + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 4, + justifyContent: 'center', + alignItems: 'flex-end', + }, + fpsText: { + color: '#0f0', + fontSize: 12, + fontFamily: 'monospace', + }, }); diff --git a/package.json b/package.json index b2fdd89..b9ad2e2 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "react-native-camera-kit", + "name": "react-native-camera-kit-no-google", "repository": { "type": "git", - "url": "https://github.com/teslamotors/react-native-camera-kit.git" + "url": "https://github.com/limpbrains/react-native-camera-kit-no-google.git" }, "publishConfig": { "registry": "https://registry.npmjs.org/"
@@ -24,8 +52,9 @@
  • Cross Platform (iOS and Android)

  • Optimized for performance and high photo capture rate

  • -
  • QR / Barcode scanning support

  • +
  • QR Code scanning support (QR only on Android)

  • Camera preview support in iOS simulator

  • +
  • No Google dependencies