1. 程式人生 > >WebRTC視訊幀渲染前處理——視訊幀裁剪

WebRTC視訊幀渲染前處理——視訊幀裁剪

十一假期寫了一篇《WebRTC視訊幀渲染前處理——等比例填充顯示視窗》,介紹了按照顯示視窗,不損失原視訊幀內容的前提下,左右或上下補黑的方式來構造視訊幀的方法。這篇文章再說一下另外一種處理方式,那就是按照顯示視窗比例,將源視訊幀進行裁剪,按照比例來獲取其中一部分,放到視窗中顯示的方法。這種方法適合任何矩形視窗比例(如1:1正方形、4:3、16:9、16:10或其他比例)。

根據顯示視窗寬高比不同,與等比例填充一樣,裁剪也有三種情況:
1. 寬高比幾乎相同,不做任何處理
2. 源視訊幀寬高比 > 顯示視窗寬高比,執行源視訊幀左右裁剪
3. 第2條的反向條件,執行源視訊幀上下裁剪

第1種情況我們不需要做裁剪處理,直接pass就行了,OpenGL ES 會為我們完成渲染時的自動縮放拉伸以適合顯示檢視。針對第2、3種情況,我們以源視訊幀中央位置為基準,來分別按照寬、高進行裁剪。

下圖是裁剪的示意圖:
這裡寫圖片描述

依然是在ViERenderer::DeliverFrame()中進行這個處理。關鍵程式碼如下:

