Compare commits
	
		
			7 Commits
		
	
	
		
			0a43d7a160
			...
			694d9cfa8c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 694d9cfa8c | ||
|  | 91767e71c8 | ||
|  | 9f2c7906e5 | ||
|  | 621bfe333c | ||
|  | 20f8fa2937 | ||
|  | b03f9ea423 | ||
|  | 98d90a6442 | 
| @@ -11,7 +11,7 @@ import AVFoundation | |||||||
| // MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate | // MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate | ||||||
|  |  | ||||||
| extension 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 |     // Type-safety | ||||||
|     let callback = Callback(jsCallback) |     let callback = Callback(jsCallback) | ||||||
|  |  | ||||||
| @@ -21,6 +21,7 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud | |||||||
|       // Start Recording with success and error callbacks |       // Start Recording with success and error callbacks | ||||||
|       cameraSession.startRecording( |       cameraSession.startRecording( | ||||||
|         options: options, |         options: options, | ||||||
|  |         filePath: filePath, | ||||||
|         onVideoRecorded: { video in |         onVideoRecorded: { video in | ||||||
|           callback.resolve(video.toJSValue()) |           callback.resolve(video.toJSValue()) | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -342,6 +342,7 @@ public final class CameraView: UIView, CameraSessionDelegate { | |||||||
|     ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)") |     ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)") | ||||||
|      |      | ||||||
|     guard let onVideoChunkReady, let onInitReady else { |     guard let onVideoChunkReady, let onInitReady else { | ||||||
|  |       ReactLogger.log(level: .warning, message: "Either onInitReady or onVideoChunkReady are not valid!") | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -64,7 +64,8 @@ RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock); | |||||||
| // Camera View Functions | // Camera View Functions | ||||||
| RCT_EXTERN_METHOD(startRecording | RCT_EXTERN_METHOD(startRecording | ||||||
|                   : (nonnull NSNumber*)node options |                   : (nonnull NSNumber*)node options | ||||||
|                   : (NSDictionary*)options onRecordCallback |                   : (NSDictionary*)options filePath | ||||||
|  |                   : (NSString*)filePath onRecordCallback | ||||||
|                   : (RCTResponseSenderBlock)onRecordCallback); |                   : (RCTResponseSenderBlock)onRecordCallback); | ||||||
| RCT_EXTERN_METHOD(pauseRecording | RCT_EXTERN_METHOD(pauseRecording | ||||||
|                   : (nonnull NSNumber*)node resolve |                   : (nonnull NSNumber*)node resolve | ||||||
|   | |||||||
| @@ -43,9 +43,9 @@ final class CameraViewManager: RCTViewManager { | |||||||
|   //       This means that any errors that occur in this function have to be delegated through |   //       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. |   //       the callback, but I'd prefer for them to throw for the original function instead. | ||||||
|   @objc |   @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) |     let component = getCameraView(withTag: node) | ||||||
|     component.startRecording(options: options, callback: onRecordCallback) |     component.startRecording(options: options, filePath: filePath as String, callback: onRecordCallback) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @objc |   @objc | ||||||
|   | |||||||
| @@ -176,6 +176,7 @@ enum CaptureError { | |||||||
|   case noRecordingInProgress |   case noRecordingInProgress | ||||||
|   case fileError |   case fileError | ||||||
|   case createTempFileError(message: String? = nil) |   case createTempFileError(message: String? = nil) | ||||||
|  |   case createRecordingDirectoryError(message: String? = nil) | ||||||
|   case createRecorderError(message: String? = nil) |   case createRecorderError(message: String? = nil) | ||||||
|   case videoNotEnabled |   case videoNotEnabled | ||||||
|   case photoNotEnabled |   case photoNotEnabled | ||||||
| @@ -193,6 +194,8 @@ enum CaptureError { | |||||||
|       return "file-io-error" |       return "file-io-error" | ||||||
|     case .createTempFileError: |     case .createTempFileError: | ||||||
|       return "create-temp-file-error" |       return "create-temp-file-error" | ||||||
|  |     case .createRecordingDirectoryError: | ||||||
|  |       return "create-recording-directory-error" | ||||||
|     case .createRecorderError: |     case .createRecorderError: | ||||||
|       return "create-recorder-error" |       return "create-recorder-error" | ||||||
|     case .videoNotEnabled: |     case .videoNotEnabled: | ||||||
| @@ -218,6 +221,8 @@ enum CaptureError { | |||||||
|       return "An unexpected File IO error occured!" |       return "An unexpected File IO error occured!" | ||||||
|     case let .createTempFileError(message: message): |     case let .createTempFileError(message: message): | ||||||
|       return "Failed to create a temporary file! \(message ?? "(no additional 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): |     case let .createRecorderError(message: message): | ||||||
|       return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" |       return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" | ||||||
|     case .videoNotEnabled: |     case .videoNotEnabled: | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ extension CameraSession { | |||||||
|    Starts a video + audio recording with a custom Asset Writer. |    Starts a video + audio recording with a custom Asset Writer. | ||||||
|    */ |    */ | ||||||
|   func startRecording(options: RecordVideoOptions, |   func startRecording(options: RecordVideoOptions, | ||||||
|  |                       filePath: String, | ||||||
|                       onVideoRecorded: @escaping (_ video: Video) -> Void, |                       onVideoRecorded: @escaping (_ video: Video) -> Void, | ||||||
|                       onError: @escaping (_ error: CameraError) -> Void) { |                       onError: @escaping (_ error: CameraError) -> Void) { | ||||||
|     // Run on Camera Queue |     // Run on Camera Queue | ||||||
| @@ -70,7 +71,7 @@ extension CameraSession { | |||||||
|         } else { |         } else { | ||||||
|           if status == .completed { |           if status == .completed { | ||||||
|             // Recording was successfully saved |             // Recording was successfully saved | ||||||
|             let video = Video(path: recordingSession.url.absoluteString, |             let video = Video(path: recordingSession.outputDiretory.absoluteString, | ||||||
|                               duration: recordingSession.duration, |                               duration: recordingSession.duration, | ||||||
|                               size: recordingSession.size ?? CGSize.zero) |                               size: recordingSession.size ?? CGSize.zero) | ||||||
|             onVideoRecorded(video) |             onVideoRecorded(video) | ||||||
| @@ -81,21 +82,20 @@ extension CameraSession { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Create temporary file |       if !FileManager.default.fileExists(atPath: filePath) { | ||||||
|       let errorPointer = ErrorPointer(nilLiteral: ()) |         do { | ||||||
|       let fileExtension = options.fileType.descriptor ?? "mov" |           try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true) | ||||||
|       guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else { |         } catch { | ||||||
|         let message = errorPointer?.pointee?.description |           onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription))) | ||||||
|         onError(.capture(.createTempFileError(message: message))) |           return | ||||||
|         return |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       ReactLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)") |       ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)") | ||||||
|       let tempURL = URL(string: "file://\(tempFilePath)")! |  | ||||||
|  |  | ||||||
|       do { |       do { | ||||||
|         // Create RecordingSession for the temp file |         // Create RecordingSession for the temp file | ||||||
|         let recordingSession = try RecordingSession(url: tempURL, |         let recordingSession = try RecordingSession(outputDiretory: filePath, | ||||||
|                                                     fileType: options.fileType, |                                                     fileType: options.fileType, | ||||||
|                                                     onChunkReady: onChunkReady, |                                                     onChunkReady: onChunkReady, | ||||||
|                                                     completion: onFinish) |                                                     completion: onFinish) | ||||||
|   | |||||||
| @@ -25,10 +25,10 @@ class ChunkedRecorder: NSObject { | |||||||
|   let outputURL: URL |   let outputURL: URL | ||||||
|   let onChunkReady: ((Chunk) -> Void) |   let onChunkReady: ((Chunk) -> Void) | ||||||
|    |    | ||||||
|   private var index: UInt64 = 0 |   private var chunkIndex: UInt64 = 0 | ||||||
|    |    | ||||||
|   init(url: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws { |   init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws { | ||||||
|     outputURL = url |     self.outputURL = outputURL | ||||||
|     self.onChunkReady = onChunkReady |     self.onChunkReady = onChunkReady | ||||||
|     guard FileManager.default.fileExists(atPath: outputURL.path) else { |     guard FileManager.default.fileExists(atPath: outputURL.path) else { | ||||||
|       throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil) |       throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil) | ||||||
| @@ -61,13 +61,11 @@ extension ChunkedRecorder: AVAssetWriterDelegate { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   private func saveSegment(_ data: Data) { |   private func saveSegment(_ data: Data) { | ||||||
|     defer { |     let name = "\(chunkIndex).mp4" | ||||||
|       index += 1 |  | ||||||
|     } |  | ||||||
|     let name = String(format: "%06d.mp4", index) |  | ||||||
|     let url = outputURL.appendingPathComponent(name) |     let url = outputURL.appendingPathComponent(name) | ||||||
|     save(data: data, url: url) |     save(data: data, url: url) | ||||||
|     onChunkReady(url: url, type: .data(index: index)) |     onChunkReady(url: url, type: .data(index: chunkIndex)) | ||||||
|  |     chunkIndex += 1 | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   private func save(data: Data, url: URL) { |   private func save(data: Data, url: URL) { | ||||||
|   | |||||||
| @@ -49,8 +49,7 @@ class RecordingSession { | |||||||
|   /** |   /** | ||||||
|    Gets the file URL of the recorded video. |    Gets the file URL of the recorded video. | ||||||
|    */ |    */ | ||||||
|   var url: URL { |   var outputDiretory: URL { | ||||||
|     // FIXME: |  | ||||||
|     return recorder.outputURL |     return recorder.outputURL | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -72,14 +71,15 @@ class RecordingSession { | |||||||
|     return (lastWrittenTimestamp - startTimestamp).seconds |     return (lastWrittenTimestamp - startTimestamp).seconds | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   init(url: URL, |   init(outputDiretory: String, | ||||||
|        fileType: AVFileType, |        fileType: AVFileType, | ||||||
|        onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void), |        onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void), | ||||||
|        completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { |        completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { | ||||||
|     completionHandler = completion |     completionHandler = completion | ||||||
|  |  | ||||||
|     do { |     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 = AVAssetWriter(contentType: UTType(fileType.rawValue)!) | ||||||
|       assetWriter.shouldOptimizeForNetworkUse = false |       assetWriter.shouldOptimizeForNetworkUse = false | ||||||
|       assetWriter.outputFileTypeProfile = .mpeg4AppleHLS |       assetWriter.outputFileTypeProfile = .mpeg4AppleHLS | ||||||
|   | |||||||
| @@ -10,18 +10,18 @@ import UIKit | |||||||
|  |  | ||||||
|  |  | ||||||
| enum RCTLogLevel: String { | enum RCTLogLevel: String { | ||||||
|     case trace |   case trace | ||||||
|     case info |   case info | ||||||
|     case warning |   case warning | ||||||
|     case error |   case error | ||||||
| } | } | ||||||
|  |  | ||||||
| enum RCTLogSource { | enum RCTLogSource { | ||||||
|     case native |   case native | ||||||
| } | } | ||||||
|  |  | ||||||
| func RCTDefaultLogFunction(_ level: RCTLogLevel, _ source: RCTLogSource, _ file: String, _ line: NSNumber, _ message: String) { | func RCTDefaultLogFunction(_ level: RCTLogLevel, _ source: RCTLogSource, _ file: String, _ line: NSNumber, _ message: String) { | ||||||
|     print(level.rawValue, "-", message) |   print(level.rawValue, "-", message) | ||||||
| } | } | ||||||
|  |  | ||||||
| typealias RCTDirectEventBlock = (Any?) -> Void | typealias RCTDirectEventBlock = (Any?) -> Void | ||||||
| @@ -30,53 +30,73 @@ typealias RCTPromiseRejectBlock = (String, String, NSError?) -> Void | |||||||
| typealias RCTResponseSenderBlock = (Any) -> Void | typealias RCTResponseSenderBlock = (Any) -> Void | ||||||
|  |  | ||||||
| func NSNull() -> [String: String] { | func NSNull() -> [String: String] { | ||||||
|     return [:] |   return [:] | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| func makeReactError(_ cameraError: CameraError, cause: NSError?) -> [String: Any] { | func makeReactError(_ cameraError: CameraError, cause: NSError?) -> [String: Any] { | ||||||
|     var causeDictionary: [String: Any]? |   var causeDictionary: [String: Any]? | ||||||
|     if let cause = cause { |   if let cause = cause { | ||||||
|         causeDictionary = [ |     causeDictionary = [ | ||||||
|             "cause": "\(cause.domain): \(cause.code) \(cause.description)", |       "cause": "\(cause.domain): \(cause.code) \(cause.description)", | ||||||
|             "userInfo": cause.userInfo |       "userInfo": cause.userInfo | ||||||
|         ] |  | ||||||
|     } |  | ||||||
|     return [ |  | ||||||
|         "error": "\(cameraError.code): \(cameraError.message)", |  | ||||||
|         "extra": [ |  | ||||||
|             "code": cameraError.code, |  | ||||||
|             "message": cameraError.message, |  | ||||||
|             "cause": causeDictionary ?? NSNull(), |  | ||||||
|         ] |  | ||||||
|     ] |     ] | ||||||
|  |   } | ||||||
|  |   return [ | ||||||
|  |     "error": "\(cameraError.code): \(cameraError.message)", | ||||||
|  |     "extra": [ | ||||||
|  |       "code": cameraError.code, | ||||||
|  |       "message": cameraError.message, | ||||||
|  |       "cause": causeDictionary ?? NSNull(), | ||||||
|  |     ] | ||||||
|  |   ] | ||||||
| } | } | ||||||
|  |  | ||||||
| func makeReactError(_ cameraError: CameraError) -> [String: Any] { | func makeReactError(_ cameraError: CameraError) -> [String: Any] { | ||||||
|     return makeReactError(cameraError, cause: nil) |   return makeReactError(cameraError, cause: nil) | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| class RCTFPSGraph: UIView { | class RCTFPSGraph: UIView { | ||||||
|     convenience init(frame: CGRect, color: UIColor) { |   convenience init(frame: CGRect, color: UIColor) { | ||||||
|         self.init(frame: frame) |     self.init(frame: frame) | ||||||
|     } |   } | ||||||
|    |    | ||||||
|     func onTick(_ tick: CFTimeInterval) { |   func onTick(_ tick: CFTimeInterval) { | ||||||
|      |      | ||||||
|     } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? { | func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? { | ||||||
|     let directory = NSTemporaryDirectory().appending("ReactNative") |   let directory = NSTemporaryDirectory().appending("ReactNative") | ||||||
|     let fm = FileManager.default |   let fm = FileManager.default | ||||||
|     if fm.fileExists(atPath: directory) { |   if fm.fileExists(atPath: directory) { | ||||||
|         try! fm.removeItem(atPath: directory) |     try! fm.removeItem(atPath: directory) | ||||||
|     } |   } | ||||||
|     if !fm.fileExists(atPath: directory) { |   if !fm.fileExists(atPath: directory) { | ||||||
|         try! fm.createDirectory(atPath: directory, withIntermediateDirectories: true) |     try! fm.createDirectory(atPath: directory, withIntermediateDirectories: true) | ||||||
|     } |   } | ||||||
|     return directory |   return directory | ||||||
|         .appending("/").appending(UUID().uuidString) |     .appending("/").appending(UUID().uuidString) | ||||||
|         .appending(".").appending(ext) |     .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() | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,95 +11,107 @@ import AVFoundation | |||||||
|  |  | ||||||
| class ViewController: UIViewController { | class ViewController: UIViewController { | ||||||
|    |    | ||||||
|     @IBOutlet weak var recordButton: UIButton! |   @IBOutlet weak var recordButton: UIButton! | ||||||
|    |    | ||||||
|     let cameraView = CameraView() |   let cameraView = CameraView() | ||||||
|  |   let filePath: String = { | ||||||
|  |     NSTemporaryDirectory() + "TestRecorder" | ||||||
|  |   }() | ||||||
|    |    | ||||||
|     override func viewDidLoad() { |   override func viewDidLoad() { | ||||||
|         super.viewDidLoad() |     super.viewDidLoad() | ||||||
|      |      | ||||||
|         cameraView.translatesAutoresizingMaskIntoConstraints = false; |     try? FileManager.default.removeItem(atPath: filePath) | ||||||
|         view.insertSubview(cameraView, at: 0) |  | ||||||
|         NSLayoutConstraint.activate([ |  | ||||||
|             cameraView.topAnchor.constraint(equalTo: view.topAnchor), |  | ||||||
|             cameraView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |  | ||||||
|             cameraView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |  | ||||||
|             cameraView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |  | ||||||
|         ]) |  | ||||||
|      |      | ||||||
|         recordButton.isHidden = true |     cameraView.translatesAutoresizingMaskIntoConstraints = false; | ||||||
|         cameraView.onInitialized = { _ in |     view.insertSubview(cameraView, at: 0) | ||||||
|             DispatchQueue.main.async { |     NSLayoutConstraint.activate([ | ||||||
|                 self.recordButton.isHidden = false |       cameraView.topAnchor.constraint(equalTo: view.topAnchor), | ||||||
|             } |       cameraView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | ||||||
|  |       cameraView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | ||||||
|  |       cameraView.bottomAnchor.constraint(equalTo: view.bottomAnchor), | ||||||
|  |     ]) | ||||||
|  |      | ||||||
|  |     recordButton.isHidden = true | ||||||
|  |     cameraView.onInitialized = { _ in | ||||||
|  |       DispatchQueue.main.async { | ||||||
|  |         self.recordButton.isHidden = false | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     cameraView.onInitReady = { json in | ||||||
|  |       print("onInitReady:", json ?? "nil") | ||||||
|  |     } | ||||||
|  |     cameraView.onVideoChunkReady = { json in | ||||||
|  |       print("onVideoChunkReady:", json ?? "nil") | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     Task { @MainActor in | ||||||
|  |       await requestAuthorizations() | ||||||
|  |        | ||||||
|  |       cameraView.photo = true | ||||||
|  |       cameraView.video = true | ||||||
|  |       cameraView.audio = false | ||||||
|  |       cameraView.isActive = true | ||||||
|  |       cameraView.cameraId = getCameraDeviceId() as NSString? | ||||||
|  |       cameraView.didSetProps([]) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   func isAuthorized(for mediaType: AVMediaType) async -> Bool { | ||||||
|  |     let status = AVCaptureDevice.authorizationStatus(for: mediaType) | ||||||
|  |     var isAuthorized = status == .authorized | ||||||
|  |     if status == .notDetermined { | ||||||
|  |       isAuthorized = await AVCaptureDevice.requestAccess(for: mediaType) | ||||||
|  |     } | ||||||
|  |     return isAuthorized | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |    | ||||||
|  |   func requestAuthorizations() async { | ||||||
|  |     guard await isAuthorized(for: .video) else { return } | ||||||
|  |     guard await isAuthorized(for: .audio) else { return } | ||||||
|  |     // Set up the capture session. | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private func getCameraDeviceId() -> String? { | ||||||
|  |     let deviceTypes: [AVCaptureDevice.DeviceType] = [ | ||||||
|  |       .builtInWideAngleCamera | ||||||
|  |     ] | ||||||
|  |     let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: .video, position: .back) | ||||||
|  |      | ||||||
|  |     let device = discoverySession.devices.first | ||||||
|  |      | ||||||
|  |     return device?.uniqueID | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   @IBAction | ||||||
|  |   func toggleRecord(_ button: UIButton) { | ||||||
|  |     if button.title(for: .normal) == "Stop" { | ||||||
|  |        | ||||||
|  |       cameraView.stopRecording(promise: Promise( | ||||||
|  |         resolver: { result in | ||||||
|  |           print("result") | ||||||
|  |         }, rejecter: { code, message, cause in | ||||||
|  |           print("error") | ||||||
|  |         })) | ||||||
|  |        | ||||||
|  |       button.setTitle("Record", for: .normal) | ||||||
|  |       button.configuration = .filled() | ||||||
|  |        | ||||||
|  |     } else { | ||||||
|  |       cameraView.startRecording( | ||||||
|  |         options: [ | ||||||
|  |           "fileType": "mp4", | ||||||
|  |           "videoCodec": "h265", | ||||||
|  |         ], | ||||||
|  |         filePath: filePath) { callback in | ||||||
|  |           print("callback", callback) | ||||||
|         } |         } | ||||||
|        |        | ||||||
|         Task { @MainActor in |       button.setTitle("Stop", for: .normal) | ||||||
|             await requestAuthorizations() |       button.configuration = .bordered() | ||||||
|              |  | ||||||
|             cameraView.photo = true |  | ||||||
|             cameraView.video = true |  | ||||||
|             cameraView.audio = false |  | ||||||
|             cameraView.isActive = true |  | ||||||
|             cameraView.cameraId = getCameraDeviceId() as NSString? |  | ||||||
|             cameraView.didSetProps([]) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     func isAuthorized(for mediaType: AVMediaType) async -> Bool { |  | ||||||
|         let status = AVCaptureDevice.authorizationStatus(for: mediaType) |  | ||||||
|         var isAuthorized = status == .authorized |  | ||||||
|         if status == .notDetermined { |  | ||||||
|             isAuthorized = await AVCaptureDevice.requestAccess(for: mediaType) |  | ||||||
|         } |  | ||||||
|         return isAuthorized |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     func requestAuthorizations() async { |  | ||||||
|         guard await isAuthorized(for: .video) else { return } |  | ||||||
|         guard await isAuthorized(for: .audio) else { return } |  | ||||||
|         // Set up the capture session. |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     private func getCameraDeviceId() -> String? { |  | ||||||
|         let deviceTypes: [AVCaptureDevice.DeviceType] = [ |  | ||||||
|             .builtInWideAngleCamera |  | ||||||
|         ] |  | ||||||
|         let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: .video, position: .back) |  | ||||||
|          |  | ||||||
|         let device = discoverySession.devices.first |  | ||||||
|          |  | ||||||
|         return device?.uniqueID |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     @IBAction |  | ||||||
|     func toggleRecord(_ button: UIButton) { |  | ||||||
|         if button.title(for: .normal) == "Stop" { |  | ||||||
|              |  | ||||||
|             cameraView.stopRecording(promise: Promise( |  | ||||||
|                 resolver: { result in |  | ||||||
|                     print("result") |  | ||||||
|                 }, rejecter: { code, message, cause in |  | ||||||
|                     print("error") |  | ||||||
|                 })) |  | ||||||
|              |  | ||||||
|             button.setTitle("Record", for: .normal) |  | ||||||
|             button.configuration = .filled() |  | ||||||
|              |  | ||||||
|         } else { |  | ||||||
|             cameraView.startRecording( |  | ||||||
|                 options: [ |  | ||||||
|                     "fileType": "mp4", |  | ||||||
|                     "videoCodec": "h265", |  | ||||||
|                 ]) { callback in |  | ||||||
|                     print("callback", callback) |  | ||||||
|                 } |  | ||||||
|              |  | ||||||
|             button.setTitle("Stop", for: .normal) |  | ||||||
|             button.configuration = .bordered() |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|    |    | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import AVFoundation | |||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| struct RecordVideoOptions { | struct RecordVideoOptions { | ||||||
|   var fileType: AVFileType = .mov |   var fileType: AVFileType = .mp4 | ||||||
|   var flash: Torch = .off |   var flash: Torch = .off | ||||||
|   var codec: AVVideoCodecType? |   var codec: AVVideoCodecType? | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -7,6 +7,9 @@ | |||||||
| 	objects = { | 	objects = { | ||||||
|  |  | ||||||
| /* Begin PBXBuildFile section */ | /* 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 */; }; | 		B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E852C410FB700CC198C /* ReactStubs.m */; }; | ||||||
| 		B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; }; | 		B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; }; | ||||||
| 		B3AF8E892C41159300CC198C /* 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 */, | 				B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */, | ||||||
| 				B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */, | 				B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */, | ||||||
| 				B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.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 */, | 				B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */, | ||||||
| 				B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */, | 				B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */, | ||||||
| 				B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */, | 				B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */, | ||||||
| @@ -709,6 +714,7 @@ | |||||||
| 				B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */, | 				B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */, | ||||||
| 				B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */, | 				B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */, | ||||||
| 				B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */, | 				B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */, | ||||||
|  | 				B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */, | ||||||
| 				B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */, | 				B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */, | ||||||
| 				B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */, | 				B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */, | ||||||
| 				B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */, | 				B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */, | ||||||
|   | |||||||
| @@ -26,6 +26,9 @@ interface OnErrorEvent { | |||||||
|   message: string |   message: string | ||||||
|   cause?: ErrorWithCause |   cause?: ErrorWithCause | ||||||
| } | } | ||||||
|  | interface OnInitReadyEvent { | ||||||
|  |   filepath: string | ||||||
|  | } | ||||||
| interface OnVideoChunkReadyEvent { | interface OnVideoChunkReadyEvent { | ||||||
|   filepath: string |   filepath: string | ||||||
|   index: number |   index: number | ||||||
| @@ -39,6 +42,7 @@ type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onE | |||||||
|   onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void |   onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void | ||||||
|   onStarted?: (event: NativeSyntheticEvent<void>) => void |   onStarted?: (event: NativeSyntheticEvent<void>) => void | ||||||
|   onStopped?: (event: NativeSyntheticEvent<void>) => void |   onStopped?: (event: NativeSyntheticEvent<void>) => void | ||||||
|  |   onInitReady?: (event: NativeSyntheticEvent<OnInitReadyEvent>) => void | ||||||
|   onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void |   onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void | ||||||
|   onViewReady: () => void |   onViewReady: () => void | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user