Spaces:
Runtime error
Runtime error
| // Copyright 2019 The TensorFlow Authors. All Rights Reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| import AVFoundation | |
| import UIKit | |
| import os | |
| // MARK: - CameraFeedManagerDelegate Declaration | |
| @objc protocol CameraFeedManagerDelegate: class { | |
| /// This method delivers the pixel buffer of the current frame seen by the device's camera. | |
| @objc optional func cameraFeedManager( | |
| _ manager: CameraFeedManager, didOutput pixelBuffer: CVPixelBuffer | |
| ) | |
| /// This method initimates that a session runtime error occured. | |
| func cameraFeedManagerDidEncounterSessionRunTimeError(_ manager: CameraFeedManager) | |
| /// This method initimates that the session was interrupted. | |
| func cameraFeedManager( | |
| _ manager: CameraFeedManager, sessionWasInterrupted canResumeManually: Bool | |
| ) | |
| /// This method initimates that the session interruption has ended. | |
| func cameraFeedManagerDidEndSessionInterruption(_ manager: CameraFeedManager) | |
| /// This method initimates that there was an error in video configurtion. | |
| func presentVideoConfigurationErrorAlert(_ manager: CameraFeedManager) | |
| /// This method initimates that the camera permissions have been denied. | |
| func presentCameraPermissionsDeniedAlert(_ manager: CameraFeedManager) | |
| } | |
| /// This enum holds the state of the camera initialization. | |
| // MARK: - Camera Initialization State Enum | |
| enum CameraConfiguration { | |
| case success | |
| case failed | |
| case permissionDenied | |
| } | |
| /// This class manages all camera related functionalities. | |
| // MARK: - Camera Related Functionalies Manager | |
| class CameraFeedManager: NSObject { | |
| // MARK: Camera Related Instance Variables | |
| private let session: AVCaptureSession = AVCaptureSession() | |
| private let previewView: PreviewView | |
| private let sessionQueue = DispatchQueue(label: "sessionQueue") | |
| private var cameraConfiguration: CameraConfiguration = .failed | |
| private lazy var videoDataOutput = AVCaptureVideoDataOutput() | |
| private var isSessionRunning = false | |
| // MARK: CameraFeedManagerDelegate | |
| weak var delegate: CameraFeedManagerDelegate? | |
| // MARK: Initializer | |
| init(previewView: PreviewView) { | |
| self.previewView = previewView | |
| super.init() | |
| // Initializes the session | |
| session.sessionPreset = .high | |
| self.previewView.session = session | |
| self.previewView.previewLayer.connection?.videoOrientation = .portrait | |
| self.previewView.previewLayer.videoGravity = .resizeAspectFill | |
| self.attemptToConfigureSession() | |
| } | |
| // MARK: Session Start and End methods | |
| /// This method starts an AVCaptureSession based on whether the camera configuration was successful. | |
| func checkCameraConfigurationAndStartSession() { | |
| sessionQueue.async { | |
| switch self.cameraConfiguration { | |
| case .success: | |
| self.addObservers() | |
| self.startSession() | |
| case .failed: | |
| DispatchQueue.main.async { | |
| self.delegate?.presentVideoConfigurationErrorAlert(self) | |
| } | |
| case .permissionDenied: | |
| DispatchQueue.main.async { | |
| self.delegate?.presentCameraPermissionsDeniedAlert(self) | |
| } | |
| } | |
| } | |
| } | |
| /// This method stops a running an AVCaptureSession. | |
| func stopSession() { | |
| self.removeObservers() | |
| sessionQueue.async { | |
| if self.session.isRunning { | |
| self.session.stopRunning() | |
| self.isSessionRunning = self.session.isRunning | |
| } | |
| } | |
| } | |
| /// This method resumes an interrupted AVCaptureSession. | |
| func resumeInterruptedSession(withCompletion completion: @escaping (Bool) -> Void) { | |
| sessionQueue.async { | |
| self.startSession() | |
| DispatchQueue.main.async { | |
| completion(self.isSessionRunning) | |
| } | |
| } | |
| } | |
| /// This method starts the AVCaptureSession | |
| private func startSession() { | |
| self.session.startRunning() | |
| self.isSessionRunning = self.session.isRunning | |
| } | |
| // MARK: Session Configuration Methods. | |
| /// This method requests for camera permissions and handles the configuration of the session and stores the result of configuration. | |
| private func attemptToConfigureSession() { | |
| switch AVCaptureDevice.authorizationStatus(for: .video) { | |
| case .authorized: | |
| self.cameraConfiguration = .success | |
| case .notDetermined: | |
| self.sessionQueue.suspend() | |
| self.requestCameraAccess(completion: { granted in | |
| self.sessionQueue.resume() | |
| }) | |
| case .denied: | |
| self.cameraConfiguration = .permissionDenied | |
| default: | |
| break | |
| } | |
| self.sessionQueue.async { | |
| self.configureSession() | |
| } | |
| } | |
| /// This method requests for camera permissions. | |
| private func requestCameraAccess(completion: @escaping (Bool) -> Void) { | |
| AVCaptureDevice.requestAccess(for: .video) { (granted) in | |
| if !granted { | |
| self.cameraConfiguration = .permissionDenied | |
| } else { | |
| self.cameraConfiguration = .success | |
| } | |
| completion(granted) | |
| } | |
| } | |
| /// This method handles all the steps to configure an AVCaptureSession. | |
| private func configureSession() { | |
| guard cameraConfiguration == .success else { | |
| return | |
| } | |
| session.beginConfiguration() | |
| // Tries to add an AVCaptureDeviceInput. | |
| guard addVideoDeviceInput() == true else { | |
| self.session.commitConfiguration() | |
| self.cameraConfiguration = .failed | |
| return | |
| } | |
| // Tries to add an AVCaptureVideoDataOutput. | |
| guard addVideoDataOutput() else { | |
| self.session.commitConfiguration() | |
| self.cameraConfiguration = .failed | |
| return | |
| } | |
| session.commitConfiguration() | |
| self.cameraConfiguration = .success | |
| } | |
| /// This method tries to an AVCaptureDeviceInput to the current AVCaptureSession. | |
| private func addVideoDeviceInput() -> Bool { | |
| /// Tries to get the default back camera. | |
| guard | |
| let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) | |
| else { | |
| fatalError("Cannot find camera") | |
| } | |
| do { | |
| let videoDeviceInput = try AVCaptureDeviceInput(device: camera) | |
| if session.canAddInput(videoDeviceInput) { | |
| session.addInput(videoDeviceInput) | |
| return true | |
| } else { | |
| return false | |
| } | |
| } catch { | |
| fatalError("Cannot create video device input") | |
| } | |
| } | |
| /// This method tries to an AVCaptureVideoDataOutput to the current AVCaptureSession. | |
| private func addVideoDataOutput() -> Bool { | |
| let sampleBufferQueue = DispatchQueue(label: "sampleBufferQueue") | |
| videoDataOutput.setSampleBufferDelegate(self, queue: sampleBufferQueue) | |
| videoDataOutput.alwaysDiscardsLateVideoFrames = true | |
| videoDataOutput.videoSettings = [ | |
| String(kCVPixelBufferPixelFormatTypeKey): kCMPixelFormat_32BGRA | |
| ] | |
| if session.canAddOutput(videoDataOutput) { | |
| session.addOutput(videoDataOutput) | |
| videoDataOutput.connection(with: .video)?.videoOrientation = .portrait | |
| return true | |
| } | |
| return false | |
| } | |
| // MARK: Notification Observer Handling | |
| private func addObservers() { | |
| NotificationCenter.default.addObserver( | |
| self, selector: #selector(CameraFeedManager.sessionRuntimeErrorOccured(notification:)), | |
| name: NSNotification.Name.AVCaptureSessionRuntimeError, object: session) | |
| NotificationCenter.default.addObserver( | |
| self, selector: #selector(CameraFeedManager.sessionWasInterrupted(notification:)), | |
| name: NSNotification.Name.AVCaptureSessionWasInterrupted, object: session) | |
| NotificationCenter.default.addObserver( | |
| self, selector: #selector(CameraFeedManager.sessionInterruptionEnded), | |
| name: NSNotification.Name.AVCaptureSessionInterruptionEnded, object: session) | |
| } | |
| private func removeObservers() { | |
| NotificationCenter.default.removeObserver( | |
| self, name: NSNotification.Name.AVCaptureSessionRuntimeError, object: session) | |
| NotificationCenter.default.removeObserver( | |
| self, name: NSNotification.Name.AVCaptureSessionWasInterrupted, object: session) | |
| NotificationCenter.default.removeObserver( | |
| self, name: NSNotification.Name.AVCaptureSessionInterruptionEnded, object: session) | |
| } | |
| // MARK: Notification Observers | |
| @objc func sessionWasInterrupted(notification: Notification) { | |
| if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] | |
| as AnyObject?, | |
| let reasonIntegerValue = userInfoValue.integerValue, | |
| let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) | |
| { | |
| os_log("Capture session was interrupted with reason: %s", type: .error, reason.rawValue) | |
| var canResumeManually = false | |
| if reason == .videoDeviceInUseByAnotherClient { | |
| canResumeManually = true | |
| } else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps { | |
| canResumeManually = false | |
| } | |
| delegate?.cameraFeedManager(self, sessionWasInterrupted: canResumeManually) | |
| } | |
| } | |
| @objc func sessionInterruptionEnded(notification: Notification) { | |
| delegate?.cameraFeedManagerDidEndSessionInterruption(self) | |
| } | |
| @objc func sessionRuntimeErrorOccured(notification: Notification) { | |
| guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { | |
| return | |
| } | |
| os_log("Capture session runtime error: %s", type: .error, error.localizedDescription) | |
| if error.code == .mediaServicesWereReset { | |
| sessionQueue.async { | |
| if self.isSessionRunning { | |
| self.startSession() | |
| } else { | |
| DispatchQueue.main.async { | |
| self.delegate?.cameraFeedManagerDidEncounterSessionRunTimeError(self) | |
| } | |
| } | |
| } | |
| } else { | |
| delegate?.cameraFeedManagerDidEncounterSessionRunTimeError(self) | |
| } | |
| } | |
| } | |
| /// AVCaptureVideoDataOutputSampleBufferDelegate | |
| extension CameraFeedManager: AVCaptureVideoDataOutputSampleBufferDelegate { | |
| /// This method delegates the CVPixelBuffer of the frame seen by the camera currently. | |
| func captureOutput( | |
| _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, | |
| from connection: AVCaptureConnection | |
| ) { | |
| // Converts the CMSampleBuffer to a CVPixelBuffer. | |
| let pixelBuffer: CVPixelBuffer? = CMSampleBufferGetImageBuffer(sampleBuffer) | |
| guard let imagePixelBuffer = pixelBuffer else { | |
| return | |
| } | |
| // Delegates the pixel buffer to the ViewController. | |
| delegate?.cameraFeedManager?(self, didOutput: imagePixelBuffer) | |
| } | |
| } | |