AVFoundation -AVCaptureSession 仅在进入后台并返回断点时停止和启动 运行

AVFoundation -AVCaptureSession only stops and starts running when going to background and back with a breakpoint

这个问题在Xcode 10.2.1 和iOS 12 中没有出现。它开始于Xcode 11.1 和iOS 13

我的应用程序录制视频,当应用程序进入后台时,我从 运行 停止捕获会话并删除预览层。当应用程序返回前台时,我重新启动捕获会话并将预览层添加回:

let captureSession = AVCaptureSession()
var previewLayer: AVCaptureVideoPreviewLayer?
var movieFileOutput = AVCaptureMovieFileOutput()

// *** I initially didn't remove the preview layer in this example but I did remove it in the other 2 examples below ***
@objc fileprivate func stopCaptureSession() {
    DispatchQueue.main.async {
        [weak self] in
        if self?.captureSession.isRunning == true {
            self?.captureSession.stopRunning()
        }
    }
}

@objc func restartCaptureSession() {
    DispatchQueue.main.async {
        [weak self] in
        if self?.captureSession.isRunning == false {
            self?.captureSession.startRunning()
        }
    }
}

当我转到背景并返回预览层时,ui 完全冻结。但是在进入后台之前,如果我在行 if self?.captureSession.isRunning == true 上放置一个断点并在行 if self?.captureSession.isRunning == false 上放置另一个断点,一旦我触发断点,预览层和 ui 工作正常。

经过进一步研究,我发现了 this question,@HotLicks 在评论中说:

Obviously, it's likely that the breakpoint gives time for some async activity to complete before the above code starts mucking with things. However, it's also the case that 0.03 seconds is an awfully short repeat interval for a timer, and it may simply be the case that the breakpoint allows the UI setup to proceed before the timer ties up the CPU.

我做了更多研究 Apple said:

The startRunning() method is a blocking call which can take some time, therefore you should perform session setup on a serial queue so that the main queue isn't blocked (which keeps the UI responsive). See AVCam-iOS: Using AVFoundation to Capture Images and Movies for an implementation example.

根据@HotLicks 的评论和来自 Apple 的信息,我切换到使用 DispatchQueue.main.sync,然后是 Dispatch Group,从后台返回后,预览层和 ui 是仍然冻结。但是一旦我像在第一个示例中那样添加断点并触发它们预览层并且 ui 工作正常。

我做错了什么?

更新

我从debug模式切换到release模式,还是不行。

我也试过使用 DispatchQueue.global(qos: .background).async 和计时器 DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) 就像@MohyG 建议的那样,但没有任何区别。

在没有断点的情况下进一步检查后,后台通知工作正常,但当应用程序进入 fg 时,前台通知没有被调用。出于某种原因,fg 通知仅在我第一次在 stopCaptureSession() 函数中放置一个断点时触发。

问题是前台通知仅在我上面描述的断点处触发。

我试过了DispatchQueue.main.sync:

@objc fileprivate func stopCaptureSession() {

    if captureSession.isRunning { // adding a breakpoint here is the only thing that triggers the foreground notification when the the app comes back

        DispatchQueue.global(qos: .default).async {
            [weak self] in

            DispatchQueue.main.sync {
                self?.captureSession.stopRunning()
            }

            DispatchQueue.main.async {
                self?.previewLayer?.removeFromSuperlayer()
                self?.previewLayer = nil
            }
        }
    }
}

@objc func restartCaptureSession() {

    if !captureSession.isRunning {

        DispatchQueue.global(qos: .default).async {
            [weak self] in
            DispatchQueue.main.sync {
                self?.captureSession.startRunning()
            }

            DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
                self?.previewLayer = AVCaptureVideoPreviewLayer(session: self!.captureSession)
                self?.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
                guard let previewLayer = self?.previewLayer else { return }
                previewLayer.frame = self!.containerViewForPreviewLayer.bounds
                self?.containerViewForPreviewLayer.layer.insertSublayer(previewLayer, at: 0)
            }
        }
    }
}

我试过调度组:

@objc fileprivate func stopCaptureSession() {

    let group = DispatchGroup()

    if captureSession.isRunning { // adding a breakpoint here is the only thing that triggers the foreground notification when the the app comes back

        group.enter()
        DispatchQueue.global(qos: .default).async {
            [weak self] in

            self?.captureSession.stopRunning()
            group.leave()

            group.notify(queue: .main) {
                self?.previewLayer?.removeFromSuperlayer()
                self?.previewLayer = nil
            }
        }
    }
}

@objc func restartCaptureSession() {

    let group = DispatchGroup()

    if !captureSession.isRunning {

        group.enter()

        DispatchQueue.global(qos: .default).async {
            [weak self] in

            self?.captureSession.startRunning()
            group.leave()

            group.notify(queue: .main) {
                self?.previewLayer = AVCaptureVideoPreviewLayer(session: self!.captureSession)
                self?.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
                guard let previewLayer = self?.previewLayer else { return }
                previewLayer.frame = self!.containerViewForPreviewLayer.bounds
                self?.containerViewForPreviewLayer.layer.insertSublayer(previewLayer, at: 0)
            }
        }
    }
}

如果需要,这里是其余代码:

NotificationCenter.default.addObserver(self, selector: #selector(appHasEnteredBackground),
                                           name: UIApplication.willResignActiveNotification,
                                           object: nil)

NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground),
                                           name: UIApplication.willEnterForegroundNotification,
                                           object: nil)

NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted),
                                           name: .AVCaptureSessionWasInterrupted,
                                           object: captureSession)

NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded),
                                           name: .AVCaptureSessionInterruptionEnded,
                                           object: captureSession)

NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError),
                                           name: .AVCaptureSessionRuntimeError,
                                           object: captureSession)

func stopMovieShowControls() {

    if movieFileOutput.isRecording {
        movieFileOutput.stopRecording()
    }

    recordButton.isHidden = false
    saveButton.isHidden = false
}

@objc fileprivate func appWillEnterForeground() {

    restartCaptureSession()
}

@objc fileprivate func appHasEnteredBackground() {

    stopMovieShowControls()

    imagePicker.dismiss(animated: false, completion: nil)

    stopCaptureSession()
}

@objc func sessionRuntimeError(notification: NSNotification) {
    guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }

    stopMovieRecordigShowControls()

    if error.code == .mediaServicesWereReset {
        if !captureSession.isRunning {
            DispatchQueue.main.async {  [weak self] in
                self?.captureSession.startRunning()
            }
        } else {
            restartCaptureSession()
        }
    } else {
        restartCaptureSession()
    }
}

@objc func sessionWasInterrupted(notification: NSNotification) {

    if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
        let reasonIntegerValue = userInfoValue.integerValue,
        let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {

        switch reason {

        case .videoDeviceNotAvailableInBackground:

            stopMovieShowControls()

        case .audioDeviceInUseByAnotherClient, .videoDeviceInUseByAnotherClient:

            stopMovieShowControls()

        case .videoDeviceNotAvailableWithMultipleForegroundApps:

            print("2. The toggleButton was pressed")

        case .videoDeviceNotAvailableDueToSystemPressure:
            // no documentation
            break

        @unknown default:
            break
        }
    }
}

@objc func sessionInterruptionEnded(notification: NSNotification) {

    restartCaptureSession()

    stopMovieShowControls()
}

你试过了吗DispatchQueue.global(qos: .background).async? 基本上从我得到的你需要在 self?.captureSession.startRunning()self?.captureSession.stopRunning() 之前造成延迟。 您的问题的一个肮脏的解决方案是使用这样的手动延迟:

DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {

但是不建议

您可以试试看它是否能解决您的问题,如果可以,您需要在 AppDelegate

中处理应用程序转换状态

基本上,当您过渡到背景和前景时,您需要以某种方式管理触发 AppDelegate 中的 captureSession 的 start/stop :

func applicationDidEnterBackground(_ application: UIApplication) {}

func applicationDidBecomeActive(_ application: UIApplication) {}

我发现了这个错误,这是一个非常奇怪的错误。

按钮图像的色调为白色。我没有使用普通的黑色背景,而是想要模糊的背景,所以我使用了这个:

func addBackgroundFrostToButton(_ backgroundBlur: UIVisualEffectView, vibrancy: UIVisualEffectView, button: UIButton, width: CGFloat?, height: CGFloat?){

    backgroundBlur.frame = button.bounds
    vibrancy.frame = button.bounds
    backgroundBlur.contentView.addSubview(vibrancy)
    button.insertSubview(backgroundBlur, at: 0)

    if let width = width {
        backgroundBlur.frame.size.width += width
    }

    if let height = height {
        backgroundBlur.frame.size.height += height
    }

    backgroundBlur.center = CGPoint(x: button.bounds.midX, y: button.bounds.midY)
}

然后我打电话给 viewDidLayoutSubview() 就像:

lazy var cancelButto: UIButton = {
    let button = UIButton(type: .system)
    //....
    return button
}()

let cancelButtoBackgroundBlur: UIVisualEffectView = {
    let blur = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
    //...
    return blur
}()

let cancelButtoVibrancy: UIVisualEffectView = {
    let vibrancyEffect = UIVibrancyEffect(blurEffect: UIBlurEffect(style: .extraLight))
    // ...
    return vibrancyView
}()

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

     // I did this with 4 buttons, this is just one of them
     addBackgroundFrostToButton(cancelButtoBackgroundBlur,
                               vibrancy: cancelButtoVibrancy,
                               button: cancelButto,
                               width: 10, height: 2.5)
}

一旦我注释掉上面的代码,foreground notification 就开始毫无问题地触发,我不再需要断点了。

由于 viewDidLayoutSubviews() 可以多次调用 UIVisualEffectViewUIVibrancyEffect 一直在相互叠加,并且出于某些 非常奇怪的原因 它影响了 foreground notification.

为了解决这个问题,我简单地创建了一个 Bool 来检查按钮是否添加了模糊。一旦我这样做了,我就没有更多的问题了。

var wasBlurAdded = false

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

     if !wasBlurAdded {
         addBackgroundFrostToButton(cancelButtoBackgroundBlur,
                                   vibrancy: cancelButtoVibrancy,
                                   button: cancelButto,
                                   width: 10, height: 2.5)
         wasBlurAdded = true
     }
}

我不知道为什么或如何影响 foreground notification observer 但正如我所说,这是一个非常奇怪的错误。