feat: New Core/ library (#1975)

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
This commit is contained in:
Marc Rousavy
2023-10-13 18:33:20 +02:00
committed by GitHub
parent 54871022f4
commit cd0b413706
72 changed files with 2326 additions and 1521 deletions

View File

@@ -0,0 +1,41 @@
//
// AutoFocusSystem.swift
// VisionCamera
//
// Created by Marc Rousavy on 13.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
enum AutoFocusSystem: String, JSUnionValue {
case contrastDetection = "contrast-detection"
case phaseDetection
case none
init(jsValue: String) throws {
if let parsed = AutoFocusSystem(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "autoFocusSystem", receivedValue: jsValue))
}
}
init(fromFocusSystem focusSystem: AVCaptureDevice.Format.AutoFocusSystem) {
switch focusSystem {
case .none:
self = .none
case .contrastDetection:
self = .contrastDetection
case .phaseDetection:
self = .phaseDetection
@unknown default:
self = .none
}
}
var jsValue: String {
return rawValue
}
}

View File

@@ -0,0 +1,119 @@
//
// CameraDeviceFormat.swift
// VisionCamera
//
// Created by Marc Rousavy on 13.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
/**
A serializable representation of [AVCaptureDevice.Format]
*/
struct CameraDeviceFormat: Equatable, CustomStringConvertible {
let videoWidth: Int
let videoHeight: Int
let photoWidth: Int
let photoHeight: Int
let minFps: Double
let maxFps: Double
let minISO: Float
let maxISO: Float
let fieldOfView: Float
let maxZoom: Double
let videoStabilizationModes: [VideoStabilizationMode]
let autoFocusSystem: AutoFocusSystem
let supportsVideoHDR: Bool
let supportsPhotoHDR: Bool
let pixelFormats: [PixelFormat]
let supportsDepthCapture: Bool
init(fromFormat format: AVCaptureDevice.Format) {
videoWidth = Int(format.videoDimensions.width)
videoHeight = Int(format.videoDimensions.height)
photoWidth = Int(format.photoDimensions.width)
photoHeight = Int(format.photoDimensions.height)
minFps = format.minFps
maxFps = format.maxFps
minISO = format.minISO
maxISO = format.maxISO
fieldOfView = format.videoFieldOfView
maxZoom = format.videoMaxZoomFactor
videoStabilizationModes = format.videoStabilizationModes.map { VideoStabilizationMode(from: $0) }
autoFocusSystem = AutoFocusSystem(fromFocusSystem: format.autoFocusSystem)
supportsVideoHDR = format.supportsVideoHDR
supportsPhotoHDR = format.supportsPhotoHDR
pixelFormats = CameraDeviceFormat.getAllPixelFormats()
supportsDepthCapture = format.supportsDepthCapture
}
init(jsValue: NSDictionary) throws {
// swiftlint:disable force_cast
videoWidth = jsValue["videoWidth"] as! Int
videoHeight = jsValue["videoHeight"] as! Int
photoWidth = jsValue["photoWidth"] as! Int
photoHeight = jsValue["photoHeight"] as! Int
minFps = jsValue["minFps"] as! Double
maxFps = jsValue["maxFps"] as! Double
minISO = jsValue["minISO"] as! Float
maxISO = jsValue["maxISO"] as! Float
fieldOfView = jsValue["fieldOfView"] as! Float
maxZoom = jsValue["maxZoom"] as! Double
let jsVideoStabilizationModes = jsValue["videoStabilizationModes"] as! [String]
videoStabilizationModes = try jsVideoStabilizationModes.map { try VideoStabilizationMode(jsValue: $0) }
let jsAutoFocusSystem = jsValue["autoFocusSystem"] as! String
autoFocusSystem = try AutoFocusSystem(jsValue: jsAutoFocusSystem)
supportsVideoHDR = jsValue["supportsVideoHDR"] as! Bool
supportsPhotoHDR = jsValue["supportsPhotoHDR"] as! Bool
let jsPixelFormats = jsValue["pixelFormats"] as! [String]
pixelFormats = try jsPixelFormats.map { try PixelFormat(jsValue: $0) }
supportsDepthCapture = jsValue["supportsDepthCapture"] as! Bool
// swiftlint:enable force_cast
}
func isEqualTo(format other: AVCaptureDevice.Format) -> Bool {
let other = CameraDeviceFormat(fromFormat: other)
return self == other
}
func toJSValue() -> NSDictionary {
return [
"videoStabilizationModes": videoStabilizationModes.map(\.jsValue),
"autoFocusSystem": autoFocusSystem.jsValue,
"photoHeight": photoHeight,
"photoWidth": photoWidth,
"videoHeight": videoHeight,
"videoWidth": videoWidth,
"maxISO": maxISO,
"minISO": minISO,
"fieldOfView": fieldOfView,
"maxZoom": maxZoom,
"supportsVideoHDR": supportsVideoHDR,
"supportsPhotoHDR": supportsPhotoHDR,
"minFps": minFps,
"maxFps": maxFps,
"pixelFormats": pixelFormats.map(\.jsValue),
"supportsDepthCapture": supportsDepthCapture,
]
}
var description: String {
return "\(photoWidth)x\(photoHeight) | \(videoWidth)x\(videoHeight)@\(maxFps) (ISO: \(minISO)..\(maxISO), Pixel Formats: \(pixelFormats))"
}
// On iOS, all PixelFormats are always supported for every format (it can convert natively)
private static func getAllPixelFormats() -> [PixelFormat] {
let availablePixelFormats = AVCaptureVideoDataOutput().availableVideoPixelFormatTypes
return availablePixelFormats.map { format in PixelFormat(mediaSubType: format) }
}
}

