feat: Code Scanner API (#1912)
* feat: CodeScanner JS API * feat: iOS * Use guard * Format * feat: Android base * fix: Attach Surfaces * Use isBusy var * fix: Use separate Queue * feat: Finish iOS types * feat: Implement all other code types on Android * fix: Call JS event * fix: Pass codetypes on Android * fix: iOS use Preview coordinate system * docs: Add comments * chore: Format code * Update CameraView+AVCaptureSession.swift * docs: Add Code Scanner docs * docs: Update * feat: Use lazily downloaded model on Android * Revert changes in CameraPage * Format * fix: Fix empty QR codes * Update README.md
This commit is contained in:
@@ -229,6 +229,31 @@ enum CaptureError {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CodeScannerError
|
||||
|
||||
enum CodeScannerError {
|
||||
case notCompatibleWithOutputs
|
||||
case codeTypeNotSupported(codeType: String)
|
||||
|
||||
var code: String {
|
||||
switch self {
|
||||
case .notCompatibleWithOutputs:
|
||||
return "not-compatible-with-outputs"
|
||||
case .codeTypeNotSupported:
|
||||
return "code-type-not-supported"
|
||||
}
|
||||
}
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .notCompatibleWithOutputs:
|
||||
return "The Code Scanner is not supported in combination with the current outputs! Either disable video or photo outputs."
|
||||
case let .codeTypeNotSupported(codeType: codeType):
|
||||
return "The codeType \"\(codeType)\" is not supported by the Code Scanner!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CameraError
|
||||
|
||||
enum CameraError: Error {
|
||||
@@ -238,6 +263,7 @@ enum CameraError: Error {
|
||||
case format(_ id: FormatError)
|
||||
case session(_ id: SessionError)
|
||||
case capture(_ id: CaptureError)
|
||||
case codeScanner(_ id: CodeScannerError)
|
||||
case unknown(message: String? = nil)
|
||||
|
||||
var code: String {
|
||||
@@ -254,6 +280,8 @@ enum CameraError: Error {
|
||||
return "session/\(id.code)"
|
||||
case let .capture(id: id):
|
||||
return "capture/\(id.code)"
|
||||
case let .codeScanner(id: id):
|
||||
return "code-scanner/\(id.code)"
|
||||
case .unknown:
|
||||
return "unknown/unknown"
|
||||
}
|
||||
@@ -273,6 +301,8 @@ enum CameraError: Error {
|
||||
return id.message
|
||||
case let .capture(id: id):
|
||||
return id.message
|
||||
case let .codeScanner(id: id):
|
||||
return id.message
|
||||
case let .unknown(message: message):
|
||||
return message ?? "An unexpected error occured."
|
||||
}
|
||||
|
@@ -24,6 +24,13 @@ public class CameraQueues: NSObject {
|
||||
autoreleaseFrequency: .inherit,
|
||||
target: nil)
|
||||
|
||||
/// The serial execution queue for output processing of QR/barcodes.
|
||||
@objc public static let codeScannerQueue = DispatchQueue(label: "mrousavy/VisionCamera.codeScanner",
|
||||
qos: .userInteractive,
|
||||
attributes: [],
|
||||
autoreleaseFrequency: .inherit,
|
||||
target: nil)
|
||||
|
||||
/// The serial execution queue for output processing of audio buffers.
|
||||
@objc public static let audioQueue = DispatchQueue(label: "mrousavy/VisionCamera.audio",
|
||||
qos: .userInteractive,
|
||||
|
@@ -124,6 +124,34 @@ extension CameraView {
|
||||
captureSession.addOutput(videoOutput!)
|
||||
}
|
||||
|
||||
// Code Scanner
|
||||
if let codeScannerOptions = codeScannerOptions {
|
||||
guard let codeScanner = try? CodeScanner(fromJsValue: codeScannerOptions) else {
|
||||
invokeOnError(.parameter(.invalid(unionName: "codeScanner", receivedValue: codeScannerOptions.description)))
|
||||
return
|
||||
}
|
||||
let metadataOutput = AVCaptureMetadataOutput()
|
||||
guard captureSession.canAddOutput(metadataOutput) else {
|
||||
invokeOnError(.codeScanner(.notCompatibleWithOutputs))
|
||||
return
|
||||
}
|
||||
captureSession.addOutput(metadataOutput)
|
||||
|
||||
for codeType in codeScanner.codeTypes {
|
||||
// swiftlint:disable:next for_where
|
||||
if !metadataOutput.availableMetadataObjectTypes.contains(codeType) {
|
||||
invokeOnError(.codeScanner(.codeTypeNotSupported(codeType: codeType.descriptor)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: CameraQueues.codeScannerQueue)
|
||||
metadataOutput.metadataObjectTypes = codeScanner.codeTypes
|
||||
if let rectOfInterest = codeScanner.regionOfInterest {
|
||||
metadataOutput.rectOfInterest = rectOfInterest
|
||||
}
|
||||
}
|
||||
|
||||
if outputOrientation != .portrait {
|
||||
updateOrientation()
|
||||
}
|
||||
|
45
package/ios/CameraView+CodeScanner.swift
Normal file
45
package/ios/CameraView+CodeScanner.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// CameraView+CodeScanner.swift
|
||||
// VisionCamera
|
||||
//
|
||||
// Created by Marc Rousavy on 03.10.23.
|
||||
// Copyright © 2023 mrousavy. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
extension CameraView: AVCaptureMetadataOutputObjectsDelegate {
|
||||
public func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) {
|
||||
guard let onCodeScanned = onCodeScanned else {
|
||||
return
|
||||
}
|
||||
guard !metadataObjects.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
// Map codes to JS values
|
||||
let codes = metadataObjects.map { object in
|
||||
var value: String?
|
||||
if let code = object as? AVMetadataMachineReadableCodeObject {
|
||||
value = code.stringValue
|
||||
}
|
||||
let frame = previewView.layerRectConverted(fromMetadataOutputRect: object.bounds)
|
||||
|
||||
return [
|
||||
"type": object.type.descriptor,
|
||||
"value": value as Any,
|
||||
"frame": [
|
||||
"x": frame.origin.x,
|
||||
"y": frame.origin.y,
|
||||
"width": frame.size.width,
|
||||
"height": frame.size.height,
|
||||
],
|
||||
]
|
||||
}
|
||||
// Call JS event
|
||||
onCodeScanned([
|
||||
"codes": codes,
|
||||
])
|
||||
}
|
||||
}
|
@@ -27,7 +27,8 @@ private let propsThatRequireReconfiguration = ["cameraId",
|
||||
"video",
|
||||
"enableFrameProcessor",
|
||||
"hdr",
|
||||
"pixelFormat"]
|
||||
"pixelFormat",
|
||||
"codeScannerOptions"]
|
||||
private let propsThatRequireDeviceReconfiguration = ["fps",
|
||||
"lowLightBoost"]
|
||||
|
||||
@@ -46,6 +47,7 @@ public final class CameraView: UIView {
|
||||
@objc var video: NSNumber? // nullable bool
|
||||
@objc var audio: NSNumber? // nullable bool
|
||||
@objc var enableFrameProcessor = false
|
||||
@objc var codeScannerOptions: NSDictionary?
|
||||
@objc var pixelFormat: NSString?
|
||||
// props that require format reconfiguring
|
||||
@objc var format: NSDictionary?
|
||||
@@ -69,6 +71,7 @@ public final class CameraView: UIView {
|
||||
@objc var onInitialized: RCTDirectEventBlock?
|
||||
@objc var onError: RCTDirectEventBlock?
|
||||
@objc var onViewReady: RCTDirectEventBlock?
|
||||
@objc var onCodeScanned: RCTDirectEventBlock?
|
||||
// zoom
|
||||
@objc var enableZoomGesture = false {
|
||||
didSet {
|
||||
|
@@ -51,6 +51,9 @@ RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock);
|
||||
// Code Scanner
|
||||
RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
|
||||
|
||||
// Camera View Functions
|
||||
RCT_EXTERN_METHOD(startRecording
|
||||
|
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// AVMetadataObject.ObjectType+descriptor.swift
|
||||
// VisionCamera
|
||||
//
|
||||
// Created by Marc Rousavy on 03.10.23.
|
||||
// Copyright © 2023 mrousavy. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
extension AVMetadataObject.ObjectType {
|
||||
init(withString string: String) throws {
|
||||
switch string {
|
||||
case "code-128":
|
||||
self = .code128
|
||||
return
|
||||
case "code-39":
|
||||
self = .code39
|
||||
return
|
||||
case "code-93":
|
||||
self = .code93
|
||||
return
|
||||
case "codabar":
|
||||
if #available(iOS 15.4, *) {
|
||||
self = .codabar
|
||||
} else {
|
||||
throw CameraError.codeScanner(.codeTypeNotSupported(codeType: string))
|
||||
}
|
||||
return
|
||||
case "ean-13":
|
||||
self = .ean13
|
||||
return
|
||||
case "ean-8":
|
||||
self = .ean8
|
||||
return
|
||||
case "itf":
|
||||
self = .itf14
|
||||
return
|
||||
case "upc-e":
|
||||
self = .upce
|
||||
return
|
||||
case "qr":
|
||||
self = .qr
|
||||
return
|
||||
case "pdf-417":
|
||||
self = .pdf417
|
||||
return
|
||||
case "aztec":
|
||||
self = .aztec
|
||||
return
|
||||
case "data-matrix":
|
||||
self = .dataMatrix
|
||||
return
|
||||
default:
|
||||
throw EnumParserError.invalidValue
|
||||
}
|
||||
}
|
||||
|
||||
var descriptor: String {
|
||||
if #available(iOS 15.4, *) {
|
||||
if self == .codabar {
|
||||
return "codabar"
|
||||
}
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .code128:
|
||||
return "code-128"
|
||||
case .code39:
|
||||
return "code-39"
|
||||
case .code93:
|
||||
return "code-93"
|
||||
case .ean13:
|
||||
return "ean-13"
|
||||
case .ean8:
|
||||
return "ean-8"
|
||||
case .itf14:
|
||||
return "itf"
|
||||
case .upce:
|
||||
return "upce"
|
||||
case .qr:
|
||||
return "qr"
|
||||
case .pdf417:
|
||||
return "pdf-417"
|
||||
case .aztec:
|
||||
return "aztec"
|
||||
case .dataMatrix:
|
||||
return "data-matrix"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
@@ -38,6 +38,10 @@ class PreviewView: UIView {
|
||||
return AVCaptureVideoPreviewLayer.self
|
||||
}
|
||||
|
||||
func layerRectConverted(fromMetadataOutputRect rect: CGRect) -> CGRect {
|
||||
return videoPreviewLayer.layerRectConverted(fromMetadataOutputRect: rect)
|
||||
}
|
||||
|
||||
init(frame: CGRect, session: AVCaptureSession) {
|
||||
super.init(frame: frame)
|
||||
videoPreviewLayer.session = session
|
||||
|
45
package/ios/Types/CodeScanner.swift
Normal file
45
package/ios/Types/CodeScanner.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// CodeScanner.swift
|
||||
// VisionCamera
|
||||
//
|
||||
// Created by Marc Rousavy on 03.10.23.
|
||||
// Copyright © 2023 mrousavy. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
class CodeScanner {
|
||||
let codeTypes: [AVMetadataObject.ObjectType]
|
||||
let interval: Int
|
||||
let regionOfInterest: CGRect?
|
||||
|
||||
init(fromJsValue dictionary: NSDictionary) throws {
|
||||
if let codeTypes = dictionary["codeTypes"] as? [String] {
|
||||
self.codeTypes = try codeTypes.map { value in
|
||||
return try AVMetadataObject.ObjectType(withString: value)
|
||||
}
|
||||
} else {
|
||||
throw CameraError.parameter(.invalidCombination(provided: "codeScanner", missing: "codeTypes"))
|
||||
}
|
||||
|
||||
if let interval = dictionary["interval"] as? Double {
|
||||
self.interval = Int(interval)
|
||||
} else {
|
||||
interval = 300
|
||||
}
|
||||
|
||||
if let regionOfInterest = dictionary["regionOfInterest"] as? NSDictionary {
|
||||
guard let x = regionOfInterest["x"] as? Double,
|
||||
let y = regionOfInterest["y"] as? Double,
|
||||
let width = regionOfInterest["width"] as? Double,
|
||||
let height = regionOfInterest["height"] as? Double else {
|
||||
throw CameraError.parameter(.invalid(unionName: "regionOfInterest", receivedValue: regionOfInterest.description))
|
||||
}
|
||||
|
||||
self.regionOfInterest = CGRect(x: x, y: y, width: width, height: height)
|
||||
} else {
|
||||
regionOfInterest = nil
|
||||
}
|
||||
}
|
||||
}
|
@@ -65,6 +65,9 @@
|
||||
B8DB3BCA263DC4D8004C18D7 /* RecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */; };
|
||||
B8DB3BCC263DC97E004C18D7 /* AVFileType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */; };
|
||||
B8E957D02A693AD2008F5480 /* CameraView+Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E957CF2A693AD2008F5480 /* CameraView+Torch.swift */; };
|
||||
B8FF60AC2ACC93EF009D612F /* CameraView+CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */; };
|
||||
B8FF60AE2ACC9731009D612F /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60AD2ACC9731009D612F /* CodeScanner.swift */; };
|
||||
B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -152,6 +155,9 @@
|
||||
B8F0825E2A6046FC00C17EB6 /* FrameProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessor.h; sourceTree = "<group>"; };
|
||||
B8F0825F2A60491900C17EB6 /* FrameProcessor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessor.mm; sourceTree = "<group>"; };
|
||||
B8F7DDD1266F715D00120533 /* Frame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Frame.m; sourceTree = "<group>"; };
|
||||
B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+CodeScanner.swift"; sourceTree = "<group>"; };
|
||||
B8FF60AD2ACC9731009D612F /* CodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = "<group>"; };
|
||||
B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVMetadataObject.ObjectType+descriptor.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -188,6 +194,7 @@
|
||||
B887518025E0102000DB86D6 /* CameraView+Focus.swift */,
|
||||
B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */,
|
||||
B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */,
|
||||
B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */,
|
||||
B887518225E0102000DB86D6 /* CameraView+Zoom.swift */,
|
||||
B86400512784A23400E9D2CA /* CameraView+Orientation.swift */,
|
||||
B887515F25E0102000DB86D6 /* CameraViewManager.m */,
|
||||
@@ -208,6 +215,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */,
|
||||
B8FF60AD2ACC9731009D612F /* CodeScanner.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
@@ -264,6 +272,7 @@
|
||||
B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */,
|
||||
B864004F27849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift */,
|
||||
B87B11BE2A8E63B700732EBF /* PixelFormat.swift */,
|
||||
B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */,
|
||||
);
|
||||
path = Parsers;
|
||||
sourceTree = "<group>";
|
||||
@@ -413,6 +422,7 @@
|
||||
B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */,
|
||||
B887518B25E0102000DB86D6 /* AVCaptureDevice.Format+isBetterThan.swift in Sources */,
|
||||
B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */,
|
||||
B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */,
|
||||
B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */,
|
||||
B864005027849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift in Sources */,
|
||||
B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */,
|
||||
@@ -429,10 +439,12 @@
|
||||
B881D35E2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift in Sources */,
|
||||
B87B11BF2A8E63B700732EBF /* PixelFormat.swift in Sources */,
|
||||
B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */,
|
||||
B8FF60AC2ACC93EF009D612F /* CameraView+CodeScanner.swift in Sources */,
|
||||
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
|
||||
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
|
||||
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
|
||||
B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */,
|
||||
B8FF60AE2ACC9731009D612F /* CodeScanner.swift in Sources */,
|
||||
B8446E502ABA14C900E56077 /* CameraDevicesManager.m in Sources */,
|
||||
B887519025E0102000DB86D6 /* AVCaptureDevice.Format+matchesFilter.swift in Sources */,
|
||||
B887518F25E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift in Sources */,
|
||||
|
Reference in New Issue
Block a user