feat: Separate usecases (decouple microphone, video, photo) (#168)
* Add props
* add props (iOS)
* Add use-cases conditionally
* Update CameraView+RecordVideo.swift
* Update RecordingSession.swift
* reconfigure on change
* Throw correct errors
* Check for audio permission
* Move `#if` outward
* Throw appropriate errors
* Update CameraView+RecordVideo.swift
* fix Splashscreen
* Dynamic filePath
* Fix video extension
* add `avci` and `m4v` file types
* Fix RecordVideo errors
* Fix audio setup
* Enable `photo`, `video` and `audio`
* Check for `video={true}` in frameProcessor
* format
* Remove unused DispatchQueue
* Update docs
* Add `supportsPhotoAndVideoCapture`
* Fix view manager
* Fix error not being propagated
* Catch normal errors too
* Update DEVICES.mdx
* Update CAPTURING.mdx
* Update classdocs
This commit is contained in:
@@ -16,72 +16,95 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
||||
/**
|
||||
Starts a video + audio recording with a custom Asset Writer.
|
||||
*/
|
||||
func startRecording(options: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
|
||||
func startRecording(options: NSDictionary, callback jsCallbackFunc: @escaping RCTResponseSenderBlock) {
|
||||
cameraQueue.async {
|
||||
ReactLogger.log(level: .info, message: "Starting Video recording...")
|
||||
let callback = Callback(jsCallbackFunc)
|
||||
|
||||
do {
|
||||
let errorPointer = ErrorPointer(nilLiteral: ())
|
||||
guard let tempFilePath = RCTTempFilePath("mov", errorPointer) else {
|
||||
return callback([NSNull(), makeReactError(.capture(.createTempFileError), cause: errorPointer?.pointee)])
|
||||
var fileType = AVFileType.mov
|
||||
if let fileTypeOption = options["fileType"] as? String {
|
||||
guard let parsed = try? AVFileType(withString: fileTypeOption) else {
|
||||
return callback.reject(error: .parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption)))
|
||||
}
|
||||
fileType = parsed
|
||||
}
|
||||
|
||||
let tempURL = URL(string: "file://\(tempFilePath)")!
|
||||
if let flashMode = options["flash"] as? String {
|
||||
// use the torch as the video's flash
|
||||
self.setTorchMode(flashMode)
|
||||
let errorPointer = ErrorPointer(nilLiteral: ())
|
||||
let fileExtension = fileType.descriptor ?? "mov"
|
||||
guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else {
|
||||
return callback.reject(error: .capture(.createTempFileError), cause: errorPointer?.pointee)
|
||||
}
|
||||
|
||||
ReactLogger.log(level: .info, message: "File path: \(tempFilePath)")
|
||||
let tempURL = URL(string: "file://\(tempFilePath)")!
|
||||
|
||||
if let flashMode = options["flash"] as? String {
|
||||
// use the torch as the video's flash
|
||||
self.setTorchMode(flashMode)
|
||||
}
|
||||
|
||||
guard let videoOutput = self.videoOutput else {
|
||||
if self.video?.boolValue == true {
|
||||
return callback.reject(error: .session(.cameraNotReady))
|
||||
} else {
|
||||
return callback.reject(error: .capture(.videoNotEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
var fileType = AVFileType.mov
|
||||
if let fileTypeOption = options["fileType"] as? String {
|
||||
fileType = AVFileType(withString: fileTypeOption)
|
||||
}
|
||||
// TODO: The startRecording() func cannot be async because RN doesn't allow
|
||||
// both a callback and a Promise in a single function. Wait for TurboModules?
|
||||
// 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.
|
||||
|
||||
// TODO: The startRecording() func cannot be async because RN doesn't allow
|
||||
// both a callback and a Promise in a single function. Wait for TurboModules?
|
||||
// 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.
|
||||
let enableAudio = self.audio?.boolValue == true
|
||||
|
||||
let onFinish = { (status: AVAssetWriter.Status, error: Error?) -> Void in
|
||||
defer {
|
||||
self.recordingSession = nil
|
||||
let onFinish = { (status: AVAssetWriter.Status, error: Error?) -> Void in
|
||||
defer {
|
||||
self.recordingSession = nil
|
||||
if enableAudio {
|
||||
self.audioQueue.async {
|
||||
self.deactivateAudioSession()
|
||||
}
|
||||
}
|
||||
ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).")
|
||||
if let error = error {
|
||||
let description = (error as NSError).description
|
||||
return callback([NSNull(), CameraError.capture(.unknown(message: "An unknown recording error occured! \(description)"))])
|
||||
}
|
||||
ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).")
|
||||
if let error = error as NSError? {
|
||||
let description = error.description
|
||||
return callback.reject(error: .capture(.unknown(message: "An unknown recording error occured! \(description)")), cause: error)
|
||||
} else {
|
||||
if status == .completed {
|
||||
return callback.resolve([
|
||||
"path": self.recordingSession!.url.absoluteString,
|
||||
"duration": self.recordingSession!.duration,
|
||||
])
|
||||
} else {
|
||||
if status == .completed {
|
||||
return callback([[
|
||||
"path": self.recordingSession!.url.absoluteString,
|
||||
"duration": self.recordingSession!.duration,
|
||||
], NSNull()])
|
||||
} else {
|
||||
return callback([NSNull(), CameraError.unknown(message: "AVAssetWriter completed with status: \(status.descriptor)")])
|
||||
}
|
||||
return callback.reject(error: .unknown(message: "AVAssetWriter completed with status: \(status.descriptor)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
self.recordingSession = try RecordingSession(url: tempURL,
|
||||
fileType: fileType,
|
||||
completion: onFinish)
|
||||
} catch let error as NSError {
|
||||
return callback.reject(error: .capture(.createRecorderError(message: nil)), cause: error)
|
||||
}
|
||||
|
||||
// Init Video
|
||||
guard let videoOutput = self.videoOutput,
|
||||
let videoSettings = videoOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType),
|
||||
!videoSettings.isEmpty else {
|
||||
throw CameraError.capture(.createRecorderError(message: "Failed to get video settings!"))
|
||||
}
|
||||
self.recordingSession!.initializeVideoWriter(withSettings: videoSettings,
|
||||
isVideoMirrored: self.videoOutput!.isMirrored)
|
||||
// Init Video
|
||||
guard let videoSettings = videoOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType),
|
||||
!videoSettings.isEmpty else {
|
||||
return callback.reject(error: .capture(.createRecorderError(message: "Failed to get video settings!")))
|
||||
}
|
||||
self.recordingSession!.initializeVideoWriter(withSettings: videoSettings,
|
||||
isVideoMirrored: self.videoOutput!.isMirrored)
|
||||
|
||||
// Init Audio (optional, async)
|
||||
// Init Audio (optional, async)
|
||||
if enableAudio {
|
||||
self.audioQueue.async {
|
||||
// Activate Audio Session (blocking)
|
||||
self.activateAudioSession()
|
||||
|
||||
guard let recordingSession = self.recordingSession else {
|
||||
// recording has already been cancelled
|
||||
return
|
||||
@@ -95,10 +118,10 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
||||
recordingSession.start()
|
||||
self.isRecording = true
|
||||
}
|
||||
} catch EnumParserError.invalidValue {
|
||||
return callback([NSNull(), EnumParserError.invalidValue])
|
||||
} catch let error as NSError {
|
||||
return callback([NSNull(), makeReactError(.capture(.createTempFileError), cause: error)])
|
||||
} else {
|
||||
// start recording session without audio.
|
||||
self.recordingSession!.start()
|
||||
self.isRecording = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,8 +198,8 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
||||
}
|
||||
}
|
||||
|
||||
public final func captureOutput(_ captureOutput: AVCaptureOutput, didDrop buffer: CMSampleBuffer, from _: AVCaptureConnection) {
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
public final func captureOutput(_ captureOutput: AVCaptureOutput, didDrop buffer: CMSampleBuffer, from _: AVCaptureConnection) {
|
||||
if frameProcessorCallback != nil && !hasLoggedFrameDropWarning && captureOutput is AVCaptureVideoDataOutput {
|
||||
let reason = findFrameDropReason(inBuffer: buffer)
|
||||
ReactLogger.log(level: .warning,
|
||||
@@ -185,16 +208,16 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
||||
alsoLogToJS: true)
|
||||
hasLoggedFrameDropWarning = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private final func findFrameDropReason(inBuffer buffer: CMSampleBuffer) -> String {
|
||||
var mode: CMAttachmentMode = 0
|
||||
guard let reason = CMGetAttachment(buffer,
|
||||
key: kCMSampleBufferAttachmentKey_DroppedFrameReason,
|
||||
attachmentModeOut: &mode) else {
|
||||
return "unknown"
|
||||
}
|
||||
return String(describing: reason)
|
||||
}
|
||||
|
||||
private final func findFrameDropReason(inBuffer buffer: CMSampleBuffer) -> String {
|
||||
var mode: CMAttachmentMode = 0
|
||||
guard let reason = CMGetAttachment(buffer,
|
||||
key: kCMSampleBufferAttachmentKey_DroppedFrameReason,
|
||||
attachmentModeOut: &mode) else {
|
||||
return "unknown"
|
||||
}
|
||||
return String(describing: reason)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user