Android-音视频学习系列-(六)掌握视频基础知识并使用-OpenGL-ES-2-0-渲染-YUV-数据

复制代码

这也是 Bitmap 在内存中所占用的大小,所以每一张图像的裸数据都是很大的。对于图像的裸数据来说,直接来网络中进行传输也是不大可能的,所以就有了图像的压缩格式,比如我之前开源过一个基于 JPEG 压缩 :JPEG 是静态图像压缩标准,由 ISO 制定。 JPEG 图像压缩算法在提供良好的压缩性能的同时,具有较好的重建质量。这种算法被广泛应用于图像处理领域,当然它也是一种有损压缩。在很多网站如淘宝上使用的都是这种压缩之后的图像,但是,这种压缩不能直接应用于视频压缩,因为对于视频来讲,还有一个时域上的因素需要考虑,也就是说不仅仅要考虑帧内编码,还要考虑帧间编码。视频采用的是更加成熟的算法,关于视频压缩算法的相关内容我们会在后面进行介绍。

YUV 表示方式

对于视频帧的裸数据表示,其实更多的是 YUV 数据格式的表示, YUV 主要应用于优化彩色视频信号的传输,使其向后兼容老式黑白电视。在 RGB 视频信号传输相比,它最大的优点在于只需要占用极少的频宽(RGB 要求三个独立的视频信号同时传输)。其中 Y 表示明亮度,而 “U”,“V” 表示的则是色度值,它们的作用是描述影像的色彩及饱和度,用于指定像素的颜色。“亮度” 是透过 RGB 输入信号来建立的,方法时将 RGB 信号的特定部分叠加到一起。“色度” 则定义了颜色的两个方面 - 色调与饱和度,分别用 Cr 和 Cb 来表示。其中,Cr 反应了 RGB 输入信号红色部分与 RGB 信号亮度值之间的差异,而 Cb 反映的则是 RGB 输入信号蓝色部分与 RGB 信号亮度值之间的差异。

之所以采用 YUV 色彩空间,是因为它的亮度信号 Y 和色度信号 U、V 是分离的。如果只有 Y 信号分量而没有 U 、V 分量,那么这样表示的图像就是黑白灰图像。彩色电视采用 YUV 空间正是为了用亮度信号 Y 解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号,最常用的表示形式是 Y、U、V 都使用 8 字节来表示,所以取值范围是 0 ~ 255 。 在广播电视系统中不传输很低和很高的数值,实际上是为了防止信号变动造成过载, Y 的取值范围都是 16 ~ 235 ,UV 的取值范围都是 16 ~ 240。

YUV 最常用的采样格式是 4:2:0 , 4:2:0 并不意味着只有 Y 、Cb 而没有 Cr 分量。它指的是对每行扫描线来说,只有一种色度分量是以 2:1 的抽样率来存储的。相邻的扫描行存储着不同的色度分量,也就是说,如果某一行是 4:2:0,那么下一行就是 4:0:2,在下一行是 4:2:0,以此类推。对于每个色度分量来说,水平方向和竖直方向的抽象率都是 2:1,所以可以说色度的抽样率是 4:1。对非压缩的 8 bit 量化的视频来说,8*4 的一张图片需要占用 48 byte 内存。

相较于 RGB ,我们可以计算一帧为 1920 * 1080 的视频帧,用 YUV420P 的格式来表示,其数据量的大小如下:

(1920 * 1080 * 1 + 1920 * 1080 * 0.5 ) / 1024 /1024 ≈ 2.966MB
复制代码

如果 fps(1 s 的视频帧数量)是 25 ,按照 5 分钟的一个短视频来计算,那么这个短视频用 YUV420P 的数据格式来表示的话,其数据量的大小就是 :

2.966MB * 25fps * 5min * 60s / 1024 ≈ 21GB
复制代码