View File

@@ -9,7 +9,7 @@
import AVFoundation
import Foundation
class CodeScanner {
struct CodeScanner: Equatable {
let codeTypes: [AVMetadataObject.ObjectType]
let interval: Int
let regionOfInterest: CGRect?

View File

@@ -0,0 +1,15 @@
//
// JSUnionValue.swift
// VisionCamera
//
// Created by Marc Rousavy on 13.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import Foundation
protocol JSUnionValue {
init(jsValue: String) throws
var jsValue: String { get }
}

View File

@@ -0,0 +1,83 @@
//
// Orientation.swift
// VisionCamera
//
// Created by Marc Rousavy on 11.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
/**
The Orientation used for the Preview, Photo, Video and Frame Processor outputs.
*/
enum Orientation: String, JSUnionValue {
/**
Phone is in upright portrait mode, home button/indicator is at the bottom
*/
case portrait
/**
Phone is in landscape mode, home button/indicator is on the left
*/
case landscapeLeft = "landscape-left"
/**
Phone is in upside-down portrait mode, home button/indicator is at the top
*/
case portraitUpsideDown = "portrait-upside-down"
/**
Phone is in landscape mode, home button/indicator is on the right
*/
case landscapeRight = "landscape-right"
init(jsValue: String) throws {
if let parsed = Orientation(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "orientation", receivedValue: jsValue))
}
}
var jsValue: String {
return rawValue
}
func toAVCaptureVideoOrientation() -> AVCaptureVideoOrientation {
switch self {
case .portrait:
return .portrait
case .landscapeLeft:
return .landscapeLeft
case .portraitUpsideDown:
return .portraitUpsideDown
case .landscapeRight:
return .landscapeRight
}
}
func toDegrees() -> Double {
switch self {
case .portrait:
return 0
case .landscapeLeft:
return 90
case .portraitUpsideDown:
return 180
case .landscapeRight:
return 270
}
}
func rotateRight() -> Orientation {
switch self {
case .portrait:
return .landscapeLeft
case .landscapeLeft:
return .portraitUpsideDown
case .portraitUpsideDown:
return .landscapeRight
case .landscapeRight:
return .portrait
}
}
}

View File

