react-native-vision-camera/package/ios/Core/CameraConfiguration.swift
Marc Rousavy cd0b413706
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
2023-10-13 18:33:20 +02:00

232 lines
7.7 KiB
Swift

//
// 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"))
}
}
}