0

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()
    }
  }
}
Nero
  • 31
  • 4

0 Answers0