react-native-vision-camera/package/ios/Core/CameraSession+Video.swift

233 lines
8.4 KiB
Swift
Raw Normal View History

//
// 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) {
2024-09-27 02:35:29 -06:00
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
}
}
}
2024-09-27 02:35:29 -06:00
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
}
2024-09-27 02:35:29 -06:00
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)")
}
}
}