可以看到仅仅 5 分钟的视频数据量就能达到 21 G, 像抖音,快手这样短视频领域的代表这样的话还不卡死,那么如何对短视频进行存储以及流媒体播放呢?答案肯定是需要进行视频编码,后面会介绍视频编码的内容。

如果对 YUV 采样或者存储不明白的可以看这篇文章:音视频基础知识—像素格式YUV

YUV 和 RGB 的转化

前面已经讲过,凡是渲染到屏幕上的文字、图片、或者其它,都需要转为 RGB 的表示形式,那么 YUV 的表示形式和 RGB 的表示形式之间是如何进行转换的呢?可以参考该篇文章YUV <——> RGB 转换算法, 相互转换 C++ 代码可以参考地址

视频的编码方式

视频编码

还记得上一篇文章我们学习的音频编码方式吗?音频的编码主要是去除冗余信息,从而实现数据量的压缩。那么对于视频压缩,又该从哪几个方面来对数据进行压缩呢?其实与之前提到的音频编码类似,视频压缩也是通过去除冗余信息来进行压缩的。相较于音频数据,视频数据有极强的相关性,也就是说有大量的冗余信息,包括空间上的冗余信息和时间上的冗余信息,具体包括以下几个部分。

  • 运动补偿: 运动补偿是通过先前的局部图像来预测,,补偿当前的局部图像,它是减少帧序列冗余信息的有效方法。
  • 运动表示: 不同区域的图像需要使用不同的运动矢量来描述运动信息。
  • 运动估计: 运动估计是从视频序列中抽取运动信息的一整套技术。

使用帧内编码技术可以去除空间上的冗余信息。

大家还记得之前提到的图像编码 JPEG 吗?对于视频, ISO 同样也制定了标准: Motion JPEG 即 MPEG ,MPEG 算法是适用于动态视频的压缩算法,它除了对单幅图像进行编码外,还利用图像序列中的相关原则去除冗余,这样可以大大提高视频的压缩比,截至目前,MPEG 的版本一直在不断更新中,主要包括这样几个版本: Mpeg1(用于 VCD)、Mpeg2(用于 DVD)、Mpeg4 AVC(现在流媒体使用最多的就是它了)。

想比较 ISO 指定的 MPEG 的视频压缩标准,ITU-T 指定的 H.261、H.262、H.263、H.264 一系列视频编码标准是一套单独的体系。其中,H.264 集中了以往标准的所有优点,并吸取了以往标准的经验,采样的是简洁设计,这使得它比 Mpeg4 更容易推广。现在使用最多的就是 H.264 标准, H.264 创造了多参考帧、多块类型、整数变换、帧内预测等新的压缩技术,使用了更精准的分像素运动矢量(1/4、1/8) 和新一代的环路滤波器,这使得压缩性能得到大大提高,系统也变得更加完善。

编码概念

视频编码中,每帧都代表着一幅静止的图像。而在进行实际压缩时,会采取各种算法以减少数据的容量,其中 IPB 帧就是最常见的一种。

IPB 帧
  • I 帧: 表示关键帧,你可以理解为这一帧画面的完整保留,解码时只需要本帧数据就可以完成(包含完整画面)。
  • P 帧: 表示的是当前 P 帧与上一帧( I 帧或者 P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别生成最终画面。(也就是差别帧, P 帧没有完整画面数据,只有与前一帧的画面差别的数据。)
  • B 帧: 表示双向差别帧,也就是 B 帧记录的是当前帧与前后帧(前一个 I 帧或 P 帧和后面的 P 帧)的差别(具体比较复杂,有 4 种情况), 换言之,要解码 B 帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面数据与本帧数据的叠加取得最终的画面。B 帧压缩率高,但是解码时 CPU 会比较吃力。
IDR 帧与 I 帧的理解

在 H264 的概念中有一个帧称为 IDR 帧,那么 IDR 帧与 I 帧的区别是什么呢 ? 首先要看下 IDR 的英文全称 instantaneous decoding refresh picture , 因为 H264 采用了多帧预测,所以 I 帧之后的 P 帧有可能会参考 I 帧之前的帧,这就使得在随机访问的时候不能以找到 I 帧作为参考条件,因为即使找到 I 帧,I 帧之后的帧还是有可能解析不出来,而 IDR 帧就是一种特殊的 I 帧,即这一帧之后的所有参考帧只会参考到这个 IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到第一个 IDR 帧,就会立即清理参考帧缓冲区,并将 IDR 帧作为被参考的帧。

PTS 与 DTS

DTS 主要用视频的解码,全称为(Decoding Time Stamp), PTS 主要用于解码阶段进行视频的同步和输出, 全称为 (Presentation Time Stamp) 。在没有 B 帧的情况下, DTS 和 PTS 的输出顺序是一样的。因为 B 帧打乱了解码和显示的顺序,所以一旦存在 B 帧, PTS 与 DTS 势必就会不同。在大多数编解码标准(H.264 或者 HEVC) 中,编码顺序和输入顺序并不一致,于是才会需要 PTS 和 DTS 这两种不同的时间戳。

GOP 的概念

两个 I 帧之间形成的一组图片,就是 GOP (Group Of Picture) 的概念。通常在为编码器设置参数的时候,必须要设置 gop_size 的值,其代表的是两个 I 帧之间的帧数目。一个 GOP 中容量最大的帧就是 I 帧,所以相对来讲,gop_size 设置得越大,整个画面的质量就会越好,但是在解码端必须从接收到的第一个 I 帧开始才可以正确的解码出原始图像,否则会无法正确解码,在提高视频质量的技巧中,还有个技巧是多使用 B 帧,一般来说,I 的压缩率是 7 (与 JPG 差不多),P 是 20 ,B 可以达到 50 ,可见使用 B 帧能节省大量空间,节省出来的空间可以用来更多地保存 I 帧,这样就能在相同的码率下提供更好的画质,所以我们要根据不同的业务场景,适当地设置 gop_size 的大小,以得到更高质量的视频。

结合下图,希望可以帮组大家更好的理解 DTS 和 PTS 的概念。

视频渲染

OpenGL ES

实现效果

介绍

OpenGL (Open Graphics Lib) 定义了一个跨编程语言、跨平台编程的专业图形程序接口。可用于二维或三维图像的处理与渲染,它是一个功能强大、调用方便的底层图形库。对于嵌入式的设备,其提供了 OpenGL ES(OpenGL for Embedded System) 版本,该版本是针对手机、Pad 等嵌入式设备而设计的,是 OpenGL 的一个子集。到目前为止,OpenGL ES 已经经历过很多版本的迭代与更新,到目前为止运用最广泛的还是 OpenGL ES 2.0 版本。我们接下来所实现的 Demo 就是基于 OpenGL ES 2.0 接口进行编程并实现图像的渲染。

由于 OpenGL ES 是基于跨平台的设计,所以在每个平台上都要有它的具体实现,既要提供 OpenGL ES 的上下文环境以及窗口的管理。在 OpenGL 的设计中,OpenGL 是不负责管理窗口的。那么在 Android 平台上其实是使用 EGL 提供本地平台对 OpenGL ES 的实现。

使用

要在 Android 平台下使用 OpenGL ES , 第一种方式是直接使用 GLSurfaceView ,通过这种方式使用 OpenGL ES 比较简单,因为不需要开发者搭建 OpenGL ES 的上下文环境,以及创建 OpenGL ES 的显示设备。但是凡事都有两面,有好处也有坏处,使用 GLSurfaceView 不够灵活,很多真正的 OpenGL ES 的核心用法(比如共享上下文来达到多线程使用 EGL 的 API 来搭建的,并且是基于 C++ 的环境搭建的。因为如果仅仅在 Java 层编写 ,那么对于普通的应用也许可行,但是对于要进行解码或者使用第三方库的场景(比如人脸识别),则需要到 C++ 层来实施。处于效率和性能的考虑,这里的架构将直接使用 Native 层的 EGL 搭建一个 OpenGL ES 的开发环境。要想在 Native 层使用 EGL ,那么就必须在 CmakeLists.txt 中添加 EGL 库(可以参考如下提供的 CMakeLists 文件配置),并在使用该库的 C++ 文件中引入对应的头文件,需要引如的头文件地址如下:

//1. 在开发中如果要使用 EGL 需要在 CMakeLists.txt 中添加 EGL 库,并指定头文件

//使用 EGL 需要添加的头文件
#include <EGL/egl.h>
#include <EGL/eglext.h>

//2. 使用 OpenGL ES 2.0 也需要在 CMakeLists.txt 中添加 GLESv2 库,并指定头文件

//使用 OpenGL ES 2.0 需要添加的头文件
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

CMakeLists 文件配置:

cmake_minimum_required(VERSION 3.4.1)

#音频渲染
set(OpenSL ${CMAKE_SOURCE_DIR}/opensl)
#视频渲染
set(OpenGL ${CMAKE_SOURCE_DIR}/gles)

#批量添加自己编写的 cpp 文件,不要把 .h 加入进来了
file(GLOB ALL_CPP ${OpenSL}/
.cpp ${OpenGL}/*.cpp)

#添加自己编写 cpp 源文件生成动态库
add_library(audiovideo SHARED ${ALL_CPP})

#找系统中 NDK log库
find_library(log_lib
log)

#最后才开始链接库
target_link_libraries(
#最后生成的 so 库名称
audiovideo
#音频渲染
OpenSLES

OpenGL 与 NativeWindow 连接本地窗口的中间者

EGL
#视频渲染
GLESv2
#添加本地库
android

${log_lib}
)

至此,对于 OpenGL 的开发需要用到的头文件以及库文件就引入完毕了,下面再来看看如何使用 EGL 搭建出 OpenGL 的上下文环境以及渲染视频数据。

    1. 使用 EGL 首先必须创建,建立本地窗口系统和 OpenGL ES 的连接

//1.获取原始窗口
nativeWindow = ANativeWindow_fromSurface(env, surface);
//获取Display
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
LOGD(“egl display failed”);
showMessage(env, “egl display failed”, false);
return;
}

    1. 初始化 EGL

//初始化egl,后两个参数为主次版本号
if (EGL_TRUE != eglInitialize(display, 0, 0)) {
LOGD(“eglInitialize failed”);
showMessage(env, “eglInitialize failed”, false);
return;
}

    1. 确定可用的渲染表面( Surface )的配置。

//surface 配置,可以理解为窗口
EGLConfig eglConfig;
EGLint configNum;
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};

if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {
LOGD(“eglChooseConfig failed”);
showMessage(env, “eglChooseConfig failed”, false);
return;
}

    1. 创建渲染表面 surface(4/5步骤可互换)

//创建surface(egl和NativeWindow进行关联。最后一个参数为属性信息,0表示默认版本)
winSurface = eglCreateWindowSurface(display, eglConfig, nativeWindow, 0);
if (winSurface == EGL_NO_SURFACE) {
LOGD(“eglCreateWindowSurface failed”);
showMessage(env, “eglCreateWindowSurface failed”, false);
return;
}

    1. 创建渲染上下文 Context

//4 创建关联上下文
const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
//EGL_NO_CONTEXT表示不需要多个设备共享上下文
context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);
if (context == EGL_NO_CONTEXT) {
LOGD(“eglCreateContext failed”);
showMessage(env, “eglCreateContext failed”, false);
return;
}

    1. 指定某个 EGLContext 为当前上下文, 关联起来

//将egl和opengl关联
//两个surface一个读一个写。第二个一般用来离线渲染
if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
LOGD(“eglMakeCurrent failed”);
showMessage(env, “eglMakeCurrent failed”, false);
return;
}

    1. 使用 OpenGL 相关的 API 进行绘制操作

GLint vsh = initShader(vertexShader, GL_VERTEX_SHADER);
GLint fsh = initShader(fragYUV420P, GL_FRAGMENT_SHADER);

//创建渲染程序
GLint program = glCreateProgram();
if (program == 0) {
LOGD(“glCreateProgram failed”);
showMessage(env, “glCreateProgram failed”, false);
return;
}

//向渲染程序中加入着色器
glAttachShader(program, vsh);
glAttachShader(program, fsh);

//链接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == 0) {
LOGD(“glLinkProgram failed”);
showMessage(env, “glLinkProgram failed”, false);
return;
}
LOGD(“glLinkProgram success”);
//激活渲染程序
glUseProgram(program);

//加入三维顶点数据
static float ver[] = {
1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f
};

GLuint apos = static_cast(glGetAttribLocation(program, “aPosition”));
glEnableVertexAttribArray(apos);
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);

//加入纹理坐标数据
static float fragment[] = {
1.0f, 0.0f,
0.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f
};
GLuint aTex = static_cast(glGetAttribLocation(program, “aTextCoord”));
glEnableVertexAttribArray(aTex);
glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);

//纹理初始化
//设置纹理层对应的对应采样器?

/**

  • //获取一致变量的存储位置
    GLint textureUniformY = glGetUniformLocation(program, “SamplerY”);
    GLint textureUniformU = glGetUniformLocation(program, “SamplerU”);
    GLint textureUniformV = glGetUniformLocation(program, “SamplerV”);
    //对几个纹理采样器变量进行设置
    glUniform1i(textureUniformY, 0);
    glUniform1i(textureUniformU, 1);
    glUniform1i(textureUniformV, 2);
    */
    //对sampler变量,使用函数glUniform1i和glUniform1iv进行设置
    glUniform1i(glGetUniformLocation(program, “yTexture”), 0);
    glUniform1i(glGetUniformLocation(program, “uTexture”), 1);
    glUniform1i(glGetUniformLocation(program, “vTexture”), 2);
    //纹理ID
    GLuint texts[3] = {0};
    //创建若干个纹理对象,并且得到纹理ID
    glGenTextures(3, texts);

//绑定纹理。后面的的设置和加载全部作用于当前绑定的纹理对象
//GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是纹理单元,GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP为纹理目标
//通过 glBindTexture 函数将纹理目标和纹理绑定后,对纹理目标所进行的操作都反映到对纹理上
glBindTexture(GL_TEXTURE_2D, texts[0]);

要如何成为Android架构师?

搭建自己的知识框架,全面提升自己的技术体系,并且往底层源码方向深入钻研。
大多数技术人喜欢用思维脑图来构建自己的知识体系,一目了然。这里给大家分享一份大厂主流的Android架构师技术体系,可以用来搭建自己的知识框架,或者查漏补缺;

对应这份技术大纲,我也整理了一套Android高级架构师完整系列的视频教程,主要针对3-5年Android开发经验以上,需要往高级架构师层次学习提升的同学,希望能帮你突破瓶颈,跳槽进大厂;

最后我必须强调几点:

1.搭建知识框架可不是说你整理好要学习的知识顺序,然后看一遍理解了能复制粘贴就够了,大多都是需要你自己读懂源码和原理,能自己手写出来的。
2.学习的时候你一定要多看多练几遍,把知识才吃透,还要记笔记,这些很重要! 最后你达到什么水平取决你消化了多少知识
3.最终你的知识框架应该是一个完善的,兼顾广度和深度的技术体系。然后经过多次项目实战积累经验,你才能达到高级架构师的层次。

你只需要按照在这个大的框架去填充自己,年薪40W一定不是终点,技术无止境

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

3.最终你的知识框架应该是一个完善的,兼顾广度和深度的技术体系。然后经过多次项目实战积累经验,你才能达到高级架构师的层次。

你只需要按照在这个大的框架去填充自己,年薪40W一定不是终点,技术无止境

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值