如何在预览来自 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
赋值 NULL
。 D
运行 并锁定 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。
希望对您有所帮助
感谢您突出显示这些行 - 希望这会使答案更容易理解。
让我们一步一步来:
-outputPreviewPixelBuffer:
被调用。 self.currentPreviewPixelBuffer
在 @synchronized
块中被覆盖 而不是 :这意味着它被强制覆盖,对所有线程都有效(我掩盖了 currentPreviewPixelBuffer
是 nonatomic
;这实际上是不安全的,这里有一场比赛——你真的需要它是 strong, atomic
才能真正做到这一点)。如果那里有缓冲区,那么下次线程要查找它时它就会消失。这就是文档所暗示的——如果 self.currentPreviewPixelBuffer
中有一个值,而委托尚未处理以前的值,那就太糟糕了!现在没了。
块被发送到委托异步处理。实际上,这可能会在将来 的某个时间发生,但会有一些不确定的延迟。这意味着在调用 -outputPreviewPixelBuffer:
和处理块之间, -outputPreviewPixelBuffer:
可以再次调用很多次!这就是丢弃陈旧帧的方式——如果代理花费很长时间来处理块,最新的 self.currentPreviewPixelBuffer
将一次又一次地被最新值覆盖,从而有效地丢弃前一帧。
C 到 H 行拥有 self.currentPreviewPixelBuffer
。您确实有一个本地像素缓冲区,最初设置为 NULL
。 self
周围的 @synchronized
块含蓄地说:“我将适度访问 self
,以确保在我查看时没有人编辑 self
,而且我将确保获取 self
的实例变量的最新值,即使跨线程也是如此”。这就是委托如何确保它拥有最新的 self.currentPreviewPixelBuffer
;如果不是 @synchronized
,您可以获得一份过时的副本。
也在@synchronized
块中覆盖了self.currentPreviewPixelBuffer
,在保留它之后。这段代码隐含地说:“嘿,如果 self.currentPreviewPixelBuffer
不是 NULL
,那么必须有一个像素缓冲区来处理;如果有(行 F),那么我会坚持下去(行E, G),并将其重置为 self
(H 行)”。实际上,这取得了 self
的 currentPreviewPixelBuffer
的所有权,因此没有其他人会处理它。这是对在 self
上运行的所有委托回调块的隐式检查:第一个要触发的查看 self.currentPreviewPixelBuffer
的块将保留它,将它设置为 NULL
以查看所有其他块self
,并且确实可以使用它。其他人已经阅读了 F 行的 NULL
,什么也不做。
第 I 行和第 J 行实际上使用了像素缓冲区,第 K 行正确处理了它。
没错,这段代码可以使用一些注释——实际上是 E 到 G 行在这里做了很多隐含的工作,取得 self
的预览缓冲区的所有权以防止其他人处理也阻止。 A 行上方的评论 not 说的是,“请注意,对 currentPreviewPixelBuffer 的访问受 @synchronized
... 的保护, 与此处相反不;因为它在这里不受保护,我们可以在有人处理它之前随意覆盖 self.currentPreviewPixelBuffer
多次,删除中间值 "
希望对您有所帮助。
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
赋值 NULL
。 D
运行 并锁定 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。
希望对您有所帮助
感谢您突出显示这些行 - 希望这会使答案更容易理解。
让我们一步一步来:
-outputPreviewPixelBuffer:
被调用。self.currentPreviewPixelBuffer
在@synchronized
块中被覆盖 而不是 :这意味着它被强制覆盖,对所有线程都有效(我掩盖了currentPreviewPixelBuffer
是nonatomic
;这实际上是不安全的,这里有一场比赛——你真的需要它是strong, atomic
才能真正做到这一点)。如果那里有缓冲区,那么下次线程要查找它时它就会消失。这就是文档所暗示的——如果self.currentPreviewPixelBuffer
中有一个值,而委托尚未处理以前的值,那就太糟糕了!现在没了。块被发送到委托异步处理。实际上,这可能会在将来 的某个时间发生,但会有一些不确定的延迟。这意味着在调用
-outputPreviewPixelBuffer:
和处理块之间,-outputPreviewPixelBuffer:
可以再次调用很多次!这就是丢弃陈旧帧的方式——如果代理花费很长时间来处理块,最新的self.currentPreviewPixelBuffer
将一次又一次地被最新值覆盖,从而有效地丢弃前一帧。C 到 H 行拥有
self.currentPreviewPixelBuffer
。您确实有一个本地像素缓冲区,最初设置为NULL
。self
周围的@synchronized
块含蓄地说:“我将适度访问self
,以确保在我查看时没有人编辑self
,而且我将确保获取self
的实例变量的最新值,即使跨线程也是如此”。这就是委托如何确保它拥有最新的self.currentPreviewPixelBuffer
;如果不是@synchronized
,您可以获得一份过时的副本。也在
@synchronized
块中覆盖了self.currentPreviewPixelBuffer
,在保留它之后。这段代码隐含地说:“嘿,如果self.currentPreviewPixelBuffer
不是NULL
,那么必须有一个像素缓冲区来处理;如果有(行 F),那么我会坚持下去(行E, G),并将其重置为self
(H 行)”。实际上,这取得了self
的currentPreviewPixelBuffer
的所有权,因此没有其他人会处理它。这是对在self
上运行的所有委托回调块的隐式检查:第一个要触发的查看self.currentPreviewPixelBuffer
的块将保留它,将它设置为NULL
以查看所有其他块self
,并且确实可以使用它。其他人已经阅读了 F 行的NULL
,什么也不做。第 I 行和第 J 行实际上使用了像素缓冲区,第 K 行正确处理了它。
没错,这段代码可以使用一些注释——实际上是 E 到 G 行在这里做了很多隐含的工作,取得 self
的预览缓冲区的所有权以防止其他人处理也阻止。 A 行上方的评论 not 说的是,“请注意,对 currentPreviewPixelBuffer 的访问受 @synchronized
... 的保护, 与此处相反不;因为它在这里不受保护,我们可以在有人处理它之前随意覆盖 self.currentPreviewPixelBuffer
多次,删除中间值 "
希望对您有所帮助。