Эта проблема не возникала в 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()
}
}
}
Что происходит, когда я перехожу на фон и возвращаюсь к слою предварительного просмотра, и пользовательский интерфейс полностью заморожен,Но прежде чем перейти к фону, если я поставлю точку останова на линии if self?.captureSession.isRunning == true
, а другую точку останова на линии if self?.captureSession.isRunning == false
, как только я вызову точки останова, появится слой предварительного просмотра, и пользовательский интерфейс будет работать нормально.
При дальнейшем исследовании янатолкнулся на этот вопрос и в комментариях @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 сказала :
Метод startRunning () является блокирующим вызовом, который может занять некоторое время, поэтому вы должны выполнить настройку сеанса в последовательной очереди, чтобы основная очередь не была заблокирована (что обеспечивает отзывчивость пользовательского интерфейса). См. AVCam-iOS: Использование AVFoundation для захвата изображений и фильмов для примера реализации.
Используя комментарий @HotLicks и информацию от Apple, я переключился на использование DispatchQueue.main.sync
, а затем Dispatch Group
и после возвращения с фона слой предварительного просмотра и пользовательский интерфейс все еще были заморожены. Но как только я добавляю точки останова, как я делал в первом примере, и запускаю их, слой предварительного просмотра и пользовательский интерфейс работают нормально.
Что я делаю не так?
Обновление
Я перешел из режима отладки в режим выпуска, и он все еще не работал.
Я также пытался переключиться на использование DispatchQueue.global(qos: .background).async
и таймера DispatchQueue.main.asyncAfter(deadline: .now() + 1.5)
, как предложил @MohyG, но это не имело никакого значения.
При дальнейшей проверке без точки останова фоновое уведомление работает нормально, но оноуведомление переднего плана, которое не вызывается, когда приложение входит в фг. По какой-то причине уведомление 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)
}
}
}
}
Я попробовал Dispatch Group:
@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()
}