Moves everything Camera related into `core/` / `Core/` so that it is better encapsulated from React Native.
Benefits:
1. Code is much better organized. Should be easier for collaborators now, and cleaner codebase for me.
2. Locking is fully atomically as you can now only configure the session through a lock/Mutex which is batch-overridable
    * On iOS, this makes Camera startup time **MUCH** faster, I measured speedups from **1.5 seconds** to only **240 milliseconds** since we only lock/commit once! 🚀 
    * On Android, this fixes a few out-of-sync/concurrency issues like "Capture Request contains unconfigured Input/Output Surface!" since it is now a single lock-operation! 💪 
3. It is easier to integrate VisionCamera outside of React Native (e.g. Native iOS Apps, NativeScript, Flutter, etc)
With this PR, VisionCamera V3 is up to **7x** faster than V2
		
	
		
			
				
	
	
		
			323 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			323 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  CameraView.swift
 | 
						|
//  mrousavy
 | 
						|
//
 | 
						|
//  Created by Marc Rousavy on 09.11.20.
 | 
						|
//  Copyright © 2020 mrousavy. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import AVFoundation
 | 
						|
import Foundation
 | 
						|
import UIKit
 | 
						|
 | 
						|
// TODOs for the CameraView which are currently too hard to implement either because of AVFoundation's limitations, or my brain capacity
 | 
						|
//
 | 
						|
// CameraView+RecordVideo
 | 
						|
// TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI)
 | 
						|
//
 | 
						|
// CameraView+TakePhoto
 | 
						|
// TODO: Photo HDR
 | 
						|
 | 
						|
// MARK: - CameraView
 | 
						|
 | 
						|
