depth-anything-example/DepthApp/Camera.swift (208 lines of code) (raw):
import AVFoundation
import CoreImage
import OSLog
import UIKit
fileprivate let logger = Logger(subsystem: "com.apple.createml.depth", category: "Camera")
final class Camera: NSObject {
let captureSession = AVCaptureSession()
private var isCaptureSessionConfigured = false
private var deviceInput: AVCaptureDeviceInput?
private var videoOutput: AVCaptureVideoDataOutput?
private var sessionQueue: DispatchQueue!
var captureDevice: AVCaptureDevice? {
didSet {
guard let captureDevice = captureDevice else { return }
logger.debug("Using capture device: \(captureDevice.localizedName)")
sessionQueue.async {
self.updateSessionForCaptureDevice(captureDevice)
}
}
}
var isRunning: Bool {
captureSession.isRunning
}
var isUsingFrontCaptureDevice: Bool {
guard let captureDevice = captureDevice else { return false }
return frontCaptureDevices.contains(captureDevice)
}
private var addToPreviewStream: ((CIImage) -> Void)?
var isPreviewPaused = false
lazy var previewStream: AsyncStream<CIImage> = {
AsyncStream { continuation in
addToPreviewStream = { ciImage in
if !self.isPreviewPaused {
continuation.yield(ciImage)
}
}
}
}()
override init() {
super.init()
sessionQueue = DispatchQueue(label: "com.apple.createml.depth.camera")
captureDevice = availableCaptureDevices.first ?? AVCaptureDevice.default(for: .video)
}
private func configureCaptureSession() -> Bool {
self.captureSession.beginConfiguration()
defer {
self.captureSession.commitConfiguration()
}
captureSession.sessionPreset = .hd1280x720
guard
let captureDevice = captureDevice,
let deviceInput = try? AVCaptureDeviceInput(device: captureDevice)
else {
logger.error("Failed to obtain video input.")
return false
}
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "VideoDataOutputQueue"))
guard captureSession.canAddInput(deviceInput) else {
logger.error("Unable to add device input to capture session.")
return false
}
guard captureSession.canAddOutput(videoOutput) else {
logger.error("Unable to add video output to capture session.")
return false
}
captureSession.addInput(deviceInput)
captureSession.addOutput(videoOutput)
self.deviceInput = deviceInput
self.videoOutput = videoOutput
updateVideoOutputConnection()
isCaptureSessionConfigured = true
return true
}
private func checkAuthorization() async -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
logger.debug("Camera access authorized.")
return true
case .notDetermined:
logger.debug("Camera access not determined.")
sessionQueue.suspend()
let status = await AVCaptureDevice.requestAccess(for: .video)
sessionQueue.resume()
return status
case .denied:
logger.debug("Camera access denied.")
return false
case .restricted:
logger.debug("Camera library access restricted.")
return false
@unknown default:
return false
}
}
private func deviceInputFor(device: AVCaptureDevice?) -> AVCaptureDeviceInput? {
guard let validDevice = device else { return nil }
do {
return try AVCaptureDeviceInput(device: validDevice)
} catch let error {
logger.error("Error getting capture device input: \(error.localizedDescription)")
return nil
}
}
private func updateSessionForCaptureDevice(_ captureDevice: AVCaptureDevice) {
guard isCaptureSessionConfigured else { return }
captureSession.beginConfiguration()
defer { captureSession.commitConfiguration() }
for input in captureSession.inputs {
if let deviceInput = input as? AVCaptureDeviceInput {
captureSession.removeInput(deviceInput)
}
}
if let deviceInput = deviceInputFor(device: captureDevice) {
if !captureSession.inputs.contains(deviceInput), captureSession.canAddInput(deviceInput) {
captureSession.addInput(deviceInput)
}
}
updateVideoOutputConnection()
}
private func updateVideoOutputConnection() {
if let videoOutput = videoOutput, let videoOutputConnection = videoOutput.connection(with: .video) {
if videoOutputConnection.isVideoMirroringSupported {
videoOutputConnection.isVideoMirrored = isUsingFrontCaptureDevice
}
}
}
func start() async {
let authorized = await checkAuthorization()
guard authorized else {
logger.error("Camera access was not authorized.")
return
}
if isCaptureSessionConfigured {
if !captureSession.isRunning {
sessionQueue.async { [self] in
self.captureSession.startRunning()
}
}
return
}
sessionQueue.async { [self] in
guard self.configureCaptureSession() else { return }
self.captureSession.startRunning()
}
}
func stop() {
guard isCaptureSessionConfigured else { return }
if captureSession.isRunning {
sessionQueue.async {
self.captureSession.stopRunning()
}
}
}
}
extension Camera: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = sampleBuffer.imageBuffer else {
return
}
// Match rotation to the preview layer, if present
if let previewConnection = captureSession.connections.first(where: { $0 != connection }),
connection.isVideoRotationAngleSupported(previewConnection.videoRotationAngle) {
connection.videoRotationAngle = previewConnection.videoRotationAngle
}
addToPreviewStream?(CIImage(cvPixelBuffer: pixelBuffer))
}
}
// MARK: Capture device
extension Camera {
private var availableCaptureDevices: [AVCaptureDevice] {
captureDevices
.filter( { $0.isConnected } )
.filter( { !$0.isSuspended } )
}
private var captureDevices: [AVCaptureDevice] {
var devices = [AVCaptureDevice]()
if let backDevice = backCaptureDevices.first {
devices += [backDevice]
}
if let frontDevice = frontCaptureDevices.first {
devices += [frontDevice]
}
return devices
}
private var frontCaptureDevices: [AVCaptureDevice] {
allCaptureDevices.filter({ $0.position == .front })
}
private var backCaptureDevices: [AVCaptureDevice] {
allCaptureDevices.filter({ $0.position == .back })
}
private var allCaptureDevices: [AVCaptureDevice] {
let deviceTypes: [AVCaptureDevice.DeviceType] = [
.builtInWideAngleCamera,
.builtInTripleCamera,
.builtInDualCamera,
.builtInDualWideCamera,
]
let session = AVCaptureDevice.DiscoverySession(
deviceTypes: deviceTypes,
mediaType: .video,
position: .unspecified
)
return session.devices
}
}