react-native-vision-camera/ios/CameraView+RecordVideo.swift
Marc Rousavy 71730a73ef
fix: Fix AVAudioSession not allowing background music playback (#155)
* Set category always if different

* rename org

* Fix video format sorting

* fix format filtering

* Update AVAudioSession+setCategoryIfNotSet.swift

* upgrade all dependencies

* Also run dependabot for JS codebase

* Update MediaPage.tsx

* Use typescript 4.2.4

* Also run TS in check-all

* Downgrade typescript to 4.2.3

* f

* recreate lockfiles

* docs: Revert package.json changes

* revert all package.json changes

* Update Podfile.lock

* bump all dependencies, pin typescript to 4.2.4

* Downgrade react-native-navigation for now

* upgrade to later snapshot

* Update yarn.lock

* remove yeet
2021-06-01 13:07:57 +02:00

168 lines
6.7 KiB
Swift

//
// CameraView+RecordVideo.swift
// Cuvent
//
// Created by Marc Rousavy on 16.12.20.
// Copyright © 2020 mrousavy. All rights reserved.
//
import AVFoundation
private var hasLoggedFrameDropWarning = false
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
func startRecording(options: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
cameraQueue.async {
ReactLogger.log(level: .info, message: "Starting Video recording...")
do {
let errorPointer = ErrorPointer(nilLiteral: ())
guard let tempFilePath = RCTTempFilePath("mov", errorPointer) else {
return callback([NSNull(), makeReactError(.capture(.createTempFileError), cause: errorPointer?.pointee)])
}
let tempURL = URL(string: "file://\(tempFilePath)")!
if let flashMode = options["flash"] as? String {
// use the torch as the video's flash
self.setTorchMode(flashMode)
}
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.
let onFinish = { (status: AVAssetWriter.Status, error: Error?) -> Void in
defer {
self.recordingSession = nil
}
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)"))])
} 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)")])
}
}
}
let videoSettings = self.videoOutput!.recommendedVideoSettingsForAssetWriter(writingTo: fileType)
let audioSettings = self.audioOutput!.recommendedAudioSettingsForAssetWriter(writingTo: fileType) as? [String: Any]
self.recordingSession = try RecordingSession(url: tempURL,
fileType: fileType,
videoSettings: videoSettings ?? [:],
audioSettings: audioSettings ?? [:],
isVideoMirrored: self.videoOutput!.isMirrored,
completion: onFinish)
self.isRecording = true
} catch EnumParserError.invalidValue {
return callback([NSNull(), EnumParserError.invalidValue])
} catch let error as NSError {
return callback([NSNull(), makeReactError(.capture(.createTempFileError), cause: error)])
}
}
}
func stopRecording(promise: Promise) {
isRecording = false
cameraQueue.async {
withPromise(promise) {
guard let recordingSession = self.recordingSession else {
throw CameraError.capture(.noRecordingInProgress)
}
recordingSession.finish()
return nil
}
}
}
// TODO: Implement for JS
func pauseRecording(promise: Promise) {
cameraQueue.async {
withPromise(promise) {
if self.isRecording {
self.isRecording = false
return nil
} else {
throw CameraError.capture(.noRecordingInProgress)
}
}
}
}
// TODO: Implement for JS
func resumeRecording(promise: Promise) {
cameraQueue.async {
withPromise(promise) {
if !self.isRecording {
self.isRecording = true
return nil
} else {
throw CameraError.capture(.noRecordingInProgress)
}
}
}
}
public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) {
if isRecording {
guard let recordingSession = recordingSession else {
return invokeOnError(.capture(.unknown(message: "isRecording was true but the RecordingSession was null!")))
}
switch captureOutput {
case is AVCaptureVideoDataOutput:
recordingSession.appendBuffer(sampleBuffer, type: .video)
case is AVCaptureAudioDataOutput:
recordingSession.appendBuffer(sampleBuffer, type: .audio)
default:
break
}
}
if let frameProcessor = frameProcessorCallback, captureOutput is AVCaptureVideoDataOutput {
// check if last frame was x nanoseconds ago, effectively throttling FPS
let diff = DispatchTime.now().uptimeNanoseconds - lastFrameProcessorCall.uptimeNanoseconds
let secondsPerFrame = 1.0 / frameProcessorFps.doubleValue
let nanosecondsPerFrame = secondsPerFrame * 1_000_000_000.0
if diff > UInt64(nanosecondsPerFrame) {
frameProcessor(sampleBuffer)
lastFrameProcessorCall = DispatchTime.now()
}
}
}
public final func captureOutput(_ captureOutput: AVCaptureOutput, didDrop buffer: CMSampleBuffer, from _: AVCaptureConnection) {
if frameProcessorCallback != nil && !hasLoggedFrameDropWarning && captureOutput is AVCaptureVideoDataOutput {
let reason = findFrameDropReason(inBuffer: buffer)
// TODO: Show in React console?
ReactLogger.log(level: .warning, message: "Dropped a Frame. This might indicate that your Frame Processor is doing too much work. " +
"Either throttle the frame processor's frame rate, or optimize your frame processor's execution speed. Frame drop reason: \(reason)")
hasLoggedFrameDropWarning = true
}
}
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)
}
}