Skip to content

Instantly share code, notes, and snippets.

@robertofrontado
Last active March 5, 2023 16:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robertofrontado/78355e586fe13105de9d6ac4588202d0 to your computer and use it in GitHub Desktop.
Save robertofrontado/78355e586fe13105de9d6ac4588202d0 to your computer and use it in GitHub Desktop.
Camera Swift UI
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"
}
}
}
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