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:
231
package/ios/Core/CameraConfiguration.swift
Normal file
231
package/ios/Core/CameraConfiguration.swift
Normal file
@@ -0,0 +1,231 @@
|
||||
//
|
||||
// CameraConfiguration.swift
|
||||
// VisionCamera
|
||||
//
|
||||
// Created by Marc Rousavy on 11.10.23.
|
||||
// Copyright © 2023 mrousavy. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
// MARK: - CameraConfiguration
|
||||
|
||||
class CameraConfiguration {
|
||||
// pragma MARK: Configuration Props
|
||||
|
||||
// Input
|
||||
var cameraId: String?
|
||||
|
||||
// Outputs
|
||||
var photo: OutputConfiguration<Photo> = .disabled
|
||||
var video: OutputConfiguration<Video> = .disabled
|
||||
var codeScanner: OutputConfiguration<CodeScanner> = .disabled
|
||||
|
||||
// Orientation
|
||||
var orientation: Orientation = .portrait
|
||||
|
||||
// Format
|
||||
var format: CameraDeviceFormat?
|
||||
|
||||
// Side-Props
|
||||
var fps: Int32?
|
||||
var enableLowLightBoost = false
|
||||
var torch: Torch = .off
|
||||
|
||||
// Zoom
|
||||
var zoom: CGFloat?
|
||||
|
||||
// isActive (Start/Stop)
|
||||
var isActive = false
|
||||
|
||||
// Audio Session
|
||||
var audio: OutputConfiguration<Audio> = .disabled
|
||||
|
||||
init(copyOf other: CameraConfiguration?) {
|
||||
if let other {
|
||||
// copy over all values
|
||||
cameraId = other.cameraId
|
||||
photo = other.photo
|
||||
video = other.video
|
||||
codeScanner = other.codeScanner
|
||||
orientation = other.orientation
|
||||
format = other.format
|
||||
fps = other.fps
|
||||
enableLowLightBoost = other.enableLowLightBoost
|
||||
torch = other.torch
|
||||
zoom = other.zoom
|
||||
isActive = other.isActive
|
||||
audio = other.audio
|
||||
} else {
|
||||
// self will just be initialized with the default values.
|
||||
}
|
||||
}
|
||||
|
||||
// pragma MARK: Types
|
||||
|
||||
struct Difference {
|
||||
let inputChanged: Bool
|
||||
let outputsChanged: Bool
|
||||
let orientationChanged: Bool
|
||||
let formatChanged: Bool
|
||||
let sidePropsChanged: Bool
|
||||
let zoomChanged: Bool
|
||||
|
||||
let audioSessionChanged: Bool
|
||||
|
||||
/**
|
||||
Returns `true` when props that affect the AVCaptureSession configuration (i.e. props that require beginConfiguration()) have changed.
|
||||
[`inputChanged`, `outputsChanged`, `orientationChanged`]
|
||||
*/
|
||||
var isSessionConfigurationDirty: Bool {
|
||||
return inputChanged || outputsChanged || orientationChanged
|
||||
}
|
||||
|
||||
/**
|
||||
Returns `true` when props that affect the AVCaptureDevice configuration (i.e. props that require lockForConfiguration()) have changed.
|
||||
[`formatChanged`, `sidePropsChanged`, `zoomChanged`]
|
||||
*/
|
||||
var isDeviceConfigurationDirty: Bool {
|
||||
return isSessionConfigurationDirty || formatChanged || sidePropsChanged || zoomChanged
|
||||
}
|
||||
|
||||
init(between left: CameraConfiguration?, and right: CameraConfiguration) {
|
||||
// cameraId
|
||||
inputChanged = left?.cameraId != right.cameraId
|
||||
// photo, video, codeScanner
|
||||
outputsChanged = inputChanged || left?.photo != right.photo || left?.video != right.video || left?.codeScanner != right.codeScanner
|
||||
// orientation
|
||||
orientationChanged = outputsChanged || left?.orientation != right.orientation
|
||||
// format (depends on cameraId)
|
||||
formatChanged = inputChanged || left?.format != right.format
|
||||
// side-props (depends on format)
|
||||
sidePropsChanged = formatChanged || left?.fps != right.fps || left?.enableLowLightBoost != right.enableLowLightBoost || left?.torch != right.torch
|
||||
// zoom (depends on format)
|
||||
zoomChanged = formatChanged || left?.zoom != right.zoom
|
||||
|
||||
// audio session
|
||||
audioSessionChanged = left?.audio != right.audio
|
||||
}
|
||||
}
|
||||
|
||||
enum OutputConfiguration<T: Equatable>: Equatable {
|
||||
case disabled
|
||||
case enabled(config: T)
|
||||
|
||||
public static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.disabled, .disabled):
|
||||
return true
|
||||
case let (.enabled(a), .enabled(b)):
|
||||
return a == b
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A Photo Output configuration
|
||||
*/
|
||||
struct Photo: Equatable {
|
||||
var enableHighQualityPhotos = false
|
||||
var enableDepthData = false
|
||||
var enablePortraitEffectsMatte = false
|
||||
}
|
||||
|
||||
/**
|
||||
A Video Output configuration
|
||||
*/
|
||||
struct Video: Equatable {
|
||||
var pixelFormat: PixelFormat = .native
|
||||
var enableBufferCompression = false
|
||||
var enableHdr = false
|
||||
var enableFrameProcessor = false
|
||||
}
|
||||
|
||||
/**
|
||||
An Audio Output configuration
|
||||
*/
|
||||
struct Audio: Equatable {
|
||||
// no props for audio at the moment
|
||||
}
|
||||
}
|
||||
|
||||
extension CameraConfiguration.Video {
|
||||
/**
|
||||
Returns the pixel format that should be used for the given AVCaptureVideoDataOutput.
|
||||
If HDR is enabled, this will return YUV 4:2:0 10-bit.
|
||||
If HDR is disabled, this will return whatever the user specified as a pixelFormat, or the most efficient format as a fallback.
|
||||
*/
|
||||
func getPixelFormat(for videoOutput: AVCaptureVideoDataOutput) throws -> OSType {
|
||||
// as per documentation, the first value is always the most efficient format
|
||||
var defaultFormat = videoOutput.availableVideoPixelFormatTypes.first!
|
||||
if enableBufferCompression {
|
||||
// use compressed format instead if we enabled buffer compression
|
||||
if defaultFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
videoOutput.availableVideoPixelFormatTypes.contains(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange) {
|
||||
// YUV 4:2:0 8-bit (limited video colors; compressed)
|
||||
defaultFormat = kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange
|
||||
}
|
||||
if defaultFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange &&
|
||||
videoOutput.availableVideoPixelFormatTypes.contains(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange) {
|
||||
// YUV 4:2:0 8-bit (full video colors; compressed)
|
||||
defaultFormat = kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange
|
||||
}
|
||||
}
|
||||
|
||||
// If the user enabled HDR, we can only use the YUV 4:2:0 10-bit pixel format.
|
||||
if enableHdr == true {
|
||||
guard pixelFormat == .native || pixelFormat == .yuv else {
|
||||
throw CameraError.format(.incompatiblePixelFormatWithHDR)
|
||||
}
|
||||
|
||||
var targetFormats = [kCVPixelFormatType_420YpCbCr10BiPlanarFullRange,
|
||||
kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange]
|
||||
if enableBufferCompression {
|
||||
// If we enable buffer compression, try to use a lossless compressed YUV format first, otherwise fall back to the others.
|
||||
targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr10PackedBiPlanarVideoRange, at: 0)
|
||||
}
|
||||
|
||||
// Find the best matching format
|
||||
guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else {
|
||||
throw CameraError.format(.invalidHdr)
|
||||
}
|
||||
// YUV 4:2:0 10-bit (compressed/uncompressed)
|
||||
return format
|
||||
}
|
||||
|
||||
// If we don't use HDR, we can use any other custom pixel format.
|
||||
switch pixelFormat {
|
||||
case .yuv:
|
||||
// YUV 4:2:0 8-bit (full/limited video colors; uncompressed)
|
||||
var targetFormats = [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]
|
||||
if enableBufferCompression {
|
||||
// YUV 4:2:0 8-bit (full/limited video colors; compressed)
|
||||
targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange, at: 0)
|
||||
targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange, at: 0)
|
||||
}
|
||||
guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else {
|
||||
throw CameraError.device(.pixelFormatNotSupported)
|
||||
}
|
||||
return format
|
||||
case .rgb:
|
||||
// RGBA 8-bit (uncompressed)
|
||||
var targetFormats = [kCVPixelFormatType_32BGRA]
|
||||
if enableBufferCompression {
|
||||
// RGBA 8-bit (compressed)
|
||||
targetFormats.insert(kCVPixelFormatType_Lossless_32BGRA, at: 0)
|
||||
}
|
||||
guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else {
|
||||
throw CameraError.device(.pixelFormatNotSupported)
|
||||
}
|
||||
return format
|
||||
case .native:
|
||||
return defaultFormat
|
||||
case .unknown:
|
||||
throw CameraError.parameter(.invalid(unionName: "pixelFormat", receivedValue: "unknown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user