如何在预览来自 AVFoundation 的视频期间保持低延迟?

How to keep low latency during the preview of video coming from AVFoundation?

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

在这部分代码中,在 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 的行,以便于理解代码的执行顺序。

因此,该方法开始并且 A 运行s 并且缓冲区被存储到 属性 self.currentPreviewPixelBuffer 中。 B 运行s 和局部变量 currentPreviewPixelBuffer 赋值 NULLD 运行 并锁定 self。然后 E 运行s 将局部变量 currentPreviewPixelBuffer 从 NULL 修改为 self.currentPreviewPixelBuffer.

的值

这是第一个说不通的。为什么我要创建一个变量 currentPreviewPixelBuffer 将其分配给 NULL 并在下一行将其分配给 self.currentPreviewPixelBuffer?

下一行更疯狂。为什么我要问 currentPreviewPixelBuffer 是否不是 NULL 如果我只是将它分配给 E 上的非 NULL 值?然后 H 被执行并且 nulls self.currentPreviewPixelBuffer?

有一件事我不明白:invokeDelegateCallbackAsync: 是异步的,对吧?如果它是异步的,那么每次 outputPreviewPixelBuffer 方法 运行s 都是设置 self.currentPreviewPixelBuffer = previewPixelBuffer 并分派一个块来执行,可以再次自由 运行 。

如果outputPreviewPixelBuffer发射得更快,我们就会有一堆积木等待执行。

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

此外,@syncronized(self) 锁定到底是什么?是否阻止 self.currentPreviewPixelBuffer 被写入或读取?还是锁定了局部变量currentPreviewPixelBuffer?如果 @syncronized(self) 下的块与范围同步,则 I 处的行将永远不会是 NULL,因为它是在 E.

上设置的

好的,这就是有趣的部分:

// 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 被更频繁地调用 ('faster'),委托可以处理它们。 然而,在这种情况下,属性(下一帧 'enqueues')将设置为 NULL 并且回调将立即 return,只为最近的帧留出空间。

你觉得有意义吗?

编辑:

想象一下调用顺序(非常简单):

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 的值是多少?

NULL

所以这是空操作。

T5 和 T6 的回调相同。

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

希望对您有所帮助

感谢您突出显示这些行 - 希望这会使答案更容易理解。

让我们一步一步来:

  1. -outputPreviewPixelBuffer: 被调用。 self.currentPreviewPixelBuffer@synchronized 块中被覆盖 而不是 :这意味着它被强制覆盖,对所有线程都有效(我掩盖了 currentPreviewPixelBuffernonatomic;这实际上是不安全的,这里有一场比赛——你真的需要它是 strong, atomic 才能真正做到这一点)。如果那里有缓冲区,那么下次线程要查找它时它就会消失。这就是文档所暗示的——如果 self.currentPreviewPixelBuffer 中有一个值,而委托尚未处理以前的值,那就太糟糕了!现在没了。

  2. 块被发送到委托异步处理。实际上,这可能会在将来 的某个时间发生,但会有一些不确定的延迟。这意味着在调用 -outputPreviewPixelBuffer: 和处理块之间, -outputPreviewPixelBuffer: 可以再次调用很多次!这就是丢弃陈旧帧的方式——如果代理花费很长时间来处理块,最新的 self.currentPreviewPixelBuffer 将一次又一次地被最新值覆盖,从而有效地丢弃前一帧。

  3. C 到 H 行拥有 self.currentPreviewPixelBuffer。您确实有一个本地像素缓冲区,最初设置为 NULLself 周围的 @synchronized 块含蓄地说:“我将适度访问 self,以确保在我查看时没有人编辑 self,而且我将确保获取 self 的实例变量的最新值,即使跨线程也是如此”。这就是委托如何确保它拥有最新的 self.currentPreviewPixelBuffer;如果不是 @synchronized,您可以获得一份过时的副本。

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

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

没错,这段代码可以使用一些注释——实际上是 E 到 G 行在这里做了很多隐含的工作,取得 self 的预览缓冲区的所有权以防止其他人处理也阻止。 A 行上方的评论 not 说的是,“请注意,对 currentPreviewPixelBuffer 的访问受 @synchronized... 的保护, 与此处相反不;因为它在这里不受保护,我们可以在有人处理它之前随意覆盖 self.currentPreviewPixelBuffer 多次,删除中间值 "

希望对您有所帮助。