这个问题在Xcode 10.2.1和iOS 12中没有出现。
我的应用程序记录视频,当应用程序转到后台时,我停止捕捉会话的运行,并删除预览层。当应用程序返回到前台时,我重新启动捕获会话并将预览层添加回:
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就可以正常工作了。
经过进一步的研究,我发现了这个问题,并在评论@HotLick中说:
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.
我做了更多的研究和苹果说
startRunning()方法是一个阻塞调用,可能需要一些时间,因此您应该对串行队列执行会话设置,这样主队列就不会被阻塞(这将保持UI响应)。参见AVCam:使用AVFoundation捕获图像和电影作为实现示例。
使用@HotLick的评论和苹果的信息,我切换到使用DispatchQueue.main.sync
,然后使用Dispatch Group
,在从后台返回之后,预览层和ui仍然被冻结。但是,一旦我添加断点,就像我在第一个示例中所做的那样,并触发它们,预览层和ui工作得很好。
我做错什么了?
更新
我从调试模式切换到了发布模式,但它仍然没有工作。
我还试着使用DispatchQueue.global(qos: .background).async
和@MohyG建议的计时器DispatchQueue.main.asyncAfter(deadline: .now() + 1.5)
,但是没有什么不同。
在没有断点的情况下进行进一步检查时,背景通知工作正常,但是当应用程序进入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()
}
发布于 2019-10-15 15:58:34
我发现了窃听器,这是一个非常奇怪的错误。
按钮图像的色调是白色的。我不想使用普通的黑色背景,而是想要一个模糊的背景,所以我使用了以下方法:
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
,但正如我所说,这是一个非常奇怪的错误。
发布于 2019-10-14 10:54:46
你试过DispatchQueue.global(qos: .background).async
了吗?基本上,从我得到的情况来看,您需要在self?.captureSession.startRunning()
和self?.captureSession.stopRunning()
之前造成延迟。对您的问题的一个快速解决方案是使用手动延迟,如下所示:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
但是它的并不意味着
您可以尝试它,看看它是否解决了您的问题,如果是的话,您需要处理AppDelegate
中的应用过渡状态。
基本上,当您转换到背景和前台时,您需要以某种方式管理在AppDelegate
中触发您的AppDelegate
的开始/停止:
func applicationDidEnterBackground(_ application: UIApplication) {}
和
func applicationDidBecomeActive(_ application: UIApplication) {}
https://stackoverflow.com/questions/58369609
复制相似问题