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
中的应用转换状态
基本上,当您转换到后台和前台时,您需要以某种方式触发captureSession
在AppDelegate
中的启动/停止:
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()
可以被多次调用,UIVisualEffectView
和UIVibrancyEffect
不断叠加,并且由于某些非常奇怪的原因,它影响了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 仅在进入后台并返回断点时停止并开始运行的主要内容,如果未能解决你的问题,请参考以下文章