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:
41
package/ios/Types/AutoFocusSystem.swift
Normal file
41
package/ios/Types/AutoFocusSystem.swift
Normal 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
|
||||
}
|
||||
}
|
119
package/ios/Types/CameraDeviceFormat.swift
Normal file
119
package/ios/Types/CameraDeviceFormat.swift
Normal 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) }
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
class CodeScanner {
|
||||
struct CodeScanner: Equatable {
|
||||
let codeTypes: [AVMetadataObject.ObjectType]
|
||||
let interval: Int
|
||||
let regionOfInterest: CGRect?
|
||||
|
15
package/ios/Types/JSUnionValue.swift
Normal file
15
package/ios/Types/JSUnionValue.swift
Normal 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 }
|
||||
}
|
83
package/ios/Types/Orientation.swift
Normal file
83
package/ios/Types/Orientation.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
46
package/ios/Types/PixelFormat.swift
Normal file
46
package/ios/Types/PixelFormat.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
30
package/ios/Types/RecordVideoOptions.swift
Normal file
30
package/ios/Types/RecordVideoOptions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
45
package/ios/Types/Torch.swift
Normal file
45
package/ios/Types/Torch.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
28
package/ios/Types/Video.swift
Normal file
28
package/ios/Types/Video.swift
Normal 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,
|
||||
]
|
||||
}
|
||||
}
|
47
package/ios/Types/VideoStabilizationMode.swift
Normal file
47
package/ios/Types/VideoStabilizationMode.swift
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user