0

I have a bug in my camera app. If you open the app while on a phone call, the entire app freezes. I've tried using AVCaptureSessionWasInterrupted and AVCaptureSessionInterruptionEnded notifications to handle the audio input management during a phone call, but have had no luck fixing the issue. When I comment out the audio input setup, the app no longer freezes during a phone call, so I'm pretty confident the issue lies somewhere with the audio management.

Why is the app freezing during phone calls and how can I fix it?

Thanks in advance!

Relevant code:

class CameraManager: NSObject {
    static let shared = CameraManager()

    private let notificationQueue = OperationQueue.main

    var delegate: CameraManagerDelegate? = nil

    let session = AVCaptureSession()
    var captureDeviceInput: AVCaptureDeviceInput? = nil
    var audioInput: AVCaptureDeviceInput? = nil
    let photoOutput = AVCapturePhotoOutput()
    let videoOutput = AVCaptureMovieFileOutput()

    var isRecording: Bool {
        return videoOutput.isRecording
    }

    func getCurrentVideoCaptureDevice() throws -> AVCaptureDevice {
        guard let device = self.captureDeviceInput?.device else {
            throw CameraManagerError.missingCaptureDeviceInput
        }
        return device
    }

    func getZoomFactor() throws -> CGFloat {
        return try getCurrentVideoCaptureDevice().videoZoomFactor
    }

    func getMaxZoomFactor() throws -> CGFloat {
        return try getCurrentVideoCaptureDevice().activeFormat.videoMaxZoomFactor
    }

    override init() {
        super.init()

        NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationDidBecomeActive, object: nil, queue: notificationQueue) { [unowned self] (notification) in
            self.session.startRunning()
            try? self.setupCamera()
            try? self.setZoomLevel(zoomLevel: 1.0)

            if Settings.shared.autoRecord {
                try? self.startRecording()
            }
        }

        NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillResignActive, object: nil, queue: notificationQueue) { [unowned self] (notification) in
            self.stopRecording()
            self.session.stopRunning()
        }

        NotificationCenter.default.addObserver(forName: Notification.Name.AVCaptureSessionWasInterrupted, object: nil, queue: notificationQueue) { [unowned self] (notification) in
            if let audioInput = self.audioInput {
                self.session.removeInput(audioInput)
            }
        }

        NotificationCenter.default.addObserver(forName: Notification.Name.AVCaptureSessionInterruptionEnded, object: nil, queue: notificationQueue) { [unowned self] (notification) in
            try? self.setupAudio()
        }

        try? self.setupSession()
    }

    func setupSession() throws {
        session.sessionPreset = .high

        if !session.isRunning {
            session.startRunning()
        }

        if Utils.checkPermissions() {
            try setupInputs()
            setupOutputs()
        }
    }

    func setupInputs() throws {
        try setupCamera()
        try setupAudio()
    }

    func setupCamera() throws {
        do {
            try setCamera(position: Settings.shared.defaultCamera)
        } catch CameraManagerError.unableToFindCaptureDevice(let position) {
            //some devices don't have a front camera, so try the back for setup
            if position == .front {
                try setCamera(position: .back)
            }
        }
    }

    func setupAudio() throws {
        if let audioInput = self.audioInput {
            self.session.removeInput(audioInput)
        }

        guard let audioDevice = AVCaptureDevice.default(for: .audio) else {
            throw CameraManagerError.unableToGetAudioDevice
        }

        let audioInput = try AVCaptureDeviceInput(device: audioDevice)

        if session.canAddInput(audioInput) {
            session.addInput(audioInput)
            self.audioInput = audioInput
        } else {
            self.delegate?.unableToAddAudioInput()
        }
    }

    func setupOutputs() {
        self.photoOutput.isHighResolutionCaptureEnabled = true
        guard session.canAddOutput(self.photoOutput) else {
            //error
            return
        }

        session.addOutput(self.photoOutput)

        guard session.canAddOutput(self.videoOutput) else {
            //error
            return
        }
        session.addOutput(self.videoOutput)
    }

    func startRecording() throws {
        if !self.videoOutput.isRecording {
            let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
            let url = documentDirectory.appendingPathComponent(UUID().uuidString + ".mov")

            self.videoOutput.startRecording(to: url, recordingDelegate: self)
        }
    }

    func stopRecording() {
        if self.videoOutput.isRecording {
            self.videoOutput.stopRecording()
        }
    }

    func setZoomLevel(zoomLevel: CGFloat) throws {
        guard let captureDevice = self.captureDeviceInput?.device else {
            throw CameraManagerError.missingCaptureDevice
        }

        try captureDevice.lockForConfiguration()
        captureDevice.videoZoomFactor = zoomLevel
        captureDevice.unlockForConfiguration()
    }

    func capturePhoto() {
        let photoOutputSettings = AVCapturePhotoSettings()
        photoOutputSettings.flashMode = Settings.shared.flash
        photoOutputSettings.isAutoStillImageStabilizationEnabled = true
        photoOutputSettings.isHighResolutionPhotoEnabled = true

        self.photoOutput.capturePhoto(with: photoOutputSettings, delegate: self)
    }

    func toggleCamera() throws {
        if let captureDeviceInput = self.captureDeviceInput,
            captureDeviceInput.device.position == .back {
            try setCamera(position: .front)
        } else {
            try setCamera(position: .back)
        }
    }

    func setCamera(position: AVCaptureDevice.Position) throws {
        if let captureDeviceInput = self.captureDeviceInput {
            if captureDeviceInput.device.position == position {
                return
            } else {
                session.removeInput(captureDeviceInput)
            }
        }

        var device: AVCaptureDevice? = nil

        switch position {
        case .front:
            device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
        default:
            device = AVCaptureDevice.default(for: .video)
        }

        guard let nonNilDevice = device else {
            throw CameraManagerError.unableToFindCaptureDevice(position)
        }

        try nonNilDevice.lockForConfiguration()

        if nonNilDevice.isFocusModeSupported(.continuousAutoFocus) {
            nonNilDevice.focusMode = .continuousAutoFocus
        }

        if nonNilDevice.isExposureModeSupported(.continuousAutoExposure) {
            nonNilDevice.exposureMode = .continuousAutoExposure
        }

        nonNilDevice.unlockForConfiguration()

        let input = try AVCaptureDeviceInput(device: nonNilDevice)

        guard session.canAddInput(input) else {
            throw CameraManagerError.unableToAddCaptureDeviceInput
        }

        session.addInput(input)

        self.captureDeviceInput = input
    }

    func setFocus(point: CGPoint) throws {
        guard let device = self.captureDeviceInput?.device else {
            throw CameraManagerError.missingCaptureDeviceInput
        }

        guard device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) else {
            throw CameraManagerError.notSupportedByDevice
        }

        try device.lockForConfiguration()

        device.focusPointOfInterest = point
        device.focusMode = .autoFocus

        device.unlockForConfiguration()
    }

    func setExposure(point: CGPoint) throws {
        guard let device = self.captureDeviceInput?.device else {
            throw CameraManagerError.missingCaptureDeviceInput
        }

        guard device.isExposurePointOfInterestSupported && device.isExposureModeSupported(.autoExpose) else {
            throw CameraManagerError.notSupportedByDevice
        }

        try device.lockForConfiguration()

        device.exposurePointOfInterest = point
        device.exposureMode = .autoExpose

        device.unlockForConfiguration()
    }
}

extension CameraManager: AVCapturePhotoCaptureDelegate {

    func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
        self.delegate?.cameraManagerWillCapturePhoto()
    }

    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        guard let imageData = photo.fileDataRepresentation() else {
            //error
            return
        }

        let capturedImage = UIImage.init(data: imageData , scale: 1.0)
        if let image = capturedImage {
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        }

        self.delegate?.cameraManagerDidFinishProcessingPhoto()
    }
}

extension CameraManager: AVCaptureFileOutputRecordingDelegate {

    func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
        self.delegate?.cameraManagerDidStartRecording()
    }

    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {

        self.delegate?.cameraManagerDidFinishRecording()

        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputFileURL)
        }) { saved, error in
            if saved {
                do {
                    try FileManager.default.removeItem(at: outputFileURL)
                } catch _ as NSError {
                    //error
                }
            }
        }
    }
}
Jake
  • 13,097
  • 9
  • 44
  • 73
  • did you check https://stackoverflow.com/questions/30540857/avcapturevideopreviewlayer-camera-preview-freezes-stuck-after-moving-to-backgr/30707170 – Prashant Tukadiya Feb 20 '18 at 06:05
  • I've check out that post. I'm not having the same issue. My entire app is freezing, not just the preview layer. Also, this only happens during a phone call while trying to setup audio input. – Jake Feb 22 '18 at 04:20
  • Jake, this is a bit of a shot in the dark, but I wonder if you're trying to do `AVCaptureSession` work on the main thread when maybe you don't want to be? Glancing at the `CameraManager` I don't see any dispatch queues anywhere to manage how you access underlying AVFoundation apis. – Aaron Oct 05 '22 at 22:30

0 Answers0