public final class CameraView: UIView, CameraSessionDelegate {
 | 
						|
  // pragma MARK: React Properties
 | 
						|
  // props that require reconfiguring
 | 
						|
  @objc var cameraId: NSString?
 | 
						|
  @objc var enableDepthData = false
 | 
						|
  @objc var enableHighQualityPhotos = false
 | 
						|
  @objc var enablePortraitEffectsMatteDelivery = false
 | 
						|
  @objc var enableBufferCompression = false
 | 
						|
  // use cases
 | 
						|
  @objc var photo = false
 | 
						|
  @objc var video = false
 | 
						|
  @objc var audio = false
 | 
						|
  @objc var enableFrameProcessor = false
 | 
						|
  @objc var codeScannerOptions: NSDictionary?
 | 
						|
  @objc var pixelFormat: NSString?
 | 
						|
  // props that require format reconfiguring
 | 
						|
  @objc var format: NSDictionary?
 | 
						|
  @objc var fps: NSNumber?
 | 
						|
  @objc var hdr = false
 | 
						|
  @objc var lowLightBoost = false
 | 
						|
  @objc var orientation: NSString?
 | 
						|
  // other props
 | 
						|
  @objc var isActive = false
 | 
						|
  @objc var torch = "off"
 | 
						|
  @objc var zoom: NSNumber = 1.0 // in "factor"
 | 
						|
  @objc var enableFpsGraph = false
 | 
						|
  @objc var videoStabilizationMode: NSString?
 | 
						|
  @objc var resizeMode: NSString = "cover" {
 | 
						|
    didSet {
 | 
						|
      let parsed = try? ResizeMode(jsValue: resizeMode as String)
 | 
						|
      previewView.resizeMode = parsed ?? .cover
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // events
 | 
						|
  @objc var onInitialized: RCTDirectEventBlock?
 | 
						|
  @objc var onError: RCTDirectEventBlock?
 | 
						|
  @objc var onViewReady: RCTDirectEventBlock?
 | 
						|
  @objc var onCodeScanned: RCTDirectEventBlock?
 | 
						|
  // zoom
 | 
						|
  @objc var enableZoomGesture = false {
 | 
						|
    didSet {
 | 
						|
      if enableZoomGesture {
 | 
						|
        addPinchGestureRecognizer()
 | 
						|
      } else {
 | 
						|
        removePinchGestureRecognizer()
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // pragma MARK: Internal Properties
 | 
						|
  var cameraSession: CameraSession
 | 
						|
  var isMounted = false
 | 
						|
  var isReady = false
 | 
						|
  #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
 | 
						|
    @objc public var frameProcessor: FrameProcessor?
 | 
						|
  #endif
 | 
						|
  // CameraView+Zoom
 | 
						|
  var pinchGestureRecognizer: UIPinchGestureRecognizer?
 | 
						|
  var pinchScaleOffset: CGFloat = 1.0
 | 
						|
 | 
						|
  var previewView: PreviewView
 | 
						|
  #if DEBUG
 | 
						|
    var fpsGraph: RCTFPSGraph?
 | 
						|
  #endif
 | 
						|
 | 
						|
  // pragma MARK: Setup
 | 
						|
 | 
						|
  override public init(frame: CGRect) {
 | 
						|
    // Create CameraSession
 | 
						|
    cameraSession = CameraSession()
 | 
						|
    previewView = cameraSession.createPreviewView(frame: frame)
 | 
						|
    super.init(frame: frame)
 | 
						|
    cameraSession.delegate = self
 | 
						|
 | 
						|
    addSubview(previewView)
 | 
						|
  }
 | 
						|
 | 
						|
  @available(*, unavailable)
 | 
						|
  required init?(coder _: NSCoder) {
 | 
						|
    fatalError("init(coder:) is not implemented.")
 | 
						|
  }
 | 
						|
 | 
						|
  override public func willMove(toSuperview newSuperview: UIView?) {
 | 
						|
    super.willMove(toSuperview: newSuperview)
 | 
						|
 | 
						|
    if newSuperview != nil {
 | 
						|
      if !isMounted {
 | 
						|
        isMounted = true
 | 
						|
        onViewReady?(nil)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  override public func layoutSubviews() {
 | 
						|
    previewView.frame = frame
 | 
						|
    previewView.bounds = bounds
 | 
						|
  }
 | 
						|
 | 
						|
  func getPixelFormat() -> PixelFormat {
 | 
						|
    // TODO: Use ObjC RCT enum parser for this
 | 
						|
    if let pixelFormat = pixelFormat as? String {
 | 
						|
      do {
 | 
						|
        return try PixelFormat(jsValue: pixelFormat)
 | 
						|
      } catch {
 | 
						|
        if let error = error as? CameraError {
 | 
						|
          onError(error)
 | 
						|
        } else {
 | 
						|
          onError(.unknown(message: error.localizedDescription, cause: error as NSError))
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return .native
 | 
						|
  }
 | 
						|
 | 
						|
  func getTorch() -> Torch {
 | 
						|
    // TODO: Use ObjC RCT enum parser for this
 | 
						|
    if let torch = try? Torch(jsValue: torch) {
 | 
						|
      return torch
 | 
						|
    }
 | 
						|
    return .off
 | 
						|
  }
 | 
						|
 | 
						|
  // pragma MARK: Props updating
 | 
						|
  override public final func didSetProps(_ changedProps: [String]!) {
 | 
						|
    ReactLogger.log(level: .info, message: "Updating \(changedProps.count) props: [\(changedProps.joined(separator: ", "))]")
 | 
						|
 | 
						|
    cameraSession.configure { config in
 | 
						|
      // Input Camera Device
 | 
						|
      config.cameraId = cameraId as? String
 | 
						|
 | 
						|
      // Photo
 | 
						|
      if photo {
 | 
						|
        config.photo = .enabled(config: CameraConfiguration.Photo(enableHighQualityPhotos: enableHighQualityPhotos,
 | 
						|
                                                                  enableDepthData: enableDepthData,
 | 
						|
                                                                  enablePortraitEffectsMatte: enablePortraitEffectsMatteDelivery))
 | 
						|
      } else {
 | 
						|
        config.photo = .disabled
 | 
						|
      }
 | 
						|
 | 
						|
      // Video/Frame Processor
 | 
						|
      if video || enableFrameProcessor {
 | 
						|
        config.video = .enabled(config: CameraConfiguration.Video(pixelFormat: getPixelFormat(),
 | 
						|
                                                                  enableBufferCompression: enableBufferCompression,
 | 
						|
                                                                  enableHdr: hdr,
 | 
						|
                                                                  enableFrameProcessor: enableFrameProcessor))
 | 
						|
      } else {
 | 
						|
        config.video = .disabled
 | 
						|
      }
 | 
						|
 | 
						|
      // Audio
 | 
						|
      if audio {
 | 
						|
        config.audio = .enabled(config: CameraConfiguration.Audio())
 | 
						|
      } else {
 | 
						|
        config.audio = .disabled
 | 
						|
      }
 | 
						|
 | 
						|
      // Code Scanner
 | 
						|
      if let codeScannerOptions {
 | 
						|
        let codeScanner = try CodeScanner(fromJsValue: codeScannerOptions)
 | 
						|
        config.codeScanner = .enabled(config: codeScanner)
 | 
						|
      } else {
 | 
						|
        config.codeScanner = .disabled
 | 
						|
      }
 | 
						|
 | 
						|
      // Orientation
 | 
						|
      if let jsOrientation = orientation as? String {
 | 
						|
        let orientation = try Orientation(jsValue: jsOrientation)
 | 
						|
        config.orientation = orientation
 | 
						|
      } else {
 | 
						|
        config.orientation = .portrait
 | 
						|
      }
 | 
						|
 | 
						|
      // Format
 | 
						|
      if let jsFormat = format {
 | 
						|
        let format = try CameraDeviceFormat(jsValue: jsFormat)
 | 
						|
        config.format = format
 | 
						|
      } else {
 | 
						|
        config.format = nil
 | 
						|
      }
 | 
						|
 | 
						|
      // Side-Props
 | 
						|
      config.fps = fps?.int32Value
 | 
						|
      config.enableLowLightBoost = lowLightBoost
 | 
						|
      config.torch = getTorch()
 | 
						|
 | 
						|
      // Zoom
 | 
						|
      config.zoom = zoom.doubleValue
 | 
						|
 | 
						|
      // isActive
 | 
						|
      config.isActive = isActive
 | 
						|
    }
 | 
						|
 | 
						|
    // Store `zoom` offset for native pinch-gesture
 | 
						|
    if changedProps.contains("zoom") {
 | 
						|
      pinchScaleOffset = zoom.doubleValue
 | 
						|
    }
 | 
						|
 | 
						|
    // Set up Debug FPS Graph
 | 
						|
    if changedProps.contains("enableFpsGraph") {
 | 
						|
      DispatchQueue.main.async {
 | 
						|
        self.setupFpsGraph()
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  func setupFpsGraph() {
 | 
						|
    #if DEBUG
 | 
						|
      if enableFpsGraph {
 | 
						|
        if fpsGraph != nil { return }
 | 
						|
        fpsGraph = RCTFPSGraph(frame: CGRect(x: 10, y: 54, width: 75, height: 45), color: .red)
 | 
						|
        fpsGraph!.layer.zPosition = 9999.0
 | 
						|
        addSubview(fpsGraph!)
 | 
						|
      } else {
 | 
						|
        fpsGraph?.removeFromSuperview()
 | 
						|
        fpsGraph = nil
 | 
						|
      }
 | 
						|
    #endif
 | 
						|
  }
 | 
						|
 | 
						|
  // pragma MARK: Event Invokers
 | 
						|
 | 
						|
  func onError(_ error: CameraError) {
 | 
						|
    ReactLogger.log(level: .error, message: "Invoking onError(): \(error.message)")
 | 
						|
    guard let onError = onError else {
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    var causeDictionary: [String: Any]?
 | 
						|
    if case let .unknown(_, cause) = error,
 | 
						|
       let cause = cause {
 | 
						|
      causeDictionary = [
 | 
						|
        "code": cause.code,
 | 
						|
        "domain": cause.domain,
 | 
						|
        "message": cause.description,
 | 
						|
        "details": cause.userInfo,
 | 
						|
      ]
 | 
						|
    }
 | 
						|
    onError([
 | 
						|
      "code": error.code,
 | 
						|
      "message": error.message,
 | 
						|
      "cause": causeDictionary ?? NSNull(),
 | 
						|
    ])
 | 
						|
  }
 | 
						|
 | 
						|
  func onSessionInitialized() {
 | 
						|
    ReactLogger.log(level: .info, message: "Camera initialized!")
 | 
						|
    guard let onInitialized = onInitialized else {
 | 
						|
      return
 | 
						|
    }
 | 
						|
    onInitialized([String: Any]())
 | 
						|
  }
 | 
						|
 | 
						|
  func onFrame(sampleBuffer: CMSampleBuffer) {
 | 
						|
    #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
 | 
						|
      if let frameProcessor = frameProcessor {
 | 
						|
        // Call Frame Processor
 | 
						|
        let frame = Frame(buffer: sampleBuffer, orientation: bufferOrientation)
 | 
						|
        frameProcessor.call(frame)
 | 
						|
      }
 | 
						|
    #endif
 | 
						|
 | 
						|
    #if DEBUG
 | 
						|
      if let fpsGraph {
 | 
						|
        fpsGraph.onTick(CACurrentMediaTime())
 | 
						|
      }
 | 
						|
    #endif
 | 
						|
  }
 | 
						|
 | 
						|
  func onCodeScanned(codes: [CameraSession.Code]) {
 | 
						|
    guard let onCodeScanned = onCodeScanned else {
 | 
						|
      return
 | 
						|
    }
 | 
						|
    onCodeScanned([
 | 
						|
      "codes": codes.map { $0.toJSValue() },
 | 
						|
    ])
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   Gets the orientation of the CameraView's images (CMSampleBuffers).
 | 
						|
   */
 | 
						|
  private var bufferOrientation: UIImage.Orientation {
 | 
						|
    guard let cameraPosition = cameraSession.videoDeviceInput?.device.position else {
 | 
						|
      return .up
 | 
						|
    }
 | 
						|
    let orientation = cameraSession.configuration?.orientation ?? .portrait
 | 
						|
 | 
						|
    // TODO: I think this is wrong.
 | 
						|
    switch orientation {
 | 
						|
    case .portrait:
 | 
						|
      return cameraPosition == .front ? .leftMirrored : .right
 | 
						|
    case .landscapeLeft:
 | 
						|
      return cameraPosition == .front ? .downMirrored : .up
 | 
						|
    case .portraitUpsideDown:
 | 
						|
      return cameraPosition == .front ? .rightMirrored : .left
 | 
						|
    case .landscapeRight:
 | 
						|
      return cameraPosition == .front ? .upMirrored : .down
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |