十一假期写了一篇《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的视频渲染框架中制作的,实际应用中,可以根据代码思路,运用到类似场景中。