? ? ? ?記錄一些實(shí)際開發(fā)中,比較難碰到與遇見的坑。大多是較深的操作,一般項(xiàng)目都碰不上,只有對(duì)自定義視頻定制性較高的項(xiàng)目才會(huì)碰上。其他的像首幀黑幀,常見崩潰之類問題的社區(qū)有大把現(xiàn)成解決方案,不提也罷。本文著重于記錄,并提供個(gè)人的一些解決問題思路,不喜勿噴~ 寫得不好,還請(qǐng)多多包涵~
1?GPUImageVideoCamera 的回調(diào)
? ? videoCamera提供了willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer的代理方法,并在videoProcessQueue串行隊(duì)列里回調(diào)上層做視頻幀處理。值得注意的是
(1)、出現(xiàn)willOutputSampleBuffer回調(diào)的幀數(shù)與實(shí)際設(shè)置的幀數(shù)相差太遠(yuǎn)
captureOuput的回調(diào)是在cameraProcessingQueue里,并用信號(hào)量進(jìn)行控制,先看源碼:
看代碼可知,如果上層處理視頻幀時(shí)間過久,就會(huì)產(chǎn)生丟幀。如果上層處理時(shí)間每次都超時(shí),會(huì)導(dǎo)致每?jī)蓭紩?huì)掉一幀,卡頓就會(huì)很明顯。具體解決方案就要根據(jù)產(chǎn)品的需求和上層處理時(shí)長(zhǎng)來定了,我們假設(shè)第二幀回調(diào)到時(shí),信號(hào)量仍未釋放,則丟棄了該幀,但很快信號(hào)量釋放了,在信號(hào)量釋放到第三幀到達(dá)之前,時(shí)間是浪費(fèi)的,所以可以嘗試調(diào)大相機(jī)的幀數(shù),或者優(yōu)化上層處理時(shí)間。要想完美控制幀數(shù),只能在錄制時(shí)調(diào)節(jié)recordWriter的幀數(shù)來實(shí)現(xiàn)。
(2)、willOutputSampleBuffer:回調(diào)的buffer與你想像中一樣嗎?
? ? ?通過GPU...Camera拿到的buffer,我們用GPUImageView來顯示,一般我們?cè)O(shè)置outputImageOrientation = UIDeviceOrientationPortrait,因?yàn)槲覀兛吹降氖秦Q的視頻幀,我們就以為得到的是豎的視頻幀,但實(shí)際上這是個(gè)坑。
? ? ?系統(tǒng)默認(rèn)采集方向?yàn)橄蜃筠D(zhuǎn)90度,GPU...Camera是通過改變輸出方向來調(diào)整,也就是你此時(shí)采集到的幀其實(shí)是翻轉(zhuǎn)的,只不過GPU..Camera通過outputImageOrientation,在輸出的時(shí)候又幫你做相應(yīng)的翻轉(zhuǎn)。所以此時(shí),如果你拿看到的是豎的視頻幀做處理,就會(huì)出問題了。坑就坑在你看到的和你想的完全是兩回事。
? ? 解決方法:使用AVCaptureConnection自己控制視頻幀的方向,由于GPU...Camera的videoOutput只有子類有權(quán)限使用,那就繼承它,
記得把outputImageOrientation 設(shè)為UIDeviceOrientationUnknown; 翻轉(zhuǎn)攝像頭時(shí),記得做相應(yīng)切換操作,要不會(huì)恢復(fù)左翻轉(zhuǎn)90度。
ps:僅對(duì)sampleBuffer有特殊要求時(shí)才需要這么做
2?非主線程執(zhí)行同步主隊(duì)列操作導(dǎo)致死鎖
? ? 我們先來看一段代碼,談?wù)撓滤目尚行?/p>
? ?咋一看,老鐵,沒毛病。非主隊(duì)列同步操作主隊(duì)列沒問題。博主在之前也是這么認(rèn)為的。那我們把這段代碼放到GPUVideoImageCamera的回調(diào)中試試:
你會(huì)發(fā)現(xiàn),主線程卡死了!在非主隊(duì)列,同步主隊(duì)列,這有毛病嗎?明面沒毛病。那為什么主線程卡死,而且還不崩?只有一種情況:互等 -- 我等你,你等我(思路)。GPU...Camera的回調(diào)的串行隊(duì)列(videoProcessQueue)在等主隊(duì)列同步完成后回調(diào),那主隊(duì)列在等誰呢?我們來看看堆棧:
主隊(duì)列同步等待串行隊(duì)列videoProcessQueue!這就形成了互等,主隊(duì)列同步等待串行隊(duì)列,串行隊(duì)列同步等待主隊(duì)列,死鎖。故意在串行隊(duì)列sleep(1)也就是為了卡這個(gè)bug(實(shí)際上操作都可能卡死主線程,操作時(shí)間越長(zhǎng)越容易出現(xiàn))。看來,在非主隊(duì)列同步調(diào)用主隊(duì)列也并不是一個(gè)絕對(duì)安全的做法。此處,也咨詢過其他iOS開發(fā)者,同步操作前有沒辦法判斷是不是在等待另一個(gè)隊(duì)列,得到的答案都是NO,若有有緣人知道安全的做法,還請(qǐng)留言賜教!
解決方案:此處不宜通過同步主隊(duì)列的方法來處理事件,一定要同步處理的話,可以自己創(chuàng)建一個(gè)隊(duì)列,或者用GPU...Camera的videoProcessQueue.
3?隊(duì)列的同步異步操作判斷
GPUImage提供了自建隊(duì)列同步的操作方法
同步操作隊(duì)列:
runSynchronouslyOnVideoProcessingQueue(void (^block)(void))
runSynchronouslyOnContextQueue(GPUImageContext *context, void (^block)(void))
我們?cè)賮砜纯磳?shí)現(xiàn):
runSynchronouslyOnContextQueue實(shí)現(xiàn)方法類似,不再貼圖。重點(diǎn)就在這句:dispatch_get_specific([GPUImageContext contextKey]);-- ?dispatch_get_specific與dispatch_set_specific,先通過dispatch_set_specific給一個(gè)隊(duì)列做一個(gè)標(biāo)識(shí),然后在當(dāng)前隊(duì)列dispatch_get_specific當(dāng)前隊(duì)列是不是該標(biāo)識(shí)的隊(duì)列,不明白的自己科普。
我們看看[GPUImageContext contextKey],該標(biāo)識(shí)是openGLESContextQueueKey.
通過[GPUImageContext contextKey]拿到的key都是一樣的,這會(huì)有什么后果呢?就是在videoProcessingQueue或與contextQueue的隊(duì)列上通過dispatch_get_specific([GPUImageContext contextKey]),他一定會(huì)返回YES。這就會(huì)出問題了:
假設(shè)我在contextQueue里希望同步到一個(gè)里videoProcessingQueue操作,我們調(diào)用runSynchronouslyOnVideoProcessingQueue,但其實(shí)由于dispatch_get_specific([GPUImageContext contextKey])一定返回YES,block里的操作會(huì)直接進(jìn)行,你所希望的操作將會(huì)在contextQueue里進(jìn)行,而不會(huì)在videoProcessingQueue中進(jìn)行。因?yàn)闆]法同步videoProcessingQueue,如果此時(shí)你有希望在videoProcessingQueue同步的操作,就會(huì)出現(xiàn)偏差了。GPU的異步隊(duì)列大同小異,就不再重復(fù)舉例。
????????解決方案:(二選一)
????????1?改寫源碼?
? ? ? ? 2?同等對(duì)待這兩個(gè)queue,把他們當(dāng)同個(gè)queue(實(shí)際上GPUImage在同步上就是這么對(duì)待的)。
4?recordWriter 與 imageMovie配合的坑
大部分人都是直接Ctrl + c , Ctrl + V,加上,項(xiàng)目用得少,也基本不會(huì)有問題。但當(dāng)你隨意調(diào)換順序或是自定義的時(shí)候,就可能會(huì)出現(xiàn)你摸不著頭腦的坑。先簡(jiǎn)單說下imageMovie的創(chuàng)建方式(算有點(diǎn)小坑):
1?- (id)initWithAsset:(AVAsset *)asset;(有圖像和聲音回調(diào),但不會(huì)播放聲音,得自己添加播放,但可以直接與recordWriter配合)
2?- (id)initWithPlayerItem:(AVPlayerItem *)playerItem;(自己創(chuàng)建AVPlayer去解析和控制item,可以做到和播放器一樣,播放也有聲音。但只有圖像回調(diào),沒有聲音回調(diào),因?yàn)橄到y(tǒng)只提供了AVPlayerItemVideoOutput。如果和recordWriter配合,制作完成后得自己添加音軌)
3?- (id)initWithURL:(NSURL *)url;本質(zhì)就是initWithAsset,有興趣自己看底層
下面說說坑(出現(xiàn)概率低于1%。):
(一)、野指針,出現(xiàn)如圖
? ? ? ? ? ??bug排查:synchronizedMovieWriter或movie已經(jīng)被銷毀,很繞,這也是GPUImage出問題難排查的原因,全是block,你調(diào)我,我調(diào)他,他還調(diào)你,重點(diǎn)你還不知道他什么時(shí)候會(huì)調(diào),不想看的(看了還得研究源碼)直接看解決方案。只分析野指針一,二的原理差不多。
? ? ? ? 首先:movieWriter通過?
????????videoQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.videoReadingQueue", GPUImageDefaultQueueAttribute());
? ? ? ? 和塊:videoInputReadyCallback()進(jìn)行下一幀的讀取操作movie里的readNextVideoFrameFromOutput,而終止錄制用的是runSynchronouslyOnContextQueue;也就是存在掉用finish后繼續(xù)讀取下一幀的可能,
? ? ? ?當(dāng)movie里的synchronizedMovieWriter賦值videoInputReadyCallback,同時(shí)讀取下一幀,此時(shí)發(fā)現(xiàn)isKeepLooping為NO,在block里調(diào)用本身的endProcessing(見下圖),此時(shí)來到崩潰的地方,由于block并沒有強(qiáng)引用,而此時(shí)如果synchronizedMovieWriter已經(jīng)置空(上層銷毀),就會(huì)產(chǎn)生野指針。野指針二出現(xiàn)的情況就是movie先置空,writer后置空
????????????解決方案: ? ???
? ? ? 調(diào)了這兩句后,上層保證你的recordMovie,movieWriter不會(huì)馬上致空(其實(shí)大概也就0.01秒的時(shí)間)即可
? ?(二)、上層渲染沒問題,record完成卻全部變黑幀
? ? ? ? 出現(xiàn)這情況,邏輯上是先判斷是否有videoTrack,沒videoTrack肯定是業(yè)務(wù)邏輯出問題,上層排查。有videoTrack,渲染沒問題,卻全部黑幀:時(shí)間戳出問題(思路),定位
? ??????[assetWriter startSessionAtSourceTime:frameTime];
? ? ? ? 正常這里應(yīng)該是0,看看frameTime是否異常,或者是這里根本沒有走就直接進(jìn)行write了。
????????再定位
????????[assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer ????????withPresentationTime:frameTime],打印frameTime一步步排查
? ? ? ? 可能出現(xiàn)此異常的操作:先processing,再startRecording
? ? ? ? [self.recordMovie startProcessing];
? ??????[self.movieWriter startRecording];
? ? ? ? 出現(xiàn)概率低,需要天時(shí)地利人和,但他就是存在。
? ? ? ? 解決方案:把這兩句代碼順序調(diào)過來就可以了
? ? ? ? PS:此處出現(xiàn)狀況較多,僅提供排查思路
? (三)、錄制完成后,通過主線程回調(diào)上層,通過視頻地址獲取的視頻馬上播放后,發(fā)現(xiàn)無法播放,隔一段時(shí)間又能播放了(出現(xiàn)概率極低,當(dāng)處理較大的視頻是概率稍大)
? ? ? ? bug排查:估計(jì)還是這兩貨出問題(不確定)
? ? ? ? ? ? [self.recordMovie startProcessing];
? ? ? ? ? ? [self.movieWriter startRecording];
? ? ? ? 由于馬上回調(diào)上層,可能movieWriter還在操作該路徑,導(dǎo)致馬上播放失敗
? ? ? ? 解決方案:
? ? ? ? 延時(shí)回調(diào)主線程(0.1秒);
(四)、設(shè)置_movie.audioEncodingTarget = _writer后,出現(xiàn)
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[AVAssetWriterInput appendSampleBuffer:] Cannot append sample buffer: Input buffer must be in an uncompressed format when outputSettings is not nil'
設(shè)置outputSetting支持PCM,兩個(gè)方法,一個(gè)改源碼,不想破壞源碼的寫子類,重寫GPUImageMovie里的createAssetReader:
原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明出處