try: Improvements from WWDC 2021 1:1 workshop (#197)

* perf: Automatically determine Pixel Format depending on active format. (More efficient video recording 🚀)
* perf: Skip `AVAssetWriter` transform by directly correctly orienting the Video Output connection
* feat: Support camera flipping while recording
* feat: Run frame processor on separate queue, avoids stutters in video recordigns
* feat: Automatically drop late frame processor frames
This commit is contained in:
Marc Rousavy
2021-06-11 21:06:19 +02:00
committed by GitHub
parent 26cf21ff5f
commit 9c579c65aa
14 changed files with 103 additions and 74 deletions

View File

@@ -8,7 +8,8 @@
import AVFoundation
private var hasLoggedFrameDropWarning = false
private var hasLoggedVideoFrameDropWarning = false
private var hasLoggedFrameProcessorFrameDropWarning = false
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
@@ -54,6 +55,10 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
return
}
}
guard let videoInput = self.videoDeviceInput else {
callback.reject(error: .session(.cameraNotReady))
return
}
// 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?
@@ -108,8 +113,10 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
callback.reject(error: .capture(.createRecorderError(message: "Failed to get video settings!")))
return
}
// get pixel format (420f, 420v)
let pixelFormat = CMFormatDescriptionGetMediaSubType(videoInput.device.activeFormat.formatDescription)
self.recordingSession!.initializeVideoWriter(withSettings: videoSettings,
isVideoMirrored: self.videoOutput!.isMirrored)
pixelFormat: pixelFormat)
// Init Audio (optional, async)
if enableAudio {
@@ -196,30 +203,60 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
}
}
if let frameProcessor = frameProcessorCallback, captureOutput is AVCaptureVideoDataOutput {
// TODO: resize using VideoToolbox (VTPixelTransferSession)
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) {
let frame = Frame(buffer: sampleBuffer, orientation: bufferOrientation)
frameProcessor(frame)
lastFrameProcessorCall = DispatchTime.now()
if !isRunningFrameProcessor {
// we're not in the middle of executing the Frame Processor, so prepare for next call.
var bufferCopy: CMSampleBuffer?
CMSampleBufferCreateCopy(allocator: kCFAllocatorDefault,
sampleBuffer: sampleBuffer,
sampleBufferOut: &bufferCopy)
if let bufferCopy = bufferCopy {
// successfully copied buffer, dispatch frame processor call.
CameraQueues.frameProcessorQueue.async {
self.isRunningFrameProcessor = true
let frame = Frame(buffer: bufferCopy, orientation: self.bufferOrientation)
frameProcessor(frame)
self.isRunningFrameProcessor = false
}
lastFrameProcessorCall = DispatchTime.now()
} else {
// failed to create a buffer copy.
ReactLogger.log(level: .error, message: "Failed to copy buffer! Frame Processor cannot be called.", alsoLogToJS: true)
}
} else {
// we're still in the middle of executing a Frame Processor for a previous frame, notify user about dropped frame.
if !hasLoggedFrameProcessorFrameDropWarning {
ReactLogger.log(level: .warning,
message: "Your Frame Processor took so long to execute that a frame was dropped. " +
"Either throttle your Frame Processor's frame rate using the `frameProcessorFps` prop, or optimize " +
"it's execution speed. (This warning will only be shown once)",
alsoLogToJS: true)
hasLoggedFrameProcessorFrameDropWarning = true
}
}
}
}
}
#if DEBUG
public final func captureOutput(_ captureOutput: AVCaptureOutput, didDrop buffer: CMSampleBuffer, from _: AVCaptureConnection) {
if frameProcessorCallback != nil && !hasLoggedFrameDropWarning && captureOutput is AVCaptureVideoDataOutput {
if !hasLoggedVideoFrameDropWarning && captureOutput is AVCaptureVideoDataOutput {
let reason = findFrameDropReason(inBuffer: buffer)
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 using the `frameProcessorFps` prop, or optimize " +
"your frame processor's execution speed. Frame drop reason: \(reason)",
message: "Dropped a Frame - This might indicate that your frame rate is higher than the phone can currently process. " +
"Throttle the Camera frame rate using the `fps` prop and make sure the device stays in optimal condition for recording. " +
"Frame drop reason: \(reason). (This warning will only be shown once)",
alsoLogToJS: true)
hasLoggedFrameDropWarning = true
hasLoggedVideoFrameDropWarning = true
}
}