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

在早期的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 于北京·家中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值