WebRTC視訊幀渲染前處理——視訊幀裁剪
阿新 • • 發佈:2019-01-23
十一假期寫了一篇《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的視訊渲染框架中製作的,實際應用中,可以根據程式碼思路,運用到類似場景中。