FFMpeg-3、基于QT实现音视频播放显示

1、音视频播放的基础知识

内容来自雷神博客
1、在Windows平台下的视频播放技术主要有以下三种:GDI,Direct3D和OpenGL;音频播放技术主要是DirectSound。
SDL本身并不具有播放显示的功能,它只是封装了底层播放显示的代码
记录三种视频显示技术:GDI,Direct3D,OpenGL。其中Direct3D包含简单和复杂的两种显示方式:使用Surface和使用Texture;OpenGL也包含简单和复杂的两种显示方式:直接画像素和使用Texture。
GDI 微软显示窗口提供的一套显示机制,他工作的主要流程就是构建BMP文件(原始的RGB数据,构建bmp文件头),调用接口将bmp文件绘制到屏幕上。注意都需要转换为RGB格式数据。
Direct3D微软开发的一套3D绘图API,Direct3D的抽象概念包括:Devices(设备),Swap Chains(交换链)和Resources(资源)。
Device(设备)用于渲染3D场景。例如单色设备就会渲染黑白图片,而彩色设备则会渲染彩色图片。
每一个Device至少要有一个Swap Chain(交换链)。一个Swap Chain由一个或多个Back Buffer Surfaces(后台缓冲表面)组成。渲染在Back Buffer中完成。
Device包含了一系列的Resources(资源),用于定义渲染时候的数据
Direct3D API定义了一组Vertices(顶点), Textures(纹理), Buffers(缓冲区)转换到屏幕上的流程。这样的流程称为Rendering Pipeline(渲染流水线),具体就是从应用程序里读取vertex顶点数据,对每个顶点属性进行着色,最后将着色器处理的数据输出给使用者。
2、视频显示的基础知识
三角形;在绘图中所有平面都可以用三角形绘制出来的,并且显卡的性能强弱都是比较单位时间内画三角形的个数的。
后台缓冲表面,前台表面,交换链,离屏表面。
后台缓冲表面和前台表面的概念总是同时出现的。简单解释一下它们的作用。当我们进行复杂的绘图操作时,画面可能有明显的闪烁。这是由于绘制的东西没有同时出现在屏幕上而导致的。“前台表面”+“后台缓冲表面”的技术可以解决这个问题,就是一个缓冲的作用。
交换链;其实就是一个后台缓冲表面,一个前台表面。所谓的“交换”,即是在需要呈现后台缓冲表面中的内容的时候,交换这两个表面的“地位”。即前台表面变成后台缓冲表面,后台缓冲表面变成前台表面。如此一来,后台缓冲表面的内容就呈现在屏幕上了。原先的前台表面,则扮演起了新的后台缓冲表面的角色,准备进行新的绘图操作。当下一次需要显示画面的时候,这两个表面再次交换,如此循环往复,永不停止。
离屏表面、离屏表面是永远看不到的表面(所谓“离屏”),它通常被用来存放位图,并对其中的数据做一些处理。通常的做法是把离屏表面上的位图复制到后台缓冲表面,后台缓冲表面再显示到前台表面。
使用Direct3D的Surface播放视频一般情况下需要如下步骤:

  1. 创建一个窗口(不属于D3D的API)
  2. 初始化
    1. 创建一个Device
    2. 基于Device创建一个Surface(离屏表面)
  3. 循环显示画面
    1. 清理
    2. 一帧视频数据拷贝至 Surface
    3. 开始一个Scene
    4. Surface数据拷贝至 后台缓冲表面
    5. 结束Scene
    6. 显示( 后台缓冲表面-> 前台表面)

