fix: Fix Video Recording delay and improve startRecording()
performance (#2192)
* fix: Add more logs * perf: Init audio AVAssetWriter in parallel * chore: Log Audio Session activation * perf: Init Audio Session asynchronously * chore: Log when category changed * fix: Revert lazy audio initialization * Update Podfile.lock * Pass `sourceFormatHint` to video and audio AVAssetWriter * fix: Remove `sourceFormatHint` from Video Asset Writer * Use default options for Audio Asset Writer * Update Podfile.lock * Revert "Use default options for Audio Asset Writer" This reverts commit e575a14c5342ddc7f9db557d5e3915328ed9e798. * Add time logs * fix: Don't synchronize audio buffers, they are already in sync * shouldOptimizeForNetworkUse = false * fix: Only update `latestTimestamp` once video buffer has been written * perf: Use `AVAssetWriterInput` instead of `AVAssetWriterInputPixelBufferAdaptor` * fix: Fix Audio not being synchronized with Video * Remove logs add comments * Format * feat: Set `.videoRecording` AVAudioSession mode * Refactor `startRecording()` a bit * Format * chore: Throw error directly instead of double-checking
This commit is contained in:
parent
49d58d0d0c
commit
cf8f3d05e3
@ -507,7 +507,7 @@ PODS:
|
|||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.10)
|
- SDWebImage/Core (~> 5.10)
|
||||||
- SocketRocket (0.6.1)
|
- SocketRocket (0.6.1)
|
||||||
- VisionCamera (3.6.6):
|
- VisionCamera (3.6.8):
|
||||||
- React
|
- React
|
||||||
- React-callinvoker
|
- React-callinvoker
|
||||||
- React-Core
|
- React-Core
|
||||||
@ -747,9 +747,9 @@ SPEC CHECKSUMS:
|
|||||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||||
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
||||||
VisionCamera: 47d2342b724c78fb9ff3e2607c21ceda4ba21e75
|
VisionCamera: ce927c396e1057199dd01bf412ba3777d900e166
|
||||||
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
|
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
|
||||||
|
|
||||||
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb
|
PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb
|
||||||
|
|
||||||
COCOAPODS: 1.13.0
|
COCOAPODS: 1.14.2
|
||||||
|
@ -23,6 +23,7 @@ extension CameraSession {
|
|||||||
let audioSession = AVAudioSession.sharedInstance()
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
|
||||||
try audioSession.updateCategory(AVAudioSession.Category.playAndRecord,
|
try audioSession.updateCategory(AVAudioSession.Category.playAndRecord,
|
||||||
|
mode: .videoRecording,
|
||||||
options: [.mixWithOthers,
|
options: [.mixWithOthers,
|
||||||
.allowBluetoothA2DP,
|
.allowBluetoothA2DP,
|
||||||
.defaultToSpeaker,
|
.defaultToSpeaker,
|
||||||
@ -39,6 +40,7 @@ extension CameraSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
audioCaptureSession.startRunning()
|
audioCaptureSession.startRunning()
|
||||||
|
ReactLogger.log(level: .info, message: "Audio Session activated!")
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
ReactLogger.log(level: .error, message: "Failed to activate audio session! Error \(error.code): \(error.description)")
|
ReactLogger.log(level: .error, message: "Failed to activate audio session! Error \(error.code): \(error.description)")
|
||||||
switch error.code {
|
switch error.code {
|
||||||
@ -54,6 +56,7 @@ extension CameraSession {
|
|||||||
ReactLogger.log(level: .info, message: "Deactivating Audio Session...")
|
ReactLogger.log(level: .info, message: "Deactivating Audio Session...")
|
||||||
|
|
||||||
audioCaptureSession.stopRunning()
|
audioCaptureSession.stopRunning()
|
||||||
|
ReactLogger.log(level: .info, message: "Audio Session deactivated!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -19,6 +19,7 @@ extension CameraSession {
|
|||||||
onError: @escaping (_ error: CameraError) -> Void) {
|
onError: @escaping (_ error: CameraError) -> Void) {
|
||||||
// Run on Camera Queue
|
// Run on Camera Queue
|
||||||
CameraQueues.cameraQueue.async {
|
CameraQueues.cameraQueue.async {
|
||||||
|
let start = DispatchTime.now()
|
||||||
ReactLogger.log(level: .info, message: "Starting Video recording...")
|
ReactLogger.log(level: .info, message: "Starting Video recording...")
|
||||||
|
|
||||||
if options.flash != .off {
|
if options.flash != .off {
|
||||||
@ -59,8 +60,8 @@ extension CameraSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.recordingSession = nil
|
|
||||||
self.isRecording = false
|
self.isRecording = false
|
||||||
|
self.recordingSession = nil
|
||||||
ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).")
|
ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).")
|
||||||
|
|
||||||
if let error = error as NSError? {
|
if let error = error as NSError? {
|
||||||
@ -93,47 +94,20 @@ extension CameraSession {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactLogger.log(level: .info, message: "File path: \(tempFilePath)")
|
ReactLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)")
|
||||||
let tempURL = URL(string: "file://\(tempFilePath)")!
|
let tempURL = URL(string: "file://\(tempFilePath)")!
|
||||||
|
|
||||||
let recordingSession: RecordingSession
|
|
||||||
do {
|
do {
|
||||||
recordingSession = try RecordingSession(url: tempURL,
|
// Create RecordingSession for the temp file
|
||||||
fileType: options.fileType,
|
let recordingSession = try RecordingSession(url: tempURL,
|
||||||
completion: onFinish)
|
fileType: options.fileType,
|
||||||
} catch let error as NSError {
|
completion: onFinish)
|
||||||
onError(.capture(.createRecorderError(message: error.description)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.recordingSession = recordingSession
|
|
||||||
|
|
||||||
// Init Video
|
// Init Audio + Activate Audio Session (optional)
|
||||||
guard var videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput,
|
if enableAudio,
|
||||||
fileType: options.fileType,
|
let audioOutput = self.audioOutput,
|
||||||
videoCodec: options.codec),
|
let audioInput = self.audioDeviceInput {
|
||||||
!videoSettings.isEmpty else {
|
ReactLogger.log(level: .trace, message: "Enabling Audio for Recording...")
|
||||||
onError(.capture(.createRecorderError(message: "Failed to get video settings!")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ReactLogger.log(level: .trace, message: "Recommended Video Settings: \(videoSettings.description)")
|
|
||||||
|
|
||||||
// Custom Video Bit Rate
|
|
||||||
if let videoBitRate = options.bitRate {
|
|
||||||
// Convert from Mbps -> bps
|
|
||||||
let bitsPerSecond = videoBitRate * 1_000_000
|
|
||||||
videoSettings[AVVideoCompressionPropertiesKey] = [
|
|
||||||
AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// get pixel format (420f, 420v, x420)
|
|
||||||
let pixelFormat = videoOutput.pixelFormat
|
|
||||||
recordingSession.initializeVideoWriter(withSettings: videoSettings,
|
|
||||||
pixelFormat: pixelFormat)
|
|
||||||
|
|
||||||
// Enable/Activate Audio Session (optional)
|
|
||||||
if enableAudio {
|
|
||||||
if let audioOutput = self.audioOutput {
|
|
||||||
// Activate Audio Session asynchronously
|
// Activate Audio Session asynchronously
|
||||||
CameraQueues.audioQueue.async {
|
CameraQueues.audioQueue.async {
|
||||||
do {
|
do {
|
||||||
@ -145,16 +119,27 @@ extension CameraSession {
|
|||||||
|
|
||||||
// Initialize audio asset writer
|
// Initialize audio asset writer
|
||||||
let audioSettings = audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: options.fileType)
|
let audioSettings = audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: options.fileType)
|
||||||
recordingSession.initializeAudioWriter(withSettings: audioSettings)
|
recordingSession.initializeAudioWriter(withSettings: audioSettings,
|
||||||
|
format: audioInput.device.activeFormat.formatDescription)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// start recording session with or without audio.
|
// Init Video
|
||||||
do {
|
let videoSettings = try videoOutput.recommendedVideoSettings(forOptions: options)
|
||||||
|
recordingSession.initializeVideoWriter(withSettings: videoSettings)
|
||||||
|
|
||||||
|
// start recording session with or without audio.
|
||||||
try recordingSession.startAssetWriter()
|
try recordingSession.startAssetWriter()
|
||||||
|
self.recordingSession = recordingSession
|
||||||
self.isRecording = true
|
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 {
|
} catch let error as NSError {
|
||||||
onError(.capture(.createRecorderError(message: "RecordingSession failed to start asset writer. \(error.description)")))
|
if let error = error as? CameraError {
|
||||||
|
onError(error)
|
||||||
|
} else {
|
||||||
|
onError(.capture(.createRecorderError(message: "RecordingSession failed with unknown error: \(error.description)")))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,14 +193,4 @@ extension CameraSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func recommendedVideoSettings(videoOutput: AVCaptureVideoDataOutput,
|
|
||||||
fileType: AVFileType,
|
|
||||||
videoCodec: AVVideoCodecType?) -> [String: Any]? {
|
|
||||||
if videoCodec != nil {
|
|
||||||
return videoOutput.recommendedVideoSettings(forVideoCodecType: videoCodec!, assetWriterOutputFileType: fileType)
|
|
||||||
} else {
|
|
||||||
return videoOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -272,12 +272,12 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
|
|||||||
|
|
||||||
switch captureOutput {
|
switch captureOutput {
|
||||||
case is AVCaptureVideoDataOutput:
|
case is AVCaptureVideoDataOutput:
|
||||||
recordingSession.appendBuffer(sampleBuffer, type: .video, timestamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
|
// Write the Video Buffer to the .mov/.mp4 file, this is the first timestamp if nothing has been recorded yet
|
||||||
|
recordingSession.appendBuffer(sampleBuffer, type: .video)
|
||||||
case is AVCaptureAudioDataOutput:
|
case is AVCaptureAudioDataOutput:
|
||||||
let timestamp = CMSyncConvertTime(CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
|
// Synchronize the Audio Buffer with the Video Session's time because it's two separate AVCaptureSessions
|
||||||
from: audioCaptureSession.masterClock ?? CMClockGetHostTimeClock(),
|
audioCaptureSession.synchronizeBuffer(sampleBuffer, toSession: captureSession)
|
||||||
to: captureSession.masterClock ?? CMClockGetHostTimeClock())
|
recordingSession.appendBuffer(sampleBuffer, type: .audio)
|
||||||
recordingSession.appendBuffer(sampleBuffer, type: .audio, timestamp: timestamp)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -16,18 +16,12 @@ enum BufferType {
|
|||||||
case video
|
case video
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - RecordingSessionError
|
|
||||||
|
|
||||||
enum RecordingSessionError: Error {
|
|
||||||
case failedToStartSession
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - RecordingSession
|
// MARK: - RecordingSession
|
||||||
|
|
||||||
class RecordingSession {
|
class RecordingSession {
|
||||||
private let assetWriter: AVAssetWriter
|
private let assetWriter: AVAssetWriter
|
||||||
private var audioWriter: AVAssetWriterInput?
|
private var audioWriter: AVAssetWriterInput?
|
||||||
private var bufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?
|
private var videoWriter: AVAssetWriterInput?
|
||||||
private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void
|
private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void
|
||||||
|
|
||||||
private var initialTimestamp: CMTime?
|
private var initialTimestamp: CMTime?
|
||||||
@ -61,6 +55,7 @@ class RecordingSession {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType)
|
assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType)
|
||||||
|
assetWriter.shouldOptimizeForNetworkUse = false
|
||||||
} catch let error as NSError {
|
} catch let error as NSError {
|
||||||
throw CameraError.capture(.createRecorderError(message: error.description))
|
throw CameraError.capture(.createRecorderError(message: error.description))
|
||||||
}
|
}
|
||||||
@ -76,36 +71,38 @@ class RecordingSession {
|
|||||||
/**
|
/**
|
||||||
Initializes an AssetWriter for video frames (CMSampleBuffers).
|
Initializes an AssetWriter for video frames (CMSampleBuffers).
|
||||||
*/
|
*/
|
||||||
func initializeVideoWriter(withSettings settings: [String: Any], pixelFormat: OSType) {
|
func initializeVideoWriter(withSettings settings: [String: Any]) {
|
||||||
guard !settings.isEmpty else {
|
guard !settings.isEmpty else {
|
||||||
ReactLogger.log(level: .error, message: "Tried to initialize Video Writer with empty settings!")
|
ReactLogger.log(level: .error, message: "Tried to initialize Video Writer with empty settings!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard bufferAdaptor == nil else {
|
guard videoWriter == nil else {
|
||||||
ReactLogger.log(level: .error, message: "Tried to add Video Writer twice!")
|
ReactLogger.log(level: .error, message: "Tried to add Video Writer twice!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
|
ReactLogger.log(level: .info, message: "Initializing Video AssetWriter with settings: \(settings.description)")
|
||||||
videoWriter.expectsMediaDataInRealTime = true
|
videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
|
||||||
|
videoWriter!.expectsMediaDataInRealTime = true
|
||||||
assetWriter.add(videoWriter)
|
assetWriter.add(videoWriter!)
|
||||||
bufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriter,
|
|
||||||
withVideoSettings: settings,
|
|
||||||
pixelFormat: pixelFormat)
|
|
||||||
ReactLogger.log(level: .info, message: "Initialized Video AssetWriter.")
|
ReactLogger.log(level: .info, message: "Initialized Video AssetWriter.")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Initializes an AssetWriter for audio frames (CMSampleBuffers).
|
Initializes an AssetWriter for audio frames (CMSampleBuffers).
|
||||||
*/
|
*/
|
||||||
func initializeAudioWriter(withSettings settings: [String: Any]?) {
|
func initializeAudioWriter(withSettings settings: [String: Any]?, format: CMFormatDescription) {
|
||||||
guard audioWriter == nil else {
|
guard audioWriter == nil else {
|
||||||
ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!")
|
ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
audioWriter = AVAssetWriterInput(mediaType: .audio, outputSettings: settings)
|
if let settings = settings {
|
||||||
|
ReactLogger.log(level: .info, message: "Initializing Audio AssetWriter with settings: \(settings.description)")
|
||||||
|
} else {
|
||||||
|
ReactLogger.log(level: .info, message: "Initializing Audio AssetWriter default settings...")
|
||||||
|
}
|
||||||
|
audioWriter = AVAssetWriterInput(mediaType: .audio, outputSettings: settings, sourceFormatHint: format)
|
||||||
audioWriter!.expectsMediaDataInRealTime = true
|
audioWriter!.expectsMediaDataInRealTime = true
|
||||||
assetWriter.add(audioWriter!)
|
assetWriter.add(audioWriter!)
|
||||||
ReactLogger.log(level: .info, message: "Initialized Audio AssetWriter.")
|
ReactLogger.log(level: .info, message: "Initialized Audio AssetWriter.")
|
||||||
@ -118,17 +115,18 @@ class RecordingSession {
|
|||||||
ReactLogger.log(level: .info, message: "Starting Asset Writer(s)...")
|
ReactLogger.log(level: .info, message: "Starting Asset Writer(s)...")
|
||||||
|
|
||||||
let success = assetWriter.startWriting()
|
let success = assetWriter.startWriting()
|
||||||
if !success {
|
if success {
|
||||||
|
ReactLogger.log(level: .info, message: "Asset Writer(s) started!")
|
||||||
|
} else {
|
||||||
ReactLogger.log(level: .error, message: "Failed to start Asset Writer(s)!")
|
ReactLogger.log(level: .error, message: "Failed to start Asset Writer(s)!")
|
||||||
throw RecordingSessionError.failedToStartSession
|
throw CameraError.capture(.createRecorderError(message: "Failed to start Asset Writer(s)!"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Appends a new CMSampleBuffer to the Asset Writer. Use bufferType to specify if this is a video or audio frame.
|
Appends a new CMSampleBuffer to the Asset Writer. Use bufferType to specify if this is a video or audio frame.
|
||||||
The timestamp parameter represents the presentation timestamp of the buffer, which should be synchronized across video and audio frames.
|
|
||||||
*/
|
*/
|
||||||
func appendBuffer(_ buffer: CMSampleBuffer, type bufferType: BufferType, timestamp: CMTime) {
|
func appendBuffer(_ buffer: CMSampleBuffer, type bufferType: BufferType) {
|
||||||
guard assetWriter.status == .writing else {
|
guard assetWriter.status == .writing else {
|
||||||
ReactLogger.log(level: .error, message: "Frame arrived, but AssetWriter status is \(assetWriter.status.descriptor)!")
|
ReactLogger.log(level: .error, message: "Frame arrived, but AssetWriter status is \(assetWriter.status.descriptor)!")
|
||||||
return
|
return
|
||||||
@ -138,23 +136,18 @@ class RecordingSession {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
latestTimestamp = timestamp
|
|
||||||
|
|
||||||
switch bufferType {
|
switch bufferType {
|
||||||
case .video:
|
case .video:
|
||||||
guard let bufferAdaptor = bufferAdaptor else {
|
guard let videoWriter = videoWriter else {
|
||||||
ReactLogger.log(level: .error, message: "Video Frame arrived but VideoWriter was nil!")
|
ReactLogger.log(level: .error, message: "Video Frame arrived but VideoWriter was nil!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !bufferAdaptor.assetWriterInput.isReadyForMoreMediaData {
|
if !videoWriter.isReadyForMoreMediaData {
|
||||||
ReactLogger.log(level: .warning,
|
ReactLogger.log(level: .warning,
|
||||||
message: "The Video AVAssetWriterInput was not ready for more data! Is your frame rate too high?")
|
message: "The Video AVAssetWriterInput was not ready for more data! Is your frame rate too high?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
|
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||||
ReactLogger.log(level: .error, message: "Failed to get the CVImageBuffer!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Start the writing session before we write the first video frame
|
// Start the writing session before we write the first video frame
|
||||||
if !hasStartedWritingSession {
|
if !hasStartedWritingSession {
|
||||||
initialTimestamp = timestamp
|
initialTimestamp = timestamp
|
||||||
@ -162,7 +155,10 @@ class RecordingSession {
|
|||||||
ReactLogger.log(level: .info, message: "Started RecordingSession at \(timestamp.seconds) seconds.")
|
ReactLogger.log(level: .info, message: "Started RecordingSession at \(timestamp.seconds) seconds.")
|
||||||
hasStartedWritingSession = true
|
hasStartedWritingSession = true
|
||||||
}
|
}
|
||||||
bufferAdaptor.append(imageBuffer, withPresentationTime: timestamp)
|
// Write Video Buffer!
|
||||||
|
videoWriter.append(buffer)
|
||||||
|
// Update state
|
||||||
|
latestTimestamp = timestamp
|
||||||
if !hasWrittenFirstVideoFrame {
|
if !hasWrittenFirstVideoFrame {
|
||||||
hasWrittenFirstVideoFrame = true
|
hasWrittenFirstVideoFrame = true
|
||||||
}
|
}
|
||||||
@ -178,9 +174,11 @@ class RecordingSession {
|
|||||||
// first video frame has not been written yet, so skip this audio frame.
|
// first video frame has not been written yet, so skip this audio frame.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Write Audio Sample!
|
||||||
audioWriter.append(buffer)
|
audioWriter.append(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we failed to write the frames, stop the Recording
|
||||||
if assetWriter.status == .failed {
|
if assetWriter.status == .failed {
|
||||||
ReactLogger.log(level: .error,
|
ReactLogger.log(level: .error,
|
||||||
message: "AssetWriter failed to write buffer! Error: \(assetWriter.error?.localizedDescription ?? "none")")
|
message: "AssetWriter failed to write buffer! Error: \(assetWriter.error?.localizedDescription ?? "none")")
|
||||||
@ -206,7 +204,7 @@ class RecordingSession {
|
|||||||
completionHandler(self, .failed, error)
|
completionHandler(self, .failed, error)
|
||||||
} else if assetWriter.status == .writing {
|
} else if assetWriter.status == .writing {
|
||||||
isFinishing = true
|
isFinishing = true
|
||||||
bufferAdaptor?.assetWriterInput.markAsFinished()
|
videoWriter?.markAsFinished()
|
||||||
audioWriter?.markAsFinished()
|
audioWriter?.markAsFinished()
|
||||||
assetWriter.finishWriting {
|
assetWriter.finishWriting {
|
||||||
self.isFinishing = false
|
self.isFinishing = false
|
||||||
|
@ -13,11 +13,14 @@ extension AVAudioSession {
|
|||||||
/**
|
/**
|
||||||
Calls [setCategory] if the given category or options are not equal to the currently set category and options.
|
Calls [setCategory] if the given category or options are not equal to the currently set category and options.
|
||||||
*/
|
*/
|
||||||
func updateCategory(_ category: AVAudioSession.Category, options: AVAudioSession.CategoryOptions = []) throws {
|
func updateCategory(_ category: AVAudioSession.Category,
|
||||||
if self.category != category || categoryOptions.rawValue != options.rawValue {
|
mode: AVAudioSession.Mode,
|
||||||
|
options: AVAudioSession.CategoryOptions = []) throws {
|
||||||
|
if self.category != category || categoryOptions.rawValue != options.rawValue || self.mode != mode {
|
||||||
ReactLogger.log(level: .info,
|
ReactLogger.log(level: .info,
|
||||||
message: "Changing AVAudioSession category from \(self.category.rawValue) -> \(category.rawValue)")
|
message: "Changing AVAudioSession category from \(self.category.rawValue) -> \(category.rawValue)")
|
||||||
try setCategory(category, options: options)
|
try setCategory(category, mode: mode, options: options)
|
||||||
|
ReactLogger.log(level: .info, message: "AVAudioSession category changed!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// AVCaptureSession+synchronizeBuffer.swift
|
||||||
|
// VisionCamera
|
||||||
|
//
|
||||||
|
// Created by Marc Rousavy on 22.11.23.
|
||||||
|
// Copyright © 2023 mrousavy. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AVCaptureSession {
|
||||||
|
private var clock: CMClock {
|
||||||
|
if #available(iOS 15.4, *), let synchronizationClock {
|
||||||
|
return synchronizationClock
|
||||||
|
}
|
||||||
|
|
||||||
|
return masterClock ?? CMClockGetHostTimeClock()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Synchronizes a Buffer received from this [AVCaptureSession] to the timebase of the other given [AVCaptureSession].
|
||||||
|
*/
|
||||||
|
func synchronizeBuffer(_ buffer: CMSampleBuffer, toSession to: AVCaptureSession) {
|
||||||
|
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||||
|
let synchronizedTimestamp = CMSyncConvertTime(timestamp, from: clock, to: to.clock)
|
||||||
|
ReactLogger.log(level: .info, message: "Synchronized Timestamp \(timestamp.seconds) -> \(synchronizedTimestamp.seconds)")
|
||||||
|
CMSampleBufferSetOutputPresentationTimeStamp(buffer, newValue: synchronizedTimestamp)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// AVCaptureVideoDataOutput+recommendedVideoSettings.swift
|
||||||
|
// VisionCamera
|
||||||
|
//
|
||||||
|
// Created by Marc Rousavy on 22.11.23.
|
||||||
|
// Copyright © 2023 mrousavy. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AVCaptureVideoDataOutput {
|
||||||
|
/**
|
||||||
|
Get the recommended options for an [AVAssetWriter] with the desired [RecordVideoOptions].
|
||||||
|
*/
|
||||||
|
func recommendedVideoSettings(forOptions options: RecordVideoOptions) throws -> [String: Any] {
|
||||||
|
let settings: [String: Any]?
|
||||||
|
if let videoCodec = options.codec {
|
||||||
|
settings = recommendedVideoSettings(forVideoCodecType: videoCodec, assetWriterOutputFileType: options.fileType)
|
||||||
|
} else {
|
||||||
|
settings = recommendedVideoSettingsForAssetWriter(writingTo: options.fileType)
|
||||||
|
}
|
||||||
|
guard var settings else {
|
||||||
|
throw CameraError.capture(.createRecorderError(message: "Failed to get video settings!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let bitRate = options.bitRate {
|
||||||
|
// Convert from Mbps -> bps
|
||||||
|
let bitsPerSecond = bitRate * 1_000_000
|
||||||
|
settings[AVVideoCompressionPropertiesKey] = [
|
||||||
|
AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension AVVideoCodecType {
|
extension AVVideoCodecType {
|
||||||
init?(withString string: String) {
|
init(withString string: String) throws {
|
||||||
switch string {
|
switch string {
|
||||||
case "h264":
|
case "h264":
|
||||||
self = .h264
|
self = .h264
|
||||||
@ -19,7 +19,7 @@ extension AVVideoCodecType {
|
|||||||
self = .hevc
|
self = .hevc
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
return nil
|
throw CameraError.parameter(.invalid(unionName: "codec", receivedValue: string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,24 +21,15 @@ struct RecordVideoOptions {
|
|||||||
init(fromJSValue dictionary: NSDictionary) throws {
|
init(fromJSValue dictionary: NSDictionary) throws {
|
||||||
// File Type (.mov or .mp4)
|
// File Type (.mov or .mp4)
|
||||||
if let fileTypeOption = dictionary["fileType"] as? String {
|
if let fileTypeOption = dictionary["fileType"] as? String {
|
||||||
guard let parsed = try? AVFileType(withString: fileTypeOption) else {
|
fileType = try AVFileType(withString: fileTypeOption)
|
||||||
throw CameraError.parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption))
|
|
||||||
}
|
|
||||||
fileType = parsed
|
|
||||||
}
|
}
|
||||||
// Flash
|
// Flash
|
||||||
if let flashOption = dictionary["flash"] as? String {
|
if let flashOption = dictionary["flash"] as? String {
|
||||||
guard let parsed = try? Torch(jsValue: flashOption) else {
|
flash = try Torch(jsValue: flashOption)
|
||||||
throw CameraError.parameter(.invalid(unionName: "flash", receivedValue: flashOption))
|
|
||||||
}
|
|
||||||
flash = parsed
|
|
||||||
}
|
}
|
||||||
// Codec
|
// Codec
|
||||||
if let codecOption = dictionary["codec"] as? String {
|
if let codecOption = dictionary["codec"] as? String {
|
||||||
guard let parsed = try? AVVideoCodecType(withString: codecOption) else {
|
codec = try AVVideoCodecType(withString: codecOption)
|
||||||
throw CameraError.parameter(.invalid(unionName: "codec", receivedValue: codecOption))
|
|
||||||
}
|
|
||||||
codec = parsed
|
|
||||||
}
|
}
|
||||||
// BitRate
|
// BitRate
|
||||||
if let parsed = dictionary["bitRate"] as? Double {
|
if let parsed = dictionary["bitRate"] as? Double {
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; };
|
B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; };
|
||||||
B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
|
B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
|
||||||
B81BE1BF26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */; };
|
B81BE1BF26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */; };
|
||||||
|
B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */; };
|
||||||
|
B8207AAF2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */; };
|
||||||
B83D5EE729377117000AFD2F /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83D5EE629377117000AFD2F /* PreviewView.swift */; };
|
B83D5EE729377117000AFD2F /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83D5EE629377117000AFD2F /* PreviewView.swift */; };
|
||||||
B8446E4D2ABA147C00E56077 /* CameraDevicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */; };
|
B8446E4D2ABA147C00E56077 /* CameraDevicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */; };
|
||||||
B8446E502ABA14C900E56077 /* CameraDevicesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */; };
|
B8446E502ABA14C900E56077 /* CameraDevicesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */; };
|
||||||
@ -101,6 +103,8 @@
|
|||||||
B8103E5725FF56F0007A1684 /* Frame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Frame.h; sourceTree = "<group>"; };
|
B8103E5725FF56F0007A1684 /* Frame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Frame.h; sourceTree = "<group>"; };
|
||||||
B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+dimensions.swift"; sourceTree = "<group>"; };
|
B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+dimensions.swift"; sourceTree = "<group>"; };
|
||||||
B81D41EF263C86F900B041FD /* JSINSObjectConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSINSObjectConversion.h; sourceTree = "<group>"; };
|
B81D41EF263C86F900B041FD /* JSINSObjectConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSINSObjectConversion.h; sourceTree = "<group>"; };
|
||||||
|
B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureSession+synchronizeBuffer.swift"; sourceTree = "<group>"; };
|
||||||
|
B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureVideoDataOutput+recommendedVideoSettings.swift"; sourceTree = "<group>"; };
|
||||||
B83D5EE629377117000AFD2F /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = "<group>"; };
|
B83D5EE629377117000AFD2F /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = "<group>"; };
|
||||||
B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraDevicesManager.swift; sourceTree = "<group>"; };
|
B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraDevicesManager.swift; sourceTree = "<group>"; };
|
||||||
B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraDevicesManager.m; sourceTree = "<group>"; };
|
B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraDevicesManager.m; sourceTree = "<group>"; };
|
||||||
@ -275,6 +279,8 @@
|
|||||||
B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */,
|
B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */,
|
||||||
B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */,
|
B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */,
|
||||||
B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */,
|
B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */,
|
||||||
|
B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */,
|
||||||
|
B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -479,6 +485,7 @@
|
|||||||
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
|
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
|
||||||
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
|
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
|
||||||
B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */,
|
B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */,
|
||||||
|
B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
|
||||||
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
|
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
|
||||||
B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */,
|
B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */,
|
||||||
B8FF60AE2ACC9731009D612F /* CodeScannerOptions.swift in Sources */,
|
B8FF60AE2ACC9731009D612F /* CodeScannerOptions.swift in Sources */,
|
||||||
@ -499,6 +506,7 @@
|
|||||||
B8A1AEC62AD7F08E00169C0D /* CameraView+Focus.swift in Sources */,
|
B8A1AEC62AD7F08E00169C0D /* CameraView+Focus.swift in Sources */,
|
||||||
B88103E32AD7065C0087F063 /* CameraSessionDelegate.swift in Sources */,
|
B88103E32AD7065C0087F063 /* CameraSessionDelegate.swift in Sources */,
|
||||||
B887519E25E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */,
|
B887519E25E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */,
|
||||||
|
B8207AAF2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user