2016-11-23 74 views
1

Apple有一个示例代码,名为Rosy Writer,显示如何捕捉视频并将效果应用于该视频。如何在AVFoundation预览视频时保持低延迟?

在代码的这一部分,在outputPreviewPixelBuffer部分,苹果公司展示了它们如何通过删除陈旧的帧来保持预览延迟低。

- (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运行并且缓冲区被存储到属性self.currentPreviewPixelBuffer中。 B运行,并且本地变量currentPreviewPixelBuffer被指定为NULLD运行并锁定self。然后E运行并将本地变量currentPreviewPixelBuffer从NULL更改为值self.currentPreviewPixelBuffer

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

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

我不明白的一件事是:invokeDelegateCallbackAsync:是异步的,对吗?如果它是异步的,则每次运行outputPreviewPixelBuffer的方法是设置self.currentPreviewPixelBuffer = previewPixelBuffer并调度一个块执行,可以自由运行。

如果outputPreviewPixelBuffer被激发得更快,我们将有一堆堆积的执行块。

由于Kamil Kocemba的解释,我不确定这些异步块是否正在测试,如果前一个完成执行并丢弃帧,如果不是。

另外,究竟是什么@syncronized(self)锁定?它是否阻止self.currentPreviewPixelBuffer被写入或读取?或者它是否锁定本地变量currentPreviewPixelBuffer?如果@syncronized(self)下的块与示波器同步,则I的行将永远不会为NULL,因为它正在E上设置。

+0

你能分享链接到源代码吗?我也有兴趣学习如何编辑来自摄像头的样本缓存器时的低延迟 – omarojo

+2

链接位于第一段。 – SpaceDog

回答

2

感谢您凸显线条 - 这将有望使答案有点更容易执行。

让我们通过一步一步:

  1. -outputPreviewPixelBuffer:被调用。 self.currentPreviewPixelBuffer是在@synchronized块覆盖:这意味着它被强制覆盖,有效地对所有线程(我粉饰事实currentPreviewPixelBuffernonatomic;这其实是不安全的,有一场比赛在这里 - 你真的需要它是strong, atomic这是真的)。如果那里有一个缓冲区,那么下一次线程将要去寻找它时,它已经不存在了。这就是文档所暗示的 - 如果self.currentPreviewPixelBuffer中有一个值,并且代表还没有处理先前的值,那太糟糕了!它现在消失了。
  2. 该块被发送给委托进行异步处理。实际上,这将在未来的某个时间发生,并有一些不确定的延迟。这意味着在调用-outputPreviewPixelBuffer:时以及处理该块时,-outputPreviewPixelBuffer:可以再次被调用很多次!这就是如何删除过时的帧 - 如果委托人处理该块需要很长时间,则最新的self.currentPreviewPixelBuffer将被最新的值一次又一次地覆盖,从而有效地丢弃前一帧。
  3. C到H行取得self.currentPreviewPixelBuffer的所有权。你确实有一个本地像素缓冲区,最初设置为NULL。 大约在self块左右隐含地说:“我将适度访问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的预览缓冲区的所有权以防止其他人处理该块作为好。请注意,对currentPreviewPixelBuffer的访问受到@synchronized ...,的保护,与此处不同的是,因为此处不受此保护,因此我们可以覆盖很多个self.currentPreviewPixelBuffer我们希望有人处理它之前的时间,降低中间值

希望有所帮助。

+1

我想我现在开始掌握它。你的解释是惊人的。我必须在这里写下苹果公司撰写文件的方式。我认为99.99%的Apple作为文档写入的内容都是纯垃圾。他们的示例代码太复杂,无法解释基本的东西,推荐不足。他们的参考指南显示他们如何讨厌开发人员和编写文档。我来自旧学校,其中的文件是教学和解释的主要部分。我自己是一名书籍作家,我看到苹果文档有多糟糕。如果不是这样,我们都将注定要失败。万岁! – SpaceDog

+0

@SpaceDog很高兴有帮助。 FWIW,我认为苹果不喜欢开发人员或文档是公平的 - 如果是这样的话,就没有什么可说的了。我们非常关心如何使我们面向公众的文档尽可能易于访问(以及您是否认为我有偏见,我认为我们的框架有一些出色的文档)。我同意一些代码示例可能难以通过,但请记住它是代码,而不是散文。 –

+0

@SpaceDog如果没有做出关于minutae的决定,就不可能将代码样本放在一起;在散文中,你可以掩盖一些细节,但在代码中,这是不可能的。这意味着你要么不得不做出一些不透明的决定(例如,这是如何放弃框架?),或者你必须在各个地方留下大量的评论来尝试接触所有技能水平的人。这会让代码被无关的解释淹没,难以阅读;很难达到最佳平衡。这只是钻研别人代码库的经验;这绝非易事,但我们可以填补空白。 –

2

OK,这是一个有趣的现象:

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

这是NULL

因此它是无操作的。

与T5和T6的回调相同。

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

希望这有助于

+0

对不起,我没有看到它。让我们考虑代码'self.currentPreviewPixelBuffer = previewPixelBuffer;'的第一行,在这一行,该属性被设置为'previewPixelBuffer'值,理论上不是零。所以,我们有一个有效的缓冲区。然后一个异步块运行,我看不到这个块如何检测旧的帧。我在那里看到一个@synchronized指令。我知道这条指令在理论上的作用,但我也没有理解视频帧的来历,这到底是在做什么。此外,代码的第一行也有类似的评论 – SpaceDog

+0

记住...解释它像我3岁... – SpaceDog

+0

这段代码没有任何意义:'CVPixelBufferRef currentPreviewPixelBuffer = NULL;'执行使其为空。然后,一个@syncronize会在末尾运行变量not null,并询问它是否为空。一个完全疯狂的代码。 – SpaceDog