233 lines
8.4 KiB
Swift
233 lines
8.4 KiB
Swift
//
|
|
// CameraSession+Video.swift
|
|
// VisionCamera
|
|
//
|
|
// Created by Marc Rousavy on 11.10.23.
|
|
// Copyright © 2023 mrousavy. All rights reserved.
|
|
//
|
|
|
|
import AVFoundation
|
|
import Foundation
|
|
import UIKit
|
|
|
|
extension CameraSession {
|
|
/**
|
|
Starts a video + audio recording with a custom Asset Writer.
|
|
*/
|
|
func startRecording(options: RecordVideoOptions,
|
|
filePath: String,
|
|
onVideoRecorded: @escaping (_ video: Video) -> Void,
|
|
onError: @escaping (_ error: CameraError) -> Void) {
|
|
|
|
lockCurrentExposure(for: captureSession)
|
|
// Run on Camera Queue
|
|
CameraQueues.cameraQueue.async {
|
|
let start = DispatchTime.now()
|
|
ReactLogger.log(level: .info, message: "Starting Video recording...")
|
|
|
|
// Get Video Output
|
|
guard let videoOutput = self.videoOutput else {
|
|
if self.configuration?.video == .disabled {
|
|
onError(.capture(.videoNotEnabled))
|
|
} else {
|
|
onError(.session(.cameraNotReady))
|
|
}
|
|
return
|
|
}
|
|
|
|
let enableAudio = self.configuration?.audio != .disabled
|
|
|
|
// Callback for when new chunks are ready
|
|
let onChunkReady: (ChunkedRecorder.Chunk) -> Void = { chunk in
|
|
guard let delegate = self.delegate else {
|
|
return
|
|
}
|
|
delegate.onVideoChunkReady(chunk: chunk)
|
|
}
|
|
|
|
// Callback for when the recording ends
|
|
let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in
|
|
defer {
|
|
// Disable Audio Session again
|
|
if enableAudio {
|
|
CameraQueues.audioQueue.async {
|
|
self.deactivateAudioSession()
|
|
}
|
|
}
|
|
}
|
|
|
|
self.isRecording = false
|
|
self.recordingSession = nil
|
|
ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).")
|
|
|
|
if let error = error as NSError? {
|
|
ReactLogger.log(level: .error, message: "RecordingSession Error \(error.code): \(error.description)")
|
|
// Something went wrong, we have an error
|
|
if error.domain == "capture/aborted" {
|
|
onError(.capture(.aborted))
|
|
} else if error.code == -11807 {
|
|
onError(.capture(.insufficientStorage))
|
|
} else {
|
|
onError(.capture(.unknown(message: "An unknown recording error occured! \(error.code) \(error.description)")))
|
|
}
|
|
} else {
|
|
if status == .completed {
|
|
// Recording was successfully saved
|
|
let video = Video(path: recordingSession.outputDiretory.absoluteString,
|
|
duration: recordingSession.duration,
|
|
size: recordingSession.size ?? CGSize.zero)
|
|
onVideoRecorded(video)
|
|
} else {
|
|
// Recording wasn't saved and we don't have an error either.
|
|
onError(.unknown(message: "AVAssetWriter completed with status: \(status.descriptor)"))
|
|
}
|
|
}
|
|
}
|
|
|
|
if !FileManager.default.fileExists(atPath: filePath) {
|
|
do {
|
|
try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true)
|
|
} catch {
|
|
onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription)))
|
|
return
|
|
}
|
|
}
|
|
|
|
ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)")
|
|
|
|
do {
|
|
// Create RecordingSession for the temp file
|
|
let recordingSession = try RecordingSession(outputDiretory: filePath,
|
|
fileType: options.fileType,
|
|
onChunkReady: onChunkReady,
|
|
completion: onFinish)
|
|
|
|
// Init Audio + Activate Audio Session (optional)
|
|
if enableAudio,
|
|
let audioOutput = self.audioOutput,
|
|
let audioInput = self.audioDeviceInput {
|
|
ReactLogger.log(level: .trace, message: "Enabling Audio for Recording...")
|
|
// Activate Audio Session asynchronously
|
|
CameraQueues.audioQueue.async {
|
|
do {
|
|
try self.activateAudioSession()
|
|
} catch {
|
|
self.onConfigureError(error)
|
|
}
|
|
}
|
|
|
|
// Initialize audio asset writer
|
|
let audioSettings = audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: options.fileType)
|
|
recordingSession.initializeAudioWriter(withSettings: audioSettings,
|
|
format: audioInput.device.activeFormat.formatDescription)
|
|
}
|
|
|
|
// Init Video
|
|
let videoSettings = try videoOutput.recommendedVideoSettings(forOptions: options)
|
|
recordingSession.initializeVideoWriter(withSettings: videoSettings)
|
|
|
|
// start recording session with or without audio.
|
|
// Use Video [AVCaptureSession] clock as a timebase - all other sessions (here; audio) have to be synced to that Clock.
|
|
try recordingSession.start(clock: self.captureSession.clock)
|
|
self.recordingSession = recordingSession
|
|
self.isRecording = true
|
|
|
|
let end = DispatchTime.now()
|
|
ReactLogger.log(level: .info, message: "RecordingSesssion started in \(Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000)ms!")
|
|
} catch let error as NSError {
|
|
if let error = error as? CameraError {
|
|
onError(error)
|
|
} else {
|
|
onError(.capture(.createRecorderError(message: "RecordingSession failed with unknown error: \(error.description)")))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Stops an active recording.
|
|
*/
|
|
func stopRecording(promise: Promise) {
|
|
CameraQueues.cameraQueue.async {
|
|
withPromise(promise) {
|
|
guard let recordingSession = self.recordingSession else {
|
|
throw CameraError.capture(.noRecordingInProgress)
|
|
}
|
|
// Use Video [AVCaptureSession] clock as a timebase - all other sessions (here; audio) have to be synced to that Clock.
|
|
recordingSession.stop(clock: self.captureSession.clock)
|
|
// There might be late frames, so maybe we need to still provide more Frames to the RecordingSession. Let's keep isRecording true for now.
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Pauses an active recording.
|
|
*/
|
|
func pauseRecording(promise: Promise) {
|
|
CameraQueues.cameraQueue.async {
|
|
withPromise(promise) {
|
|
guard self.recordingSession != nil else {
|
|
// there's no active recording!
|
|
throw CameraError.capture(.noRecordingInProgress)
|
|
}
|
|
self.isRecording = false
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Resumes an active, but paused recording.
|
|
*/
|
|
func resumeRecording(promise: Promise) {
|
|
CameraQueues.cameraQueue.async {
|
|
withPromise(promise) {
|
|
guard self.recordingSession != nil else {
|
|
// there's no active recording!
|
|
throw CameraError.capture(.noRecordingInProgress)
|
|
}
|
|
self.isRecording = true
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func lockCurrentExposure(for session: AVCaptureSession) {
|
|
guard let captureDevice = AVCaptureDevice.default(for: .video) else {
|
|
print("No capture device available")
|
|
return
|
|
}
|
|
|
|
guard captureDevice.isExposureModeSupported(.custom) else {
|
|
ReactLogger.log(level: .info, message: "Custom exposure mode not supported")
|
|
return
|
|
}
|
|
|
|
do {
|
|
// Lock the device for configuration
|
|
try captureDevice.lockForConfiguration()
|
|
|
|
// Get the current exposure duration and ISO
|
|
let currentExposureDuration = captureDevice.exposureDuration
|
|
let currentISO = captureDevice.iso
|
|
|
|
// Check if the device supports custom exposure settings
|
|
if captureDevice.isExposureModeSupported(.custom) {
|
|
// Lock the current exposure and ISO by setting custom exposure mode
|
|
captureDevice.setExposureModeCustom(duration: currentExposureDuration, iso: currentISO, completionHandler: nil)
|
|
ReactLogger.log(level: .info, message: "Exposure and ISO locked at current values")
|
|
} else {
|
|
ReactLogger.log(level: .info, message:"Custom exposure mode not supported")
|
|
}
|
|
|
|
// Unlock the device after configuration
|
|
captureDevice.unlockForConfiguration()
|
|
|
|
} catch {
|
|
ReactLogger.log(level: .warning, message:"Error locking exposure: \(error)")
|
|
}
|
|
}
|
|
}
|