@@ -0,0 +1,46 @@
//
// PixelFormat.swift
// VisionCamera
//
// Created by Marc Rousavy on 17.08.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
enum PixelFormat: String, JSUnionValue {
case yuv
case rgb
case native
case unknown
init(jsValue: String) throws {
if let parsed = PixelFormat(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "pixelFormat", receivedValue: jsValue))
}
}
var jsValue: String {
return rawValue
}
init(mediaSubType: OSType) {
switch mediaSubType {
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
kCVPixelFormatType_420YpCbCr10BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange,
kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange,
kCVPixelFormatType_Lossless_420YpCbCr10PackedBiPlanarVideoRange:
self = .yuv
case kCVPixelFormatType_32BGRA, kCVPixelFormatType_Lossless_32BGRA:
self = .rgb
default:
self = .unknown
}
}
}

View File

@@ -0,0 +1,30 @@
//
// RecordVideoOptions.swift
// VisionCamera
//
// Created by Marc Rousavy on 12.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
struct RecordVideoOptions {
var fileType: AVFileType = .mov
var flash: Torch = .off
var codec: AVVideoCodecType?
/**
Bit-Rate of the Video, in Megabits per second (Mbps)
*/
var bitRate: Double?
init(fromJSValue dictionary: NSDictionary) throws {
// File Type (.mov or .mp4)
if let fileTypeOption = dictionary["fileType"] as? String {
guard let parsed = try? AVFileType(withString: fileTypeOption) else {
throw CameraError.parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption))
}
fileType = parsed
}
}
}

View File

@@ -6,12 +6,13 @@
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
/**
A ResizeMode used for the PreviewView.
*/
enum ResizeMode {
enum ResizeMode: String, JSUnionValue {
/**
Keep aspect ratio, but fill entire parent view (centered).
*/
@@ -21,15 +22,15 @@ enum ResizeMode {
*/
case contain
init(fromTypeScriptUnion union: String) {
switch union {
case "cover":
self = .cover
case "contain":
self = .contain
default:
// TODO: Use the onError event for safer error handling!
fatalError("Invalid value passed for resizeMode! (\(union))")
init(jsValue: String) throws {
if let parsed = ResizeMode(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "resizeMode", receivedValue: jsValue))
}
}
var jsValue: String {
return rawValue
}
}

View File

@@ -0,0 +1,45 @@
//
// Torch.swift
// VisionCamera
//
// Created by Marc Rousavy on 11.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
/**
A Torch used for permanent flash.
*/
enum Torch: String, JSUnionValue {
/**
Torch (flash unit) is always off.
*/
case off
/**
Torch (flash unit) is always on.
*/
case on
init(jsValue: String) throws {
if let parsed = Torch(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "torch", receivedValue: jsValue))
}
}
var jsValue: String {
return rawValue
}
func toTorchMode() -> AVCaptureDevice.TorchMode {
switch self {
case .on:
return .on
case .off:
return .off
}
}
}

View File

@@ -0,0 +1,28 @@
//
// Video.swift
// VisionCamera
//
// Created by Marc Rousavy on 12.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
struct Video {
/**
Path to the temporary video file
*/
var path: String
/**
Duration of the recorded video (in seconds)
*/
var duration: Double
func toJSValue() -> NSDictionary {
return [
"path": path,
"duration": duration,
]
}
}

View File

@@ -0,0 +1,47 @@
//
// VideoStabilizationMode.swift
// VisionCamera
//
// Created by Marc Rousavy on 13.10.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
enum VideoStabilizationMode: String, JSUnionValue {
case off
case standard
case cinematic
case cinematicExtended = "cinematic-extended"
case auto
init(jsValue: String) throws {
if let parsed = VideoStabilizationMode(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "videoStabilizationMode", receivedValue: jsValue))
}
}
init(from mode: AVCaptureVideoStabilizationMode) {
switch mode {
case .off:
self = .off
case .standard:
self = .standard
case .cinematic:
self = .cinematic
case .cinematicExtended:
self = .cinematicExtended
case .auto:
self = .auto
default:
self = .off
}
}
var jsValue: String {
return rawValue
}
}