diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt index a78e09f..89799dd 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt @@ -39,8 +39,11 @@ class ReactNativeScannerView(context: Context) : LinearLayout(context) { .build() private lateinit var cameraControl: CameraControl + private var isCameraRunning: Boolean = false + private var pauseAfterCapture: Boolean = false + private var isActive: Boolean = false + companion object { - private const val REQUEST_CODE_PERMISSIONS = 10 private val REQUIRED_PERMISSIONS = mutableListOf( Manifest.permission.CAMERA @@ -117,14 +120,15 @@ class ReactNativeScannerView(context: Context) : LinearLayout(context) { // newSingleThreadExecutor() will let us perform analysis on a single worker thread Executors.newSingleThreadExecutor() ) { imageProxy -> - processImageProxy(scanner, imageProxy) + processImageProxy(scanner, imageProxy, reactApplicationContext) } } @SuppressLint("UnsafeOptInUsageError") private fun processImageProxy( barcodeScanner: BarcodeScanner, - imageProxy: ImageProxy + imageProxy: ImageProxy, + reactApplicationContext: ReactApplicationContext ) { imageProxy.image?.let { image -> val inputImage = @@ -133,19 +137,30 @@ class ReactNativeScannerView(context: Context) : LinearLayout(context) { imageProxy.imageInfo.rotationDegrees ) + if (!isCameraRunning) { + return; + } + barcodeScanner.process(inputImage) .addOnSuccessListener { barcodeList -> - val barcode = - barcodeList.getOrNull(0) // `rawValue` is the decoded value of the barcode - - barcode?.rawValue?.let { value -> - // mCameraProvider?.unbindAll() // this line will stop the camera from scanning after the first scan + if (barcodeList.isNotEmpty()) { + if (pauseAfterCapture) { + pausePreview() + } + + val surfaceId = UIManagerHelper.getSurfaceId(reactApplicationContext) val reactContext = context as ReactContext - val eventDispatcher: EventDispatcher? = - UIManagerHelper.getEventDispatcherForReactTag( - reactContext, id - ) - eventDispatcher?.dispatchEvent(ReactNativeScannerViewEvent(id, value)) + val eventDispatcher: EventDispatcher? = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) + + barcodeList.forEach { barcode -> + barcode?.let { code -> + code.cornerPoints?.let { cornerPoints -> + code.boundingBox?.let { bounds -> + eventDispatcher?.dispatchEvent(ReactNativeScannerViewEvent(surfaceId, id, code.rawValue?: "", bounds, cornerPoints, code.format)) + } + } + } + } } } .addOnFailureListener { @@ -186,18 +201,13 @@ class ReactNativeScannerView(context: Context) : LinearLayout(context) { // Select back camera as a default val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + isCameraRunning = true + try { // Unbind use cases before rebinding cameraProvider.unbindAll() // Bind use cases to camera - cameraProvider.bindToLifecycle( - (reactApplicationContext.currentActivity as AppCompatActivity), - cameraSelector, - surfacePreview, - analysisUseCase - ) - val camera = cameraProvider.bindToLifecycle( (reactApplicationContext.currentActivity as AppCompatActivity), cameraSelector, @@ -207,9 +217,8 @@ class ReactNativeScannerView(context: Context) : LinearLayout(context) { cameraControl = camera.cameraControl } catch (exc: Exception) { - + isCameraRunning = false } - }, ContextCompat.getMainExecutor(context)) } @@ -225,4 +234,43 @@ class ReactNativeScannerView(context: Context) : LinearLayout(context) { cameraExecutor.shutdown() mCameraProvider?.unbindAll() } -} \ No newline at end of file + + private fun stopCamera() { + + } + + fun setPauseAfterCapture(value: Boolean) { + pauseAfterCapture = value + } + + fun setIsActive(value: Boolean) { + isActive = value + } + + fun pausePreview() { + if (isCameraRunning) { + isCameraRunning = false + mCameraProvider?.unbind(analysisUseCase) + } + } + + fun resumePreview() { + if (!isCameraRunning) { + isCameraRunning = true + + try { + val reactContext = context as ReactContext + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + // Bind use cases to camera + mCameraProvider?.bindToLifecycle( + (reactContext.currentActivity as AppCompatActivity), + cameraSelector, + analysisUseCase + ) + } catch (exc: Exception) { + isCameraRunning = false + } + } + } +} diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt index 590924e..f602a0c 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt @@ -1,30 +1,51 @@ package com.pushpendersingh.reactnativescanner +import android.graphics.Point +import android.graphics.Rect import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.Event -import com.facebook.react.uimanager.events.RCTModernEventEmitter +import com.google.mlkit.vision.barcode.common.Barcode - -class ReactNativeScannerViewEvent(viewId: Int, private val qrValue: String): Event(viewId) { +class ReactNativeScannerViewEvent( + surfaceId: Int, + viewId: Int, + private val qrValue: String, + private val rect: Rect, + private val origin: Array, + private val type: Int + ) : Event(surfaceId, viewId) { override fun getEventName(): String { return "onQrScanned" } - override fun dispatchModern(rctEventEmitter: RCTModernEventEmitter) { - super.dispatchModern(rctEventEmitter) // if we don't call this, the react native part won't receive the event but because of this line event call two times - rctEventEmitter.receiveEvent( - -1, - viewTag, eventName, - Arguments.createMap() - ) - } - override fun getEventData(): WritableMap { val event: WritableMap = Arguments.createMap() - event.putString("value", qrValue) + val bounds = Arguments.createMap() + bounds.putArray("origin", getPoints(origin)) + bounds.putInt("width", rect.width()) + bounds.putInt("height", rect.height()) + + event.putMap("bounds", bounds) + event.putString("data", qrValue) + if (type == Barcode.FORMAT_QR_CODE) + event.putString("type", "QR_CODE") + else + event.putString("type", "UNKNOWN") + return event } -} \ No newline at end of file + private fun getPoints(points: Array): WritableArray { + val origin: WritableArray = Arguments.createArray() + for (point in points) { + val pointData: WritableMap = Arguments.createMap() + pointData.putInt("x", point.x) + pointData.putInt("y", point.y) + origin.pushMap(pointData); + } + return origin + } +} diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt index 18e21f0..996627f 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt @@ -1,11 +1,11 @@ package com.pushpendersingh.reactnativescanner import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.common.MapBuilder import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.viewmanagers.ReactNativeScannerViewManagerInterface import com.facebook.react.viewmanagers.ReactNativeScannerViewManagerDelegate @@ -27,6 +27,16 @@ class ReactNativeScannerViewManager(private val mCallerContext: ReactApplication return NAME } + @ReactProp(name = "pauseAfterCapture") + override fun setPauseAfterCapture(view: ReactNativeScannerView?, value: Boolean) { + view?.setPauseAfterCapture(value) + } + + @ReactProp(name = "isActive") + override fun setIsActive(view: ReactNativeScannerView?, value: Boolean) { + view?.setIsActive(value) + } + override fun enableFlashlight(view: ReactNativeScannerView?) { view?.enableFlashlight() } @@ -39,6 +49,14 @@ class ReactNativeScannerViewManager(private val mCallerContext: ReactApplication view?.releaseCamera() } + override fun pausePreview(view: ReactNativeScannerView?) { + view?.pausePreview() + } + + override fun resumePreview(view: ReactNativeScannerView?) { + view?.resumePreview() + } + override fun createViewInstance(reactContext: ThemedReactContext): ReactNativeScannerView { val reactnativeScannerView = ReactNativeScannerView(mCallerContext) reactnativeScannerView.setUpCamera(mCallerContext) @@ -48,11 +66,4 @@ class ReactNativeScannerViewManager(private val mCallerContext: ReactApplication companion object { const val NAME = "ReactNativeScannerView" } - - override fun getExportedCustomDirectEventTypeConstants(): Map { - return MapBuilder.of( - "onQrScanned", - MapBuilder.of("registrationName", "onQrScanned") - ) - } } diff --git a/ios/ReactNativeScannerView.mm b/ios/ReactNativeScannerView.mm index 175910b..1fe3323 100644 --- a/ios/ReactNativeScannerView.mm +++ b/ios/ReactNativeScannerView.mm @@ -10,17 +10,34 @@ using namespace facebook::react; @interface ReactNativeScannerView () - @end @implementation ReactNativeScannerView { UIView * _view; - + AVCaptureSession *_session; AVCaptureDevice *_device; AVCaptureDeviceInput *_input; AVCaptureMetadataOutput *_output; AVCaptureVideoPreviewLayer *_prevLayer; + + BOOL pauseAfterCapture; + BOOL isActive; +} + ++ (NSArray *)metadataObjectTypes +{ + return @[AVMetadataObjectTypeUPCECode, + AVMetadataObjectTypeCode39Code, + AVMetadataObjectTypeCode39Mod43Code, + AVMetadataObjectTypeEAN13Code, + AVMetadataObjectTypeEAN8Code, + AVMetadataObjectTypeCode93Code, + AVMetadataObjectTypeCode128Code, + AVMetadataObjectTypePDF417Code, + AVMetadataObjectTypeQRCode, + AVMetadataObjectTypeAztecCode, + AVMetadataObjectTypeDataMatrixCode]; } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -30,75 +47,148 @@ + (ComponentDescriptorProvider)componentDescriptorProvider - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); - _props = defaultProps; - - _view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; - + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; + _session = [[AVCaptureSession alloc] init]; + _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + + NSError *error = nil; + _input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error]; + if (_input) { + [_session addInput:_input]; + } else { + NSLog(@"%@", [error localizedDescription]); + } + + _output = [[AVCaptureMetadataOutput alloc] init]; + [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; + [_session addOutput:_output]; + + _output.metadataObjectTypes = [ReactNativeScannerView metadataObjectTypes]; + + _prevLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session]; + _prevLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; + [_view.layer addSublayer:_prevLayer]; + + + // Create a dispatch queue. + dispatch_queue_t sessionQueue = dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL); - _session = [[AVCaptureSession alloc] init]; - _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; - NSError *error = nil; + // Use dispatch_async to call the startRunning method on the sessionQueue. + dispatch_async(sessionQueue, ^{ + [self->_session startRunning]; + }); - _input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error]; - if (_input) { - [_session addInput:_input]; - } else { - NSLog(@"%@", [error localizedDescription]); + self.contentView = _view; } - - _output = [[AVCaptureMetadataOutput alloc] init]; - [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; - [_session addOutput:_output]; - - _output.metadataObjectTypes = [_output availableMetadataObjectTypes]; - - _prevLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session]; - - _prevLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; - [_view.layer addSublayer:_prevLayer]; - - // Create a dispatch queue. - dispatch_queue_t sessionQueue = dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL); - - // Use dispatch_async to call the startRunning method on the sessionQueue. - dispatch_async(sessionQueue, ^{ - [self->_session startRunning]; - }); - - self.contentView = _view; - } - - return self; + + return self; } - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { - CGRect highlightViewRect = CGRectZero; - AVMetadataMachineReadableCodeObject *barCodeObject; - NSString *detectionString = nil; - NSArray *barCodeTypes = @[AVMetadataObjectTypeUPCECode, AVMetadataObjectTypeCode39Code, AVMetadataObjectTypeCode39Mod43Code, - AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode93Code, AVMetadataObjectTypeCode128Code, - AVMetadataObjectTypePDF417Code, AVMetadataObjectTypeQRCode, AVMetadataObjectTypeAztecCode]; - - for (AVMetadataObject *metadata in metadataObjects) { - for (NSString *type in barCodeTypes) { - if ([metadata.type isEqualToString:type]) { - barCodeObject = (AVMetadataMachineReadableCodeObject *)[_prevLayer transformedMetadataObjectForMetadataObject:(AVMetadataMachineReadableCodeObject *)metadata]; - highlightViewRect = barCodeObject.bounds; - detectionString = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; - break; - } + if (_eventEmitter == nullptr) { + return; + } + + NSMutableArray *validBarCodes = [[NSMutableArray alloc] init]; + NSArray *barCodeTypes = [ReactNativeScannerView metadataObjectTypes]; + + for (AVMetadataObject *metadata in metadataObjects) { + BOOL isValidCode = NO; + for (NSString *type in barCodeTypes) { + if ([metadata.type isEqualToString:type]) { + isValidCode = YES; + break; + } + } + + if (isValidCode == YES) { + [validBarCodes addObject:metadata]; + } } - if (detectionString != nil) { - if (_eventEmitter != nullptr) { + + // pauseAfterCapture: + // * Pause AVCaptureSession for further processing, after valid barcodes found, + // * Can be resumed back by calling resumePreview from the owner of the component + if (pauseAfterCapture == YES && validBarCodes.count > 0) { + [self pausePreview]; + } + + for (AVMetadataObject *metadata in validBarCodes) { + AVMetadataMachineReadableCodeObject *barCodeObject = (AVMetadataMachineReadableCodeObject *)[_prevLayer transformedMetadataObjectForMetadataObject:(AVMetadataMachineReadableCodeObject *)metadata]; + CGRect highlightViewRect = barCodeObject.bounds; + NSArray *corners = barCodeObject.corners; + NSString *codeString = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; + + CGPoint topLeft, bottomLeft, bottomRight, topRight = CGPointMake(0, 0); + + if (corners.count >= 0) { + topLeft = [self mapObject: corners[0]]; + } + + if (corners.count >= 1) { + bottomLeft = [self mapObject: corners[1]]; + } + + if (corners.count >= 2) { + bottomRight = [self mapObject: corners[2]]; + } + + if (corners.count >= 3) { + topRight = [self mapObject: corners[3]]; + } + + facebook::react::ReactNativeScannerViewEventEmitter::OnQrScannedBounds bounds = { + .width = highlightViewRect.size.width, + .height = highlightViewRect.size.height, + .origin = { + .topLeft = {.x = topLeft.x, .y = topLeft.y}, + .bottomLeft = {.x = bottomLeft.x, .y = bottomLeft.y}, + .bottomRight = {.x = bottomRight.x, .y = bottomRight.y}, + .topRight = {.x = topRight.x, .y = topRight.y} + } + }; + std::dynamic_pointer_cast(_eventEmitter)->onQrScanned(facebook::react::ReactNativeScannerViewEventEmitter::OnQrScanned{ - .value = std::string([detectionString UTF8String]) + .bounds = bounds, + .type = std::string([metadata.type UTF8String]), + .data = std::string([codeString UTF8String]), + .target = std::int32_t([codeString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) }); - } } - } +} + +- (CGPoint)mapObject:(NSDictionary *)object { + if (object == nil) { + return CGPointMake(0, 0); + } + + return CGPointMake([[object objectForKey:@"X"] doubleValue], [[object objectForKey:@"Y"] doubleValue]); +} + +- (void)setIsActive:(BOOL)active { + isActive = active; + + // Enable/Disable Preview Layer + if (isActive) { + [self resumePreview]; + } else { + [self pausePreview]; + } + + if (isActive == _session.isRunning) { + return; + } + // Start/Stop session + if (isActive) { + [_session startRunning]; + } else { + [_session stopRunning]; + } } - (void)releaseCamera { @@ -110,10 +200,10 @@ - (void)releaseCamera { [_session stopRunning]; // Release the session, input, output, and preview layer - _session = nil; - _input = nil; - _output = nil; - _prevLayer = nil; +// _session = nil; +// _input = nil; +// _output = nil; +// _prevLayer = nil; } } @@ -144,14 +234,32 @@ - (void)disableFlashlight { } } +- (void)pausePreview { + if ([[_prevLayer connection] isEnabled]) { + [[_prevLayer connection] setEnabled:NO]; + } +} + +- (void)resumePreview { + if (![[_prevLayer connection] isEnabled]) { + [[_prevLayer connection] setEnabled:YES]; + } +} + - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { + const auto &oldViewProps = *std::static_pointer_cast(_props); + const auto &newViewProps = *std::static_pointer_cast(props); + + pauseAfterCapture = newViewProps.pauseAfterCapture; + [self setIsActive:newViewProps.isActive]; + [super updateProps:props oldProps:oldProps]; } - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics{ - [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; - _prevLayer.frame = [_view.layer bounds]; + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + _prevLayer.frame = [_view.layer bounds]; } - (void)handleCommand:(nonnull const NSString *)commandName args:(nonnull const NSArray *)args { diff --git a/ios/ReactNativeScannerViewManager.mm b/ios/ReactNativeScannerViewManager.mm index c76fe90..8d5f603 100644 --- a/ios/ReactNativeScannerViewManager.mm +++ b/ios/ReactNativeScannerViewManager.mm @@ -11,5 +11,7 @@ @implementation ReactNativeScannerViewManager RCT_EXPORT_MODULE(ReactNativeScannerView) RCT_EXPORT_VIEW_PROPERTY(onQrScanned, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(pauseAfterCapture, BOOL) +RCT_EXPORT_VIEW_PROPERTY(isActive, BOOL) @end diff --git a/src/ReactNativeScannerViewNativeComponent.ts b/src/ReactNativeScannerViewNativeComponent.ts index ae17419..61e715f 100644 --- a/src/ReactNativeScannerViewNativeComponent.ts +++ b/src/ReactNativeScannerViewNativeComponent.ts @@ -1,10 +1,26 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; import type { ViewProps, HostComponent } from 'react-native'; -import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes'; +import type { + DirectEventHandler, + Int32, + Double, +} from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; type Event = Readonly<{ - value: string; + bounds: Readonly<{ + width: Double; + height: Double; + origin: Readonly<{ + topLeft: Readonly<{ x: Double; y: Double }>; + bottomLeft: Readonly<{ x: Double; y: Double }>; + bottomRight: Readonly<{ x: Double; y: Double }>; + topRight: Readonly<{ x: Double; y: Double }>; + }>; + }>; + type: string; + data: string; + target: Int32; }>; interface NativeCommands { @@ -17,14 +33,28 @@ interface NativeCommands { releaseCamera: ( viewRef: React.ElementRef> ) => Promise; + pausePreview: ( + viewRef: React.ElementRef> + ) => void; + resumePreview: ( + viewRef: React.ElementRef> + ) => void; } interface NativeProps extends ViewProps { + pauseAfterCapture?: boolean; + isActive?: boolean; onQrScanned?: DirectEventHandler; // Event name should start with "on" } export const Commands = codegenNativeCommands({ - supportedCommands: ['enableFlashlight', 'disableFlashlight', 'releaseCamera'], + supportedCommands: [ + 'enableFlashlight', + 'disableFlashlight', + 'releaseCamera', + 'pausePreview', + 'resumePreview', + ], }); export default codegenNativeComponent('ReactNativeScannerView');