My goal is to implement a custom camera in SwiftUI which is able to capture Photos and Videos.
Based on this tutorial https://www.raywenderlich.com/26244793-building-a-camera-app-with-swiftui-and-combine I implemented functions to capture photos und movies. Everything works fine, but my AVCaptureVideoDataOutputSampleBufferDelegate doesn't get called if add movieOutput to the session. Therefore I can either take pictures with a preview or I can capture videos and photos without a preview. I want to achieve both simultaneously.
I've already found out that the problem is related to: Simultaneous AVCaptureVideoDataOutput and AVCaptureMovieFileOutput and https://developer.apple.com/forums/thread/98113. But there are no fitting solutions for my case.
Is there a way to capture movies and still use this kind of preview? (I do not wanna use a previewLayer)
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: "sessionQ")
private let videoOutput = AVCaptureVideoDataOutput()
private let photoOutput = AVCapturePhotoOutput()
private let movieOutput = AVCaptureMovieFileOutput()
@objc dynamic var videoDeviceInput: AVCaptureDeviceInput!
private var status = Status.unconfigured
private var currentCameraPosition: AVCaptureDevice.Position = .back
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()
defer {
session.commitConfiguration()
}
let device = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back)
guard let camera = device else {
set(error: .cameraUnavailable)
status = .failed
return
}
do {
try camera.lockForConfiguration()
defer {camera.unlockForConfiguration()}
camera.videoZoomFactor = camera.activeFormat.videoMaxZoomFactor
camera.ramp(toVideoZoomFactor: 16, withRate: 1)
// Camera Input
let cameraInput = try AVCaptureDeviceInput(device: camera)
if session.canAddInput(cameraInput) {
session.addInput(cameraInput)
self.videoDeviceInput = cameraInput
} else {
set(error: .cannotAddInput)
status = .failed
return
}
} catch {
set(error: .createCaptureInput(error))
status = .failed
return
}
// Add Output for captured Photos
if session.canAddOutput(photoOutput) {
session.addOutput(photoOutput)
let photoConnection = photoOutput.connection(with: .video)
photoConnection!.videoOrientation = .portrait
} else {
set(error: .cannotAddOutput)
status = .failed
return
}
// Add Output for Camera Preview
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
}
// if I uncomment this part I can capture movies, but the preview isnt shown anymore
/*
// Add Output for Captured Movie
if session.canAddOutput(movieOutput) {
session.addOutput(movieOutput)
let movieConnection = movieOutput.connection(with: .video)
movieConnection!.videoOrientation = .portrait
} else {
set(error: .cannotAddOutput)
status = .failed
return
}
*/
status = .configured
}
private func configure() {
checkPermissions()
sessionQueue.async {
self.configureCaptureSession()
self.session.startRunning()
}
}
func set(
_ delegate: AVCaptureVideoDataOutputSampleBufferDelegate,
queue: DispatchQueue
) {
sessionQueue.async {
self.videoOutput.setSampleBufferDelegate(delegate, queue: queue)
}
}
func capturePhoto(
with settings: AVCapturePhotoSettings,
_ delegate: AVCapturePhotoCaptureDelegate
) {
sessionQueue.async {
self.photoOutput.capturePhoto(with: settings, delegate: delegate)
}
}
func startRecording(
with settings: AVCapturePhotoSettings,
_ delegate: AVCaptureFileOutputRecordingDelegate
) {
sessionQueue.async {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let fileUrl = paths[0].appendingPathComponent("output.mov")
self.movieOutput.startRecording(to: fileUrl, recordingDelegate: delegate)
}
}
func stopRecording() {
sessionQueue.async {
self.movieOutput.stopRecording()
}
}
}
import AVFoundation
import UIKit
class FrameManager: NSObject, ObservableObject {
static let shared = FrameManager()
@Published var current: CVPixelBuffer?
@Published var currentPhoto: UIImage?
let videoOutputQueue = DispatchQueue(
label: "videoOutputQ",
qos: .userInitiated,
attributes: [],
autoreleaseFrequency: .workItem)
private override init() {
super.init()
CameraManager.shared.set(self, queue: videoOutputQueue)
}
func set(error: CameraError?) {
print(error)
}
var position: AVCaptureDevice.Position = .back
func capturePhoto(with settings: AVCapturePhotoSettings) {
CameraManager.shared.capturePhoto(with: settings, self)
}
func startRecording() {
CameraManager.shared.startRecording(with: AVCapturePhotoSettings(), self)
print("start recording")
}
func stopRecording() {
CameraManager.shared.stopRecording()
print("stop recording")
}
}
extension FrameManager: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
if let buffer = sampleBuffer.imageBuffer {
DispatchQueue.main.async {
self.current = buffer
}
}
}
}
extension FrameManager: AVCaptureFileOutputRecordingDelegate {
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
print(output.recordedDuration)
print(outputFileURL)
}
}
extension FrameManager: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
// captured photo
if let imageData = photo.fileDataRepresentation() {
self.currentPhoto = UIImage(data: imageData)?.imageFlippedForRightToLeftLayoutDirection()
}
}
}