3、OpenGL开放图形库,定义了一个跨编程语言、跨平台的应用程序接口(API)的规范,它用于生成二维、三维图像。
OpenGL渲染管线(OpenGL Pipeline)按照特定的顺序对图形信息进行处理,这些图形信息可以分为两个部分:顶点信息(坐标、法向量等)和像素信息(图像、纹理等)。图形信息最终被写入帧缓存中,存储在帧缓存中的数据(图像),可以被应用程序获得(用于保存结果,或作为应用程序的输入等。
关键就是顶点操作和纹理操作。
通过openGL的纹理显示图片
注意通过纹理显示则;输入的YUV420P像素数据通过Shader转换为YUV数据,传送给OpenGL播放。像素的转换YUV到RGB是通过显卡上的GPU完成的,则效率更高视频播放性能更好。
使用Shader通过OpenGL的纹理(Texture)播放视频一般情况下需要如下步骤:

  1. 初始化
  1. 初始化
  2. 创建窗口
  3. 设置绘图函数
  4. 设置定时器
  5. 初始化Shader
    初始化Shader的步骤比较多,主要可以分为3步:创建Shader,创建Program,初始化Texture。
    (1) 创建一个Shader对象;Shader有点类似于一个程序的编译器
    1)编写Vertex Shader和Fragment Shader源码。
    2)创建两个shader 实例 。
    3)给Shader实例指定源码。
    4)在线编译shaer源码。
    (2) 创建一个Program对象
    1)创建program。
    2)绑定shader到program。
    3)链接program。
    4)使用porgram。
    (3) 初始化Texture。可以分为以下步骤。
    1)定义定点数组
    2)设置顶点数组
    3)初始化纹理
  6. 进入消息循环
  1. 循环显示画面
  1. 设置纹理
  2. 绘制
  3. 显示

2、QT的音频录制 QAudioFormat

QAudioFormat记录音频参数的格式
setSampleRate 样本率
setSampleSize 设置的位数,S16,S8等,这里不区分layout,设置的是位数而不是字节数,则这样要根据不同的样本类型进行转换。
setChannelCount 设置通道数量
setCodec(“audio/pcm”)设置编码格式,暂时可能只支持pcm格式的,与声卡硬件有关的。
setByteOrder(QAudioFormat::LittleEndian)设置大小端模式,注意网络传输一般是采用大端的(低字节在高位),x86windows一般都是小端的(低位在低字节)。
setSampleType(QAudioFormat::UnSignedInt) 设置样本类型

QAudioOutput 打开播放音频设备
其构造函数创建是传入一个设置好的QAudioFormat对象的
QIODevice *start();创建成功后调用开始函数,会返回QIODevice ,这个是QT内部的一个类,做IO输入输出设备的类。
suspend() 挂起暂停
resume() 恢复播放
因为音频不像视频,如果音频丢帧了那么现象会有很严重的失真的,因此需要存在缓冲机制的。
bufferSize() 缓冲的大小
bytesFree() 得到缓冲内部还有多少内存
bufferSize() - bytesFree() 可以得到我们缓冲区里面还有多少内存没有释放的,在做音视频同步的时候需要考虑。
periodSize() 内部硬件读取多大才写入,

QIODevice 音频设备的抽象
qint64 write(const char *data, qint64 len) 写入,这个就是我们解码出来重采样后直接把数据通过write写入就可以播放了。返回的大小才是真正写入的大小,也可以重载一下保证把传入的len都写入进去,还可以采用判断bytesFree缓冲空间足够大的时候才把他写入进去,保证都能写入。

代码实战

//注意如果在QT CRTEAT下编写的话要在pro文件加入QT += multimediawidgets
#include <QCoreApplication>
#include <QAudioFormat>
#include <QAudioOutput>
#include <QThread>
int main(int argc, char *argv[])
{
    
    
    QCoreApplication a(argc, argv);
    QAudioFormat fmt;
    fmt.setSampleRate(44100);//样本率
    fmt.setSampleSize(16);  //样本大小 S16
    fmt.setChannelCount(2); //双通道
    fmt.setCodec("audio/pcm"); //设置pcm编码器
    fmt.setByteOrder(QAudioFormat::LittleEndian);//这是字节序
    fmt.setSampleType(QAudioFormat::UnSignedInt);//设置样本类型
    QAudioOutput *out = new QAudioOutput(fmt);//创建QAudioOutput
    QIODevice *io = out->start(); //开始播放

    int size = out->periodSize();//一个周期的大小,硬件设备一次读多大
    char *buf = new char[size];

    FILE *fp = fopen("out.pcm", "rb");
    while (!feof(fp))
    {
    
    
        //如果缓冲空间剩余内存不够size,硬件还没取走
        if (out->bytesFree() < size)
        {
    
    
            QThread::msleep(1);
            continue;
        }
        int len = fread(buf, 1, size, fp);
        if (len <= 0)break;
        io->write(buf, len);
    }
    fclose(fp);
    delete buf;
    buf = 0;

    return a.exec();
}

总结;QT音频播放基于三个类进行操作
先设置QAudioFormat音频格式,
再通过设置的音频格式创建QAudioOutput 音频输出设备启动设备和设置缓冲,
最后通过QAudioOutput 的启动得到具体声卡设备的抽象对象QIODevice ,通过向QIODevice 进行写入音频数据实现音频播放的。

3、QT视频绘制 QT openGL编程

雷神博客介绍OpenGL基于纹理显示的流程

因为视频显示存在像素格式的转换问题,要从解码出来的YUV格式转化为显示需要的RGB格式,视频的每一帧图像这么多像素点都需要转换,这是一个很大的开销,如果这一部分效率不高,那么对整体的视频播放性能都是十分有影响的,而QTopenGL也是效率很高的,直接操作显卡。

Shader有点类似于一个程序的编译器,
1)编写Vertex Shader和Fragment Shader源码。GLSL语言编写类似与C语言的一样的代码,它可以放在GPU里面被并行运行。
Vertex Shader(如果要存放到文件中就在.vsh)负责搞定像素位置,填写gl_Posizion;
Fragment Shader(Fragment Shader fsh)负责搞定像素外观,填写 gl_FragColor;

Program有点类似于一个程序的链接器。
program对象提供了把需要做的事连接在一起的机制,在一个program中,shader对象可以连接在一起。

扫描二维码关注公众号,回复: 13130614 查看本文章

QTopenGL显示的几个关键点
QOpenGLWidget (与界面如何交互)
Program GLSL 顶点与片元(如何与显卡交互的,GLSL 是跑在显卡上面的)
材质纹理Texture (如何写入ffmpeg数据的)

顶点和纹理材质
下面有相关介绍

为什么要采用QT的OpenGL三维的来绘制,直接使用QopenWidget、QWight也是可以的,但是如果使用后面的那么其实图像显示和界面的按钮是一套东西,当点击按钮刷新的时候就会存在闪屏的情况。因此采用三维的,QTOpenGL这种就是提供三维绘制,就可以自动在界面上叠加了。

使用的时候需要重载这些函数
void paintGL();//具体绘制在这里面实现
void initializeGL();//初始化
void resizeGL(int width,int height)//当窗口发生变化的时候调用,这个函数

还提供了很大的便利,如如果需要视频放缩的时候就可以直接调用OpenGL里面的函数,不然就需要在ffmpeg进行像素尺寸的转换效率就比较低了并且采用不同的算法效果可能也不一定好。而openGL中就是采用差值的算法来放缩,整体界面就减少失真的情况。

封装了操作OpenGL对象的函数类QOpenGLFunctions
QOpenGL相关的函数是单独有一个类来封装的,是QOpenGLFunctions
类,这里面就有很多与openGL相关的函数了,因此也可以进行QOpenGLFunctions继承来获取操作openGL的相关函数。

QGLShaderProgram编译运行shader和shader交互
shader 着色器
这个程序最终是在显卡上运行的,是通过QGLShaderProgram 来与显卡进行交互的。
QGLShaderProgram 主要封装的函数有
编译运行shader
addShaderFromSourceCode 把shader的源代码加载进来,有两部分代码,是顶点shader和片元shader(又称为像素shader)
bindAttributeLocation 设置传入的属性,只要是设置顶点坐标的属性和材质纹理Texture坐标的属性,后面可以传入顶点坐标或材质坐标然后对应坐标设置什么变量是通过他来设置的。
uniformLocation 获取shader变量 就是将显卡当中变量的一块地址取出来
总结;就是编译运行shader、传入shader程序,设置变量,获取变量

GLSL着色器语言 基于openGL设计的,给显卡用的语言
OpenGL Shader Language,简称GLSL。它是一种类似于C语言的专门为GPU设计的语言,它可以放在GPU里面被并行运行。

