WebRTC视频帧渲染前处理——视频帧裁剪

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

猜你喜欢

转载自blog.csdn.net/epubcn/article/details/78222632
今日推荐