ios - AVFoundation -AVCaptureSession 仅在进入后台并返回断点时停止并开始运行

标签 ios swift avfoundation avcapturesession

Xcode 10.2.1和iOS 12没有出现这个问题,Xcode 11.1和iOS 13出现了

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

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 就可以正常工作。

我做错了什么?

更新

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

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

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

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

我尝试了 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() 之前造成延迟。 对您的问题的一个快速 hacky 解决方案是使用像这样的手动延迟:

DispatchQueue.main.asyncAfter(截止日期:.now() + 1.5){

不建议

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

中处理应用程序转换状态

基本上,当您转换到后台和前台时,您需要以某种方式管理在 AppDelegate 中触发您的 captureSession 的启动/停止:

func applicationDidEnterBackground(_ application: UIApplication) {}

func applicationDidBecomeActive(_ application: UIApplication) {}

关于ios - AVFoundation -AVCaptureSession 仅在进入后台并返回断点时停止并开始运行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58369609/

相关文章:

ios - WKWebView runJavaScriptAlertPanelWithMessage 从 iOS 9.3 崩溃

ios - 在 AVMutableComposition 中的多个视频轨道之间切换

swift - 具有相同名称的多个函数

ios - 如何删除多个并行数组中的重复数据

ios - 如何更改标签栏中未选中项目的颜色?

ios - 使用 AVFoundation 创建包含 QR 的 UIView 时发生间歇性崩溃

ios - AVPlayerItem 奇怪的缓冲区观察者

ios - XLPagerTabStrip:不尊重状态栏

ios - AFNetworking - 60 秒等待后超时间隔未达到预期

objective-c - 检查条件不起作用并返回无法识别的选择器