Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 132 additions & 46 deletions package/ios/Core/CameraSession+Photo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,84 +11,170 @@ import Foundation

extension CameraSession {
/**
Takes a photo.
Captures a photo.
`takePhoto` is only available if `photo={true}`.
*/
func takePhoto(options: TakePhotoOptions, promise: Promise) {
// Run on Camera Queue
CameraQueues.cameraQueue.async {
// Get Photo Output configuration
// Validate configuration
guard let configuration = self.configuration else {
promise.reject(error: .session(.cameraNotReady))
return
}
guard configuration.photo != .disabled else {
// User needs to enable photo={true}
promise.reject(error: .capture(.photoNotEnabled))
return
}

// Check if Photo Output is available
// Validate outputs and inputs
guard let photoOutput = self.photoOutput,
let videoDeviceInput = self.videoDeviceInput else {
// Camera is not yet ready
promise.reject(error: .session(.cameraNotReady))
return
}

VisionLogger.log(level: .info, message: "Capturing photo...")

// Create photo settings
let photoSettings = AVCapturePhotoSettings()

// set photo resolution
if #available(iOS 16.0, *) {
photoSettings.maxPhotoDimensions = photoOutput.maxPhotoDimensions
} else {
photoSettings.isHighResolutionPhotoEnabled = photoOutput.isHighResolutionCaptureEnabled
}
// Ensure session and video connection are ready
self.ensureReadyToCapture(repairIfNeeded: true) { ready in
guard ready else {
VisionLogger.log(level: .error, message: "Photo capture failed: session or connection not ready.")
promise.reject(error: .session(.cameraNotReady))
return
}

// depth data
photoSettings.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliveryEnabled
if #available(iOS 12.0, *) {
photoSettings.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliveryEnabled
}
// Build photo settings
let photoSettings = AVCapturePhotoSettings()

// quality prioritization
if #available(iOS 13.0, *) {
photoSettings.photoQualityPrioritization = photoOutput.maxPhotoQualityPrioritization
}
if #available(iOS 16.0, *) {
photoSettings.maxPhotoDimensions = photoOutput.maxPhotoDimensions
} else {
photoSettings.isHighResolutionPhotoEnabled = photoOutput.isHighResolutionCaptureEnabled
}

// red-eye reduction
photoSettings.isAutoRedEyeReductionEnabled = options.enableAutoRedEyeReduction
// Depth / Portrait / Distortion
if photoOutput.isDepthDataDeliverySupported {
photoSettings.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliveryEnabled
}
if #available(iOS 12.0, *) {
photoSettings.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliveryEnabled
}
if #available(iOS 13.0, *) {
photoSettings.photoQualityPrioritization = photoOutput.maxPhotoQualityPrioritization
}
photoSettings.isAutoRedEyeReductionEnabled = options.enableAutoRedEyeReduction
if #available(iOS 14.1, *),
photoOutput.isContentAwareDistortionCorrectionSupported {
photoSettings.isAutoContentAwareDistortionCorrectionEnabled = options.enableAutoDistortionCorrection
}

// distortion correction
if #available(iOS 14.1, *) {
photoSettings.isAutoContentAwareDistortionCorrectionEnabled = options.enableAutoDistortionCorrection
}
// Flash
if options.flash != .off {
guard videoDeviceInput.device.hasFlash else {
promise.reject(error: .capture(.flashNotAvailable))
return
}
}
if videoDeviceInput.device.isFlashAvailable {
photoSettings.flashMode = options.flash.toFlashMode()
}

// flash
if options.flash != .off {
guard videoDeviceInput.device.hasFlash else {
// If user enabled flash, but the device doesn't have a flash, throw an error.
promise.reject(error: .capture(.flashNotAvailable))
// Final connection guard
guard let videoConn = photoOutput.connection(with: .video),
videoConn.isEnabled, videoConn.isActive else {
VisionLogger.log(level: .error, message: "No active and enabled video connection for photo capture.")
promise.reject(error: .session(.cameraNotReady))
return
}

// Capture
let photoCaptureDelegate = PhotoCaptureDelegate(
promise: promise,
enableShutterSound: options.enableShutterSound,
metadataProvider: self.metadataProvider,
path: options.path,
cameraSessionDelegate: self.delegate
)
photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureDelegate)

// Prepare next settings
photoOutput.setPreparedPhotoSettingsArray([photoSettings], completionHandler: nil)
}
if videoDeviceInput.device.isFlashAvailable {
photoSettings.flashMode = options.flash.toFlashMode()
}
}

// MARK: - Readiness / Repair

/// Ensures the session is running and the video connection is enabled + active.
/// Optionally tries to repair the output and waits shortly to handle race conditions.
private func ensureReadyToCapture(repairIfNeeded: Bool,
timeout: TimeInterval = 0.5,
poll: TimeInterval = 0.05,
completion: @escaping (Bool) -> Void) {
if isReadyNowActive() {
completion(true)
return
}

var repairedOnce = false
let deadline = DispatchTime.now() + timeout

func tick() {
if isReadyNowActive() {
completion(true)
return
}
if repairIfNeeded && !repairedOnce {
repairedOnce = repairPhotoConnectionIfNeeded()
}
if DispatchTime.now() < deadline {
CameraQueues.cameraQueue.asyncAfter(deadline: .now() + poll) { tick() }
} else {
completion(false)
}
}

tick()
}

/// Checks if session is running and the video connection is enabled and active.
private func isReadyNowActive() -> Bool {
guard let photoOutput = self.photoOutput else { return false }
let running = self.captureSession.isRunning
guard running, let conn = photoOutput.connection(with: .video) else { return false }
return conn.isEnabled && conn.isActive
}

// Actually do the capture!
let photoCaptureDelegate = PhotoCaptureDelegate(promise: promise,
enableShutterSound: options.enableShutterSound,
metadataProvider: self.metadataProvider,
path: options.path,
cameraSessionDelegate: self.delegate)
photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureDelegate)
/// Repairs the photo output connection by re-adding it to the session.
@discardableResult
private func repairPhotoConnectionIfNeeded() -> Bool {
guard let photoOutput = self.photoOutput else { return false }
let session = self.captureSession

// Assume that `takePhoto` is always called with the same parameters, so prepare the next call too.
photoOutput.setPreparedPhotoSettingsArray([photoSettings], completionHandler: nil)
let conn = photoOutput.connection(with: .video)
if conn?.isEnabled == true && conn?.isActive == true { return false }

VisionLogger.log(level: .info, message: "Repairing photo connection...")

session.beginConfiguration()

if session.canSetSessionPreset(.photo) {
session.sessionPreset = .photo
}

if session.outputs.contains(photoOutput) {
session.removeOutput(photoOutput)
}
if session.canAddOutput(photoOutput) {
session.addOutput(photoOutput)
}

session.commitConfiguration()

if !session.isRunning {
session.startRunning()
}

return true
}
}