-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(shutdown): implement graceful shutdown for camera session #3650
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,6 +83,18 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat | |
| NotificationCenter.default.removeObserver(self, | ||
| name: AVAudioSession.interruptionNotification, | ||
| object: AVAudioSession.sharedInstance) | ||
| let cameraCaptureSession = captureSession | ||
| CameraQueues.cameraQueue.async { | ||
| if cameraCaptureSession.isRunning { | ||
| cameraCaptureSession.stopRunning() | ||
| } | ||
| } | ||
| let cameraAudioSession = audioCaptureSession | ||
| CameraQueues.audioQueue.async { | ||
| if cameraAudioSession.isRunning { | ||
| cameraAudioSession.stopRunning() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -108,11 +120,14 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat | |
| Any changes in here will be re-configured only if required, and under a lock (in this case, the serial cameraQueue DispatchQueue). | ||
| The `configuration` object is a copy of the currently active configuration that can be modified by the caller in the lambda. | ||
| */ | ||
| func configure(_ lambda: @escaping (_ configuration: CameraConfiguration) throws -> Void) { | ||
| func configure(_ lambda: @escaping (_ configuration: CameraConfiguration) throws -> Void, | ||
| completion: (() -> Void)? = nil) { | ||
| initialize() | ||
|
|
||
| VisionLogger.log(level: .info, message: "configure { ... }: Waiting for lock...") | ||
|
|
||
| let completionBlock = completion | ||
|
|
||
| // Set up Camera (Video) Capture Session (on camera queue, acts like a lock) | ||
| CameraQueues.cameraQueue.async { | ||
| // Let caller configure a new configuration for the Camera. | ||
|
|
@@ -121,10 +136,12 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat | |
| try lambda(config) | ||
| } catch CameraConfiguration.AbortThrow.abort { | ||
| // call has been aborted and changes shall be discarded | ||
| completionBlock?() | ||
|
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. instead of writing that 3 times, put it in a |
||
| return | ||
| } catch { | ||
| // another error occured, possibly while trying to parse enums | ||
| self.onConfigureError(error) | ||
| completionBlock?() | ||
| return | ||
| } | ||
| let difference = CameraConfiguration.Difference(between: self.configuration, and: config) | ||
|
|
@@ -244,9 +261,25 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat | |
| } | ||
| } | ||
| } | ||
| completionBlock?() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| Gracefully stop streaming and tear down any active outputs. Completion executes on the camera queue. | ||
| */ | ||
| func shutdown(completion: (() -> Void)? = nil) { | ||
| configure({ config in | ||
| config.photo = .disabled | ||
| config.video = .disabled | ||
| config.audio = .disabled | ||
| config.codeScanner = .disabled | ||
| config.enableLocation = false | ||
| config.torch = .off | ||
| config.isActive = false | ||
| }, completion: completion) | ||
| } | ||
|
|
||
| /** | ||
| Starts or stops the CaptureSession if needed (`isActive`) | ||
| */ | ||
|
|
@@ -265,7 +298,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat | |
| } | ||
| } | ||
|
|
||
| public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| switch captureOutput { | ||
| case is AVCaptureVideoDataOutput: | ||
| onVideoFrame(sampleBuffer: sampleBuffer, orientation: connection.orientation, isMirrored: connection.isVideoMirrored) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -103,6 +103,7 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat | |||||||||||||||||||||||||||||
| var isMounted = false | ||||||||||||||||||||||||||||||
| private var currentConfigureCall: DispatchTime? | ||||||||||||||||||||||||||||||
| private let fpsSampleCollector = FpsSampleCollector() | ||||||||||||||||||||||||||||||
| private var didScheduleShutdown = false | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // CameraView+Zoom | ||||||||||||||||||||||||||||||
| var pinchGestureRecognizer: UIPinchGestureRecognizer? | ||||||||||||||||||||||||||||||
|
|
@@ -129,16 +130,22 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat | |||||||||||||||||||||||||||||
| super.willMove(toSuperview: newSuperview) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if newSuperview != nil { | ||||||||||||||||||||||||||||||
| didScheduleShutdown = false | ||||||||||||||||||||||||||||||
| fpsSampleCollector.start() | ||||||||||||||||||||||||||||||
| if !isMounted { | ||||||||||||||||||||||||||||||
| isMounted = true | ||||||||||||||||||||||||||||||
| onViewReadyEvent?(nil) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| shutdownCameraSession() | ||||||||||||||||||||||||||||||
| fpsSampleCollector.stop() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| deinit { | ||||||||||||||||||||||||||||||
| shutdownCameraSession() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+145
to
+147
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. Putting stuff like this in |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| override public func layoutSubviews() { | ||||||||||||||||||||||||||||||
| if let previewView { | ||||||||||||||||||||||||||||||
| previewView.frame = frame | ||||||||||||||||||||||||||||||
|
|
@@ -283,6 +290,31 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat | |||||||||||||||||||||||||||||
| UIApplication.shared.isIdleTimerDisabled = isActive | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private func shutdownCameraSession() { | ||||||||||||||||||||||||||||||
| if didScheduleShutdown { | ||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| didScheduleShutdown = true | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| UIApplication.shared.isIdleTimerDisabled = false | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS | ||||||||||||||||||||||||||||||
| frameProcessor = nil | ||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let slowShutdownWarning = DispatchWorkItem { | ||||||||||||||||||||||||||||||
| VisionLogger.log(level: .warning, message: "CameraSession shutdown is still running after 2 seconds.") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| CameraQueues.cameraQueue.asyncAfter(deadline: .now() + .seconds(2), execute: slowShutdownWarning) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| cameraSession.shutdown { [weak self] in | ||||||||||||||||||||||||||||||
| slowShutdownWarning.cancel() | ||||||||||||||||||||||||||||||
|
Comment on lines
+305
to
+311
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.
Suggested change
|
||||||||||||||||||||||||||||||
| DispatchQueue.main.async { | ||||||||||||||||||||||||||||||
| self?.didScheduleShutdown = false | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func updatePreview() { | ||||||||||||||||||||||||||||||
| if preview && previewView == nil { | ||||||||||||||||||||||||||||||
| // Create PreviewView and add it | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
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.
I'm 99% sure this is unnecessary. The outputs store the delegates as
weakreferences, and there's no Thread hops involved.Can you try to remove this code and check if it still works fine for you? I'd rather have that out tbh because it is super specific and feels like monkeypatching