void ViERenderer::DeliverFrame(int id,
                               I420VideoFrame* video_frame,
                               int num_csrcs,
                               const uint32_t CSRC[kRtpCsrcSize])
{
    //假設顯示檢視大小資訊存在變數 rc 中
int nViewWidth = rc.right - rc.left; int nViewHeight = rc.bottom - rc.top; double srcRatio = (double)video_frame->width() / (double)video_frame->height(); double dstRatio = (double)nViewWidth / (double)nViewHeight; //判斷視訊寬高比和顯示視窗寬高比的差 if( fabs(srcRatio - dstRatio) <= 1e-6
) { //由於浮點數存在精度,當差值的絕對值小於10的-6次方的時候,將差值視為0 //寬高比相同,不用做任何處理 } else if( srcRatio > dstRatio ) { //按照顯示檢視比例,以源視訊幀中央為基準,計算合適的寬度,超過的部分丟棄不要,相當於進行左右裁剪 //按照檢視的顯示比例,計算適合的寬度 int srcWidth = (int)(video_frame->height * dstRatio); //除8乘8,修正寬值 srcWidth = (srcWidth >> 3 << 3; //找到寬度中心 int nMidWidth = (srcWidth + 1) / 2; //關鍵的變數:計算X方向偏移位置,後面拷貝YUV資料,從這個偏移位置開始拷貝 int nOffset = (video_frame->width() - srcWidth) / 2; //修正以避免出現奇數 if(nOffset % 2) nOffset += 1; //new_frame是一個臨時幀,可以定義一個成員變數避免重複申請記憶體 //tmp_buf的3個元素分別指向new_frame的Y,U,V buffer起始位置 //src_buf的3個元素分別指向視訊幀的Y,U,V buffer起始位置 unsigned char *tmp_buf[3], *src_buf[3]; //CreateEmptyFrame後面2個引數是寬度的1/2,函式內部會用這個值乘以高度的1/2,得到的就是U,V的實際大小,以此來分配空間 new_frame.CreateEmptyFrame(srcWidth, video_frame->height(), srcWidth, nMidWidth, nMidWidth); //準備指標 tmp_buf[0] = (unsigned char*)new_frame.buffer(kYPlane); tmp_buf[1] = (unsigned char*)new_frame.buffer(kUPlane); tmp_buf[2] = (unsigned char*)new_frame.buffer(kVPlane); src_buf[0] = (unsigned char*)video_frame->buffer(kYPlane); src_buf[1] = (unsigned char*)video_frame->buffer(kUPlane); src_buf[2] = (unsigned char*)video_frame->buffer(kVPlane); //注意hStep的退出條件:因為迴圈體內部每次都拷貝2行Y,因此處理次數就是高度的一半 for(int hStep = 0; hStep < (video_frame->height()+1)/2; hStep++) { //因為video_frame是4:2:0格式,4個Y點對應1個U和1個V,所以2行Y對應1/2行U及1/2行V //拷貝2行Y memcpy(tmp_buf[0]+(hStep*2)*new_frame.stride(kYPlane), src_buf[0]+(hStep*2)*video_frame->stride(kYPlane)+nOffset, new_frame->width()); memcpy(tmp_buf[0]+(hStep*2+1)*new_frame.stride(kYPlane), src_buf[0]+(hStep*2+1)*video_frame->stride(kYPlane)+nOffset, new_frame->width()); //拷貝1/2行U memcpy(tmp_buf[1]+hStep*new_frame.stride(kUPlane), src_buf[1]+hStep*video_frame->stride(kUPlane)+(nOffset>>1), (new_frame->width()+1)/2); //拷貝1/2行V memcpy(tmp_buf[2]+hStep*new_frame.stride(kVPlane), src_buf[2]+hStep*video_frame->stride(kVPlane)+(nOffset>>1), (new_frame->width()+1)/2); } //OK,YUV資料複製完畢,把其他內容補上 new_frame.set_render_time_ms(video_frame->render_time_ms()); new_frame.set_timestamp(video_frame->timestamp()); //幀交換,現在video_frame裡是新構造好的左右補黑的新視訊幀了 video_frame->SwapFrame(&new_frame); } else { //下面是上下裁剪的情況,思路和左右裁剪相同,只是計算Offset的地方有區別,其他一樣,就不寫詳細註釋了 int srcHeight = (int)(video_frame->width() / dstRatio); int srcWidth = video_frame->width() >> 3 << 3; int nMidWidth = (srcWidth + 1) / 2; //與左右裁剪的區別在這個offset的計算 int nOffset = (video_frame->height() - srcHeight) / 2; if(nOffset % 2) nOffset += 1; unsigned char *tmp_buf[3], *src_buf[3]; new_frame.CreateEmptyFrame(srcWidth, srcHeight, srcWidth, nMidWidth, nMidWidth); tmp_buf[0] = (unsigned char*)new_frame.buffer(kYPlane); tmp_buf[1] = (unsigned char*)new_frame.buffer(kUPlane); tmp_buf[2] = (unsigned char*)new_frame.buffer(kVPlane); src_buf[0] = (unsigned char*)video_frame->buffer(kYPlane); src_buf[1] = (unsigned char*)video_frame->buffer(kUPlane); src_buf[2] = (unsigned char*)video_frame->buffer(kVPlane); for(int hStep = 0; hStep < (video_frame->height()+1)/2; hStep++) { memcpy(tmp_buf[0]+(hStep*2)*new_frame.stride(kYPlane), src_buf[0]+(hStep*2+nOffset)*video_frame->stride(kYPlane), new_frame->width()); memcpy(tmp_buf[0]+(hStep*2+1)*new_frame.stride(kYPlane), src_buf[0]+(hStep*2+1+nOffset)*video_frame->stride(kYPlane), new_frame->width()); memcpy(tmp_buf[1]+hStep*new_frame.stride(kUPlane), src_buf[1]+(hStep+(nOffset>>1))*video_frame->stride(kUPlane), (new_frame->width()+1)/2); memcpy(tmp_buf[2]+hStep*new_frame.stride(kVPlane), src_buf[2]+(hStep+(nOffset>>1))*video_frame->stride(kVPlane), (new_frame->width()+1)/2); } new_frame.set_render_time_ms(video_frame->render_time_ms()); new_frame.set_timestamp(video_frame->timestamp()); video_frame->SwapFrame(&new_frame); } //OK,接下來就交給後續流程去渲染顯示了 render_callback_->RenderFrame(render_id_, *video_frame); }

OK,讓我們來實際跑一下看看效果。

這裡寫圖片描述

這裡寫圖片描述

等比例填充,視訊幀裁剪,這兩種基本上都可以滿足正常的顯示需求了。上面是基於早期webrtc的視訊渲染框架中製作的,實際應用中,可以根據程式碼思路,運用到類似場景中。