美文网首页
[音视频]QQ视频通话、抖音的视频回显 是如何实现的

[音视频]QQ视频通话、抖音的视频回显 是如何实现的

作者: CODING技术小馆 | 来源:发表于2019-11-30 19:51 被阅读0次

    QQ视频通话、抖音的视频回显 是如何实现的

    先说为什么会有这一篇文章:
    2014年联想曾经做过一款 短视频软件,叫“魔力秀”。可以说和现在的抖音基本是一样的,但因为“魔力秀App”出生于联想,注定无法在一个硬件公司成长为一棵参天大树,最终只发了一个版本就结束了。
    当时“魔力秀App”的视频回显模块是我设计实现的,所以就有了这篇文章。
    事过多年,将这篇文章拿出来整理,因为这项技术依然不过时,反而被广泛应用...

    这篇文章之前叫做 Opengl ES中YUV420转RGB 是一个技术标题。整理时,发现用这个标题,大家实际是不知道这个技术有什么用,因此换了这个比较醒目的名字。

    Opengl ES中YUV420转RGB 这项技术主要是实现视频高效、节省带宽的回显视频图像。

    • 为什么说高效?
      因为直接用 OpenGL ES 实现,本身绕开了Androi的层层封装;
      而且Opengl 本身就是图形学接口,实现效率天然高效。
    • 为什么说节省带宽?
      因为网络传输中,采用的YUV420数据格式,本身是一种有损的数据格式。但由于格式的特性,色彩还原后基本对图像显示效果没有影响,因此在视频通话场景中广泛使用。

    这里通过以下几个方面具体说明Opengl ES中YUV420转RGB 这项技术的实现方式:

    • 先了解一个概念“灰度图”
    • YUV数据格式
    • YUV444和YUV420
    • YUV420转RGB
    • OpenGL ES中YUV420P转RGB

    一、先了解一个概念“灰度图”

    这里先了解一下灰度 Y 的概念。不知道大家是否看过老式的黑白电视机
    老式黑白电视机的图像就只有Y一个通道,老式黑白电视机上的图像就是灰度图成像(只用接收一个Y通道数据就能播放出电视画面,前辈们果然厉害... ;而后来的彩色电视用的是YUV数据信号,这样既兼容了老的黑白电视,又可以在新式彩色电视上显示彩色图像,前辈们太厉害了...)

    • 灰度图的定义:
    • 灰度值与RGB的计算公式
    • 将“彩色图转”转化为“灰度图”shader实现

    1.1、灰度图的定义:

    把白色与黑色之间按对数关系分为若干等级,称为灰度。灰度分为256阶。

    1.2、灰度值Y与RGB的计算公式:

    Y = 0.299R + 0.587G + 0.114*B
    

    电视台发出信号时,将RGB数据这样转化为Y 数据。老式黑白电视机接收到Y信号,就能展示图象了。

    1.4、将“彩色图转”转化为“灰度图”shader实现

    这里说一个技术实现,在OpenGL ES中,如何用shader片元着色器,把一个彩色纹理图转化为一个灰度图?

    效果如下:

    彩色图 转化后的灰度图

    转化当然要用到我们上边说道的RGB 转 Y的公式,下边我们看具体的片源着色器 shader 代码实现:

    // shader 片元着色器
    precision mediump float;
    varying vec2 vTextureCoord;
    uniform sampler2D sTexture;
    
    void main() {
            // 从纹理图sTexture 读取当前片元的RGB颜色
             vec4 color=texture2D(sTexture, vTextureCoord);
             // 公式计算灰度值
             float col=color.r*0.299+color.g*0.587+color.b*0.114;
             // 将生成的Y 灰度值设置给RGB通道
             color.r=col;
             color.g=col;
             color.b=col;
             // 传给片源着色器
             gl_FragColor =color;
    }
    

    在shader实现中,我特意加了注释。
    了解glsl语法的同学,可以仔细读一下上边的代码实现;
    当然不了解语法的同学,更要简单读一遍(glsl是一种类C语言,只要学过C语言应该就能读懂)

    二、YUV数据格式

    上边我们了解了灰度图的实现,这里我们介绍一个YUV数据格式。
    主要分为以下几个部分:

    • YUV定义
    • 使用YUV的好处
    • YUV与RGB转换公式
    • YUV444和YUV420

    2.1、YUV

    YUV的具体定义如下:

    Y:就是灰度值;
    UV:用来指定像素的颜色。
    

    对于UV现在有些懵没关系,我们继续往下看

    2.2、YUV与RGB转换公式

    // RGB转YUV
    Y= 0.299*R + 0.587*G + 0.114*B
    U= -0.147*R - 0.289*G + 0.436*B = 0.492*(B- Y)
    V= 0.615*R - 0.515*G - 0.100*B = 0.877*(R- Y)
    //############################################
    // YUV转RGB
    R = Y + 1.140*V
    G = Y - 0.394*U - 0.581*V
    B = Y + 2.032*U
    

    2.3、使用YUV的好处:

    • 传输信号向后兼容老式黑白电视机(用于优化彩色视频信号的传输,使其向后相容老式黑白电视)
    • YUV420占用的带宽少(这个我们在前边提过,至于具体为什么,后边会有详细介绍)

    YUV420与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(后面来介绍)

    2.4、YUV444和YUV420

    前边我们一直说的YUV数据,其实是YUV420数据格式。YUV420数据格式在传输上UV色彩是有损传输,而YUV444 其实是一种无损的数据格式。
    那为什么这里我们还是要说一下YUV444格式呢?
    其实是为了后边实现 将YUV420数据还原成RGB做准备

    首先介绍YUV444 数据格式:

    • YUV444:
      一个像素点对应一个Y一个U一个V(YUV一一对应)
      YUV444数据格式 如下图所示:
    YUV444数据格式

    YUV444 中YUV通道一一对应,理解简单。下边这个是YUV420数据格式,UV数据有损。

    • YUV420:
      一个像素点对应一个Y;
      四个像素点对应一个U一个V;

    具体数据格式如下:

    YUV420数据格式

    从上图可以看到,UV色彩通道是有损失的,这也是为什么YUV420在展示时,占用的带宽更少一下。

    a、Y、U、V没有一一对应,图像有颜色损失
    b、这也就是为什么占用的带宽少了;
    c、同样网络传输中,占用的流量也同样减少了;
    d、但对图像的色彩展示几乎没有影响

    因为占用的流量较少,对色彩展示几乎没有影响,因此广泛应用于各中视频通话场景,视频回显场景等。

    三、YUV420转RGB

    哇去,基础知识终于说完了,这里说到我们的核心技术点:YUV420转RGB

    • 第一个步骤YUV420转YUV444;
    • 第二个步骤YUV444转RGB。

    为什么要把YUV420转为YUV444?
    因为在传输时,YUV420中的UV通道数据损失了。但我们渲染时,需要把这损失掉的UV色彩数据通道还原回来,再进行YUV444 转 RGB

    先说 YUV420 转 YUV444

    3.1、YUV420转YUV444

    要把YUV420转为YUV444就得把“上图 YUV420” U与V中 “?” 的部分填满。

    通过YUV420数据中,已有的U 与 Y数据,通过差值计算的方式,填补上空缺的部分。以下是差值运算的具体实现公式,差值计算如下(建议参照YUV420数据格式图来看,要不容易懵):

    U01 = (U00 + U02)/2; // 利用已有的 U00、U02来计算U01
    U10 = (U00 + U20)/2; // 利用已有的 U00、U20来计算U10
    U11 = (U00 + U02 + U20 + U22)/4;// 利用已有的 U00、U02、U20、U22来计算U11
    
    //######################
    V01 = (V00 + V02)/2; // 利用已有的 V00、V02来计算V01
    V10 = (V00 + V20)/2; // 利用已有的 V00、V20来计算V10
    V11 = (V00 + V02 + V20 + V22)/4; // 利用已有的 V00、V02、V20、V22来计算V11
    

    经过以上公式,YUV420转YUV444 完成(数据补全成功),下边来说YUV444如何转RGB。

    3.2、YUV444转RGB

    YUV444转RGB是有现成公式的,我们直接拿来用就行了,YUV转RGB的公式:

    R = Y + 1.140*V
    G = Y - 0.394*U - 0.581*V
    B = Y + 2.032*U
    

    公式有了,那具体的代码实现是怎么实现的呢?

    注:
    一、二、三、四,这四点介绍的是YUV转RGB的基本原理,下边是具体实现。

    四、OpenGL ES中YUV420P转RGB

    这一节介绍具体技术实现,但开始时,还是要介绍两种数据格式(哎、我知道你们都烦了,我其实也烦,但还是得说)

    • YUV420p的数据格式
    • YUV420sp的数据格式(YUV420sp转RGB这里不做介绍)
    • YUV420sp 转RGB

    4.1、YUV420p的数据格式

    YUV420p的数据格式如下图所示(为一个byte[]):

    YUV420p

    其中数据的4/6为Y;1/6为U;1/6为V。

    4.2、YUV420sp的数据格式(YUV420sp转RGB这里不做介绍)

    YUV420sp的数据格式如下图所示(为一个byte[]):

    YUV420sp

    其中数据的4/6为Y;1/6为U;1/6为V。

    4.3、YUV420sp 转RGB

    其实大概原理就是:

    • 将YUV420数据中的Y U V 数据分别取出来,分别生成三张纹理图
    • 利用片元着色器每个片元执行一次的特性,将YUV420数据转为YUV444数据
    • 从YUV444数据中,取出一一对应的YUV数据
    • 最后,利用公式YUV444 转 RGB
    • 完事大吉

    以下为YUV三张纹理图效果图:

    YUV三张纹理图

    YUV420转YUV444

    这里如何补全YUV420数据中UV部分的颜色数据?

    这里有一个讨巧的方式:
    在OpenGL ES生成纹理时,采用线性纹理采样方式。线性采样出U、V纹理中“?”部分的颜色值。这样就就可以拿到一一对应的YUV444数据。

    对应的Java代码如下:

        /**
         * 
         * @param w
         * @param h
         * @param date
         *            数据
         * @param textureY
         * @param textureU
         * @param textureV
         * @param isUpdate
         *            是否为更新
         */
        public static boolean bindYUV420pTexture(int frameWidth, int frameHeight,
                byte frameData[], int textureY, int textureU, int textureV,
                boolean isUpdate) {
    
            if (frameData == null || frameData.length == 0) {
                return false;
            }
            Log.d(TAG, "----bindYUV420pTexture-----");
    
            if (isUpdate == false) {
    
                /**
                 * 数据缓冲区
                 */
                // Y
                ByteBuffer buffer = LeBuffer.byteToBuffer(frameData);
                // GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY);
    
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
    
                /**
                 * target 指定目标纹理,这个值必须是GL_TEXTURE_2D; level
                 * 执行细节级别,0是最基本的图像级别,n表示第N级贴图细化级别; internalformat
                 * 指定纹理中的颜色组件,可选的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE,
                 * GL_LUMINANCE_ALPHA 等几种; width 指定纹理图像的宽度; height 指定纹理图像的高度; border
                 * 指定边框的宽度; format 像素数据的颜色格式,可选的值参考internalformat; type
                 * 指定像素数据的数据类型,可以使用的值有GL_UNSIGNED_BYTE
                 * ,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4
                 * ,GL_UNSIGNED_SHORT_5_5_5_1; pixels 指定内存中指向图像数据的指针;
                 * 
                 */
                GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                        frameWidth, frameHeight, 0, GLES20.GL_LUMINANCE,
                        GLES20.GL_UNSIGNED_BYTE, buffer);
    
                /**
                 * 
                 */
                // U
                buffer.clear();
                buffer = LeBuffer.byteToBuffer(frameData);
                buffer.position(frameWidth * frameHeight);
                //
                // GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU);
    
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
    
                GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                        frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE,
                        GLES20.GL_UNSIGNED_BYTE, buffer);
    
                /**
                 * 
                 */
                // V
                buffer.clear();
                buffer = LeBuffer.byteToBuffer(frameData);
                buffer.position(frameWidth * frameHeight * 5 / 4);
                //
                // GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV);
    
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                        GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
    
                GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                        frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE,
                        GLES20.GL_UNSIGNED_BYTE, buffer);
    
            } else {
                /**
                 * Y
                 */
                ByteBuffer buffer = LeBuffer.byteToBuffer(frameData);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY);
                GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, frameWidth,
                        frameHeight, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
                        buffer);
    
                /**
                 * U
                 */
                //
                buffer.clear();
                buffer = LeBuffer.byteToBuffer(frameData);
                buffer.position(frameWidth * frameHeight);
                //
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU);
                GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
                        frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE,
                        GLES20.GL_UNSIGNED_BYTE, buffer);
                /**
                 * V
                 */
                //
                buffer.clear();
                buffer = LeBuffer.byteToBuffer(frameData);
                buffer.position(frameWidth * frameHeight * 5 / 4);
                //
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV);
                GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
                        frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE,
                        GLES20.GL_UNSIGNED_BYTE, buffer);
            }
            return true;
        }
    

    代码说明:
    已上代码便是将传入的帧数据byte frameData[],转为三张纹理图的代码。
    代码的16行、5051行、7576行分别为从byte frameData[]中分别取出Y、U、V数据的代码。
    代码5659行、代码8184行分别为设置U、V纹理的采样方式为线性采样的代码。
    以上代码运行结束,内存中会生成三张纹理图像。
    将三张纹理图像传入“片元着色器”执行下一步骤。

    YUV444转RGB

    YUV一一对应的纹理有了,这里该介绍如何实现YUV444转RGB了:

    按照YUV转RGB的公式,将Y、U、V一一对应的取出,进行YUV转RGB操作,生成像素点。

    对应片元着色器 shader 代码实现:

    recision mediump float;
    // 片元着色器中 输入了Y U V三张纹理
    uniform sampler2D sTexture_y;
    uniform sampler2D sTexture_u;
    uniform sampler2D sTexture_v;
    
    varying vec2 vTextureCoord;
    
    //YUV 转 RGB的 shader 实现
    void getRgbByYuv(in float y, in float u, in float v, inout float r, inout float g, inout float b){  
        //
        y = 1.164*(y - 0.0625);
        u = u - 0.5;
        v = v - 0.5;
        //
        r = y + 1.596023559570*v;
        g = y - 0.3917694091796875*u - 0.8129730224609375*v;
        b = y + 2.017227172851563*u;
    }
    
    void main() {
        //
        float r,g,b;
        
        // 从YUV三张纹理中,采样出一一对应的YUV数据
        float y = texture2D(sTexture_y, vTextureCoord).r;
        float u = texture2D(sTexture_u, vTextureCoord).r;
        float v = texture2D(sTexture_v, vTextureCoord).r;
        // YUV 转 RGB
        getRgbByYuv(y, u, v, r, g, b);
        
        // 最终颜色赋值
        gl_FragColor = vec4(r,g,b, 1.0); 
    }
    

    五、完事大吉

    源码真的是懒得整理,所以,大家还是理解了实现原理,自己动手去敲吧,不要找我要代码了!!!
    源码真的是懒得整理,所以,大家还是理解了实现原理,自己动手去敲吧,不要找我要代码了!!!
    源码真的是懒得整理,所以,大家还是理解了实现原理,自己动手去敲吧,不要找我要代码了!!!

    ========== THE END ==========

    wx_gzh.jpg

    相关文章

      网友评论

          本文标题:[音视频]QQ视频通话、抖音的视频回显 是如何实现的

          本文链接:https://www.haomeiwen.com/subject/deqawctx.html