AVFoundation -AVCaptureSession останавливается и запускается только при переходе в фоновый режим и обратно с точкой останова - PullRequest
0 голосов
/ 14 октября 2019

Эта проблема не возникала в 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()
}

Ответы [ 2 ]

1 голос
/ 14 октября 2019

Вы пробовали 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) {}

0 голосов
/ 15 октября 2019

Я нашел ошибку, и это была очень странная ошибка.

Цвет оттенков на изображениях кнопок белый. Вместо того, чтобы использовать обычный черный фон, я хотел размытый фон, поэтому я использовал это:

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, но, как я уже сказал, это была чрезвычайно странная ошибка.

...