顶点着色器,就是针对每个顶点执行一次,用于确定顶点的位置,因为在三维空间中要将所有顶点的参数都获取。一般顶点着色器都是画三角形,如计算显卡的能力都是看他能画出多少个三角形,这里图片的显示我们采用画矩形,也就是两个三角形组合。因此我们这里顶点着色器的代码就是画两个三角形组成矩形。
在这里插入图片描述
代码填充就是

float *vertexData = new float[12]{
    
    
	-1.0f,-1.0f,0.0f,
    1.0f,-1.0f,0.0f,
     -1.0f, 1.0f,0.0f,
     1.0f,1.0f,0.0f
}
//也可以第三维的0这里不传入。再传入的时候告诉他填充的是二维的要他在内部填充

片元着色器,就是针对一个平面的,针对每个片元(可以理解为每个像素)执行一次,用于确定每个片元(像素)的颜色 。传入YUV的数据过来,至于至于转换可以到显卡自己那边转换。片元着色器他有一个内置变量gl_FragColor,就是在遍历像素点的时候将这个值进行改变,那么他的颜色一会跟着变化的。
流程就是通过顶点着色器获取顶点的位置,再通过片元着色器对这些顶点构成的片元像素进行填充颜色。
GLSL基本语法与C基本相同,这里只是做简单图像显示,真正的GLSL是用来做特效的,涉及算法的,
GLSL 完美支持向量和矩阵操作,因此在像素格式的转换方面十分适合选用,并且提供了大量的内置函数来提供丰富的扩展功能的,
他是通过限定符操作来管理输入输出类型的,例如传入顶点坐标,传出材质纹理等。

材质坐标信息
在这里插入图片描述
与顶点不同的是他是二维的并且坐标都是正数。
代码设置;

float *textureVertexData = new float[8]{
    
    
	0.0f, 1.0f,
	1.0f, 1.0f,
	0.0f, 0.0f,
	1.0f, 0.0f
}
//也可以第三维的0这里不传入。再传入的时候告诉他填充的是二维的要他在内部填充

三种GLSL变量类型
varying顶点与片元共享,就是有两部分代码顶点着色器代码计算出来的顶点坐标通过这个变量传给片元着色器那段代码,然后就可以获取到材质位置。可见代码理解。
atrribute 顶点使用 由bindAttributeLocation传入,就是我们创建的数组通过他传入过去进行计算
uniform 程序传入 uniformLocation获取地址,就是我们ffmpeg解码出来的YUV数据要传入进去变成uniform。
最后再通过glUniform1i(textureUniformY,0)传入与材质层进行绑定

//自动加双引号 定义常量串都可以使用
#define GET_STR(x) #x
//顶点着色器的shader代码
const char *vString = GET_STR(
	attribute vec4 vertexIn;
	attribute vec2 textureIn;
	varying vec2 textureOut;
	void main(void)
	{
    
    
		gl_Position = vertexIn;//传入的顶点坐标记录
		textureOut = textureIn;//传入的材质坐标保存到textureOut
	}
);

//自动加双引号 定义常量串都可以使用
#define GET_STR(x) #x
//片元着色器的shader代码
const char *tString = GET_STR(
	varying vec2 textureOut;//刚刚顶点着色器算出来的坐标
	uniform sampler2D tex_y;//uniform是外部传入的变量
	uniform sampler2D tex_u;
	uniform sampler2D tex_v;
	void main(void)
	{
    
    
	//420P是平面存储的,最后都是要转换为每个像素都是有yuv再方便转换
	//用的是灰度图的形式存储的
	//如安装那边硬解码出来的都是yuv420sp,uv是打包存在一起的,则不能使用这套shader代码了,要增加透明度存储方式。
		vec3 yuv;
		vec3 rgb;
		//根据那个坐标把材质坐标计算出来
		//传入材质,和坐标   返回材质当中的颜色rgb,但是用灰度图存储的,都是一样的。
		//三个材质就可以拼出一个yuv的数据
		yuv.x = texture2D(tex_y, textureOut).r;//获取材质当中的颜色 
		yuv.y = texture2D(tex_u, textureOut).r - 0.5;
		yuv.z = texture2D(tex_v, textureOut).r - 0.5;
		//转换公式
		rgb = mat3(1.0, 1.0, 1.0,
			0.0, -0.39465, 2.03211,
			1.13983, -0.58060, 0.0) * yuv;
		gl_FragColor = vec4(rgb, 1.0);//转换成显示的颜色,再设置一下
	}

);

