From 9f2c7906e519ef4724550a5ee439e01264228d80 Mon Sep 17 00:00:00 2001 From: Rui Rodrigues Date: Tue, 16 Jul 2024 09:50:21 +0100 Subject: [PATCH] add filePath to define recording directory - add CaptureError.createRecordingDirectoryError - stub RCTViewManager to be able to compile CameraViewManager --- package/ios/CameraView+RecordVideo.swift | 3 ++- package/ios/CameraViewManager.m | 3 ++- package/ios/CameraViewManager.swift | 4 ++-- package/ios/Core/CameraError.swift | 5 +++++ package/ios/Core/CameraSession+Video.swift | 22 +++++++++---------- package/ios/Core/ChunkedRecorder.swift | 4 ++-- package/ios/Core/RecordingSession.swift | 8 +++---- package/ios/TestRecorder/ReactStubs.swift | 20 +++++++++++++++++ package/ios/TestRecorder/ViewController.swift | 8 ++++++- .../VisionCamera.xcodeproj/project.pbxproj | 6 +++++ 10 files changed, 61 insertions(+), 22 deletions(-) diff --git a/package/ios/CameraView+RecordVideo.swift b/package/ios/CameraView+RecordVideo.swift index 8ecad27..11c4906 100644 --- a/package/ios/CameraView+RecordVideo.swift +++ b/package/ios/CameraView+RecordVideo.swift @@ -11,7 +11,7 @@ import AVFoundation // MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { - func startRecording(options: NSDictionary, callback jsCallback: @escaping RCTResponseSenderBlock) { + func startRecording(options: NSDictionary, filePath: String, callback jsCallback: @escaping RCTResponseSenderBlock) { // Type-safety let callback = Callback(jsCallback) @@ -21,6 +21,7 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud // Start Recording with success and error callbacks cameraSession.startRecording( options: options, + filePath: filePath, onVideoRecorded: { video in callback.resolve(video.toJSValue()) }, diff --git a/package/ios/CameraViewManager.m b/package/ios/CameraViewManager.m index 38f126b..fcf632f 100644 --- a/package/ios/CameraViewManager.m +++ b/package/ios/CameraViewManager.m @@ -64,7 +64,8 @@ RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock); // Camera View Functions RCT_EXTERN_METHOD(startRecording : (nonnull NSNumber*)node options - : (NSDictionary*)options onRecordCallback + : (NSDictionary*)options filePath + : (NSString*)filePath onRecordCallback : (RCTResponseSenderBlock)onRecordCallback); RCT_EXTERN_METHOD(pauseRecording : (nonnull NSNumber*)node resolve diff --git a/package/ios/CameraViewManager.swift b/package/ios/CameraViewManager.swift index 437f325..250daf0 100644 --- a/package/ios/CameraViewManager.swift +++ b/package/ios/CameraViewManager.swift @@ -43,9 +43,9 @@ final class CameraViewManager: RCTViewManager { // This means that any errors that occur in this function have to be delegated through // the callback, but I'd prefer for them to throw for the original function instead. @objc - final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) { + final func startRecording(_ node: NSNumber, options: NSDictionary, filePath: NSString, onRecordCallback: @escaping RCTResponseSenderBlock) { let component = getCameraView(withTag: node) - component.startRecording(options: options, callback: onRecordCallback) + component.startRecording(options: options, filePath: filePath as String, callback: onRecordCallback) } @objc diff --git a/package/ios/Core/CameraError.swift b/package/ios/Core/CameraError.swift index e460c32..feb2e9b 100644 --- a/package/ios/Core/CameraError.swift +++ b/package/ios/Core/CameraError.swift @@ -176,6 +176,7 @@ enum CaptureError { case noRecordingInProgress case fileError case createTempFileError(message: String? = nil) + case createRecordingDirectoryError(message: String? = nil) case createRecorderError(message: String? = nil) case videoNotEnabled case photoNotEnabled @@ -193,6 +194,8 @@ enum CaptureError { return "file-io-error" case .createTempFileError: return "create-temp-file-error" + case .createRecordingDirectoryError: + return "create-recording-directory-error" case .createRecorderError: return "create-recorder-error" case .videoNotEnabled: @@ -218,6 +221,8 @@ enum CaptureError { return "An unexpected File IO error occured!" case let .createTempFileError(message: message): return "Failed to create a temporary file! \(message ?? "(no additional message)")" + case let .createRecordingDirectoryError(message: message): + return "Failed to create a recording directory! \(message ?? "(no additional message)")" case let .createRecorderError(message: message): return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" case .videoNotEnabled: diff --git a/package/ios/Core/CameraSession+Video.swift b/package/ios/Core/CameraSession+Video.swift index 0b4b8e0..d35b3c9 100644 --- a/package/ios/Core/CameraSession+Video.swift +++ b/package/ios/Core/CameraSession+Video.swift @@ -15,6 +15,7 @@ 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) { // Run on Camera Queue @@ -70,7 +71,7 @@ extension CameraSession { } else { if status == .completed { // Recording was successfully saved - let video = Video(path: recordingSession.url.absoluteString, + let video = Video(path: recordingSession.outputDiretory.absoluteString, duration: recordingSession.duration, size: recordingSession.size ?? CGSize.zero) onVideoRecorded(video) @@ -81,21 +82,20 @@ extension CameraSession { } } - // Create temporary file - let errorPointer = ErrorPointer(nilLiteral: ()) - let fileExtension = options.fileType.descriptor ?? "mov" - guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else { - let message = errorPointer?.pointee?.description - onError(.capture(.createTempFileError(message: message))) - return + 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: \(tempFilePath)") - let tempURL = URL(string: "file://\(tempFilePath)")! + ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)") do { // Create RecordingSession for the temp file - let recordingSession = try RecordingSession(url: tempURL, + let recordingSession = try RecordingSession(outputDiretory: filePath, fileType: options.fileType, onChunkReady: onChunkReady, completion: onFinish) diff --git a/package/ios/Core/ChunkedRecorder.swift b/package/ios/Core/ChunkedRecorder.swift index f242df4..6f27e7d 100644 --- a/package/ios/Core/ChunkedRecorder.swift +++ b/package/ios/Core/ChunkedRecorder.swift @@ -27,8 +27,8 @@ class ChunkedRecorder: NSObject { private var chunkIndex: UInt64 = 0 - init(url: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws { - outputURL = url + init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws { + self.outputURL = outputURL self.onChunkReady = onChunkReady guard FileManager.default.fileExists(atPath: outputURL.path) else { throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil) diff --git a/package/ios/Core/RecordingSession.swift b/package/ios/Core/RecordingSession.swift index 5224705..d25827b 100644 --- a/package/ios/Core/RecordingSession.swift +++ b/package/ios/Core/RecordingSession.swift @@ -49,8 +49,7 @@ class RecordingSession { /** Gets the file URL of the recorded video. */ - var url: URL { - // FIXME: + var outputDiretory: URL { return recorder.outputURL } @@ -72,14 +71,15 @@ class RecordingSession { return (lastWrittenTimestamp - startTimestamp).seconds } - init(url: URL, + init(outputDiretory: String, fileType: AVFileType, onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void), completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { completionHandler = completion do { - recorder = try ChunkedRecorder(url: url.deletingLastPathComponent(), onChunkReady: onChunkReady) + let outputURL = URL(fileURLWithPath: outputDiretory) + recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady) assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!) assetWriter.shouldOptimizeForNetworkUse = false assetWriter.outputFileTypeProfile = .mpeg4AppleHLS diff --git a/package/ios/TestRecorder/ReactStubs.swift b/package/ios/TestRecorder/ReactStubs.swift index 1c18cd2..9b375bb 100644 --- a/package/ios/TestRecorder/ReactStubs.swift +++ b/package/ios/TestRecorder/ReactStubs.swift @@ -80,3 +80,23 @@ func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? { .appending("/").appending(UUID().uuidString) .appending(".").appending(ext) } + + +class RCTViewManager: NSObject { + + var methodQueue: DispatchQueue! { nil } + class func requiresMainQueueSetup() -> Bool { false } + func view() -> UIView! { nil } + + struct Bridge { + let uiManager = UIManager() + } + + struct UIManager { + func view(forReactTag: NSNumber) -> UIView! { + nil + } + } + + let bridge: Bridge = Bridge() +} diff --git a/package/ios/TestRecorder/ViewController.swift b/package/ios/TestRecorder/ViewController.swift index e66b8ff..9245565 100644 --- a/package/ios/TestRecorder/ViewController.swift +++ b/package/ios/TestRecorder/ViewController.swift @@ -14,10 +14,15 @@ class ViewController: UIViewController { @IBOutlet weak var recordButton: UIButton! let cameraView = CameraView() + let filePath: String = { + NSTemporaryDirectory() + "TestRecorder" + }() override func viewDidLoad() { super.viewDidLoad() + try? FileManager.default.removeItem(atPath: filePath) + cameraView.translatesAutoresizingMaskIntoConstraints = false; view.insertSubview(cameraView, at: 0) NSLayoutConstraint.activate([ @@ -98,7 +103,8 @@ class ViewController: UIViewController { options: [ "fileType": "mp4", "videoCodec": "h265", - ]) { callback in + ], + filePath: filePath) { callback in print("callback", callback) } diff --git a/package/ios/VisionCamera.xcodeproj/project.pbxproj b/package/ios/VisionCamera.xcodeproj/project.pbxproj index 986a922..b466d6b 100644 --- a/package/ios/VisionCamera.xcodeproj/project.pbxproj +++ b/package/ios/VisionCamera.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + B31481772C46547B00084A26 /* CameraViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518125E0102000DB86D6 /* CameraViewManager.swift */; }; + B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */; }; + B31481792C46559700084A26 /* CameraView+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */; }; B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E852C410FB700CC198C /* ReactStubs.m */; }; B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; }; B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; }; @@ -691,6 +694,8 @@ B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */, B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */, B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, + B31481792C46559700084A26 /* CameraView+Focus.swift in Sources */, + B31481772C46547B00084A26 /* CameraViewManager.swift in Sources */, B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */, B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */, B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */, @@ -709,6 +714,7 @@ B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */, B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */, B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */, + B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */, B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */, B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */, B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */,