基于FFmpeg的视频播放器开发系列教程(二)

        本节课程的目的:读帧解码显示视频

        开始进入ffmepg的开发之旅。音视频的细节知识不统一讲解,我在教程中逐点渗透,容我以雷神的话开篇。

       视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。

                                                                                                                     ----雷霄骅

       对于ffmpeg的架构介绍,请参考24岁“封神”雷霄骅的博客,他已离开江湖,但江湖仍有他的传说。

       FFmpeg源代码结构图 - 编码:https://blog.csdn.net/leixiaohua1020/article/details/44226355

       FFmpeg源代码结构图 - 解码:https://blog.csdn.net/leixiaohua1020/article/details/44220151

 

一.ffmpeg开发入门

      下面是一个打开视频的小例子。

     先用Win32控制台程序来讲解ffmpeg的简单开发,建立Win32的控制台项目在项目属性中加入ffmpeg的库文件。没有ffmpeg3.2.4库文件的同学,请点击下载。

      代码如下:

// FFmpeg_打开视频文件.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>

extern "C"
{
	#include <libavformat/avformat.h>
}

#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")

using namespace std;

int main()
{
	av_register_all();  //ffmpeg程序的第一句,注册库

	AVFormatContext *afc = NULL;

	//打开视频文件
	int nRet = avformat_open_input(&afc, "天下有情人.mp4", 0, 0);
	if (nRet < 0)
	{
		cout << "找不到视频文件" << endl;
	}
	else
	{
		cout << "视频打开成功" << endl;
	}
	int durTime = afc->duration / AV_TIME_BASE;  //视频时间 4分20秒
	unsigned int numberOfStream = afc->nb_streams;  //包含流的个数2:一个视频流一个音频流

	for (int i = 0; i < afc->nb_streams; i++)
	{
		AVCodecContext *acc = afc->streams[i]->codec;
		if (acc->codec_type == AVMEDIA_TYPE_VIDEO)  //如果是视频类型
		{
			AVCodec *codec = avcodec_find_decoder(acc->codec_id);
			if (!codec)
			{
				cout << "没有该类型解码器" << endl;
			}

			int ret = avcodec_open2(acc, codec, NULL);
			if (ret != 0)
			{
				char buf[1024] = { 0 };
				av_strerror(ret, buf, sizeof(buf));
			}

			cout << "解码器打开成功" << endl;
		}
	}

	if (afc)
	{
		avformat_close_input(&afc);  //关闭视频流
	}

	system("pause");
    return 0;
}
   

    可能会出现以下编译错误:

    errorC4996: 'AVStream::codec': 被声明为已否决

    解决方法如下

    

      由于ffmpeg的源码是C语言写的,在调用它的头文件时,需要用extern"C", 例外导入的lib可以直接放到属性列表,也可以写到代码里。在写ffmpeg程序时, 第一句是av_register_all()用来注册ffmpeg库。

       我们是做播放器,需要打开视频文件,avformat_open_input()是打开一个输入流并且读它的头部信息,但编解码器不会被打开,如果打开成功,会返回一个AVFormatContext的实例.该实例包含了很多的视频信息,例如一个视频文件,会有视频流,音频流,字幕流,视频的时间,解码器类型等等信息。视频打开后,需要进行解码,而解码需要解码器,先找解码器avcodec_find_decoder 找到解码器后再打开解码器,然后进行解码,视频像素转换解析,音频解析,再用线程同步技术实现音视频同步,将视频内容显示在屏幕上。

      做视频开发,对于资源的利用要格外重视,打开的资源用完后要及时释放,避免造成过大的内存开销,造成程序的崩溃。

二.  视频播放器FFVideoPlayer的开发

      创立Qt GUI项目,工程名称:FFVideoPlayer. 目前的界面如下图,后续根据需求会逐渐优化更新。

      

      中间黑色部分是QOpenGLWidget控件,用来显示视频。

  编写各功能模块的代码。

(1)【打开视频】:选择视频文件,打开并显示在OpenGLWidget控件上。实现【打开视频】的槽函数,代码如下:

void FFVideoPlyer::slotOpenFile()
{
	QString fname = QFileDialog::getOpenFileName(this, QString::fromLocal8Bit("打开视频文件"));
	if (fname.isEmpty())
	{
		return;
	}

	ui.lineEdit_VideoName->setText(fname);

	MyFFmpeg::GetObj()->OpenVideo(fname.toLocal8Bit());

	MyFFmpeg::GetObj()->m_isPlay = true;
	ui.btn_Play->setText(QString::fromLocal8Bit("暂停"));
}

       对于的视频的打开,读帧,解码,像素转换,音频解码等等,这些方法,我封装程类MyFFmpeg. 在项目中添加C++类,类名MyFFmpeg即可。同时为了保证对象的维一性,我们使用单例模式来实现。

       本教程的开发流程如下:

       

       打开视频文件,查找解码器,打开解码器的代码如下。为了循序渐进,先实现视频读帧解码,下篇博客进行音频解码。

void MyFFmpeg::OpenVideo(const char *path)
{
	mtx.lock();
	int nRet = avformat_open_input(&m_afc, path, 0, 0);
	
	for (int i = 0; i < m_afc->nb_streams; i++)  //nb_streams打开的视频文件中流的数量,一般nb_streams = 2,音频流和视频流
	{
		AVCodecContext *acc = m_afc->streams[i]->codec; //分别获取音频流和视频流的解码器

		if (acc->codec_type == AVMEDIA_TYPE_VIDEO)   //如果是视频
		{
			m_videoStream = i;
			AVCodec *codec = avcodec_find_decoder(acc->codec_id);   // 查找解码器

			//"没有该类型的解码器"
			if (!codec)
			{
				mtx.unlock();
				return;
			}

			int err = avcodec_open2(acc, codec, NULL); //打开解码器

			if (err != 0)
			{
				//解码器打开失败
			}
		}
	}
	mtx.unlock();
}
 

