第三章:ffmpeg和QT开发播放器之视频的解码转码

写在前面:

本章对一个视频进行解码转码处理,内容对应视频的3-3~3-6。文章末尾对这几节课进行总结,然后附加源代码。

1、打开解码器

       之前使用avformat_open_input,只是打开文件流而已。

       一个视频打开之后,不止一个流,有视频流、音频流、字幕流。

       其中ic结构体里面的ic->nb_streams存放了是这个视频里面有几种流。

       AVCodecContext *enc = ic->streams[i]->codec;

       通过对ic结构体中的stream结构体中的codec进行判断,找到我们要的AVMEDIA_TYPE_VIDEO,也就是视频流

       然后使用avcodec_find_decoder(enc->codec_id)函数来判断系统有没有这个编码器

       接着再调用avcodec_open2函数将它打开这个解码器。

2、分配帧空间

        AVFrame *yuv = av_frame_alloc();

       定义一个帧空间AVFrame用来存放解码过的内容。ffmpeg中解码出来的都是yuv格式的。这个是将每一帧的色彩度和亮度存放起来的,而不是色彩信息。很多压缩格式都是用yuv这种方式来进行压缩的,这种算法压缩率更高,存储空间更小,存储一个像素点只要2个字节,而rgb需要3个字节。

       av_frame_alloc();这个函数不是分配整个解码的空间,而是AVFrame这个对象的空间,主要是因为我们并没有传递视频宽高比例给它,而且ffmpeg可以不需要知道指定大小,也能解密。
       av_frame_alloc()这个函数里面做了挺多初始化的,包括空间的申请。调用了一个动态库。在动态库申请的空间只能在动态库中释放,后面调用它的函数就能直接释放空间。

3、开始解码

       ffmpeg解码的调用用新老版本两种,先调用老版本avcodec_decode_video2函数,老版本中,对音视频的解码是分开解码的,新版的则是将它们合起来。

       对于avcodec_decode_video2函数来说,它根据每次解码的结果来申请所需要的空间。

       新版方式是使用avcodec_send_packet发送数据去解码,然后再用avcodec_receive_frame获取解码的结果,将编码后的帧存放到yuv帧空间中。

re = avcodec_send_packet(videoCtx, &pkt);
if (re != 0)
{
	av_packet_unref(&pkt);
	continue;
}
re = avcodec_receive_frame(videoCtx, yuv);
if (re != 0)
{
	av_packet_unref(&pkt);
	continue;
}

4、进行转码

      由于yuv格式没办法被播放器、显示器所接收,所以要被转换成RGB格式,需要打开一个转码器,还需要调用#include 头文件,并包含这个动态库#pragma comment(lib,"swscale.lib")

       转码器最好在一开始的时候打开。

       通过sws_getCachedContext函数获取解码器的内容,并打开编码器。

struct SwsContext *sws_getCachedContext(struct SwsContext *context,
                                        int srcW, int srcH, enum AVPixelFormat srcFormat,
                                        int dstW, int dstH, enum AVPixelFormat dstFormat,
                                        int flags, SwsFilter *srcFilter,
                                        SwsFilter *dstFilter, const double *param);

 context:转码器的内容,传null,就新创建一个转码器,如果传的不是null,会将传入的转码器context,跟缓冲里面的转码器进行比较(context后面的参数srcH等),是否一致,如果一致,就传回传入的这个context,如果不一致,就重新再创建一个。
       srcW、srcH:视频源的长和宽度,这个我们从videoCtx->width,videoCtx->height中获取。
       srcFormat:源的像素点格式,同样从这里获取videoCtx->pix_fmt,应该是yuv420
       dstW、dstH:目标的长宽度。
       dstFormat:输出的格式。这个要根据前端来设置的,我们使用的是QT,所以使用的是AV_PIX_FMT_BGRA
       flags:是指定用什么算法。我们这里使用的是SWS_BICUBIC

       后面的参数,暂时用不到。

       打开转码器后,使用sws_scale函数进行转码。

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
              const int srcStride[], int srcSliceY, int srcSliceH,
              uint8_t *const dst[], const int dstStride[]);

       c:这个也是传入刚刚打开的那个转码器cCtx。

       srcSlice:原数据的data,也就是我们解码出来的yuv的data
       srcStride:对应前一个参数data的一行数据的大小。
       srcSliceY:位置,但是目前没用到。
       srcSliceH:原数据的高度。
       dst:目标数据的data,这个data要去new一个空间,空间大小则是我们输出一帧画面的大小。
       dstStride:指的是目标数据,一行像素点的大小,即width*4,这个4就是RGBA(各占1个字节,所以要用到4字节)。
       返回值则是转码后的高度。