创建材质
shader其实就是绘制某一个材质,那么就要看创建材质了。qt也有创建材质的函数,但是openGL这边要简单一点。

glGenTextures(1,t);创建几个材质,传入二维数组存放创建的材质地址

glBindTexture(GL_TEXTURE_2D *t)进行绑定 绑定为2d的图像

glTexPatameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR)设置属性  放大的属性  
glTexPatameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR)设置属性  缩小的属性
GL_TEXTURE_2D操作的是2D纹理
GL_TEXTURE_MAG_FILTER  放大过滤
GL_TEXTURE_MIN_FILTER  缩小过滤
GL_LINEAR用线性差值的方式进行缩放 使用距离当前渲染像素中心最近的四个纹素加权平均值,则最后的图像变化就比较荣和

写入和绘制材质

glActiveTexture(GL_TEXTURE0);激活材质 序号0
glBindTexture(GL_TEXTURE_2D,id_y);绑定之前创建的材质
在显存当中创建材质 
//将openGL里面的创建的材质设置传入到内存当中,就是显存到内存的操作
//GL_LUMINANCE,pixel_w,pixel_h 这个就表示已灰度图的形式存放的,传入宽高
glTexImage2D(GL_TEXTURE_2D,//创建为了
 0, //细节显示 0默认 是拉远拉近摄像机相关的显示
 GL_LUMINANCE,//gpu显卡内部的格式,就是创建的材质是存放到显卡当中,这是灰度图
 pixel_w,pixel_h,
 0,
 GL_LUMINANCE, 数据格式,内存到显存的格式,但是无法转换因此要一致
 GL_UNSIGNED_BYTE,//单个像素的存放格式
 plane[0])
 //因为创建纹理有很大的开销,因此提供了一个修改纹理的函数
 glTexSubImage2D()  修改纹理  存在偏移值的变量就是表示从当前的纹理当中只取一部分

glUniform1i(textureUniformY, 0);材质设完之后与shaeder相关联,通过uniform变量出入到材质0glDrawArrays(GL_TRINGLE_STRIP,0,4)//绘制矩形,四个顶点

显示播放整体流程分析
重载QOpenGLWidget的三个函数
//重载那三个函数
//刷新初始化
void paintGL();//具体绘制在这里面实现
//初始化gl
void initializeGL();//初始化
//窗口大小变化
void resizeGL(int width,int height);//当窗口发生变化的时候调用,这个函数

initializeGL 在OPenGL创建的时候就会调用
初始化OpenGL函数
	initializeOpenGLFunctions()
调用着色器program添加shader代码
QGLShaderProgram.addShaderFromSourceCode(QGLShader::Fragment,tString);
QGLShaderProgram.addShaderFromSourceCode(QGLShader::Vertex,vString);
	//QGLShader::Fragment  纹理材质类型   tString,vString常量串是Shader具体源代码
	//这个addShader还可以从文件获取 Fragment是fsh文件,Vertex是vsh文件
	
//给shader绑定属性 给顶点shader的attribute赋值,A_VER、T_VER这个只是标记作用
	program.bindAttributeLocation("vertexIn", A_VER);
	program.bindAttributeLocation("textureIn",T_VER);
	
//编译,绑定shader
	program.link();
	program.bind();
	
//定义顶点和材质纹理的坐标
	static const GLfloat ver[] = {
    
    }
	static const GLfloat tex[] = {
    
    }

//将顶点和材质纹理的坐标设置到shader当中去,A_VER、T_VER就是之前赋值属性的时候绑定的flag
	glVertexAttribPointer(A_VER,2,GL_FLOAT,0,0,ver);
	glEnableVertexAttribArray(A_VER);
	glVertexAttribPointer(T_VER,2, GL_FLOAT, 0,0,tex);
	glEnableVertexAttribArray(T_VER);

//从shader当中获取uniform sampler2D的位置,之后好传入
	unis[0] = program.uniformLocation("tex_y");
    unis[1] = program.uniformLocation("tex_u");
    unis[2] = program.uniformLocation("tex_v");
	
