Last active
March 5, 2023 16:32
-
-
Save robertofrontado/78355e586fe13105de9d6ac4588202d0 to your computer and use it in GitHub Desktop.
Camera Swift UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AVFoundation | |
import CoreImage | |
import CoreGraphics | |
/// | |
class CameraManager: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDelegate { | |
enum Status { | |
case unconfigured | |
case configured | |
case unauthorized | |
case failed | |
} | |
@Published var cgImage: CGImage? | |
@Published var error: CameraError? | |
@Published var status = Status.unconfigured | |
var isTorchOn: Bool = false { | |
didSet { | |
toggleTorch() | |
} | |
} | |
var isRunning: Bool { session.isRunning } | |
private let session = AVCaptureSession() | |
private let sessionQueue = DispatchQueue(label: "com.frontado.app.sessionQueue") | |
private let videoOutput = AVCaptureVideoDataOutput() | |
private let videoOutputQueue = DispatchQueue( | |
label: "com.frontado.app.videoQueue", | |
qos: .userInitiated, | |
attributes: [], | |
autoreleaseFrequency: .workItem | |
) | |
private let context = CIContext() | |
func start(onStarted: @escaping () -> Void) { | |
checkPermissions() | |
sessionQueue.async { | |
self.configureCaptureSession() | |
self.session.startRunning() | |
self.toggleTorch() | |
onStarted() | |
} | |
} | |
func stop() { | |
sessionQueue.async { | |
self.session.stopRunning() | |
} | |
} | |
func toggleTorch() { | |
guard let device = getDevice(), device.hasTorch, device.isTorchModeSupported(.on) else { return } | |
do { | |
try device.lockForConfiguration() | |
device.torchMode = isTorchOn ? .on : .off | |
if isTorchOn { | |
try device.setTorchModeOn(level: AVCaptureDevice.maxAvailableTorchLevel) | |
} | |
device.unlockForConfiguration() | |
} catch { | |
print("Error: \(error)") | |
} | |
} | |
// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate | |
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | |
DispatchQueue.main.async { | |
self.cgImage = self.generateImage(from: sampleBuffer) | |
} | |
} | |
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.status = .configured | |
self.sessionQueue.resume() | |
self.toggleTorch() | |
} | |
case .restricted: | |
status = .unauthorized | |
set(error: .restrictedAuthorization) | |
case .denied: | |
status = .unauthorized | |
set(error: .deniedAuthorization) | |
case .authorized: | |
self.status = .configured | |
@unknown default: | |
status = .unauthorized | |
set(error: .unknownAuthorization) | |
} | |
} | |
// MARK: - Private | |
private func generateImage(from sampleBuffer: CMSampleBuffer) -> CGImage? { | |
guard let imageBuffer = sampleBuffer.imageBuffer else { | |
return nil | |
} | |
let ciImage = CIImage(cvImageBuffer: imageBuffer) | |
return context.createCGImage(ciImage, from: ciImage.extent) | |
} | |
private func set(error: CameraError?) { | |
DispatchQueue.main.async { | |
self.error = error | |
} | |
} | |
private func configureCaptureSession() { | |
guard status == .unconfigured else { | |
return | |
} | |
session.beginConfiguration() | |
defer { | |
session.commitConfiguration() | |
} | |
guard let device = getDevice() else { | |
set(error: .cameraUnavailable) | |
status = .failed | |
return | |
} | |
do { | |
let deviceInput = try AVCaptureDeviceInput(device: device) | |
if session.canAddInput(deviceInput) { | |
session.addInput(deviceInput) | |
} else { | |
set(error: .cannotAddInput) | |
status = .failed | |
return | |
} | |
} catch { | |
set(error: .createCaptureInput(error)) | |
status = .failed | |
return | |
} | |
if session.canAddOutput(videoOutput) { | |
session.addOutput(videoOutput) | |
videoOutput.videoSettings = | |
[kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | |
let videoConnection = videoOutput.connection(with: .video) | |
videoConnection?.videoOrientation = .portrait | |
videoOutput.setSampleBufferDelegate(self, queue: videoOutputQueue) | |
} else { | |
set(error: .cannotAddOutput) | |
status = .failed | |
return | |
} | |
status = .configured | |
} | |
private func getDevice() -> AVCaptureDevice? { | |
AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) | |
} | |
} | |
enum CameraError: Error { | |
case cameraUnavailable | |
case cannotAddInput | |
case cannotAddOutput | |
case createCaptureInput(Error) | |
case deniedAuthorization | |
case restrictedAuthorization | |
case unknownAuthorization | |
} | |
extension CameraError: LocalizedError { | |
var errorDescription: String? { | |
switch self { | |
case .cameraUnavailable: | |
return "Camera unavailable" | |
case .cannotAddInput: | |
return "Cannot add capture input to session" | |
case .cannotAddOutput: | |
return "Cannot add video output to session" | |
case .createCaptureInput(let error): | |
return "Creating capture input for camera: \(error.localizedDescription)" | |
case .deniedAuthorization: | |
return "Camera access denied" | |
case .restrictedAuthorization: | |
return "Attempting to access a restricted capture device" | |
case .unknownAuthorization: | |
return "Unknown authorization status for capture device" | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AVFoundation | |
import SwiftUI | |
/// | |
struct CameraView: View { | |
@StateObject var cameraManager: CameraManager | |
init(cameraManager: CameraManager) { | |
self._cameraManager = StateObject(wrappedValue: cameraManager) | |
} | |
var body: some View { | |
if let cgImage = cameraManager.cgImage { | |
GeometryReader { geometry in | |
Image(decorative: cgImage, scale: 1.0, orientation: .upMirrored) | |
.resizable() | |
.scaledToFill() | |
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center) | |
.clipped() | |
} | |
} else { | |
Color.black | |
} | |
} | |
} | |
struct CameraView_Previews: PreviewProvider { | |
static var previews: some View { | |
CameraView(cameraManager: .init()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment