2022-06-10 21:20:49 +02:00
|
|
|
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
|
|
|
|
|
2022-06-21 19:38:51 +02:00
|
|
|
private var camera: AVCaptureDevice?
|
|
|
|
|
2022-06-10 21:20:49 +02:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-06-21 19:38:51 +02:00
|
|
|
self.camera = AVCaptureDevice.default(
|
2022-06-10 21:20:49 +02:00
|
|
|
.builtInWideAngleCamera,
|
|
|
|
for: .video,
|
|
|
|
position: .back)
|
2022-06-21 19:38:51 +02:00
|
|
|
guard let camera = camera else {
|
2022-06-10 21:20:49 +02:00
|
|
|
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)
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2022-06-21 19:38:51 +02:00
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
2022-06-10 21:20:49 +02:00
|
|
|
}
|