GPUImage-多路視頻繪制

前言

最近做多路視頻的渲染,本文是其渲染方案的預研。
效果大概如下:

正文

一、多GPUImageView方案

用GPUImage進行多路視頻的渲染,有一個非常簡單的方案:多個GPUImageView方式,每路視頻畫面單獨渲染
一路視頻對應一個濾鏡鏈,拿到視頻數據后進行裁剪,直接顯示到對應的GPUImageView上;多個GPUImageView組成多路視頻畫面,通過改變GPUImageView的坐標可以實現畫面拼接的效果。

方案很簡單,寫了一個demo,地址在這里
demo用兩個mp4視頻作為源數據,用GPUImageMovie作為濾鏡鏈的起始,用GPUImageCropFilter對視頻進行裁剪,再經過GPUImageTransformFilter轉置,最終顯示在GPUImageView上。
這個方案的優缺點也很明顯:
優點:實現簡單,畫面拼接由UIKit層的API實現;
缺點:渲染到屏幕的次數增多,渲染頻率遠大于屏幕顯示幀率;

二、單GPUImageView方案

上面的方案最明顯的問題就是渲染到屏幕的次數比屏幕刷新的次數還多,那么自然延伸出來一種方案:只用一個GPUImageView,視頻統一渲染到GPUImageView
GPUImageView顯示區域劃分成多個區域,每個區域對應一路視頻;多路視頻畫面都采用離屏渲染的方式,繪制到紋理的對應區域中,再由multiTexFilter進行處理;multiTexFilter集合多路視頻的渲染,如果沒有更新則使用上次的數據,再把紋理傳遞給GPUImageView,最終GPUImageView渲染到屏幕上;

方案較為復雜,同樣實現了demo,地址在這里
demo用兩個mp4視頻作為源數據,仍然用GPUImageMovie作為濾鏡鏈的起始,再經過GPUImageCropFilter、GPUImageTransformFilter處理,交給LYMultiTextureFilter,最終顯示在GPUImageView上。
此方案的核心在于LYMultiTextureFilter類,我們來仔細分析下其構成:


/**
 把多個Texture渲染到同一個FrameBuffer

 使用時需要先綁定frameBuffer和顯示區域rect
 在newFrame回調的時候,會根據index把圖像繪制在綁定的rect
 特別的,會制定一個mainIndex,當此index就緒時調用newFrame通知響應鏈的下一個
 */
@interface LYMultiTextureFilter : GPUImageFilter

- (instancetype)initWithMaxFilter:(NSInteger)maxFilter;

/**
 設置繪制區域rect,并且綁定紋理id

 @param rect 繪制區域 origin是起始點,size是矩形大小;(取值范圍是0~1,點(0,0)表示左下角,點(1,1)表示右上角, Size(1,1)表示最大區域)

 @param filterIndex 紋理id
 */
- (void)setDrawRect:(CGRect)rect atIndex:(NSInteger)filterIndex;

- (void)setMainIndex:(NSInteger)filterIndex;

LYMultiTextureFilter繼承自GPUImageFilter,通過-setDrawRect綁定紋理的繪制區域;特別的,可以設置一個mainIndex,當此index就緒時調用newFrame通知濾鏡鏈的下一個目標。
具體實現有幾個注意事項:
1、通過頂點指定特別的渲染區域;
2、因為GPUImageFramebuffer默認會被復用,導致無法保存上一次的紋理,這里需要修改邏輯-renderToTextureWithVertices:-informTargetsAboutNewFrameAtTime,使得LYMultiTextureFilter一直使用同一個GPUImageFramebuffer;
3、dealloc的時候需要釋放outputFramebuffer,避免內存泄露;

這個方案的特點:
優點:統一渲染,避免的渲染次數大于屏幕幀率;
缺點:multiTexFilter實現復雜,畫面拼接需要用shader來實現;

三、兩個方案的對比

通過對比兩個方案的CPU、GPU占比,分析性能。

CPU對比:如下。選中前30s的區間,singleImageView的方式有0.5s(大概2%)的微弱優勢。整個過程的cpu消耗波形類似,占比也大致相同。

GPU對比:方案1的GPU消耗比方案2的消耗更小!!!
難以置信,因為方案2是對方案1的優化,但是為何會占用更多的GPU?
通過instruments工具查看,方案1的presentRenderBuffer調用次數超過每秒100次,方案2在30次左右。
回顧方案1的設計:
GPUImageMovie => GPUImageCropFilter => GPUImageTransformFilter => GPUImageView
方案2的設計:
GPUImageMovie => GPUImageCropFilter => GPUImageTransformFilter => LYMultiTextureFilter => GPUImageView
方案2濾鏡鏈比方案1多了LYMultiTextureFilter,雖然調用presentRenderBuffer的次數少,但是明顯離屏渲染變多。相反,因為多LYMultiTextureFilter這個路徑,導致總的gl渲染指令變多,GPU消耗更大!!
究其原因,是因為作為數據源GPUImageMovie是不同步的。即使是方案2,多個GPUImageMovie的數據輸出也是不同步,渲染給LYMultiTextureFilter的次數同樣可能出現大于使用次數的情況。方案2相對方案1,同樣存在多余的渲染問題。
于是有了進一步優化的方案。

四、屏幕幀率驅動的單GPUImageView方案

先看一張大圖:

相對于之前的方案,這里引入CADisplayLink作為渲染的驅動,同時視頻數據只保持最新的一幀。如果每個cropFilter都能驅動multiTexFilter渲染,則multiTexFilter的渲染頻率最高會達到120FPS,浪費性能。
為了保證multiTexFilter繪制的幀率不超過30FPS,cropFilter渲染順序是1~N,最后以cropFilterN 作為multiTexFilter渲染的驅動。
當cropFilterN渲染完,就進行multiTexFilter的渲染,如果其他cropFilter沒有數據則使用上一幀數據。
cropFilter在渲染的時候從DataManager取數據,如果沒有數據則使用默認數據進行渲染。

為了方便對比,同樣實現了demo,地址在這里
與方案2相比,有幾個不同點:
1、CADisplayLink驅動渲染,并且每次只讀取當前最新幀;
2、引入LYAssetReader,LYAssetReader是對AVAssetReader和AVAssetReaderTrackOutput的簡單封裝,使得能循環播放和讀取當前最新的視頻幀;
3、使用GPUImageMovie的-processMovieFrame接口作為濾鏡鏈的輸入;
4、可以控制每次渲染中,cropFilter的渲染順序;

綜合考慮,暫定方案3--屏幕幀率驅動的單GPUImageView方案作為生產環境的渲染方案。

五、Demo實現過程的坑

1、幀緩存復用

有段時間沒有接觸GPUImage,導致demo開發過程遇到幾個坑,首先第一個是如何保證畫面渲染的連續
在實現方案2的demo時,考慮到多路視頻渲染中可能某一路的視頻畫面沒有更新,比如說GPUImageMovie讀取視頻源數據較慢,此時GPUImageMovie對應的顯示區域就無法redraw,導致該區域的內容顯示異常,出現閃爍的效果。(GPUImage的幀緩存是復用的)
這里有兩種方案:
1、保留GPUImageMovie讀取的最后一幀信息,每次再傳給cropFilters進行渲染;
2、保留cropFilters上一次的渲染結果;
從性能優化的角度出發,保留上一次的渲染結果更為合理。
這里的實現需要對GPUImage以及OpenGL有所了解,保留渲染結果其實就是復用上一次的幀緩存,不調用glClear進行清理;而GPUImage的outputFramebuffer在渲染完后會回收,所以需要一些修改,如下:

    if (!outputFramebuffer) {
        outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO];
        glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
        glClear(GL_COLOR_BUFFER_BIT);
    }
    else {
        [outputFramebuffer lock];
    }

僅在第一次的時候,為outputFramebuffer申請緩存;以后再次使用的時候,只需要進行lock即可。
這里有一個坑:GPUImageContext的fetchFramebufferForSize會默認進行一次lock,在使用完之后會再unlock;如果在第二次使用outputFramebuffer的之后不進行lock,會導致retainCount<0的情況。

2、GPUImageMovie處理CMSampleBuffer

另外一個較大的坑是GPUImageMovie,方案3的Demo在實現過程中遇到一個必現Crash:在調用GPUImageMovie的-processMovieFrame 處理從視頻幀讀取的CMSampleBuffer時,會在convertYUVToRGBOutputglDrawArrays報錯,錯誤類型是EXC_BAD_ACCESS,具體關鍵詞是"gleRunVertexSubmitARM"。
懷疑過紋理有異常、頂點數據有異常、處理線程不統一等導致,均不是原因。

最后通過二分大法,插入以下代碼定位到問題:

{
                GLenum err = glGetError();
                if (err != GL_NO_ERROR) {
                    printf("glError: %04x caught at %s:%u\n", err, __FILE__, __LINE__);
                }
            }
}

問題的核心在于GPUImageMovie *imageMovie = [[GPUImageMovie alloc] init];
GPUImageMovie的convertYUVToRGBOutput需要用到數據在initWithUrl:這個接口才有初始化!

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容