1、前言
参考博客:雷霄骅:OpenGL播放YUV420P(通过Texture,使用Shader)
闲聊代码:使用OpenGL播放yuv420p数据
闲聊代码:ffmpeg解码+OpenGL播放视频,yuv420p格式
源码下载:雷霄骅:simplest media play,可直接运行
2.代码实现步骤
2.1、首先我得自己用雷神的源码实现OpenGL播放YUV420P功能
2.1.1、新建C++空项目:OpenGL-texture
将雷神的“simplest_video_play_opengl_texture”项目中的代码直接拷贝过来,包括《Shader.fsh》《Shader.vsh》文件、《test_yuv420p_320x180.yuv》格式视频。测试使用,发现
问题一:textFileRead("Shader.vsh");处,出现 “const char *“ 类型的实参与 “char *” 类型的形参不兼容
解决办法参考:
在VS2019中依次点击项目->属性->C/C+±>语言->符合模式,将原来的“是”改为“否”即可。
问题二:error X3000: unrecognized identifier 'attribute'
解决办法:
依次选中文件,右键-->属性-->常规-->项类型,将原来的“HLSL 编译器”改为“不参与生成”即可。
2.1.2、修改源码中视频格式路径
![](https://i-blog.csdnimg.cn/blog_migrate/b93a8a3934d46b3ca3423380a5c4c387.png)
![](https://i-blog.csdnimg.cn/blog_migrate/58f6c0d2e55da42e0e2949b3b595c810.png)
如此,一个glut自绘窗口便能正常运行播放了。
2.1.3、我的源码,只是丰富了备注
GitHub - van9420/OpenGL-texture: OpenGL播放YUV420P视频-参照雷神代码
2.2、应用到我的项目需求上:MFC--Picture控件中展示
2.2.1、新建MFC应用,基于对话框:MFC-OpenGL-texture
吸取前面出现的问题一、二的教训,这次我没将《Shader.fsh》《Shader.vsh》文件添加到项目中,只是复制到了文件夹内,问题一、二均没有出现。源于之前对这俩文件不熟悉,以为是需要在项目中添加这个格式的文件。
相比上面多了如下主要步骤:
1、设定像素格式,为OpenGL与HDC连接做准备
2、创建OpenGL渲染上下文,使得OpenGL可在上面绘制
3、使用定时器调用绘图函数绘制 或者 在多线程中循环绘制
2.2.2、主要代码
1、设定像素格式
/// <summary>
/// 设置像素格式,为OpenGL与HDC连接做准备
/// </summary>
/// <param name="hDC">设备上下文(dc)的句柄</param>
/// <returns></returns>
BOOL XXX::SetWindowPixelFormat(HDC hDC)
{
PIXELFORMATDESCRIPTOR pixelDesc; // 像素格式
pixelDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR);// 结构大小
pixelDesc.nVersion = 1; // 结构版本
pixelDesc.dwFlags = PFD_DRAW_TO_WINDOW | // (PFD_DRAW_TO_WINDOW) 绘制到窗口,
PFD_SUPPORT_OPENGL | // (PFD_SUPPORT_OPENGL) 绘制到窗口支持opengl
PFD_DOUBLEBUFFER | // (PFD_DOUBLEBUFFER) 告知OpenGL如何处理像素,采用双缓冲
PFD_TYPE_RGBA; // (PFD_TYPE_RGBA) 颜色模式,像素类型 RGBA
pixelDesc.iPixelType = PFD_TYPE_RGBA; // 颜色模式,像素类型 RGBA
pixelDesc.cColorBits = 32; // 颜色位数
pixelDesc.cRedBits = 0;
pixelDesc.cRedShift = 0;
pixelDesc.cGreenBits = 0;
pixelDesc.cGreenShift = 0;
pixelDesc.cBlueBits = 0;
pixelDesc.cBlueShift = 0;
pixelDesc.cAlphaBits = 0; // RGBA颜色缓存中Alpha的位数
pixelDesc.cAlphaShift = 0; // 现不支持置为0
pixelDesc.cAccumBits = 0; // 累计缓存的位数
pixelDesc.cAccumRedBits = 0;
pixelDesc.cAccumGreenBits = 0;
pixelDesc.cAccumBlueBits = 0;
pixelDesc.cAccumAlphaBits = 0;
pixelDesc.cDepthBits = 0; // 深度缓冲区位数
pixelDesc.cStencilBits = 1; // 模板缓冲位数
pixelDesc.cAuxBuffers = 0; // 辅助缓存为主
pixelDesc.iLayerType = PFD_MAIN_PLANE; // 层面类型:主层
pixelDesc.bReserved = 0;
pixelDesc.dwLayerMask = 0;
pixelDesc.dwVisibleMask = 0;
pixelDesc.dwDamageMask = 0;
int PixelFormat; // 选择匹配像素格式,并返回索引
PixelFormat = ChoosePixelFormat(hDC, &pixelDesc); // 匹配像素格式的索引
if (PixelFormat == 0) // Choose default
{
PixelFormat = 1;
if (DescribePixelFormat(hDC, PixelFormat,
sizeof(PIXELFORMATDESCRIPTOR), &pixelDesc) == 0)
{
return FALSE;
}
}
if (SetPixelFormat(hDC, PixelFormat, &pixelDesc) == FALSE)//设置像素格式
{
return FALSE;
}
return TRUE;
}
2、创建OpenGL渲染上下文
/// <summary>
/// 创建OpenGL渲染上下文,使得OpenGL可在上面绘制
/// </summary>
/// <param name="hDC"></param>
/// <returns></returns>
BOOL XXX::CreateViewGLContext(HDC hDC)
{
// 创建一个新的OpenGL渲染描述表
HGLRC hrenderRC = wglCreateContext(hDC);
if (hrenderRC == NULL)
return FALSE;
// 设定OpenGL当前线程的渲染环境
if (wglMakeCurrent(hDC, hrenderRC) == FALSE)
return FALSE;
return TRUE;
}
3、使用定时器调用绘图函数绘制 或者 在多线程中循环绘制
注意:如果 创建DC与RC关联的函数不在同一个线程,则不属于同一个线程,结果显示是黑色,因为根本没有关联。
2.2.3、我的源码
GitHub - van9420/MFC-OpenGL-texture: MFC+OpenGL播放YUV420P视频到指定Picture控件中
2.3、FFmpeg软解码
FFmpeg的基本配置这里暂且先不再重复
2.3.1、主要代码
void XXX::display()
{
//IP摄像机-视频流地址
char* url = "rtsp://192.168.1.88:554/11";
int ret;
/**************************获取码流参数信息****************************************/
AVFormatContext* fmt_ctx = NULL; //包含码流参数较多的结构体
AVDictionary* options = NULL; //健值对存储工具,类似于c++中的map
//参数设置解析
av_dict_set(&options, "buffer_size", "1044000", 0); //buffer_size:减少卡顿或者花屏现象,相当于增加或扩大了缓冲区,给予编码和发送足够的时间
av_dict_set(&options, "stimeout", "20000000", 0); //stimeout:设置超时断开,在进行连接时是阻塞状态,若没有设置超时断开则会一直去阻塞获取数据,单位是微秒。
av_dict_set(&options, "rtsp_transport", "tcp", 0); //rtsp_transport:修改优先连接发送方式,可以用udp、tcp、rtp
av_dict_set(&options, "tune", "zerolatency", 0); //zerolatency:转码延迟,以牺牲视频质量减少时延
/***
* 打开多媒体数据并且获取一些参数信息
* ps:函数调用成功之后处理过的AVFormatContext结构体。
* file:打开的视音频流的URL。
* fmt:强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat。
* dictionay:附加的一些选项,一般情况下可以设置为NULL。
**/
ret = avformat_open_input(&fmt_ctx, url, NULL, &options);
if (ret < 0)
{
fprintf(stderr, "Could not open input\n");
goto end;
}
/***
* 读取一部分视音频数据并且获得一些相关的信息(AVStream)
* ic:输入的AVFormatContext。
* options:额外的选项,目前没有深入研究过。
**/
ret = avformat_find_stream_info(fmt_ctx, NULL);
if (ret < 0)
{
fprintf(stderr, "Could not find stream information\n");
goto end;
}
/***
* 获取音视频对应的流索引(stream_index)
**/
ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (ret < 0)
{
fprintf(stderr, "Cannot find a video stream in the input file\n");
goto end;
}
int video_stream = ret; // 视频对应流索引
av_dump_format(fmt_ctx, 0, url, 0); // 打印关于输入或输出格式的详细信息,最后一个参数0:打印输入流
/*********************************获取解码器并解码***********************************/
AVCodec* pCodec = NULL; // 存储编解码器的结构体
AVCodecContext* pCodecCtx = NULL; // 解码器上下文
AVPacket packet; // 解码前的音频或者视频数据
AVFrame* frame = av_frame_alloc(); // 用来存储解码后的(或原始)音频或视频数据
// 必须由av_frame_alloc()分配内存,同时必须由av_frame_free()释放
avcodec_register_all(); // 注册解码器
if (pCodec == NULL)
pCodec = avcodec_find_decoder(fmt_ctx->streams[video_stream]->codecpar->codec_id);// 通过ID号查找解码器
pCodecCtx = avcodec_alloc_context3(pCodec); // 配置解码器,申请AVCodecContext空间。需要传递一个编码器,也可以不传,但不会包含编码器。
avcodec_parameters_to_context(pCodecCtx, fmt_ctx->streams[video_stream]->codecpar); // 该函数用于将流里面的参数,也就是AVStream里面的参数直接复制到AVCodecContext的上下文当中。
ret = avcodec_open2(pCodecCtx, pCodec, NULL); // 初始化一个视音频编解码器的AVCodecContext
if (ret < 0)
{
fprintf(stderr, "Cannot open decode\n");
goto end;
}
while (m_threadLoop)
{
uint8_t y[WIDTH * HEIGHT];
uint8_t u[WIDTH * HEIGHT / 4];
uint8_t v[WIDTH * HEIGHT / 4];
if ((ret = av_read_frame(fmt_ctx, &packet)) < 0) //读取码流中的音频若干帧或者视频一帧
break;
if (video_stream == packet.stream_index)
{
ret = avcodec_send_packet(pCodecCtx, &packet); //发送视频一帧到解码器中
if (ret < 0)
{
av_packet_unref(&packet); // 将缓存空间的引用计数-1,并将Packet中的其他字段设为初始值。如果引用计数为0,自动的释放缓存空间。
avcodec_flush_buffers(pCodecCtx); // 清空内部缓存的帧数据
continue;
}
while (ret >= 0)
{
ret = avcodec_receive_frame(pCodecCtx, frame); // 从解码器中获取解码的输出数据。ret=0:成功,返回一帧数据
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) // AVERROR(EAGAIN):当前输出无效,用户必须发送新的输入,AVERROR_EOF:解码器已经完全刷新,当前没有多余的帧可以输出
break;
else if (ret < 0) // 对应其他的解码错误
break;
/*****************使用OpenGL绘制yuvj420p******************/
//把解码后的AVFrame数据复制到为OpenGL准备的3个缓冲区里
int i;
for (i = 0; i < frame->height; i++)
memcpy(y + i * frame->width, frame->data[0] + i * frame->linesize[0], frame->width);
for (i = 0; i < frame->height / 2; i++)
memcpy(u + i * frame->width / 2, frame->data[1] + i * frame->linesize[1], frame->width / 2);
for (i = 0; i < frame->height / 2; i++)
memcpy(v + i * frame->width / 2, frame->data[2] + i * frame->linesize[2], frame->width / 2);
//----------------------OpenGL绘制主要代码-------------------------------
}
}
av_packet_unref(&packet);
}
av_packet_unref(&packet);
avcodec_flush_buffers(pCodecCtx);
avcodec_close(pCodecCtx); //close,如果为rk3399的硬件编解码,则需要等待MPP_Buff释放完成后再关闭?是否需要这样不知道
end:
av_frame_free(&frame);
avformat_close_input(&fmt_ctx);
avcodec_free_context(&pCodecCtx);
}