(2)读帧解码    

       视频打开后,需要进行读帧,解码,显示,此过程比较耗时,如果放到主线程中,一旦主线程阻塞,就会容易“界面卡死”,所以放到子线线程来实现。添加Qt线程类PlayThread, 继承于QThread,重写线程的run函数。

代码如下:

void PlayThread::run()
{
	//在子线程里做什么,当然是读视频帧,解码视频了
	//何时读,何时解码呢,在视频打开之后读帧解码, 读帧解码线程要一直运行
	//视频没打开之前线程要阻塞, run,while(1)这是基本套路
	while (1)
	{
		if (!(MyFFmpeg::GetObj()->m_isPlay))
		{
			msleep(5); //调试方便,5微秒后窗口又关闭了,线程继续阻塞,此时可以点击【打开视频按钮】选择视频
			continue;
		}

		while (g_videos.size() > 0)
		{
			AVPacket pack = g_videos.front();

			MyFFmpeg::GetObj()->DecodeFrame(&pack);
			av_packet_unref(&pack);
			g_videos.pop_front(); //解码完成的帧从list前面弹出
		}

		AVPacket pkt = MyFFmpeg::GetObj()->ReadFrame();

		if (pkt.size <= 0)
		{
			msleep(10);
		}

		g_videos.push_back(pkt);
	}
}

   有些变量的定义,这里不做指出,需要源码的请点击下载

  读帧的实现如下:

AVPacket MyFFmpeg::ReadFrame()
{
	AVPacket pkt;
	memset(&pkt, 0, sizeof(AVPacket));

	mtx.lock();
	if (!m_afc)
	{
		mtx.unlock();
		return pkt;
	}

	int err = av_read_frame(m_afc, &pkt);
	if (err != 0)
	{
		//失败
	}
	mtx.unlock();

	return  pkt;
}

解码的实现:

void MyFFmpeg::DecodeFrame(const AVPacket *pkt)
{
	mtx.lock();
	if (!m_afc)
	{
		mtx.unlock();
		return;
	}

	if (m_yuv == NULL)
	{
		m_yuv = av_frame_alloc();
	}

	AVFrame *frame = m_yuv;  //指针传值
	
	int re = avcodec_send_packet(m_afc->streams[pkt->stream_index]->codec, pkt);
	if (re != 0)
	{
		mtx.unlock();
		return;
	}

	re = avcodec_receive_frame(m_afc->streams[pkt->stream_index]->codec, frame);
	if (re != 0)
	{
		//失败
		mtx.unlock();
		return;
	}
	mtx.unlock();
}

   下面对像素做转换,为显示准备。

bool MyFFmpeg::YuvToRGB(char *out, int outweight, int outheight)
{
	mtx.lock();
	if (!m_afc || !m_yuv) //像素转换的前提是视频已经打开
	{
		mtx.unlock();
		return false;
	}

	AVCodecContext *videoCtx = m_afc->streams[this->m_videoStream]->codec;
	m_cCtx = sws_getCachedContext(m_cCtx, videoCtx->width, videoCtx->height,
		videoCtx->pix_fmt,  //像素点的格式
		outweight, outheight,  //目标宽度与高度
		AV_PIX_FMT_BGRA,  //输出的格式
		SWS_BICUBIC,  //算法标记
		NULL, NULL, NULL
		);

	if (m_cCtx)
	{
		//sws_getCachedContext 成功"
	}
	else
	{
		//"sws_getCachedContext 失败"
	}

	uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 };

	data[0] = (uint8_t *)out;  //指针传值,形参的值会被改变,out的值一直在变,所以QImage每次的画面都不一样,画面就这样显示出来了,这应该是整个开发过程最难的点
	int linesize[AV_NUM_DATA_POINTERS] = { 0 };
	linesize[0] = outweight * 4;  //每一行转码的宽度

	//返回转码后的高度
	int h = sws_scale(m_cCtx, m_yuv->data, m_yuv->linesize, 0, videoCtx->height,
		data,
		linesize
		);

	mtx.unlock();
}

        转码处理后的视频是YUV, RGB和色度的四通道, 我们需要把它转化成RGB进行显示。

(3)视频显示     

       视频的显示用OpenGLWidget显示,把每一帧当做图片来处理,即可显示。关于OpenGLWidget如何显示图片,请查看我给出的方法。下列代码是进行显示,解码后的视频是四通道,所以在给QImage分配空间时用 width() * height() * 4

void VideoViewWidget::paintEvent(QPaintEvent *e)
{
	static QImage *image;
	
	if (image == NULL)
	{
        //视频是YVU四通道的类型。
		uchar *buf = new uchar[width() * height() * 4];
		image = new QImage(buf, width(), height(), QImage::Format_ARGB32);
	}

	bool ret = MyFFmpeg::GetObj()->YuvToRGB((char *)(image->bits()), width(), height());

	QPainter painter;
	painter.begin(this);
	painter.drawImage(QPoint(0, 0), *image);
	painter.end();
}

       当然,在打开界面时,就让子线程运行,但由于视频没有打开,就会一直出阻塞状态,当添加视频文件后,子线程继续运行。画面也就显示了。

       效果如下:

       

       只有画面没有音频,而且画面刷新很快,这是由于只解码了视频,没有关音频。下篇进行解码音频。

       本篇的源码,请点击【源码下载】。很多ffmpeg的API不懂的,请自行百度深入研究。

          

猜你喜欢

转载自blog.csdn.net/yao_hou/article/details/80559161