//创建材质,并绑定类型,属性及创建空间,对yuv都进行操作,注意uv的大小变化
	glGenTextures(3,texs);
	//Y
    glBindTexture(GL_TEXTURE_2D, texs[0]);//绑定2D
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
	glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width,height,0,GL_RED,GL_UNSIGNED_BYTE,0);//创建材质显卡的空间 ,与内存的空间是不一样的
	//width/2 height/2根据yuv420的特性来的 uv是y的四分之一
    //U
    glBindTexture(GL_TEXTURE_2D, texs[1]);//绑定2D
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width/2,height/2,0,GL_RED,GL_UNSIGNED_BYTE,0);//创建材质显卡的空间 ,与内存的空间是不一样的
    //V
    glBindTexture(GL_TEXTURE_2D, texs[2]);//绑定2D
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width/2,height/2,0,GL_RED,GL_UNSIGNED_BYTE,0);//创建材质显卡的空间 ,与内存的空间是不一样的

//分配材质的内存空间
    datas[0] = new unsigned char[width*height];
    datas[1] = new unsigned char[width*height/4];
    datas[2] = new unsigned char[width*height/4];
	
//读取文件
	fp = fopen("output240X128.yuv", "rb");
    if(!fp)
    {
    
    
        qDebug() << "fopen error";
    }

//启动定时器,绑定到OPenGL的update()函数调用绘画函数
    QTimer *ti = new QTimer(this);
    connect(ti, SIGNAL(timeout()), this, SLOT(update()));//定时器刷新到update()里面取
    ti->start(40);
	
paintGL  绘画时调用
//读取帧数据,存放到分配材质的内存空间
    fread(datas[0],1,width*height, fp);
    fread(datas[1], 1, width*height/4, fp);
    fread(datas[2], 1, width*height/4, fp);
	
//在显卡中创建材质   并绑定到了0层渲染材质
    glActiveTexture(GL_TEXTURE0);//激活第0层
    glBindTexture(GL_TEXTURE_2D, texs[0]);//把0层绑定Y材质
//修改材质(复制内存内存)
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, datas[0]);
//与shader uni变量关联起来
    glUniform1i(unis[0], 0);
	
	重复三次
	glActiveTexture(GL_TEXTURE0+1);//激活第1层
    glBindTexture(GL_TEXTURE_2D, texs[1]);//把1层绑定Y材质
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height/2, GL_RED, GL_UNSIGNED_BYTE, datas[1]);
    glUniform1i(unis[1], 1);
	
	glActiveTexture(GL_TEXTURE0+2);//激活第0层
    glBindTexture(GL_TEXTURE_2D, texs[2]);//把0层绑定Y材质
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height/2, GL_RED, GL_UNSIGNED_BYTE, datas[2]);
    glUniform1i(unis[2], 2);
	
最后绘画 //画三角形 从0开始 四个顶点,
	 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//画三角形 从0开始 四个顶点,

视频播放实践
拿到output240X128.yuv也可以使用命令制作ffmpeg -i 1.mp4 -t 10 -s 240x128 -pix_fmt yuv420p out240x128.yuv

注意几点
控件这里
在这里插入图片描述
qt工程文件.pro文件
需添加
QT += opengl
QT += openglextensions

代码

XVideoWidget.h文件

#ifndef XVIDEOWIDGET_H
#define XVIDEOWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QGLShaderProgram>
/*
 * //引入这个需要在pro文件添加模块
 * QT += opengl
 * QT += openglextensions
*/

class XVideoWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    
    
    //注意Q_OBJECT这个变量如果没有添加进来则槽函数无法调用
    Q_OBJECT

public:
    XVideoWidget(QWidget *parent);
    ~XVideoWidget();
    //从shader的yuv变量地址
    GLuint unis[3] = {
    
    0};
    //opengl的texture地址
    GLuint texs[3] = {
    
    0};

    //材质的内存空间
    unsigned char *datas[3] = {
    
    0};

    //宽高度
    int width = 240;
    int height = 128;
protected:
     //重载那三个函数
    //刷新初始化
    void paintGL();//具体绘制在这里面实现

    //初始化gl
    void initializeGL();//初始化

    //窗口大小变化
    void resizeGL(int width,int height);//当窗口发生变化的时候调用,这个函数


