import Foundation import AVFoundation class CameraManager: ObservableObject { enum Status { case unconfigured case configured case unauthorized case failed } static let shared = CameraManager() @Published var error: CameraError? let session = AVCaptureSession() private let sessionQueue = DispatchQueue(label: "de.christophhagen.cam") private let videoOutput = AVCaptureVideoDataOutput() private let photoOutput = AVCapturePhotoOutput() private var status = Status.unconfigured private var camera: AVCaptureDevice? private init() { configure() } private func set(error: CameraError?) { DispatchQueue.main.async { self.error = error } } private func checkPermissions() { switch AVCaptureDevice.authorizationStatus(for: .video) { case .notDetermined: sessionQueue.suspend() AVCaptureDevice.requestAccess(for: .video) { authorized in if !authorized { self.status = .unauthorized self.set(error: .deniedAuthorization) } self.sessionQueue.resume() } case .restricted: status = .unauthorized set(error: .restrictedAuthorization) case .denied: status = .unauthorized set(error: .deniedAuthorization) case .authorized: break @unknown default: status = .unauthorized set(error: .unknownAuthorization) } } private func configureCaptureSession() { guard status == .unconfigured else { return } session.beginConfiguration() session.sessionPreset = .photo defer { session.commitConfiguration() } self.camera = AVCaptureDevice.default( .builtInWideAngleCamera, for: .video, position: .back) guard let camera = camera else { set(error: .cameraUnavailable) status = .failed return } let cameraInput: AVCaptureDeviceInput do { cameraInput = try AVCaptureDeviceInput(device: camera) } catch { set(error: .createCaptureInput(error)) status = .failed return } guard session.canAddInput(cameraInput) else { set(error: .cannotAddInput) status = .failed return } session.addInput(cameraInput) if session.canAddOutput(videoOutput) { session.addOutput(videoOutput) videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] let videoConnection = videoOutput.connection(with: .video) videoConnection?.videoOrientation = .portrait } else { set(error: .cannotAddOutput) status = .failed return } guard session.canAddOutput(photoOutput) else { set(error: .cannotAddOutput) status = .failed return } session.addOutput(photoOutput) let maxFormat = camera.formats .reduce(into: []) { $0.append(contentsOf: $1.supportedMaxPhotoDimensions) } .max { $0.width * $0.height < $1.width * $1.height } if let maxFormat { photoOutput.maxPhotoDimensions = maxFormat } //photoOutput.isHighResolutionCaptureEnabled = true photoOutput.isDepthDataDeliveryEnabled = false photoOutput.isLivePhotoCaptureEnabled = false status = .configured } private func configure() { checkPermissions() sessionQueue.async { self.configureCaptureSession() self.session.startRunning() } } func setVideoDelegate(_ delegate: AVCaptureVideoDataOutputSampleBufferDelegate, queue: DispatchQueue) { sessionQueue.async { self.videoOutput.setSampleBufferDelegate(delegate, queue: queue) } } func stopVideoCaptureSession() { sessionQueue.async { guard self.status == .configured else { return } guard self.session.isRunning else { return } self.session.stopRunning() } } func startVideoCapture() { guard status == .configured else { return } sessionQueue.async { guard !self.session.isRunning else { return } self.session.startRunning() } } // MARK: Photo Capture func capturePhoto(delegate: AVCapturePhotoCaptureDelegate) { sessionQueue.async { let photoSettings = AVCapturePhotoSettings() photoSettings.flashMode = .off self.photoOutput.capturePhoto(with: photoSettings, delegate: delegate) } } // MARK: Focus func continuouslyFocusOnMiddle() { guard let device = camera else { return } do { try device.lockForConfiguration() if device.isFocusPointOfInterestSupported { device.focusPointOfInterest = CGPoint(x: 0.5, y: 0.5) device.focusMode = .continuousAutoFocus } print("Enabled continuous autofocus") device.unlockForConfiguration() } catch { self.error("Could not lock device for configuration: \(error)") } } } extension CameraManager: Logger { }