Skip to content
Open
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions package/ios/Core/CameraSession+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ extension CameraSession {

// Remove all outputs
for output in captureSession.outputs {
if let metadataOutput = output as? AVCaptureMetadataOutput {
metadataOutput.setMetadataObjectsDelegate(nil, queue: nil)
}
if let videoOutput = output as? AVCaptureVideoDataOutput {
videoOutput.setSampleBufferDelegate(nil, queue: nil)
}
Comment on lines +64 to +69
Copy link
Owner

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 weak references, 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

captureSession.removeOutput(output)
}
photoOutput = nil
Expand Down
37 changes: 35 additions & 2 deletions package/ios/Core/CameraSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}

/**
Expand All @@ -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.
Expand All @@ -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?()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of writing that 3 times, put it in a defer { ... } block so we dont forget it when we add a fourth exit point

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)
Expand Down Expand Up @@ -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`)
*/
Expand All @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions package/ios/React/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting stuff like this in deinit is often a bad idea, especially if it runs an asynchronous operation.
Can you please test if that is absolutely necessary? Remove it if it isn't - we are calling shutdownCameraSession() at the top anyways.


override public func layoutSubviews() {
if let previewView {
previewView.frame = frame
Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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()
let logSlowShutdownWarning = DispatchWorkItem {
VisionLogger.log(level: .warning, message: "CameraSession shutdown is still running after 2 seconds.")
}
CameraQueues.cameraQueue.asyncAfter(deadline: .now() + .seconds(2), execute: logSlowShutdownWarning)
cameraSession.shutdown { [weak self] in
logSlowShutdownWarning.cancel()

DispatchQueue.main.async {
self?.didScheduleShutdown = false
}
}
}

func updatePreview() {
if preview && previewView == nil {
// Create PreviewView and add it
Expand Down
Loading