private:
    QGLShaderProgram program;

};

#endif // XVIDEOWIDGET_H

XVideoWidget.cpp文件

#include "XVideoWidget.h"
#include <QDebug>
#include <QTimer>

FILE *fp = NULL;

//定义一个宏表示数组
#define A_VER 3
#define T_VER 4
//自动加双引号 定义常量串都可以使用
#define GET_STR(x) #x
//顶点着色器的shader代码
const char *vString = GET_STR(
    attribute vec4 vertexIn;
    attribute vec2 textureIn;
    varying vec2 textureOut;//varying顶点和片元shader共享变量
    void main(void)
    {
    
    
        gl_Position = vertexIn;//传入的顶点坐标记录
        textureOut = textureIn;//传入的材质坐标保存到textureOut,从而传出去了
    }
);
//片元着色器的shader代码
const char *tString = GET_STR(
    varying vec2 textureOut;//刚刚顶点着色器算出来的坐标  共享的
    uniform sampler2D tex_y;//uniform是外部传入的变量
    uniform sampler2D tex_u;//sampler2D是一个2d图像
    uniform sampler2D tex_v;
    void main(void)
    {
    
    
    //420P是平面存储的,最后都是要转换为每个像素都是有yuv再方便转换
    //用的是灰度图的形式存储的
    //如安装那边硬解码出来的都是yuv420sp,uv是打包存在一起的,则不能使用这套shader代码了,要增加透明度存储方式。
        vec3 yuv;
        vec3 rgb;
        //根据那个坐标把材质坐标计算出来
        //传入材质,和坐标   返回材质当中的颜色rgb,但是用灰度图存储的,都是一样的。
        //三个材质就可以拼出一个yuv的数据
        yuv.x = texture2D(tex_y, textureOut).r;//获取材质当中的颜色
        yuv.y = texture2D(tex_u, textureOut).r - 0.5;//四舍五入
        yuv.z = texture2D(tex_v, textureOut).r - 0.5;
        //转换公式
        rgb = mat3(1.0, 1.0, 1.0,
            0.0, -0.39465, 2.03211,
            1.13983, -0.58060, 0.0) * yuv;
        gl_FragColor = vec4(rgb, 1.0);//转换成显示的颜色,再设置一下
    }

);
XVideoWidget::XVideoWidget(QWidget *parent)
    :QOpenGLWidget(parent)//初始化列表调用父类构造方法调用paint画出OpenGLWidget
{
    
    

    int a = 1;
}
XVideoWidget::~XVideoWidget()
{
    
    

}
//刷新初始化,每次移动都会调用一次,进行刷新
void XVideoWidget::paintGL()
{
    
    
    if(feof(fp))
    {
    
    
     fseek(fp, 0, SEEK_SET);
    }
    fread(datas[0],1,width*height, fp);
    fread(datas[1], 1, width*height/4, fp);
    fread(datas[2], 1, width*height/4, fp);

    //在显卡中创建材质   并绑定到了0层渲染材质
    glActiveTexture(GL_TEXTURE0);//激活第0层
    glBindTexture(GL_TEXTURE_2D, texs[0]);//把0层绑定Y材质
    //修改材质(复制内存内存)
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, datas[0]);
    //与shader uni变量关联起来
    glUniform1i(unis[0], 0);

    glActiveTexture(GL_TEXTURE0+1);//激活第1层
    glBindTexture(GL_TEXTURE_2D, texs[1]);//把1层绑定Y材质
    //修改材质(复制内存内存)
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height/2, GL_RED, GL_UNSIGNED_BYTE, datas[1]);
    //与shader uni变量关联起来
    glUniform1i(unis[1], 1);

    glActiveTexture(GL_TEXTURE0+2);//激活第0层
    glBindTexture(GL_TEXTURE_2D, texs[2]);//把0层绑定Y材质
    //修改材质(复制内存内存)
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height/2, GL_RED, GL_UNSIGNED_BYTE, datas[2]);
    //与shader uni变量关联起来
    glUniform1i(unis[2], 2);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//画三角形 从0开始 四个顶点,

    qDebug() << "paintGL";
}

