WebRTC视频帧渲染前处理——等比例填充显示窗口

来源:互联网 发布:四旋翼飞控编程 编辑:程序博客网 时间:2024/06/07 06:30

在早期的WebRTC版本中,视频帧在渲染前会经过ViERenderer::DeliverFrame()这个函数(源码位于vie_renderer.cc),我们可以在这里对传递过来的视频帧数据进行调整。比如,一般我们采集到的视频帧大小为640x480(4:3)的话,但显示视图大小是一个16:9甚至一个没有固定宽高比的尺寸,那么我们就面临该如何显示的问题。当然,如何显示,这跟不同产品的需求有关。

这里我介绍两种常见的显示模式:

  1. 等比例填充
  2. 等比例裁剪

这篇文章先介绍第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 于北京·家中