/ 今日科技快讯 /
https://blog.csdn.net/sinat_23092639
yuv的概念
基于ndk进行C++程序的基本编写
OpenGL纹理的绘制
4:2:2
4:2:0
UYVY
YUV420P
YUV420SP
yuv转RGB
关于OpenGL的知识,可能写20篇博文也介绍不完,这里只介绍和当前播放yuv相关的,不会很详细,详细教程可以看这个网站:
https://www.zhihu.com/question/19913939
OpenGL坐标系
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib
GLESv2
EGL
android
# Links the target library to the log library
# included in the NDK.
${log-lib} )
public class YuvPlayer extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {
//这里将yuv视频文件放在sdcard目录中
private final static String PATH = "/sdcard/sintel_640_360.yuv";
public YuvPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
setRenderer(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
new Thread(this).start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
}
@Override
public void run() {
loadYuv(PATH,getHolder().getSurface());
}
//定义一个native方法加载yuv视频文件
public native void loadYuv(String url, Object surface);
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
}
Java_com_example_yuvopengldemo_YuvPlayer_loadYuv(JNIEnv *env, jobject thiz, jstring jUrl,
jobject surface) {
const char *url = env->GetStringUTFChars(jUrl, 0);
//打开yuv视频文件
FILE *fp = fopen(url, "rb");
if (!fp) {
//打Log方法
LOGD("oepn file %s fail", url);
return;
}
LOGD("open ulr is %s", url);
//1.获取原始窗口
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);
//获取OpenGl ES的渲染目标。Display(EGLDisplay) 是对实际显示设备的抽象。
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
LOGD("egl display failed");
return;
}
//2.初始化egl与 EGLDisplay 之间的连接,后两个参数为主次版本号
if (EGL_TRUE != eglInitialize(display, 0, 0)) {
LOGD("eglInitialize failed");
return;
}
//创建渲染用的surface
//2.1 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");
return;
}
//2.2创建surface(将egl和NativeWindow进行关联,即将EGl和设备屏幕连接起来。最后一个参数为属性信息,0表示默认版本)。Surface(EGLSurface)是对用来存储图像的内存区FrameBuffer 的抽象。这就是我们要渲染的Surface
EGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0);
if (winSurface == EGL_NO_SURFACE) {
LOGD("eglCreateWindowSurface failed");
return;
}
//3 创建关联上下文
const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
//创建egl关联OpenGl的上下文环境 EGLContext 实例。EGL_NO_CONTEXT表示不需要多个设备共享上下文。Context (EGLContext) 存储 OpenGL ES绘图的一些状态信息。上面的代码只是egl和设备窗口的关联,这里是和OpenGl的关联
EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);
if (context == EGL_NO_CONTEXT) {
LOGD("eglCreateContext failed");
return;
}
//将EGLContext和opengl真正关联起来。绑定该线程的显示设备及上下文
//两个surface一个读一个写。
if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
LOGD("eglMakeCurrent failed");
return;
}
attritude:一般用于各个顶点各不相同的量。如顶点位置、纹理坐标、法向量、颜色等等。
uniform:一般用于对于物体中所有顶点或者所有的片段都相同的量。比如光源位置、统一变换矩阵、颜色等。
varying:表示易变量,一般用于顶点着色器传递到片段着色器的量。
vec2:包含了2个浮点数的向量
vec3:包含了3个浮点数的向量
vec4:包含了4个浮点数的向量
sampler1D:1D纹理着色器
sampler2D:2D纹理着色器
sampler3D:3D纹理着色器
//顶点着色器,每个顶点执行一次,可以并行执行
#define GET_STR(x) #x
static const char *vertexShader = GET_STR(
attribute vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
attribute vec2 aTextCoord;//输入的纹理坐标,会在程序指定将数据输入到该字段
varying vec2 vTextCoord;//输出的纹理坐标,输入到片段着色器
void main() {
//这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来。也可以在顶点坐标中就上下翻转)
vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);
//直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
gl_Position = aPosition;
}
);
//图元被光栅化为多少片段,就被调用多少次
static const char *fragYUV420P = GET_STR(
precision mediump float;
//接收从顶点着色器、光栅化处理传来的纹理坐标数据
varying vec2 vTextCoord;
//输入的yuv三个纹理
uniform sampler2D yTexture;//y分量纹理
uniform sampler2D uTexture;//u分量纹理
uniform sampler2D vTexture;//v分量纹理
void main() {
//存放采样之后的yuv数据
vec3 yuv;
//存放yuv数据转化后的rgb数据
vec3 rgb;
//对yuv各个分量对应vTextCoord的像素进行采样。这里texture2D得到的结果是一个vec4变量,它的r、g、b、a的值都为采样到的那个分量的值
//将采样到的y、u、v分量的数据分别保存在vec3 yuv的r、g、b(或者x、y、z)分量
yuv.r = texture2D(yTexture, vTextCoord).g;
yuv.g = texture2D(uTexture, vTextCoord).g - 0.5;
yuv.b = texture2D(vTexture, vTextCoord).g - 0.5;
//这里必须把yuv转化为RGB
rgb = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.5806, 0.0
) * yuv;
//gl_FragColor是OpenGL内置的,将rgb数据赋值给gl_FragColor,传到渲染管线的下一阶段 ,gl_FragColor 表示正在呈现的像素的 R、G、B、A 值。
gl_FragColor = vec4(rgb, 1.0);
}
);
GLint vsh = initShader(vertexShader, GL_VERTEX_SHADER);
GLint fsh = initShader(fragYUV420P, GL_FRAGMENT_SHADER);
//创建渲染程序
GLint program = glCreateProgram();
if (program == 0) {
LOGD("glCreateProgram failed");
return;
}
//向渲染程序中加入着色器
glAttachShader(program, vsh);
glAttachShader(program, fsh);
//链接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == 0) {
LOGD("glLinkProgram failed");
return;
}
LOGD("glLinkProgram success");
//激活渲染程序
glUseProgram(program);
GLint initShader(const char *source, GLint type) {
//创建shader
GLint sh = glCreateShader(type);
if (sh == 0) {
LOGD("glCreateShader %d failed", type);
return 0;
}
//加载shader
glShaderSource(sh,
1,//shader数量
&source,
0);//代码长度,传0则读到字符串结尾
//编译shader
glCompileShader(sh);
GLint status;
glGetShaderiv(sh, GL_COMPILE_STATUS, &status);
if (status == 0) {
LOGD("glCompileShader %d failed", type);
LOGD("source %s", source);
return 0;
}
LOGD("glCompileShader %d success", type);
return sh;
}
//加入三维顶点数据。这里就是整个屏幕的矩形。
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
};
//获取顶点着色器的aPosition属性引用
GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition"));
glEnableVertexAttribArray(apos);
//将顶点坐标传入顶点着色器的aPosition属性
//各个参数意义:apos:顶点着色器中aPosition变量的引用。3表示数组中三个数字表示一个顶点。GL_FLOAT表示数据类型是浮点数。
//GL_FALSE表示不进行归一化。0表示stride(跨距),在数组表示多种属性的时候使用到,这里因为这有一个属性,设置为0即可。ver表示所传入的顶点数组地址
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
};
将纹理坐标数组传入顶点着色器的aTextCoord属性
GLuint aTex = static_cast<GLuint>(glGetAttribLocation(program, "aTextCoord"));
glEnableVertexAttribArray(aTex);
//各个参数意义:aTex :顶点着色器中aTextCoord变量的引用。2表示数组中三个数字表示一个顶点。GL_FLOAT表示数据类型是浮点数。
//GL_FALSE表示不进行归一化。表示stride(跨距),在数组表示多种属性的时候使用到,这里因为这有一个属性,设置为0即可。fragment表示所传入的顶点数组地址
glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);
//指定纹理变量在哪一层纹理单元渲染
glUniform1i(glGetUniformLocation(program, "yTexture"), GL_TEXTURE0);
glUniform1i(glGetUniformLocation(program, "uTexture"), GL_TEXTURE1);
glUniform1i(glGetUniformLocation(program, "vTexture"), GL_TEXTURE2);
//纹理ID
GLuint texts[3] = {0};
//创建3个纹理对象,并且得到各自的纹理ID。之后对纹理的操作就可以通过该纹理ID进行。
glGenTextures(3, texts);
//yuv视频宽高
int width = 640;
int height = 360;
//通过 glBindTexture 函数将纹理目标和以texts[0]为ID的纹理对象绑定后,对纹理目标所进行的操作都反映到该纹理对象上
glBindTexture(GL_TEXTURE_2D, texts[0]);
//缩小的过滤器(关于过滤详细可见 [纹理](https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/))
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//放大的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//设置纹理的格式和大小
// 当前绑定的纹理对象就会被渲染上纹理
glTexImage2D(GL_TEXTURE_2D,
0,//指定要Mipmap的等级
GL_LUMINANCE,//gpu内部格式,告诉OpenGL内部用什么格式存储和使用这个纹理数据。 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思,因这里只取yuv其中一个分量)
width,//加载的纹理宽度。最好为2的次幂
height,//加载的纹理高度。最好为2的次幂
0,//纹理边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//一个像素点存储的数据类型
NULL //纹理的数据(先不传,等后面每一帧刷新的时候传)
);
//设置纹理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0,//细节基本 默认0
GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
width / 2,
height / 2,//v数据数量为屏幕的4分之1
0,//边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
unsigned char *buf[3] = {0};
buf[0] = new unsigned char[width * height];//y
buf[1] = new unsigned char[width * height / 4];//u
buf[2] = new unsigned char[width * height / 4];//v
//循环读出每一帧
for (int i = 0; i < 10000; ++i) {
//读一帧yuv420p数据
if (feof(fp) == 0) {
//读取y数据
fread(buf[0], 1, width * height, fp);
//读取u数据
fread(buf[1], 1, width * height / 4, fp);
//读取v数据
fread(buf[2], 1, width * height / 4, fp);
}
//激活第一层纹理,绑定到创建的纹理
glActiveTexture(GL_TEXTURE0);
//绑定y对应的纹理
glBindTexture(GL_TEXTURE_2D, texts[0]);
//替换纹理,比重新使用glTexImage2D性能高多
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,//相对原来的纹理的offset
width, height,//加载的纹理宽度、高度。最好为2的次幂
GL_LUMINANCE, GL_UNSIGNED_BYTE,
buf[0]);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
接触音视频开发领域时间不长,如有错误疏漏,请各位指正~项目地址:
https://github.com/yishuinanfeng/YuvVideoPlayerDemo