2021-02-19 08:28:05 -07:00
|
|
|
//
|
|
|
|
// CameraView.swift
|
2021-06-21 14:42:46 -06:00
|
|
|
// mrousavy
|
2021-02-19 08:28:05 -07:00
|
|
|
//
|
|
|
|
// Created by Marc Rousavy on 09.11.20.
|
2021-06-01 05:07:57 -06:00
|
|
|
// Copyright © 2020 mrousavy. All rights reserved.
|
2021-02-19 08:28:05 -07:00
|
|
|
//
|
|
|
|
|
|
|
|
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)
|
2023-10-13 10:33:20 -06:00
|
|
|
//
|
2021-02-19 08:28:05 -07:00
|
|
|
// CameraView+TakePhoto
|
|
|
|
// TODO: Photo HDR
|
|
|
|
|
2021-03-09 02:53:29 -07:00
|
|
|
// MARK: - CameraView
|
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
public final class CameraView: UIView, CameraSessionDelegate {
|
2021-03-31 07:43:29 -06:00
|
|
|
// pragma MARK: React Properties
|
2021-02-19 08:28:05 -07:00
|
|
|
// props that require reconfiguring
|
|
|
|
@objc var cameraId: NSString?
|
|
|
|
@objc var enableDepthData = false
|
2023-10-13 10:33:20 -06:00
|
|
|
@objc var enableHighQualityPhotos = false
|
2021-02-19 08:28:05 -07:00
|
|
|
@objc var enablePortraitEffectsMatteDelivery = false
|
2023-09-21 09:18:54 -06:00
|
|
|
@objc var enableBufferCompression = false
|
2021-06-07 05:08:40 -06:00
|
|
|
// use cases
|
2023-10-13 10:33:20 -06:00
|
|
|
@objc var photo = false
|
|
|
|
@objc var video = false
|
|
|
|
@objc var audio = false
|
2021-07-12 07:16:03 -06:00
|
|
|
@objc var enableFrameProcessor = false
|
2023-10-04 04:53:52 -06:00
|
|
|
@objc var codeScannerOptions: NSDictionary?
|
2023-08-21 04:50:14 -06:00
|
|
|
@objc var pixelFormat: NSString?
|
2021-02-19 08:28:05 -07:00
|
|
|
// props that require format reconfiguring
|
|
|
|
@objc var format: NSDictionary?
|
|
|
|
@objc var fps: NSNumber?
|
2023-11-15 10:33:12 -07:00
|
|
|
@objc var videoHdr = false
|
|
|
|
@objc var photoHdr = false
|
2023-10-13 10:33:20 -06:00
|
|
|
@objc var lowLightBoost = false
|
2022-01-04 08:57:40 -07:00
|
|
|
@objc var orientation: NSString?
|
2021-02-19 08:28:05 -07:00
|
|
|
// other props
|
|
|
|
@objc var isActive = false
|
|
|
|
@objc var torch = "off"
|
2021-07-29 03:44:22 -06:00
|
|
|
@objc var zoom: NSNumber = 1.0 // in "factor"
|
2023-11-19 07:26:43 -07:00
|
|
|
@objc var exposure: NSNumber = 1.0
|
2023-02-21 07:00:48 -07:00
|
|
|
@objc var enableFpsGraph = false
|
2021-06-03 07:42:02 -06:00
|
|
|
@objc var videoStabilizationMode: NSString?
|
2023-09-23 02:14:27 -06:00
|
|
|
@objc var resizeMode: NSString = "cover" {
|
|
|
|
didSet {
|
2023-10-13 10:33:20 -06:00
|
|
|
let parsed = try? ResizeMode(jsValue: resizeMode as String)
|
|
|
|
previewView.resizeMode = parsed ?? .cover
|
2023-09-23 02:14:27 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-19 08:28:05 -07:00
|
|
|
// events
|
|
|
|
@objc var onInitialized: RCTDirectEventBlock?
|
|
|
|
@objc var onError: RCTDirectEventBlock?
|
2023-12-09 11:09:55 -07:00
|
|
|
@objc var onStarted: RCTDirectEventBlock?
|
|
|
|
@objc var onStopped: RCTDirectEventBlock?
|
2021-10-11 10:27:23 -06:00
|
|
|
@objc var onViewReady: RCTDirectEventBlock?
|
2024-07-15 02:55:47 -06:00
|
|
|
@objc var onInitReady: RCTDirectEventBlock?
|
2024-07-15 02:50:39 -06:00
|
|
|
@objc var onVideoChunkReady: RCTDirectEventBlock?
|
2023-10-04 04:53:52 -06:00
|
|
|
@objc var onCodeScanned: RCTDirectEventBlock?
|
2021-03-31 07:43:29 -06:00
|
|
|
// zoom
|
|
|
|
@objc var enableZoomGesture = false {
|
|
|
|
didSet {
|
|
|
|
if enableZoomGesture {
|
|
|
|
addPinchGestureRecognizer()
|
|
|
|
} else {
|
|
|
|
removePinchGestureRecognizer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// pragma MARK: Internal Properties
|
2023-10-13 10:33:20 -06:00
|
|
|
var cameraSession: CameraSession
|
2023-09-01 04:58:32 -06:00
|
|
|
var isMounted = false
|
|
|
|
var isReady = false
|
2023-07-20 07:30:04 -06:00
|
|
|
#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
|
|
|
|
@objc public var frameProcessor: FrameProcessor?
|
|
|
|
#endif
|
2021-02-19 08:28:05 -07:00
|
|
|
// CameraView+Zoom
|
2023-09-01 04:58:32 -06:00
|
|
|
var pinchGestureRecognizer: UIPinchGestureRecognizer?
|
|
|
|
var pinchScaleOffset: CGFloat = 1.0
|
2024-01-08 03:41:57 -07:00
|
|
|
private var currentConfigureCall: DispatchTime?
|
2021-02-19 08:28:05 -07:00
|
|
|
|
2023-09-01 05:08:33 -06:00
|
|
|
var previewView: PreviewView
|
2023-02-21 07:00:48 -07:00
|
|
|
#if DEBUG
|
2023-09-01 04:58:32 -06:00
|
|
|
var fpsGraph: RCTFPSGraph?
|
2023-02-21 07:00:48 -07:00
|
|
|
#endif
|
|
|
|
|
2021-03-31 07:43:29 -06:00
|
|
|
// pragma MARK: Setup
|
2023-10-13 10:33:20 -06:00
|
|
|
|
2021-03-31 07:43:29 -06:00
|
|
|
override public init(frame: CGRect) {
|
2023-10-13 10:33:20 -06:00
|
|
|
// Create CameraSession
|
|
|
|
cameraSession = CameraSession()
|
|
|
|
previewView = cameraSession.createPreviewView(frame: frame)
|
2021-03-31 07:43:29 -06:00
|
|
|
super.init(frame: frame)
|
2023-10-13 10:33:20 -06:00
|
|
|
cameraSession.delegate = self
|
2021-03-31 07:43:29 -06:00
|
|
|
|
2023-09-01 07:07:16 -06:00
|
|
|
addSubview(previewView)
|
2021-03-31 07:43:29 -06:00
|
|
|
}
|
|
|
|
|
2021-05-06 06:11:55 -06:00
|
|
|
@available(*, unavailable)
|
|
|
|
required init?(coder _: NSCoder) {
|
|
|
|
fatalError("init(coder:) is not implemented.")
|
|
|
|
}
|
|
|
|
|
2021-10-11 10:27:23 -06:00
|
|
|
override public func willMove(toSuperview newSuperview: UIView?) {
|
|
|
|
super.willMove(toSuperview: newSuperview)
|
2023-02-21 07:00:48 -07:00
|
|
|
|
|
|
|
if newSuperview != nil {
|
|
|
|
if !isMounted {
|
|
|
|
isMounted = true
|
2023-07-20 07:30:04 -06:00
|
|
|
onViewReady?(nil)
|
2021-10-11 10:27:23 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-21 07:00:48 -07:00
|
|
|
override public func layoutSubviews() {
|
2023-09-01 05:08:33 -06:00
|
|
|
previewView.frame = frame
|
|
|
|
previewView.bounds = bounds
|
2023-02-21 07:00:48 -07:00
|
|
|
}
|
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-03-26 09:28:08 -06:00
|
|
|
// pragma MARK: Props updating
|
2021-03-31 07:43:29 -06:00
|
|
|
override public final func didSetProps(_ changedProps: [String]!) {
|
2023-10-13 10:33:20 -06:00
|
|
|
ReactLogger.log(level: .info, message: "Updating \(changedProps.count) props: [\(changedProps.joined(separator: ", "))]")
|
2024-01-08 03:41:57 -07:00
|
|
|
let now = DispatchTime.now()
|
|
|
|
currentConfigureCall = now
|
|
|
|
|
|
|
|
cameraSession.configure { [self] config in
|
|
|
|
// Check if we're still the latest call to configure { ... }
|
|
|
|
guard currentConfigureCall == now else {
|
|
|
|
// configure waits for a lock, and if a new call to update() happens in the meantime we can drop this one.
|
|
|
|
// this works similar to how React implemented concurrent rendering, the newer call to update() has higher priority.
|
|
|
|
ReactLogger.log(level: .info, message: "A new configure { ... } call arrived, aborting this one...")
|
|
|
|
return
|
|
|
|
}
|
2021-03-26 09:28:08 -06:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Input Camera Device
|
|
|
|
config.cameraId = cameraId as? String
|
2021-03-26 09:28:08 -06:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Photo
|
|
|
|
if photo {
|
|
|
|
config.photo = .enabled(config: CameraConfiguration.Photo(enableHighQualityPhotos: enableHighQualityPhotos,
|
|
|
|
enableDepthData: enableDepthData,
|
|
|
|
enablePortraitEffectsMatte: enablePortraitEffectsMatteDelivery))
|
|
|
|
} else {
|
|
|
|
config.photo = .disabled
|
2023-02-21 07:00:48 -07:00
|
|
|
}
|
2021-06-03 07:42:02 -06:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Video/Frame Processor
|
|
|
|
if video || enableFrameProcessor {
|
|
|
|
config.video = .enabled(config: CameraConfiguration.Video(pixelFormat: getPixelFormat(),
|
|
|
|
enableBufferCompression: enableBufferCompression,
|
2023-11-15 10:33:12 -07:00
|
|
|
enableHdr: videoHdr,
|
2023-10-13 10:33:20 -06:00
|
|
|
enableFrameProcessor: enableFrameProcessor))
|
|
|
|
} else {
|
|
|
|
config.video = .disabled
|
|
|
|
}
|
2021-03-26 09:28:08 -06:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Audio
|
|
|
|
if audio {
|
|
|
|
config.audio = .enabled(config: CameraConfiguration.Audio())
|
|
|
|
} else {
|
|
|
|
config.audio = .disabled
|
|
|
|
}
|
2021-03-26 09:28:08 -06:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Code Scanner
|
|
|
|
if let codeScannerOptions {
|
2023-10-16 08:56:39 -06:00
|
|
|
let options = try CodeScannerOptions(fromJsValue: codeScannerOptions)
|
|
|
|
config.codeScanner = .enabled(config: CameraConfiguration.CodeScanner(options: options))
|
2023-10-13 10:33:20 -06:00
|
|
|
} else {
|
|
|
|
config.codeScanner = .disabled
|
|
|
|
}
|
2021-03-26 09:28:08 -06:00
|
|
|
|
2023-11-15 09:00:41 -07:00
|
|
|
// Video Stabilization
|
|
|
|
if let jsVideoStabilizationMode = videoStabilizationMode as? String {
|
|
|
|
let videoStabilizationMode = try VideoStabilizationMode(jsValue: jsVideoStabilizationMode)
|
|
|
|
config.videoStabilizationMode = videoStabilizationMode
|
|
|
|
} else {
|
|
|
|
config.videoStabilizationMode = .off
|
|
|
|
}
|
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Orientation
|
|
|
|
if let jsOrientation = orientation as? String {
|
|
|
|
let orientation = try Orientation(jsValue: jsOrientation)
|
|
|
|
config.orientation = orientation
|
|
|
|
} else {
|
|
|
|
config.orientation = .portrait
|
|
|
|
}
|
2022-01-04 08:57:40 -07:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Format
|
|
|
|
if let jsFormat = format {
|
|
|
|
let format = try CameraDeviceFormat(jsValue: jsFormat)
|
|
|
|
config.format = format
|
|
|
|
} else {
|
|
|
|
config.format = nil
|
2021-03-26 09:28:08 -06:00
|
|
|
}
|
2021-06-07 05:08:40 -06:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// Side-Props
|
|
|
|
config.fps = fps?.int32Value
|
|
|
|
config.enableLowLightBoost = lowLightBoost
|
2023-11-23 14:19:44 -07:00
|
|
|
config.torch = try Torch(jsValue: torch)
|
2023-10-13 10:33:20 -06:00
|
|
|
|
|
|
|
// Zoom
|
|
|
|
config.zoom = zoom.doubleValue
|
|
|
|
|
2023-11-19 07:26:43 -07:00
|
|
|
// Exposure
|
|
|
|
config.exposure = exposure.floatValue
|
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
// 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()
|
2021-06-07 05:08:40 -06:00
|
|
|
}
|
2021-03-26 09:28:08 -06:00
|
|
|
}
|
2024-01-19 04:35:33 -07:00
|
|
|
|
|
|
|
// Prevent phone from going to sleep
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = isActive
|
2021-03-26 09:28:08 -06:00
|
|
|
}
|
2023-09-01 07:07:16 -06:00
|
|
|
|
2023-09-01 05:08:33 -06:00
|
|
|
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
|
|
|
|
}
|
2021-03-26 09:28:08 -06:00
|
|
|
|
2021-03-09 02:53:29 -07:00
|
|
|
// pragma MARK: Event Invokers
|
2023-10-13 10:33:20 -06:00
|
|
|
|
|
|
|
func onError(_ error: CameraError) {
|
2021-03-29 06:12:04 -06:00
|
|
|
ReactLogger.log(level: .error, message: "Invoking onError(): \(error.message)")
|
2023-10-13 10:33:20 -06:00
|
|
|
guard let onError = onError else {
|
|
|
|
return
|
|
|
|
}
|
2021-02-23 02:27:31 -07:00
|
|
|
|
2021-03-09 02:53:29 -07:00
|
|
|
var causeDictionary: [String: Any]?
|
2023-10-13 10:33:20 -06:00
|
|
|
if case let .unknown(_, cause) = error,
|
|
|
|
let cause = cause {
|
2021-03-09 02:53:29 -07:00
|
|
|
causeDictionary = [
|
|
|
|
"code": cause.code,
|
|
|
|
"domain": cause.domain,
|
2021-03-26 08:54:27 -06:00
|
|
|
"message": cause.description,
|
2021-03-09 02:53:29 -07:00
|
|
|
"details": cause.userInfo,
|
|
|
|
]
|
|
|
|
}
|
|
|
|
onError([
|
|
|
|
"code": error.code,
|
|
|
|
"message": error.message,
|
|
|
|
"cause": causeDictionary ?? NSNull(),
|
|
|
|
])
|
|
|
|
}
|
2021-02-23 02:27:31 -07:00
|
|
|
|
2023-10-13 10:33:20 -06:00
|
|
|
func onSessionInitialized() {
|
2021-03-29 06:12:04 -06:00
|
|
|
ReactLogger.log(level: .info, message: "Camera initialized!")
|
2023-10-13 10:33:20 -06:00
|
|
|
guard let onInitialized = onInitialized else {
|
|
|
|
return
|
|
|
|
}
|
2023-12-09 11:09:55 -07:00
|
|
|
onInitialized([:])
|
|
|
|
}
|
|
|
|
|
|
|
|
func onCameraStarted() {
|
|
|
|
ReactLogger.log(level: .info, message: "Camera started!")
|
|
|
|
guard let onStarted = onStarted else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
onStarted([:])
|
|
|
|
}
|
|
|
|
|
|
|
|
func onCameraStopped() {
|
|
|
|
ReactLogger.log(level: .info, message: "Camera stopped!")
|
|
|
|
guard let onStopped = onStopped else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
onStopped([:])
|
2021-03-09 02:53:29 -07:00
|
|
|
}
|
2023-10-13 10:33:20 -06:00
|
|
|
|
|
|
|
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 {
|
2023-10-14 04:41:53 -06:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
fpsGraph.onTick(CACurrentMediaTime())
|
|
|
|
}
|
2023-10-13 10:33:20 -06:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
2024-07-15 02:50:39 -06:00
|
|
|
|
|
|
|
func onVideoChunkReady(chunk: ChunkedRecorder.Chunk) {
|
|
|
|
ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)")
|
|
|
|
|
2024-07-15 02:55:47 -06:00
|
|
|
guard let onVideoChunkReady, let onInitReady else {
|
2024-07-16 03:20:13 -06:00
|
|
|
ReactLogger.log(level: .warning, message: "Either onInitReady or onVideoChunkReady are not valid!")
|
2024-07-15 02:50:39 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch chunk.type {
|
|
|
|
case .initialization:
|
2024-07-15 02:55:47 -06:00
|
|
|
onInitReady([
|
|
|
|
"filepath": chunk.url.path,
|
|
|
|
])
|
2024-07-16 03:46:24 -06:00
|
|
|
case let .data(index: index, duration: duration):
|
|
|
|
var data: [String: Any] = [
|
2024-07-15 02:50:39 -06:00
|
|
|
"filepath": chunk.url.path,
|
|
|
|
"index": index,
|
2024-07-16 03:46:24 -06:00
|
|
|
]
|
|
|
|
if let duration {
|
|
|
|
data["duration"] = duration.seconds
|
|
|
|
}
|
|
|
|
onVideoChunkReady(data)
|
2024-07-15 02:50:39 -06:00
|
|
|
}
|
|
|
|
}
|
2023-10-13 10:33:20 -06:00
|
|
|
|
2023-11-09 03:57:05 -07:00
|
|
|
func onCodeScanned(codes: [CameraSession.Code], scannerFrame: CameraSession.CodeScannerFrame) {
|
2023-10-13 10:33:20 -06:00
|
|
|
guard let onCodeScanned = onCodeScanned else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
onCodeScanned([
|
|
|
|
"codes": codes.map { $0.toJSValue() },
|
2023-11-09 03:57:05 -07:00
|
|
|
"frame": scannerFrame.toJSValue(),
|
2023-10-13 10:33:20 -06:00
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2021-02-19 08:28:05 -07:00
|
|
|
}
|