//初始化gl
void XVideoWidget::initializeGL()
{
    
    
    qDebug() << "initializeGL";

    //初始化 opengl(QOpenGLFunctions继承过来的函数)
    initializeOpenGLFunctions();

    //program(opengl和qt都提供了)加载shader脚本文件 顶点shader 片元shader
    // qt 提供的 QGLShaderProgram
    qDebug() << "addShaderFromSourceCode :" <<program.addShaderFromSourceCode(QGLShader::Fragment,tString);//在源代码中添加好一些,从文件怕有泄露
    qDebug() << "addShaderFromSourceCode :" << program.addShaderFromSourceCode(QGLShader::Vertex,vString);

    //设置顶点坐标的变量
    program.bindAttributeLocation("vertexIn", A_VER);//这个vertexIn变量对应的本地位置 vertexIn是顶点shader定义的 3的位置

    //材质坐标
    program.bindAttributeLocation("textureIn",T_VER);//下标四,等下往4的位置存

    //编译shader
    qDebug() << "program.link():" << program.link();
    //绑定shader
    qDebug() << "program.link():" << program.bind();

    //传递顶点和材质坐标,
    //这个坐标是在这个函数中时候,之后draw的时候还要使用的,只传入二维
    static const GLfloat ver[] = {
    
    
        -1.0f,-1.0f,
        1.0f,-1.0f,
        -1.0f, 1.0f,
        1.0f,1.0f
    };
    static const GLfloat tex[] = {
    
    
        0.0f, 1.0f,
        1.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 0.0f
    };

    //将坐标写入GL当中
    glVertexAttribPointer(A_VER,//索引地址
                          2,//表示一点顶点几个元素,
                          GL_FLOAT,//存放的类型,浮点数
                          0,
                          0,
                          ver //顶点地址
                          );
    //顶点坐标生效
    glEnableVertexAttribArray(A_VER);

    //材质  就是shader准备数据
    glVertexAttribPointer(T_VER,2, GL_FLOAT, 0,0,tex);
    glEnableVertexAttribArray(T_VER);

    //从shader当中获取材质
    unis[0] = program.uniformLocation("tex_y");
    unis[1] = program.uniformLocation("tex_u");
    unis[2] = program.uniformLocation("tex_v");

    //创建材质
    glGenTextures(3,texs);//创建三个对象
    //分别对每个材质进行设置

    //Y
    glBindTexture(GL_TEXTURE_2D, texs[0]);//绑定2D
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width,height,0,GL_RED,GL_UNSIGNED_BYTE,0);//创建材质显卡的空间 ,与内存的空间是不一样的

    //width/2 height/2根据yuv420的特性来的 uv是y的四分之一
    //U
    glBindTexture(GL_TEXTURE_2D, texs[1]);//绑定2D
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width/2,height/2,0,GL_RED,GL_UNSIGNED_BYTE,0);//创建材质显卡的空间 ,与内存的空间是不一样的

    //V
    glBindTexture(GL_TEXTURE_2D, texs[2]);//绑定2D
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);//设置属性  放大 线性差值的属性
    glTexImage2D(GL_TEXTURE_2D,0,GL_RED,width/2,height/2,0,GL_RED,GL_UNSIGNED_BYTE,0);//创建材质显卡的空间 ,与内存的空间是不一样的

    //分配材质的内存空间
    datas[0] = new unsigned char[width*height];
    datas[1] = new unsigned char[width*height/4];
    datas[2] = new unsigned char[width*height/4];

    //读取材质并显示
    //再通过opengl接口,实现内存空间和显卡空间的转换
    fp = fopen("output240X128.yuv", "rb");
    if(!fp)
    {
    
    
        qDebug() << "fopen error";
    }

    //启动定时器
    QTimer *ti = new QTimer(this);
    connect(ti, SIGNAL(timeout()), this, SLOT(update()));//定时器刷新到update()里面取
    ti->start(40);
}

//窗口大小变化
void XVideoWidget::resizeGL(int width,int height)
{
    
    
    qDebug() << "resizeGL width = " << width << "  height = " << height;
}

源码:Git地址
内容参考来自夏曹俊老师的ffmpeg和QT开发

猜你喜欢

转载自blog.csdn.net/zw1996/article/details/114687054