From 4837b51001a43ed15d735a89004558a8f772046d Mon Sep 17 00:00:00 2001 From: mariodeaconescu Date: Wed, 26 Jul 2023 14:22:58 +0300 Subject: [PATCH 1/8] Added video recording prototype to iOS plugin --- ios/Plugin/CameraController.swift | 96 +++++++++---------------------- 1 file changed, 27 insertions(+), 69 deletions(-) diff --git a/ios/Plugin/CameraController.swift b/ios/Plugin/CameraController.swift index a09e72a0..15cdfb37 100644 --- a/ios/Plugin/CameraController.swift +++ b/ios/Plugin/CameraController.swift @@ -17,7 +17,7 @@ class CameraController: NSObject { var frontCamera: AVCaptureDevice? var frontCameraInput: AVCaptureDeviceInput? - var dataOutput: AVCaptureVideoDataOutput? + var videoOutput: AVCaptureMovieFileOutput? var photoOutput: AVCapturePhotoOutput? var rearCamera: AVCaptureDevice? @@ -27,6 +27,7 @@ class CameraController: NSObject { var flashMode = AVCaptureDevice.FlashMode.off var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)? + var videoCaptureCompletionBlock: ((URL?, Error?) -> Void)? var sampleBufferCaptureCompletionBlock: ((UIImage?, Error?) -> Void)? @@ -42,6 +43,7 @@ extension CameraController { func prepare(cameraPosition: String, disableAudio: Bool, completionHandler: @escaping (Error?) -> Void) { func createCaptureSession() { self.captureSession = AVCaptureSession() + self.captureSession?.beginConfiguration() } func configureCaptureDevices() throws { @@ -110,25 +112,19 @@ extension CameraController { self.photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil) self.photoOutput?.isHighResolutionCaptureEnabled = self.highResolutionOutput if captureSession.canAddOutput(self.photoOutput!) { captureSession.addOutput(self.photoOutput!) } - captureSession.startRunning() } - func configureDataOutput() throws { + func configureVideoOutput() throws { guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing } - - self.dataOutput = AVCaptureVideoDataOutput() - self.dataOutput?.videoSettings = [ - (kCVPixelBufferPixelFormatTypeKey as String): NSNumber(value: kCVPixelFormatType_32BGRA as UInt32) - ] - self.dataOutput?.alwaysDiscardsLateVideoFrames = true - if captureSession.canAddOutput(self.dataOutput!) { - captureSession.addOutput(self.dataOutput!) + + self.videoOutput = AVCaptureMovieFileOutput() + + if captureSession.canAddOutput(self.videoOutput!) { + captureSession.addOutput(self.videoOutput!) + } else { + throw CameraControllerError.invalidOperation } - - captureSession.commitConfiguration() - - let queue = DispatchQueue(label: "DataOutput", attributes: []) - self.dataOutput?.setSampleBufferDelegate(self, queue: queue) + } DispatchQueue(label: "prepare").async { @@ -137,8 +133,9 @@ extension CameraController { try configureCaptureDevices() try configureDeviceInputs() try configurePhotoOutput() - try configureDataOutput() - // try configureVideoOutput() + try configureVideoOutput() + self.captureSession?.commitConfiguration() + self.captureSession?.startRunning() } catch { DispatchQueue.main.async { completionHandler(error) @@ -204,7 +201,8 @@ extension CameraController { } previewLayer?.connection?.videoOrientation = videoOrientation - dataOutput?.connections.forEach { $0.videoOrientation = videoOrientation } + //Orientation is not supported for video connections + //videoOutput?.connections.forEach { $0.videoOrientation = videoOrientation } photoOutput?.connections.forEach { $0.videoOrientation = videoOrientation } } @@ -409,8 +407,10 @@ extension CameraController { let fileUrl = path.appendingPathComponent(fileName) try? FileManager.default.removeItem(at: fileUrl) - /*videoOutput!.startRecording(to: fileUrl, recordingDelegate: self) - self.videoRecordCompletionBlock = completion*/ + + self.videoCaptureCompletionBlock = completion + + videoOutput!.startRecording(to: fileUrl, recordingDelegate: self) } func stopRecording(completion: @escaping (Error?) -> Void) { @@ -418,7 +418,7 @@ extension CameraController { completion(CameraControllerError.captureSessionIsMissing) return } - // self.videoOutput?.stopRecording() + self.videoOutput?.stopRecording() } } @@ -495,48 +495,6 @@ extension CameraController: AVCapturePhotoCaptureDelegate { } } -extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate { - func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { - guard let completion = sampleBufferCaptureCompletionBlock else { return } - - guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { - completion(nil, CameraControllerError.unknown) - return - } - - CVPixelBufferLockBaseAddress(imageBuffer, .readOnly) - defer { CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly) } - - let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer) - let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) - let width = CVPixelBufferGetWidth(imageBuffer) - let height = CVPixelBufferGetHeight(imageBuffer) - let colorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | - CGImageAlphaInfo.premultipliedFirst.rawValue - - let context = CGContext( - data: baseAddress, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: bytesPerRow, - space: colorSpace, - bitmapInfo: bitmapInfo - ) - - guard let cgImage = context?.makeImage() else { - completion(nil, CameraControllerError.unknown) - return - } - - let image = UIImage(cgImage: cgImage) - completion(image.fixedOrientation(), nil) - - sampleBufferCaptureCompletionBlock = nil - } -} - enum CameraControllerError: Swift.Error { case captureSessionAlreadyRunning case captureSessionIsMissing @@ -639,10 +597,10 @@ extension UIImage { extension CameraController: AVCaptureFileOutputRecordingDelegate { func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { - /*if error == nil { - self.videoRecordCompletionBlock?(outputFileURL, nil) - } else { - self.videoRecordCompletionBlock?(nil, error) - }*/ + if error == nil { + self.videoCaptureCompletionBlock?(outputFileURL, nil) + } else { + self.videoCaptureCompletionBlock?(nil, error) + } } } From b7d6874388563f4962343aae3315cb2e7dbd3b19 Mon Sep 17 00:00:00 2001 From: mariodeaconescu Date: Wed, 26 Jul 2023 14:23:44 +0300 Subject: [PATCH 2/8] Changed startRecordVideo type to reflect web return type --- src/definitions.ts | 2 +- src/web.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/definitions.ts b/src/definitions.ts index 1e4efe00..042c8dc7 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -60,7 +60,7 @@ export interface CameraOpacityOptions { export interface CameraPreviewPlugin { start(options: CameraPreviewOptions): Promise<{}>; - startRecordVideo(options: CameraPreviewOptions): Promise<{}>; + startRecordVideo(options: CameraPreviewOptions): Promise<{ value: string } | never>; stop(): Promise<{}>; stopRecordVideo(): Promise<{}>; capture(options: CameraPreviewPictureOptions): Promise<{ value: string }>; diff --git a/src/web.ts b/src/web.ts index 5f9bf913..5a6b99d8 100644 --- a/src/web.ts +++ b/src/web.ts @@ -98,11 +98,11 @@ export class CameraPreviewWeb extends WebPlugin implements CameraPreviewPlugin { }); } - async startRecordVideo(): Promise<{}> { + async startRecordVideo(): Promise { throw this.unimplemented('Not implemented on web.'); } - async stopRecordVideo(): Promise<{}> { + async stopRecordVideo(): Promise { throw this.unimplemented('Not implemented on web.'); } From c3b5cf3e8ce525c14953a7534e08d10b90489276 Mon Sep 17 00:00:00 2001 From: mariodeaconescu Date: Wed, 26 Jul 2023 14:54:02 +0300 Subject: [PATCH 3/8] Moved video return to stopRecordVideo, as per convention. --- ios/Plugin/CameraController.swift | 12 ++++++------ ios/Plugin/Plugin.swift | 32 +++++++++++++++++-------------- src/definitions.ts | 4 ++-- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/ios/Plugin/CameraController.swift b/ios/Plugin/CameraController.swift index 15cdfb37..28d78e79 100644 --- a/ios/Plugin/CameraController.swift +++ b/ios/Plugin/CameraController.swift @@ -394,9 +394,9 @@ extension CameraController { } - func captureVideo(completion: @escaping (URL?, Error?) -> Void) { + func captureVideo(completion: @escaping (Error?) -> Void) { guard let captureSession = self.captureSession, captureSession.isRunning else { - completion(nil, CameraControllerError.captureSessionIsMissing) + completion(CameraControllerError.captureSessionIsMissing) return } let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] @@ -408,16 +408,16 @@ extension CameraController { let fileUrl = path.appendingPathComponent(fileName) try? FileManager.default.removeItem(at: fileUrl) - self.videoCaptureCompletionBlock = completion - videoOutput!.startRecording(to: fileUrl, recordingDelegate: self) + completion(nil) } - func stopRecording(completion: @escaping (Error?) -> Void) { + func stopRecording(completion: @escaping (URL?, Error?) -> Void) { guard let captureSession = self.captureSession, captureSession.isRunning else { - completion(CameraControllerError.captureSessionIsMissing) + completion(nil, CameraControllerError.captureSessionIsMissing) return } + self.videoCaptureCompletionBlock = completion self.videoOutput?.stopRecording() } } diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 044a9e68..60f96a39 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -262,29 +262,33 @@ public class CameraPreview: CAPPlugin { let quality: Int? = call.getInt("quality", 85) - self.cameraController.captureVideo { (image, error) in - - guard let image = image else { - print(error ?? "Image capture error") - guard let error = error else { - call.reject("Image capture error") - return - } - call.reject(error.localizedDescription) + self.cameraController.captureVideo { (error) in + guard let error = error else { + call.resolve() return } - - // self.videoUrl = image - - call.resolve(["value": image.absoluteString]) + call.reject(error.localizedDescription) + return } } } @objc func stopRecordVideo(_ call: CAPPluginCall) { - self.cameraController.stopRecording { (_) in + self.cameraController.stopRecording { (video, error) in + guard let video = video else { + print(error ?? "Video capture error") + guard let error = error else { + call.reject("Video capture error") + return + } + call.reject(error.localizedDescription) + return + } + + // self.videoUrl = image + call.resolve(["value": video.absoluteString]) } } diff --git a/src/definitions.ts b/src/definitions.ts index 042c8dc7..d55f79c7 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -60,9 +60,9 @@ export interface CameraOpacityOptions { export interface CameraPreviewPlugin { start(options: CameraPreviewOptions): Promise<{}>; - startRecordVideo(options: CameraPreviewOptions): Promise<{ value: string } | never>; + startRecordVideo(options: CameraPreviewOptions): Promise<{}>; stop(): Promise<{}>; - stopRecordVideo(): Promise<{}>; + stopRecordVideo(): Promise<{ value: string } | never>; capture(options: CameraPreviewPictureOptions): Promise<{ value: string }>; captureSample(options: CameraSampleOptions): Promise<{ value: string }>; getSupportedFlashModes(): Promise<{ From 82e1eb97afbc2c846fb26f4f11373eeab4104881 Mon Sep 17 00:00:00 2001 From: mariodeaconescu Date: Tue, 1 Aug 2023 15:32:08 +0300 Subject: [PATCH 4/8] iOS stopRecordVideo() now returns { videoFilePath: string } to match Android implementation --- README.md | 1 + ios/Plugin/Plugin.swift | 2 +- src/definitions.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 18bff45f..905a40c2 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ CameraPreview.startRecordVideo(cameraPreviewOptions); ```javascript const resultRecordVideo = await CameraPreview.stopRecordVideo(); +const videoPath = resultRecordVideo.videoFilePath; ``` ### setOpacity(options: CameraOpacityOptions): Promise<{}>; ---- ANDROID only diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 60f96a39..1bc463ae 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -288,7 +288,7 @@ public class CameraPreview: CAPPlugin { // self.videoUrl = image - call.resolve(["value": video.absoluteString]) + call.resolve(["videoFilePath": video.absoluteString]) } } diff --git a/src/definitions.ts b/src/definitions.ts index d55f79c7..7f0a2f2e 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -62,7 +62,7 @@ export interface CameraPreviewPlugin { start(options: CameraPreviewOptions): Promise<{}>; startRecordVideo(options: CameraPreviewOptions): Promise<{}>; stop(): Promise<{}>; - stopRecordVideo(): Promise<{ value: string } | never>; + stopRecordVideo(): Promise<{ videoFilePath: string } | never>; capture(options: CameraPreviewPictureOptions): Promise<{ value: string }>; captureSample(options: CameraSampleOptions): Promise<{ value: string }>; getSupportedFlashModes(): Promise<{ From c2ee9b8d479b587eecec05c500c93aa9891e2da0 Mon Sep 17 00:00:00 2001 From: mariodeaconescu Date: Thu, 17 Aug 2023 21:40:39 +0300 Subject: [PATCH 5/8] Added resume() method (iOS only) Playing audio from within the WebView causes the capture session to stop and (sometimes) not resume automatically, so I added a method to manually resume the capture session without initializing the whole class. --- README.md | 7 +++++++ ios/Plugin/CameraController.swift | 9 +++++++++ ios/Plugin/Plugin.m | 1 + ios/Plugin/Plugin.swift | 9 +++++++++ src/definitions.ts | 1 + src/web.ts | 4 ++++ 6 files changed, 31 insertions(+) diff --git a/README.md b/README.md index 905a40c2..c9c747df 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,13 @@ Ex: VueJS >> App.vue component CameraPreview.stop(); ``` +### resume() ---- iOS only + +Resumes the camera preview without having to reinitialize. (in case it was interrupted) +```javascript +CameraPreview.resume() +``` + ### flip() Switch between rear and front camera only for android and ios, web is not supported ```javascript diff --git a/ios/Plugin/CameraController.swift b/ios/Plugin/CameraController.swift index 28d78e79..53cd827c 100644 --- a/ios/Plugin/CameraController.swift +++ b/ios/Plugin/CameraController.swift @@ -150,6 +150,15 @@ extension CameraController { } } + func resume() throws { + guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing } + DispatchQueue(label: "prepare").async { + if(!captureSession.isRunning){ + captureSession.startRunning() + } + } + } + func displayPreview(on view: UIView) throws { guard let captureSession = self.captureSession, captureSession.isRunning else { throw CameraControllerError.captureSessionIsMissing } diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m index 06fb55e1..06aab957 100644 --- a/ios/Plugin/Plugin.m +++ b/ios/Plugin/Plugin.m @@ -6,6 +6,7 @@ CAP_PLUGIN(CameraPreview, "CameraPreview", CAP_PLUGIN_METHOD(start, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(stop, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(resume, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(capture, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(captureSample, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(flip, CAPPluginReturnPromise); diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 1bc463ae..ea19611b 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -217,6 +217,15 @@ public class CameraPreview: CAPPlugin { } } } + + @objc func resume(_ call: CAPPluginCall) { + do { + try self.cameraController.resume() + call.resolve() + } catch { + call.reject("Can't resume capture session") + } + } @objc func getSupportedFlashModes(_ call: CAPPluginCall) { do { diff --git a/src/definitions.ts b/src/definitions.ts index 7f0a2f2e..bdd7b5b0 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -62,6 +62,7 @@ export interface CameraPreviewPlugin { start(options: CameraPreviewOptions): Promise<{}>; startRecordVideo(options: CameraPreviewOptions): Promise<{}>; stop(): Promise<{}>; + resume(): Promise<{} | never>; stopRecordVideo(): Promise<{ videoFilePath: string } | never>; capture(options: CameraPreviewPictureOptions): Promise<{ value: string }>; captureSample(options: CameraSampleOptions): Promise<{ value: string }>; diff --git a/src/web.ts b/src/web.ts index 5a6b99d8..20402fcc 100644 --- a/src/web.ts +++ b/src/web.ts @@ -98,6 +98,10 @@ export class CameraPreviewWeb extends WebPlugin implements CameraPreviewPlugin { }); } + async resume(): Promise { + throw this.unimplemented('Not implemented on web.'); + } + async startRecordVideo(): Promise { throw this.unimplemented('Not implemented on web.'); } From 11510ae30c09285658e762c82df8bc61af9eac9c Mon Sep 17 00:00:00 2001 From: mariodeaconescu Date: Thu, 17 Aug 2023 21:42:49 +0300 Subject: [PATCH 6/8] Added mirrorVideo option (iOS only) --- README.md | 1 + ios/Plugin/CameraController.swift | 11 ++++++++++- ios/Plugin/Plugin.swift | 5 ++++- src/definitions.ts | 2 ++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c9c747df..a8044641 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Starts the camera preview instance. | lockAndroidOrientation | boolean | (optional) Locks device orientation when camera is showing, default false. (applicable to Android only) | | enableOpacity | boolean | (optional) Make the camera preview see-through. Ideal for augmented reality uses. Default false (applicable to Android and web only) | enableZoom | boolean | (optional) Set if you can pinch to zoom. Default false (applicable to the android and ios platforms only) +| mirrorVideo | boolean | (optional) Set if the video should be mirrored to match the preview. Defaults to false (applicable to the iOS platform only)