2016-11-23 76 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