-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(ios): prevent crash when capturePhoto has no active video connection #3637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jeremy-Kr
wants to merge
1
commit into
mrousavy:main
Choose a base branch
from
Jeremy-Kr:fix/ios-17-pro-crash
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry but this PR is way too many changes/lines for that simple of a fix. I think all you'd need to do is add a simple |
||
|
|
||
| // 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 | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is AI - why did you (or your AI) remove/change those comments?