Как сохранить низкую задержку при просмотре видео с 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
работает и буфер сохраняется в свойстве self.currentPreviewPixelBuffer
, B
работает и локальная переменная currentPreviewPixelBuffer
назначается с NULL
, D
работает и блокирует self
, затем E
запускает и меняет локальную переменную currentPreviewPixelBuffer
от NULL до значения self.currentPreviewPixelBuffer
,
Это первое, что не имеет смысла. Зачем мне создавать переменную currentPreviewPixelBuffer
назначить его NULL
и на следующей строке назначьте его self.currentPreviewPixelBuffer
?
Следующая строка еще более безумная. Почему я спрашиваю, если currentPreviewPixelBuffer
не является NULL
Если бы я только назначил его не NULL
значение на E
? затем H
выполняется и нули self.currentPreviewPixelBuffer
?
Одна вещь, которую я не понимаю, это: invokeDelegateCallbackAsync:
является асинхронным, верно? если он асинхронный, то каждый раз outputPreviewPixelBuffer
метод работает, чтобы установить self.currentPreviewPixelBuffer = previewPixelBuffer
и отправить блок на выполнение, чтобы снова запустить его.
Если outputPreviewPixelBuffer
запускается быстрее, у нас будет куча блоков, сложенных для исполнения.
Из-за объяснений Kamil Kocemba
Я понимаю, что эти асинхронные блоки как-то тестируют, если предыдущий закончил выполнение и отбросил кадры, если нет.
Кроме того, что именно @syncronized(self)
замок? Это мешает self.currentPreviewPixelBuffer
от написания или чтения? или это блокировка локальной переменной currentPreviewPixelBuffer
? Если блок под @syncronized(self)
является синхронным по отношению к объему линии в I
никогда не будет NULL
потому что это устанавливается E
,
2 ответа
Спасибо за выделение строк - надеюсь, это поможет сделать ответ немного проще.
Давайте пройдемся по шагам:
-outputPreviewPixelBuffer:
называется.self.currentPreviewPixelBuffer
перезаписывается не в@synchronized
блок: это означает, что он принудительно перезаписывается, эффективно для всех потоков (я подчеркиваю тот факт, чтоcurrentPreviewPixelBuffer
являетсяnonatomic
; это на самом деле небезопасно, и здесь есть гонка - вам действительно нужно бытьstrong, atomic
чтобы это было действительно так). Если там был буфер, он теперь исчезнет, когда поток в следующий раз будет его искать. Это то, что подразумевается в документации - если вself.currentPreviewPixelBuffer
и делегат еще не получил обработать предыдущее значение, очень плохо! Это ушло сейчас.- Блок отправляется делегату для асинхронной обработки. По сути, это может произойти когда-нибудь в будущем с некоторой неопределенной задержкой. Это означает, что между
-outputPreviewPixelBuffer:
вызывается и когда блок обрабатывается,-outputPreviewPixelBuffer:
может быть вызван снова много, много раз! Вот как отбрасываются устаревшие кадры - если делегату требуется много времени для обработки блока, последнийself.currentPreviewPixelBuffer
будет перезаписываться с последним значением снова и снова, эффективно удаляя предыдущий кадр. Строки C – H переходят во владение
self.currentPreviewPixelBuffer
, У вас действительно есть локальный пиксельный буфер, изначально настроенный наNULL
,@synchronized
вокругself
говорит, неявно: "Я собираюсь модерировать доступ кself
, чтобы убедиться, что никто не редактируетself
пока я смотрю на это, а также я буду следить за тем, чтобы получить самую актуальнуюself
переменные экземпляра, даже между потоками ". Таким образом, делегат гарантирует, что он имеет самую последнююself.currentPreviewPixelBuffer
; если бы не было@synchronized
Вы могли бы получить несвежую копию.Также в
@synchronized
блок перезаписываетself.currentPreviewPixelBuffer
после сохранения. Этот код неявно говорит: "эй, еслиself.currentPreviewPixelBuffer
не являетсяNULL
затем должен быть пиксельный буфер для обработки; если есть (строка F), то я буду держаться за него (строка E, G) и сбросить егоself
(строка H)". По сути, это берет на себя ответственность заself
"scurrentPreviewPixelBuffer
так что никто другой не будет его обрабатывать. Это неявная проверка для всех блоков обратного вызова делегата, работающих наself
: первый блок, который стреляет, который смотрит наself.currentPreviewPixelBuffer
получает сохранить его, устанавливает егоNULL
для всех остальных блоков, глядя наself
и работает с этим. Остальные, прочитавNULL
на линии F ничего не делать.Строки 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)
замок.
Когда Т7 заканчивает, какова ценность self.currentPreviewPixelBuffer
?
Это F5.
Затем мы запускаем делегатский обратный вызов для T3.
Делать self.currentPreviewPixelBuffer = NULL
Завершение обратного вызова делегата.
Затем мы запускаем делегатский обратный вызов для T4.
Какова ценность self.currentPreviewPixelBuffer
?
Это NULL
,
Так что нет.
То же самое для обратных вызовов для T5 и T6.
Обработанные кадры: F1 и F5. Сброшенные кадры: F2, F3, F4.
Надеюсь это поможет