5、总结

       首先一个视频的处理简单来说就是先将视频文件打开,avformat_open_input打开视频文件之后,提取出视频里面的信息,存放在AVFormatContext *ic里面。

       那么一个视频文件被打开了,肯定先要被解码,那么就可以利用刚刚提取到的ic里面的视频格式信息,并使用avcodec_find_decoder函数来查找是否支持该文件格式的视频解码,如果有这个解码器,那么就使用avcodec_open2这个函数将解码器打开,解码器的信息可以使用AVCodecContext *videoCtx创建的容器里面。

       那么打开解码器之后呢,就可以开始视频解码,在代码中,就直接用一个死循环,做以下几个操作

       1)使用av_read_frame函数读取一帧数据,数据内容存放在pkt里面。

       2)通过avcodec_send_packet和avcodec_receive_frame函数来将该帧数据解码。

       3)由于解码出来的内容是yuv格式的,那么我们还得将它转换成RGB格式的,所以使用sws_getCachedContext函数打开转码器,然后再用sws_scale函数进行转码。   

       ps:这里的sws_getCachedContext函数设计的很巧妙,第一次传入的context为NULL,所以会为我们创建一个转码器,后面再调用它的时候,由于context的参数和之前一致,所以sws_getCachedContext直接return context,这样就不会多次创建转码器了。

       这次的代码如下:
#include "xplay.h"
#include 
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"swscale.lib")
extern "C"{
#include 
#include 
}

static double r2d(AVRational r)
{
	return r.num == 0 || r.den == 0 ? 0. : (double)r.num / (double)r.den;
}

#define OUT_WIDTH	800
#define OUT_HEIGHT	600

int main(int argc, char *argv[])
{
	int totalSec = 0;
	int pts = 0; 
	char re = 0;
	av_register_all();
	char *path = "my.mp4"; 
	AVFormatContext *ic = NULL;		//存放视频(流)的文件信息
	AVPacket pkt;					//存放读取视频的数据包
	AVCodecContext *videoCtx = NULL;//创建解码器的容器
	SwsContext *cCtx = NULL;		//创建转码器的容器

	int err;  
	int got_picture = 0;		//在视频解码老版本函数avcodec_decode_video2中,传入的参数,如果函数获取到视频会返回1
	int videoStream = 0;		//记录视频流存放在streams[n]数组里的第几个。
	int outwidth = OUT_WIDTH;			//输出的宽度
	int outheight = OUT_HEIGHT;			//输出的高度

	char *rgb = new char[outwidth*outheight * 4];	//图像的数据RGBA
	

	re = avformat_open_input(&ic, path, 0, 0);

	if (re != 0)
	{
		char buf[1024] = { 0 };

		av_strerror(re, buf, sizeof(buf));

		printf("open failed %s\n",path, buf);
		getchar();
		return -1;
	}

	totalSec = ic->duration / AV_TIME_BASE;		//计算出进度条时间
	printf("file totalSec is %d\n", totalSec);
	
	for (int i = 0; i < ic->nb_streams;i++)
	{
		AVCodecContext *enc = ic->streams[i]->codec;
				
		if (enc->codec_type == AVMEDIA_TYPE_VIDEO)//表示为一个视频
		{
			videoStream = i;
			videoCtx = enc;
			AVCodec *codec = avcodec_find_decoder(enc->codec_id); //判断系统有没有这个解码器

			if (!codec)
			{
				printf("video code not find!!\n");
				return -1;
			}
			err = avcodec_open2(enc, codec,NULL);	//找到系统中这个解码器的话,就打开这个解码器。
			if (err != 0)
			{
				char buf[1024] = { 0 };
				av_strerror(re, buf, sizeof(buf));
				printf("buf\n", path, buf);
				return -2;
			}
			printf("open avcodec_open2 success!!!\n");
		}
	}
	
	AVFrame *yuv = av_frame_alloc();		//分配的视频帧

	for (;;)			//解码
	{
		re = av_read_frame(ic, &pkt);
		if (re != 0) break;
		if (pkt.stream_index != videoStream)
		{
			av_packet_unref(&pkt);	//如果不是视频格式的 就释放掉
			continue;
		}

		pts = pkt.pts * r2d(ic->streams[pkt.stream_index]->time_base) *1000;
		
		re = avcodec_send_packet(videoCtx, &pkt);
		if (re != 0)
		{
			av_packet_unref(&pkt);
			continue;
		}
		re = avcodec_receive_frame(videoCtx, yuv);
		if (re != 0)
		{
			av_packet_unref(&pkt);
			continue;
		}
		printf("[D]");

		//打开转码器
		cCtx = sws_getCachedContext(cCtx, videoCtx->width,
			videoCtx->height,
			videoCtx->pix_fmt,
			outwidth, outheight,
			AV_PIX_FMT_BGRA,
			SWS_BICUBIC,
			NULL, NULL, NULL);
		
		if (!cCtx)
		{
			printf("sws_getCachedContext failed!!\n");
			break;
		}

		uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 };	//转码的目标数据
		data[0] = (uint8_t *)rgb;
		int linesize[AV_NUM_DATA_POINTERS] = { 0 };
		linesize[0] = outwidth * 4;

		int h = sws_scale(cCtx,yuv->data,yuv->linesize,0,videoCtx->height,
			data,
			linesize);

		if (h > 0)
			printf("(%d)", h);


		//re = avcodec_decode_video2(videoCtx, yuv, &got_picture, &pkt);
		//if (got_picture)
		//{
		//	printf("%d\n",re);
		//}

		printf("pts = %d\n", pts);
		av_packet_unref(&pkt);	//用于清理空间,防止内存泄露。
	}
	
	if (cCtx)
	{
		sws_freeContext(cCtx);
	}


	avformat_close_input(&ic);
	
	QApplication a(argc, argv);
	Xplay w;
	w.show();
	return a.exec();
}




你可能感兴趣的:(第三章:ffmpeg和QT开发播放器之视频的解码转码)