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