在早期的WebRTC版本中,视频帧在渲染前会经过ViERenderer::DeliverFrame()这个函数(源码位于vie_renderer.cc),我们可以在这里对传递过来的视频帧数据进行调整。比如,一般我们采集到的视频帧大小为640x480(4:3)的话,但显示视图大小是一个16:9甚至一个没有固定宽高比的尺寸,那么我们就面临该如何显示的问题。当然,如何显示,这跟不同产品的需求有关。
这里我介绍两种常见的显示模式:
- 等比例填充
- 等比例裁剪
这篇文章先介绍第1种:等比例填充。先解释一下什么叫做等比例填充:就是在不改变原有视频帧比例(如4:3)的情况下,将视频帧所有内容显示在给定视图区域中。
具体有三种情况:显示区域宽高比 (1.等于 2.大于 3. 小于)视频帧宽高比。等于就不说了,不用做任何处理。当发生显示区域宽高比大于或者小于视频帧宽高比时,我们就要进行多余部分留黑边(这里的黑边不一定是RGB颜色值0,也可以是0-255中的任何一个值)。
用一张图来表示如下:
下面来看一下两种情况的实现方法。这里说明一下,处理方法和思路有很多种,本文只是提供其中一种思路,可能不是最优方法。我相信有很多人有更多巧妙、高效的处理方法,欢迎提供。
下面代码中,省略了一些对本文所述问题无关的部分。
void ViERenderer::DeliverFrame(int id,
I420VideoFrame* video_frame,
int num_csrcs,
const uint32_t CSRC[kRtpCsrcSize])
{
//如果什么都不做,WebRTC默认的处理方法是:
//不理会视频帧宽高比例,直接拉伸至充满整个显示视图
//假设显示视图大小信息存在变量 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);
//修正宽值: 因为OpenGL渲染要求宽度是4的整数倍。这里按8的整数倍来计算。+7是避免取8整数倍时得到的是左侧值(如 8 16 24,接近24的话,不能取16)
srcWidth = (srcWidth + 7) >> 3 << 3;
//找到宽度中心
int nMidWidth = (srcWidth + 1) / 2;
//计算视频帧应该显示的左偏移位置(以下为居中显示),可以根据显示要求修正这个值
int nOffset = (srcWidth - video_frame->width()) / 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);
memset(tmp_buf[0], 0x00, new_frame.allocated_size(kYPlane)); //0x00 未被视频数据覆盖的部分就是黑色,如果需要其他颜色,可以修改这个默认值
memset(tmp_buf[1], 0x80, new_frame.allocated_size(kUPlane)); //0x80 = 128,灰度图里U,V都是0x80
memset(tmp_buf[2], 0x80, new_frame.allocated_size(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++)
{
//说明:nOffset >> 1 : 相当于 nOffset/2(奇数时不进位)
//因为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)+nOffset, src_buf[0]+(hStep*2)*video_frame->stride(kYPlane), video_frame->width());
memcpy(tmp_buf[0]+(hStep*2+1)*new_frame.stride(kYPlane)+nOffset, src_buf[0]+(hStep*2+1)*video_frame->stride(kYPlane), video_frame->width());
//拷贝1/2行U
memcpy(tmp_buf[1]+hStep*new_frame.stride(kUPlane)+(nOffset>>1), src_buf[1]+hStep*video_frame->stride(kUPlane), (video_frame->width()+1)/2);
//拷贝1/2行V
memcpy(tmp_buf[2]+hStep*new_frame.stride(kVPlane)+(nOffset>>1), src_buf[2]+hStep*video_frame->stride(kVPlane), (video_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
{
//上下补黑,实现思路与左右补黑相同,下面代码就不写详细注释了
int srcHeight = (int)(video_frame->width() / dstRatio);
int srcWidth = (video_frame->width() + 7) >> 3 << 3;
int nMidWidth = (srcWidth + 1) / 2;
int nOffset = (srcHeight - video_frame->height()) / 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);
memset(tmp_buf[0], 0x00, new_frame.allocated_size(kYPlane));
memset(tmp_buf[1], 0x80, new_frame.allocated_size(kUPlane));
memset(tmp_buf[2], 0x80, new_frame.allocated_size(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+nOffset)*new_frame.stride(kYPlane), src_buf[0]+(hStep*2)*video_frame->stride(kYPlane), video_frame->width());
memcpy(tmp_buf[0]+(hStep*2+1+nOffset)*new_frame.stride(kYPlane), src_buf[0]+(hStep*2+1)*video_frame->stride(kYPlane), video_frame->width());
memcpy(tmp_buf[1]+(hStep+(nOffset>>1))*new_frame.stride(kUPlane), src_buf[1]+hStep*video_frame->stride(kUPlane), (video_frame->width()+1)/2);
memcpy(tmp_buf[2]+(hStep+(nOffset>>1))*new_frame.stride(kVPlane), src_buf[2]+hStep*video_frame->stride(kVPlane), (video_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中跑一下看看效果:
这篇文章就先介绍到这里。另外一种等比例裁剪的方法,是根据视图大小,对原始视频帧进行局部裁剪,相当于只提取视频帧中我们需要的部分。其实现部分的示例代码及思路,我找时间另起一篇文章来描述。
最后唠叨一句: WebRTC应该是在m50甚至更早时候就逐渐将ViE开头的部分逐渐拿掉了,所以如果你手上的WebRTC找不到vie_renderer.cc这个文件以及ViERenderer、I420VideoFrame这些类,一点都不奇怪。
2017-10-2 于北京·家中