diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index c960617..2261684 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -507,7 +507,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - VisionCamera (3.6.6): + - VisionCamera (3.6.8): - React - React-callinvoker - React-Core @@ -747,9 +747,9 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - VisionCamera: 47d2342b724c78fb9ff3e2607c21ceda4ba21e75 + VisionCamera: ce927c396e1057199dd01bf412ba3777d900e166 Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.2 diff --git a/package/ios/Core/CameraSession+Audio.swift b/package/ios/Core/CameraSession+Audio.swift index 980e31c..a5c374f 100644 --- a/package/ios/Core/CameraSession+Audio.swift +++ b/package/ios/Core/CameraSession+Audio.swift @@ -23,6 +23,7 @@ extension CameraSession { let audioSession = AVAudioSession.sharedInstance() try audioSession.updateCategory(AVAudioSession.Category.playAndRecord, + mode: .videoRecording, options: [.mixWithOthers, .allowBluetoothA2DP, .defaultToSpeaker, @@ -39,6 +40,7 @@ extension CameraSession { } audioCaptureSession.startRunning() + ReactLogger.log(level: .info, message: "Audio Session activated!") } catch let error as NSError { ReactLogger.log(level: .error, message: "Failed to activate audio session! Error \(error.code): \(error.description)") switch error.code { @@ -54,6 +56,7 @@ extension CameraSession { ReactLogger.log(level: .info, message: "Deactivating Audio Session...") audioCaptureSession.stopRunning() + ReactLogger.log(level: .info, message: "Audio Session deactivated!") } @objc diff --git a/package/ios/Core/CameraSession+Video.swift b/package/ios/Core/CameraSession+Video.swift index 925b015..324e29c 100644 --- a/package/ios/Core/CameraSession+Video.swift +++ b/package/ios/Core/CameraSession+Video.swift @@ -19,6 +19,7 @@ extension CameraSession { onError: @escaping (_ error: CameraError) -> Void) { // Run on Camera Queue CameraQueues.cameraQueue.async { + let start = DispatchTime.now() ReactLogger.log(level: .info, message: "Starting Video recording...") if options.flash != .off { @@ -59,8 +60,8 @@ extension CameraSession { } } - self.recordingSession = nil self.isRecording = false + self.recordingSession = nil ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).") if let error = error as NSError? { @@ -93,47 +94,20 @@ extension CameraSession { 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 recordingSession: RecordingSession do { - recordingSession = try RecordingSession(url: tempURL, - fileType: options.fileType, - completion: onFinish) - } catch let error as NSError { - onError(.capture(.createRecorderError(message: error.description))) - return - } - self.recordingSession = recordingSession + // Create RecordingSession for the temp file + let recordingSession = try RecordingSession(url: tempURL, + fileType: options.fileType, + completion: onFinish) - // Init Video - guard var videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput, - fileType: options.fileType, - videoCodec: options.codec), - !videoSettings.isEmpty else { - 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 { + // 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 { @@ -145,16 +119,27 @@ extension CameraSession { // Initialize audio asset writer 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. - do { + // Init Video + let videoSettings = try videoOutput.recommendedVideoSettings(forOptions: options) + recordingSession.initializeVideoWriter(withSettings: videoSettings) + + // start recording session with or without audio. try recordingSession.startAssetWriter() + 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 { - 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 } } @@ -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) - } - } } diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index ac32609..d1c723a 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -272,12 +272,12 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC switch captureOutput { 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: - let timestamp = CMSyncConvertTime(CMSampleBufferGetPresentationTimeStamp(sampleBuffer), - from: audioCaptureSession.masterClock ?? CMClockGetHostTimeClock(), - to: captureSession.masterClock ?? CMClockGetHostTimeClock()) - recordingSession.appendBuffer(sampleBuffer, type: .audio, timestamp: timestamp) + // Synchronize the Audio Buffer with the Video Session's time because it's two separate AVCaptureSessions + audioCaptureSession.synchronizeBuffer(sampleBuffer, toSession: captureSession) + recordingSession.appendBuffer(sampleBuffer, type: .audio) default: break } diff --git a/package/ios/Core/RecordingSession.swift b/package/ios/Core/RecordingSession.swift index f02d929..66e9088 100644 --- a/package/ios/Core/RecordingSession.swift +++ b/package/ios/Core/RecordingSession.swift @@ -16,18 +16,12 @@ enum BufferType { case video } -// MARK: - RecordingSessionError - -enum RecordingSessionError: Error { - case failedToStartSession -} - // MARK: - RecordingSession class RecordingSession { private let assetWriter: AVAssetWriter private var audioWriter: AVAssetWriterInput? - private var bufferAdaptor: AVAssetWriterInputPixelBufferAdaptor? + private var videoWriter: AVAssetWriterInput? private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void private var initialTimestamp: CMTime? @@ -61,6 +55,7 @@ class RecordingSession { do { assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType) + assetWriter.shouldOptimizeForNetworkUse = false } catch let error as NSError { throw CameraError.capture(.createRecorderError(message: error.description)) } @@ -76,36 +71,38 @@ class RecordingSession { /** 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 { ReactLogger.log(level: .error, message: "Tried to initialize Video Writer with empty settings!") return } - guard bufferAdaptor == nil else { + guard videoWriter == nil else { ReactLogger.log(level: .error, message: "Tried to add Video Writer twice!") return } - let videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings) - videoWriter.expectsMediaDataInRealTime = true - - assetWriter.add(videoWriter) - bufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriter, - withVideoSettings: settings, - pixelFormat: pixelFormat) + ReactLogger.log(level: .info, message: "Initializing Video AssetWriter with settings: \(settings.description)") + videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings) + videoWriter!.expectsMediaDataInRealTime = true + assetWriter.add(videoWriter!) ReactLogger.log(level: .info, message: "Initialized Video AssetWriter.") } /** 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 { ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!") 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 assetWriter.add(audioWriter!) ReactLogger.log(level: .info, message: "Initialized Audio AssetWriter.") @@ -118,17 +115,18 @@ class RecordingSession { ReactLogger.log(level: .info, message: "Starting Asset Writer(s)...") 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)!") - 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. - 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 { ReactLogger.log(level: .error, message: "Frame arrived, but AssetWriter status is \(assetWriter.status.descriptor)!") return @@ -138,23 +136,18 @@ class RecordingSession { return } - latestTimestamp = timestamp - switch bufferType { case .video: - guard let bufferAdaptor = bufferAdaptor else { + guard let videoWriter = videoWriter else { ReactLogger.log(level: .error, message: "Video Frame arrived but VideoWriter was nil!") return } - if !bufferAdaptor.assetWriterInput.isReadyForMoreMediaData { + if !videoWriter.isReadyForMoreMediaData { ReactLogger.log(level: .warning, message: "The Video AVAssetWriterInput was not ready for more data! Is your frame rate too high?") return } - guard let imageBuffer = CMSampleBufferGetImageBuffer(buffer) else { - ReactLogger.log(level: .error, message: "Failed to get the CVImageBuffer!") - return - } + let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) // Start the writing session before we write the first video frame if !hasStartedWritingSession { initialTimestamp = timestamp @@ -162,7 +155,10 @@ class RecordingSession { ReactLogger.log(level: .info, message: "Started RecordingSession at \(timestamp.seconds) seconds.") hasStartedWritingSession = true } - bufferAdaptor.append(imageBuffer, withPresentationTime: timestamp) + // Write Video Buffer! + videoWriter.append(buffer) + // Update state + latestTimestamp = timestamp if !hasWrittenFirstVideoFrame { hasWrittenFirstVideoFrame = true } @@ -178,9 +174,11 @@ class RecordingSession { // first video frame has not been written yet, so skip this audio frame. return } + // Write Audio Sample! audioWriter.append(buffer) } + // If we failed to write the frames, stop the Recording if assetWriter.status == .failed { ReactLogger.log(level: .error, message: "AssetWriter failed to write buffer! Error: \(assetWriter.error?.localizedDescription ?? "none")") @@ -206,7 +204,7 @@ class RecordingSession { completionHandler(self, .failed, error) } else if assetWriter.status == .writing { isFinishing = true - bufferAdaptor?.assetWriterInput.markAsFinished() + videoWriter?.markAsFinished() audioWriter?.markAsFinished() assetWriter.finishWriting { self.isFinishing = false diff --git a/package/ios/Extensions/AVAudioSession+updateCategory.swift b/package/ios/Extensions/AVAudioSession+updateCategory.swift index c68c743..63ec9d4 100644 --- a/package/ios/Extensions/AVAudioSession+updateCategory.swift +++ b/package/ios/Extensions/AVAudioSession+updateCategory.swift @@ -13,11 +13,14 @@ extension AVAudioSession { /** 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 { - if self.category != category || categoryOptions.rawValue != options.rawValue { + func updateCategory(_ category: AVAudioSession.Category, + mode: AVAudioSession.Mode, + options: AVAudioSession.CategoryOptions = []) throws { + if self.category != category || categoryOptions.rawValue != options.rawValue || self.mode != mode { ReactLogger.log(level: .info, 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!") } } } diff --git a/package/ios/Extensions/AVCaptureSession+synchronizeBuffer.swift b/package/ios/Extensions/AVCaptureSession+synchronizeBuffer.swift new file mode 100644 index 0000000..615cf9e --- /dev/null +++ b/package/ios/Extensions/AVCaptureSession+synchronizeBuffer.swift @@ -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) + } +} diff --git a/package/ios/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift b/package/ios/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift new file mode 100644 index 0000000..ce69f06 --- /dev/null +++ b/package/ios/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift @@ -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 + } +} diff --git a/package/ios/Parsers/AVVideoCodecType+descriptor.swift b/package/ios/Parsers/AVVideoCodecType+descriptor.swift index cb57cec..11c6146 100644 --- a/package/ios/Parsers/AVVideoCodecType+descriptor.swift +++ b/package/ios/Parsers/AVVideoCodecType+descriptor.swift @@ -10,7 +10,7 @@ import AVFoundation import Foundation extension AVVideoCodecType { - init?(withString string: String) { + init(withString string: String) throws { switch string { case "h264": self = .h264 @@ -19,7 +19,7 @@ extension AVVideoCodecType { self = .hevc return default: - return nil + throw CameraError.parameter(.invalid(unionName: "codec", receivedValue: string)) } } } diff --git a/package/ios/Types/RecordVideoOptions.swift b/package/ios/Types/RecordVideoOptions.swift index 9bba5a3..83ee9d2 100644 --- a/package/ios/Types/RecordVideoOptions.swift +++ b/package/ios/Types/RecordVideoOptions.swift @@ -21,24 +21,15 @@ struct RecordVideoOptions { init(fromJSValue dictionary: NSDictionary) throws { // File Type (.mov or .mp4) if let fileTypeOption = dictionary["fileType"] as? String { - guard let parsed = try? AVFileType(withString: fileTypeOption) else { - throw CameraError.parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption)) - } - fileType = parsed + fileType = try AVFileType(withString: fileTypeOption) } // Flash if let flashOption = dictionary["flash"] as? String { - guard let parsed = try? Torch(jsValue: flashOption) else { - throw CameraError.parameter(.invalid(unionName: "flash", receivedValue: flashOption)) - } - flash = parsed + flash = try Torch(jsValue: flashOption) } // Codec if let codecOption = dictionary["codec"] as? String { - guard let parsed = try? AVVideoCodecType(withString: codecOption) else { - throw CameraError.parameter(.invalid(unionName: "codec", receivedValue: codecOption)) - } - codec = parsed + codec = try AVVideoCodecType(withString: codecOption) } // BitRate if let parsed = dictionary["bitRate"] as? Double { diff --git a/package/ios/VisionCamera.xcodeproj/project.pbxproj b/package/ios/VisionCamera.xcodeproj/project.pbxproj index e7c3897..768739e 100644 --- a/package/ios/VisionCamera.xcodeproj/project.pbxproj +++ b/package/ios/VisionCamera.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; }; 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 */; }; + 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 */; }; B8446E4D2ABA147C00E56077 /* CameraDevicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */; }; 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 = ""; }; B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+dimensions.swift"; sourceTree = ""; }; B81D41EF263C86F900B041FD /* JSINSObjectConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSINSObjectConversion.h; sourceTree = ""; }; + B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureSession+synchronizeBuffer.swift"; sourceTree = ""; }; + B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureVideoDataOutput+recommendedVideoSettings.swift"; sourceTree = ""; }; B83D5EE629377117000AFD2F /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraDevicesManager.swift; sourceTree = ""; }; B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraDevicesManager.m; sourceTree = ""; }; @@ -275,6 +279,8 @@ B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */, B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */, B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */, + B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */, + B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */, ); path = Extensions; sourceTree = ""; @@ -479,6 +485,7 @@ B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */, B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */, + B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */, B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */, B8FF60AE2ACC9731009D612F /* CodeScannerOptions.swift in Sources */, @@ -499,6 +506,7 @@ B8A1AEC62AD7F08E00169C0D /* CameraView+Focus.swift in Sources */, B88103E32AD7065C0087F063 /* CameraSessionDelegate.swift in Sources */, B887519E25E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */, + B8207AAF2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };