如何在AVFoundation预览video时保持低延迟?

Apple有一个名为Rosy Writer的示例代码,它展示了如何捕获video并对其应用效果。

在代码的这一部分中,在outputPreviewPixelBuffer部分,Apple应该通过删除过时帧来显示它们如何保持低预览延迟。

 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription( sampleBuffer ); if ( connection == _videoConnection ) { if ( self.outputVideoFormatDescription == NULL ) { // Don't render the first sample buffer. // This gives us one frame interval (33ms at 30fps) for setupVideoPipelineWithInputFormatDescription: to complete. // Ideally this would be done asynchronously to ensure frames don't back up on slower devices. [self setupVideoPipelineWithInputFormatDescription:formatDescription]; } else { [self renderVideoSampleBuffer:sampleBuffer]; } } else if ( connection == _audioConnection ) { self.outputAudioFormatDescription = formatDescription; @synchronized( self ) { if ( _recordingStatus == RosyWriterRecordingStatusRecording ) { [_recorder appendAudioSampleBuffer:sampleBuffer]; } } } } - (void)renderVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer { CVPixelBufferRef renderedPixelBuffer = NULL; CMTime timestamp = CMSampleBufferGetPresentationTimeStamp( sampleBuffer ); [self calculateFramerateAtTimestamp:timestamp]; // We must not use the GPU while running in the background. // setRenderingEnabled: takes the same lock so the caller can guarantee no GPU usage once the setter returns. @synchronized( _renderer ) { if ( _renderingEnabled ) { CVPixelBufferRef sourcePixelBuffer = CMSampleBufferGetImageBuffer( sampleBuffer ); renderedPixelBuffer = [_renderer copyRenderedPixelBuffer:sourcePixelBuffer]; } else { return; } } if ( renderedPixelBuffer ) { @synchronized( self ) { [self outputPreviewPixelBuffer:renderedPixelBuffer]; if ( _recordingStatus == RosyWriterRecordingStatusRecording ) { [_recorder appendVideoPixelBuffer:renderedPixelBuffer withPresentationTime:timestamp]; } } CFRelease( renderedPixelBuffer ); } else { [self videoPipelineDidRunOutOfBuffers]; } } // call under @synchronized( self ) - (void)outputPreviewPixelBuffer:(CVPixelBufferRef)previewPixelBuffer { // Keep preview latency low by dropping stale frames that have not been picked up by the delegate yet // Note that access to currentPreviewPixelBuffer is protected by the @synchronized lock self.currentPreviewPixelBuffer = previewPixelBuffer; // A [self invokeDelegateCallbackAsync:^{ // B CVPixelBufferRef currentPreviewPixelBuffer = NULL; // C @synchronized( self ) //D { currentPreviewPixelBuffer = self.currentPreviewPixelBuffer; // E if ( currentPreviewPixelBuffer ) { // F CFRetain( currentPreviewPixelBuffer ); // G self.currentPreviewPixelBuffer = NULL; // H } } if ( currentPreviewPixelBuffer ) { // I [_delegate capturePipeline:self previewPixelBufferReadyForDisplay:currentPreviewPixelBuffer]; // J CFRelease( currentPreviewPixelBuffer ); /K } }]; } - (void)invokeDelegateCallbackAsync:(dispatch_block_t)callbackBlock { dispatch_async( _delegateCallbackQueue, ^{ @autoreleasepool { callbackBlock(); } } ); } 

经过几个小时试图理解这段代码,我的大脑吸烟,我看不出这是怎么做的。

有人可以解释我5岁,好吧,让它3岁,这段代码是怎么做的?

谢谢。

编辑:我已使用字母标记outputPreviewPixelBuffer的行, outputPreviewPixelBuffer于理解代码执行的顺序。

因此,该方法启动并运行A ,缓冲区存储在属性self.currentPreviewPixelBufferB运行并且局部变量currentPreviewPixelBuffer被赋值为NULLD运行并锁定self 。 然后E运行并将局部变量currentPreviewPixelBuffer从NULL更改为self.currentPreviewPixelBuffer的值。

这是第一件没有意义的事情。 为什么我要创建一个变量currentPreviewPixelBuffer将其赋值为NULL并在下一行将其分配给self.currentPreviewPixelBuffer

以下几行甚至更加疯狂。 为什么我要问currentPreviewPixelBuffer是不是NULL如果我只是将它分配给E上的非NULL值? 那么H被执行并且self.currentPreviewPixelBuffer

我没有得到的一件事是: invokeDelegateCallbackAsync:是异步的,对吧? 如果它是异步的,那么每次运行outputPreviewPixelBuffer方法时outputPreviewPixelBuffer设置self.currentPreviewPixelBuffer = previewPixelBuffer并调度一个块来执行,可以再次自由运行。

如果更快地触发outputPreviewPixelBuffer ,我们将会堆积一堆块来执行。

由于Kamil Kocemba的解释,我发现这些异步块正在以某种方式测试,如果前一个完成执行并丢弃帧,如果没有。

另外, @syncronized(self)锁定究竟是什么? 是否阻止编写或读取self.currentPreviewPixelBuffer ? 还是锁定局部变量currentPreviewPixelBuffer ? 如果@syncronized(self)下的块与范围相关,则I处的行永远不会为NULL因为它是在E上设置的。

感谢您突出显示这些内容 – 这有望使答案更容易理解。

让我们一步一步走:

  1. -outputPreviewPixelBuffer:被调用。 self.currentPreviewPixelBuffer不会被self.currentPreviewPixelBuffer块覆盖:这意味着它被强制覆盖,对所有线程都有效(我正在掩盖currentPreviewPixelBuffer nonatomic的事实;这实际上是不安全的,这里有一场比赛 – 你真的需要它是strong, atomic ,这才是真的)。 如果那里有一个缓冲区,那么下一次线程将要去寻找它时它就会消失。 这就是文档所暗示的 – 如果self.currentPreviewPixelBuffer有一个值,并且委托尚未处理以前的值,那太糟糕了! 它现在已经消失了。
  2. 该块被发送给委托以异步处理。 实际上,这可能会在未来的某个时间发生,并有一些不确定的延迟。 这意味着在调用-outputPreviewPixelBuffer:和处理块之间时, -outputPreviewPixelBuffer:可以再次被调用很多次! 这就是陈旧帧的删除方式 – 如果委托花了很长时间来处理块,最新的self.currentPreviewPixelBuffer会一次又一次地被最新值覆盖,从而有效地丢弃了前一帧。
  3. C到H行取得self.currentPreviewPixelBuffer所有权。 你确实有一个局部像素缓冲区,最初设置为NULL 。 围绕self@synchronized阻止隐含地说:“我将适度访问self ,以确保在我看到它时没有人编辑self ,而且我将确保我抓住最新的self的实例变量的值,甚至跨线程“。 这就是委托如何确保它具有最新的self.currentPreviewPixelBuffer ; 如果它不是@synchronized ,你可能会得到一个陈旧的副本。

    同样在self.currentPreviewPixelBuffer块中,保留后会覆盖self.currentPreviewPixelBuffer 。 这段代码隐含地说:“嘿,如果self.currentPreviewPixelBuffer不是NULL ,那么必须有一个像素缓冲区来处理;如果有(行F),那么我将坚持它(E行,G行),并且在self (线H)重置它“。 实际上,这取得了selfcurrentPreviewPixelBuffer所有权,因此没有其他人会处理它。 这是对在self运行的所有委托回调块的隐式检查:第一个触发self.currentPreviewPixelBuffer块来保持它,为所有其他查看self块设置为NULL ,并且确实可以使用它。 在F行读取NULL的其他人什么都不做。

  4. 线I和J实际上使用像素缓冲区,线K正确地处理它。

确实,这段代码可能会使用一些评论 – 它实际上是E到G行,在这里做了大量的隐含工作,取得了self的预览缓冲区的所有权,以防止其他人处理块。 A行上面的注释没有说明,“请注意,访问currentPreviewPixelBuffer受@synchronized …保护, 与此相反,它不是;因为它不受此处的保护,我们可以覆盖self.currentPreviewPixelBuffer在某人处理它之前我们想要的时间,删除中间值

希望有所帮助。

好的,这是有趣的部分:

 // call under @synchronized( self ) - (void)outputPreviewPixelBuffer:(CVPixelBufferRef)previewPixelBuffer { // Keep preview latency low by dropping stale frames that have not been picked up by the delegate yet // Note that access to currentPreviewPixelBuffer is protected by the @synchronized lock self.currentPreviewPixelBuffer = previewPixelBuffer; [self invokeDelegateCallbackAsync:^{ CVPixelBufferRef currentPreviewPixelBuffer = NULL; @synchronized( self ) { currentPreviewPixelBuffer = self.currentPreviewPixelBuffer; if ( currentPreviewPixelBuffer ) { CFRetain( currentPreviewPixelBuffer ); self.currentPreviewPixelBuffer = NULL; } } if ( currentPreviewPixelBuffer ) { [_delegate capturePipeline:self previewPixelBufferReadyForDisplay:currentPreviewPixelBuffer]; CFRelease( currentPreviewPixelBuffer ); } }]; } 

基本上他们所做的是使用currentPreviewPixelBuffer属性来跟踪框架是否陈旧。

如果正在处理帧以进行显示( invokeDelegateCallbackAsync: ,则该属性设置为NULL有效地丢弃任何排队的帧(将等待处理)。

请注意,此异步调用是以异步方式调用的。 每个捕获的帧调用outputPreviewPixelBuffer:并且每个显示的帧都需要调用_delegate capturePipeline:previewPixelBufferReadyForDisplay: .

过时的帧意味着outputPreviewPixelBuffer被更频繁地调用(’更快’),委托可以处理它们。 但是在这种情况下,属性(’排队’下一帧)将被设置为NULL并且回调将立即返回,仅为最近的帧留出空间。

这对你有意义吗?

编辑:

想象一下下面的一系列调用(非常简化):

TX =任务X,FX =第X帧

 T1. output preview (F1) T2. delegate callback start (F1) T3. output preview (F2) T4. output preview (F3) T5. output preview (F4) T6. output preview (F5) T7. delegate callback stop (F1) 

T3,T4,T5和T6的回调在@synchronized(self)锁定上等待。

当T7完成self.currentPreviewPixelBuffer的值时?

这是F5。

然后我们为T3运行委托回调。

self.currentPreviewPixelBuffer = NULL

委托回调完成。

然后我们为T4运行委托回调。

self.currentPreviewPixelBuffer的价值是self.currentPreviewPixelBuffer

它是NULL

所以这是无操作。

对于T5和T6的回调也是如此。

处理帧:F1和F5。 丢帧:F2,F3,F4。

希望这可以帮助