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

Posted

技术标签:

【中文标题】AVFoundation -AVCaptureSession 仅在进入后台并返回断点时停止并开始运行【英文标题】:AVFoundation -AVCaptureSession only stops and starts running when going to background and back with a breakpoint 【发布时间】:2019-10-14 03:04:48 【问题描述】:

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,并且在 cmets @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:

startRunning() 方法是一个阻塞调用,可能需要一些时间, 因此,您应该在串行队列上执行会话设置,以便 主队列没有被阻塞(保持 UI 响应)。看 AVCam-iOS:使用 AVFoundation 捕捉图像和电影 实现示例。

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

我做错了什么?

更新

我从调试模式切换到发布模式,但仍然无法正常工作。

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

在没有断点的情况下进一步检查后,后台通知工作正常,但当应用程序进入 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()

【问题讨论】:

【参考方案1】:

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

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

但它不建议

你可以试试看它是否能解决你的问题,如果是,你需要处理AppDelegate中的应用转换状态

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

func applicationDidEnterBackground(_ application: UIApplication)

func applicationDidBecomeActive(_ application: UIApplication)

【讨论】:

嘿,谢谢你的建议,大约一个小时前,我实际上只是尝试了你的第一个建议。现在我正在从调试模式切换到发布模式,看看会发生什么。我会尝试您的应用委托建议并回复您。谢谢 我刚刚添加了 2 个自定义观察者,上面的代码用于监听 bg 和 fg 通知。在应用程序委托中,我在您列出该帖子到 vc 的代表中添加了 2 个通知。它没有用。我什至尝试结合您的前 2 个建议,但没有奏效。切换到释放模式也不起作用。不过谢谢:) 我找到了答案。这是一个奇怪的错误。您的回答没有解决问题,但我确实使用了您的 .background 和 dispatchtimer,因为我看到了其他几个也使用它的答案。我赞成你,因为即使它没有解决我的问题,你是对的。干杯!!! :)【参考方案2】:

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

按钮图像的色调为白色。我想要一个模糊的背景,而不是使用常规的黑色背景,所以我使用了这个:

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,但就像我说的,这是一个非常奇怪的错误。

【讨论】:

以上是关于AVFoundation -AVCaptureSession 仅在进入后台并返回断点时停止并开始运行的主要内容,如果未能解决你的问题,请参考以下文章

AVFoundation - iOS 听不到声音

AVFoundation下的视频分帧处理

AVFoundation 初识

AVFoundation自定义录制视频

AVFoundation自己定义音视频频播放

Mac OS X 上的 AVFoundation + GC