FFmepg-2、编解码及视频像素格式尺寸转换和音频重采样

一、基础知识

解封装(本来我们看到的文件都是mp4,flv流媒体格式等格式的文件,首先需要识别这些文件格式才能解析里面的内容,这部就解封装)
解码(解封装看到的数据是经过 压缩的音视频流,如果需要播放或者再次处理需要进行解码,编码的反向操作,就是视频需要显示就需要解码成显卡支持的像素格式,音频需要播放处理就需要重采样成声卡支持的格式)
常见封装格式 ; AVI 任意压缩格式,FLV,ts流媒体格式,ASF,mp4是MPEG-4压缩封装协议里面定义好的。
常用编码格式视频;H.264(存在参考帧和i帧的) ,wmv, mjpeg(每一帧都是独立的,帧内编码),帧内编码则在视频解码后播放进行拖动的时候直接播放到拖动位置的帧即可,如果是存在参考帧的在拖动的时候就需要找到后面的关键帧再解码播放的,否则不识别。
常用编码格式音频;acc(视频中一般是acc) ,MP3(早期的格式), ape ,flac(都是无损压缩的,音质很好,视频格式都是有损的压缩的)

封装格式和解码格式的整体过程
在这里插入图片描述

**像素格式;**这个需要十分关注,因为音视频方面占内存的就只有它了,在文件中是压缩好的帧格式,需要显示出来就必须解码再转换成rgb格式显示,因此内存消耗会非常大的。
YUV比RGB存储的空间更小,YUV算法压缩更强,因此一般传输使用YUV格式的,而采集和显示都需要使用RGB格式的。显卡GPU比cpu计算浮点数运算是高效的。
RGB存放方面注意内存对齐的坑,一般都是采用按行读取,效率低一点,一个像素点就是一套RGB。
YUV存在的多种格式,一般会采用软解码。YUV444就跟rgb类似一个像素点就是一套YUV,YUV422就是每个像素一个Y,两个像素使用一个U,V;YUV420一般使用这种,上下左右四个像素一起,四个Y 一个u,一个v。注意还有平面和打包的两种方式,平面的又分为sp,p两种。注意在ffmpeg里面yuv平面存放格式是有三个数组来分别存放YUV的。
海思项目中介绍过的RGB,YUV

PCM音频参数
采样率 sample_rate 44100(CD) 音频频段用一个值来存放的,1秒钟采集多少次,对应到代码里面就是1s中有多少个值。
通道 channels 左右通道,双通道(数据量增大一倍),1.5通道,
样本大小(格式)就是我们采样的值是用多大来存储的,对应有一个值的。sample_size
AV_SAMPLE_FMT_S16
AV_SAMPLE_FMT_FLTP 32位的一般声卡都不支持播放的,但是它是浮点数存储的,算法效率高,因此传输存储的都是这种形式,因此在播放的时候需要重采样过程将32位转换为16位。
对应音频的平面存储就是先将一个通道的所有采样值都保存再第二个通道如c1,c1,c1,c1…再c2c2c2,平面格式是对应有多个数组分别记录的,非平面的就是混合在一起的,双通道的话就是一个c1,c2,c1,c2,c1,c2。

视频帧中的GOP;就是在一段帧里面可以单独独立播放的,这一段帧就叫做GOP
在这里插入图片描述

二、FFmpeg基础

1、环境安装

FFmepg-VS和QT环境搭建

2、FFmpeg整体结构

在这里插入图片描述

三、API和相关数据结构

1、ffmpeg需要注意的一些点

1.1 内存申请释放;

两种内存;因为涉及到动态链接库,则一个对象可能涉及自己本身的对象内存还有对象内部data数据指向的内存。
对象创建的方式;因为涉及对象的内存创建是在工程里面建立对象或new对象,还是声明指针通过动态链接库提供的API来申请内存初始化以及释放。(建议最好还是声明指针通过他标准的函数进行操作,在代码架构,指针只需要前置声明类名而不用知道其内部成员,以及函数操作基本都是传入指针的),具体使用下面代码中展示。
内存引用计算;还有ffmpeg很多类对象本身提供了内存引用计数的管理对象内部内存空间的方法,就是创建或复制一次就引用加1,如果引用=0的时候就释放内存了,具体使用下面代码中展示。

1.2 时间基数

注意在ffmpeg里面很多时间都是参考各自的时间基数的,并且注意这里的时间基数不是秒,而是将秒拆分成了很多的一个时间基数,并且因为后面要进行音视频同步,所以我们要把各自里面管理的时间进行统一的转换位毫秒或者其他,这个时候就需要用到对应的时间基数进行转换,具体使用下面代码中展示。

1.3、新版本解封装和解码已分开

这里要注意的就是在ffmpeg老版本之前都是将解封装和解码参合在一起的,不利于管理,因此新版本都引进了不同的api将解封装和解码一刀切了,分开了。所以使用的时候要注意不要在使用老的api接口了,并且自己也要注意将解封装和解码分开,一些参数虽然解封装接口也能获取初始化,但是不要把两者合在一起,使用各种的接口。
在源码中有attribute_deprecated修饰的就是表示旧版本的,目前已经已经遗弃了。

2、整体流程图和相关API及数据结构

2.1、图解解码播放流程附相关API和数据结构在这里插入图片描述

主要分为两个步骤 解封装和解码,先阐述他们在整个解码播放过程中数据结构是怎么样的关系。

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

最首先

2.1.1、AVFormatContext 编解封装器上下文

AVFormatContext 包含的成员主要有
{
    
    
AVInputFormat iformat:输入媒体的AVInputFormat,比如指向AVInputFormat ff_flv_demuxer,就是指向具体的解封装器
unsigned int nb_streams:输入媒体的AVStream 个数
AVStream ** streams:输入媒体的AVStream []数组,一种数据流对应一个AVStream
int64_t duration:输入媒体的时长(以AV_TIME_BASE为基本单位),计算方式可以参考av_dump_format()函数。
int64_t bit_rate:输入媒体的码率
}

从AVFormatContext 可以得知包含AVStream ** streams这个成员,及可以通过封装器上下文获取到av数据流信息,注意每一种数据流对应一个AVStream 的,音频流,视频流,字幕流都对应一个AVStream 。

2.1.2、AVStream AV音视频流

AVStream  包含的成员主要有
{
    
    
int index:标识该视频/音频流 
AVRational time_base:该流帧时间的时间基数, PTS*time_base=真正的时间(秒)注意是AVRational分数类型
AVRational avg_frame_rate: 该流的帧率
int64_t duration:该视频/音频流长度
AVCodecParameters * codecpar:编解码器参数属性,其内存申请释放与avformat_new_stream() and avformat_free_context()同步
struct AVPacketList *last_in_packet_buffer;编码是该音视频流的最后一包pck的地址
}

再可以从AVStream 成员中可以得到AVCodecParameters * codecpar:一个该音视频流对应的解码器的参数信息的变量AVCodecParameters ,但是注意这个只是流数据里面记录该流的解码器的参数信息,只是参数信息而不是解码器本身,因此我们需要先定义解码器再将其参数信息进行复制过去,从而定义出一个可以解码该音视频流的解码器。

同样解码器的信息是由一个解码器上下文进行管理的。因此要先引入解码器上下文AVCodecContext

2.1.3、AVCodecContext 解码器上下文

AVCodecContext 包含的成员主要有
{
    
    
const struct AVCodec  *codec:编解码器的AVCodec,比如指向AVCodec ff_aac_latm_decoder
enum AVCodecID codec_id:
AVRational time_base;后续帧时间都是基于这个时间基数的
int width, height:图像的宽高(只针对视频)
enum AVPixelFormat pix_fmt:像素格式(只针对视频)
int sample_rate:采样率(只针对音频)
int channels:声道数(只针对音频)
enum AVSampleFormat sample_fmt:采样格式(只针对音频)
uint64_t channel_layout;通道格式(只针对音频)
int thread_count;解码时使用的线程数;线程数用于决定应该将多少独立任务传递给execute()
int64_t max_pixels;每幅图像最大限度接受的像素数。由用户设置
}

又得注意AVCodecContext 解码器上下文其实主要还是修饰解码器的,并且调用动态链接库的API的时候创建AVCodecContext的时候就得把AVCodec 进行创建并初始化。解码器上下文只是存储该解码器的参数信息的一个结构体,主体还是解码器AVCodec 。

2.1.4、AVCodec 解码器上下文

AVCodec 包含的成员主要有
const char * name:编解码器名称
enum AVMediaType  type:编解码器类型
enum AVCodecID id:编解码器ID
• 一些编解码的接口函数,比如int (*decode)()

注意再将解码器的参数进行赋值,自己也可以进一步修改或者赋值其他

解码器参数进行赋值
avcodec_parameters_to_context(vc, acformatCon->streams[i_videoStream]->codecpar);

整体操作流程主要是这四个数据结构,但是其中还有存储数据的两个数据结构,AVPacket封装前存储的形式,AVFrame解码后用来存储的形式

2.1.5、AVPacket封装前存储的形式

AVPacke 包含的成员主要有
{
    
    
int64_t pts:显示时间戳 是以AVStream->time_base为单位的
int64_t dts:解码时间戳 是以AVStream->time_base为单位的
uint8_t * data:压缩编码数据
int size:压缩编码数据大小
int64_t pos:数据的偏移地址
int stream_index:所属的AVStream
}

2.1.6、AVFrame解码后用来存储的形式

AVFrame 包含的成员主要有
{
    
    
uint8_t *data[AV_NUM_DATA_POINTERS]:解码后的图像像素数据(音频采样数据)
int linesize[AV_NUM_DATA_POINTERS]:对视频来说是图像中一行像素的大小;对音频来说是整个音频帧的大小,
int width, height:图像的宽高(只针对视频)
int key_frame:是否为关键帧(只针对视频) 。1 -> keyframe, 0-> not
enum AVPictureType pict_type:帧类型(只针对视频) 。例如I, P, B
int sample_rate:音频采样率(只针对音频)
int nb_samples:音频每通道采样数(只针对音频)
int64_t pts:显示时间戳
}

int linesize[AV_NUM_DATA_POINTERS]:为什么要存在这样的参数,如果视频的宽高不对时可能需要内存对齐,对音频来说就是对应其样本数的大小

2.2、解码播放相关API

2.2.1、解封装

解码步骤 包含libavformat/avformat.h和avformat.lib

与解封装相关的数据结构就有编解封装器、封装格式上下文、然后就可以根据封装格式上下文获取到压缩数据包了,再就可以进行解压产生数据帧了。
第一步就是编解封装器注册、需要要的一步,先有设备才有修饰存储设备参数的上下文。所以都得先执行下面两个函数。

av_register_all();  //注册所有解封装器
avformat_network_init();//初始化网络库,例如使用rtsp协议传输的时候就可以直接打开解封装

第二步就是获取该音视频流文件封装器的参数也就是封装格式上下文AVFormatContext,

AVFormatContext *acformatCon = NULL;//注意是在动态链接库内部定义的内存
const char *strPath = "wx_camera_1615072849946.mp4";
AVDictionary *options = NULL;//通过AVDictionary 设置解封装器的参数
//注意这个参数rtsp_transport,没有在options_table.h里面,在libformat/rtsp.c文件中ff_rtsp_options,
//有四个参数 udp、tcp、udp_multicast、http还可以使用+进行累加都支持的传输通道
av_dict_set(&options, "rtsp_transport", "tcp", 0);//字典设置函数
av_dict_set(&options, "max_delay", "500", 0);
//调用前必须先注册解封器
通过这个函数自动将该音视频文件对应的封装器的参数存储到AVFormatContext中
int ret = avformat_open_input(
	&acformatCon, //分配内存空间并给参数赋值,下面的解码也是根据这些参数记录来寻找数据地址的,这里分配了内存,最后也要调用API释avformat_close_input
	strPath, 
	0,//传入0则表示自动选择解封器
	&options);
//不等于0则打开设置失败,并返回错误码,av_strerror函数解析错误码
if (ret != 0)
{
    
    
	char buffer[1024];
	//这个函数在avutil.lib中 ,则要添加comment这个库
	av_strerror(ret, buffer, sizeof(buffer)-1);//将记录错误的返回值解析成字符串
	cout << "open" << strPath << " failed :" << buffer << endl;
	getchar();
	return -1;
}
cout << "open" << strPath << " success !"  << endl;

其实获取完封装器上下文之后
可以进行读取流信息
第三步;读取流信息

avformat_find_stream_info(acformatCon, 0);//注意这一步有的其实不用调用也有数据,但是这是低频操作,调用一下也不影响效率的,只有涉及解码图像,像素这些才是高频操作,需要十分注意效率

读取之后就已经可以得到关于该音视频文件的很多参数了,但是注意,因为这只是根据封装文件的头部信息记录的数据,不一定能够全部读取出来,也就是有的部分数据在这一步可能读取不到的,还是需要到解码那里读取到。

获取音视频文件的总时间
int i_totalMs = acformatCon->duration/ (AV_TIME_BASE / 1000);//返回毫秒
cout << "i_totalMs = " << i_totalMs << endl;

/*
//很好的调试工具
void av_dump_format(AVFormatContext *ic,
                   int index,  //设置对应流的打印信息
                   const char *url,  //也是打印信息
                   int is_output);  //封装的格式是输入还是输出 这里是输入则传入0即可
	*/
av_dump_format(acformatCon, 0, strPath, 0);

之后就可以获取音视频流了AVStream,有两种方法,遍历和函数直接获取

//记录stream的音视频索引,方便在读取是区分音视频
	int i_videoStream = 0;
	int i_audioStream = 1;
	//获取音视频流的信息  (遍历判断stream流数组哪个是a哪个是v,或者使用函数调用)
	//一般streams[0]是视频 1是音频,但是没有规定,因此还是需要判断
	for (int i = 0; i < acformatCon->nb_streams; i++)
	{
    
    
		i_audioStream = i;
		AVStream *as = acformatCon->streams[i];
		//音视频共用的
		cout << "format :" << as->codecpar->format << endl;//样本格式,这个format 会根据音视频转换,视频为AVPixelFormat,音频为AVSampleFormat
		cout << "codec_id :" << as->codecpar->codec_id << endl;//对应的是枚举的AVCodecID  之后解码是需要用到的
		//注意formate类型的数据在解码出来之后不能直接播放的,因为是32位的,计算机声卡一般都不支持32位的,需要重采样到16位或其他才能播放
		//判断音频
		if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)//宏在avutil.h里面定义的
		{
    
    
			cout << i << ":音频信息" << endl;
			cout << "sample_rate :" << as->codecpar->sample_rate << endl;//音频的采样率
			cout << "channels :" << as->codecpar->channels << endl;
			//音频的一帧数据代表单通道一定量的样本数,保证一帧数据的数据量,不多也不少,定量
			cout << "audio fbs = " << r2b(as->avg_frame_rate) << endl;//音频却不一定是整数,表示单通道多少个样本数
			cout << "audio frame_size = " << as->codecpar->frame_size << endl;//记录一帧的数据量,同样也是以编码那边为准的
			//audio fbs = sample_rate/ frame_size; 存在这样一个等式的,注意音频和视频的帧率是不一样的,音视频是分开做缓存的
		}
		//判断视频
		else if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
    
    
			i_videoStream = i;
			cout << i << ":视频信息" << endl;
			cout << "width :" << as->codecpar->width << endl;
			cout << "height :" << as->codecpar->height << endl;//在有的流媒体数据在打开解封装这一步,是还不能解析出来宽高的,因此写程序宽高不能写死从这里获取,但是在解码的肯定可以获取到的
			//帧率,是一个AVRational有理数也就是分数类型,保证精确的,视频的fbs是整数的表示多少张图片,但音频却不一定,表示单通道多少个样本数
			//分数需要转换的,但是注意分母为0的情况
			cout << "vedio fbs = " << r2b(as->avg_frame_rate) << endl;
		}
		//还有可能是字幕 AVMEDIA_TYPE_SUBTITLE,暂不处理
	}
	//第二种方法,直接使用函数获取音视频流信息
	/*
	int av_find_best_stream(AVFormatContext *ic,
                        enum AVMediaType type,//获取流的类型
                        int wanted_stream_nb,//自己想要流的编号 -1则表示自动选择
                        int related_stream,//相关的流信息,null表示没有
                        AVCodec **decoder_ret,//对应解码要用到的,但是这里也不用,因为解封装和解码新版本已经分割开来了。
						//如果在这里去获取解码器,则把解码和解封装混到一起了,与新版本不一致,我们是可以根据codeID自己找到的,没必要在这里设置
                        int flags);
	*/
	//直接返回对应流的索引,再根据stream就可以直接访问了
	i_videoStream = av_find_best_stream(acformatCon, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
	i_audioStream = av_find_best_stream(acformatCon, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);

2.2.2、解码

解码步骤 包含include/avcodec.h和lib/avcodec.lib

第一步;首先同样要注册解码器,才能使用解码器和解码器上下文

//注册解码器
	avcodec_register_all();

第二步;再注意因为前面就根据音视频分出了不同的数据流AVStream,所以解码器也是分开的,因此要分别对音频和视频进行解码,则创建解码器和解码器上下文也是需要两份的。

//视频解码器
	//找到解码器(可以通过av_find_best_stream来但是把解封装和解码合到一起了不好,可以通过codecid获取)
	AVCodec *vcodec = avcodec_find_decoder(acformatCon->streams[i_videoStream]->codecpar->codec_id);
	if (!vcodec)
	{
    
    
		cout << "can't find the codec id :" << acformatCon->streams[i_videoStream]->codecpar->codec_id;
		getchar();
		return -1;
	}
	cout << "find the codec id :" << acformatCon->streams[i_videoStream]->codecpar->codec_id << endl;
	//创建找到解码器上下文
	AVCodecContext *vc = avcodec_alloc_context3(vcodec);

	//配置解码器参数
	//复制的方法
	avcodec_parameters_to_context(vc, acformatCon->streams[i_videoStream]->codecpar);
	//还可以自己进行解码器参数,
	//设置8进程解码,也可以通过api获取得到系统进程数
	vc->thread_count = 8;

	//打开解码器上下文
	ret = avcodec_open2(vc, NULL, 0);
	if (ret != 0)
	{
    
    
		char buffer[1024];
		//这个函数在avutil.lib中 ,则要添加comment这个库
		av_strerror(ret, buffer, sizeof(buffer) - 1);//将记录错误的返回值解析成字符串
		cout << "avcodec_open2 failed! : " << buffer << endl;
		getchar();
		return -1;
	}
	cout << "vedio avcodec_open2 success!" << endl;
//音频解码器
	//找到解码器(可以通过av_find_best_stream来但是把解封装和解码合到一起了不好,可以通过codecid获取)
	AVCodec *acodec = avcodec_find_decoder(acformatCon->streams[i_audioStream]->codecpar->codec_id);
	if (!acodec)
	{
    
    
		cout << "can't find the codec id :" << acformatCon->streams[i_audioStream]->codecpar->codec_id;
		getchar();
		return -1;
	}
	cout << "find the codec id :" << acformatCon->streams[i_audioStream]->codecpar->codec_id << endl;
	//创建找到解码器上下文
	AVCodecContext *ac = avcodec_alloc_context3(acodec);

	//配置解码器参数
	//复制的方法
	avcodec_parameters_to_context(ac, acformatCon->streams[i_audioStream]->codecpar);
	//设置8进程解码,也可以通过api获取得到系统进程数
	ac->thread_count = 8;

	//打开解码器上下文
	ret = avcodec_open2(ac, NULL, 0);
	if (ret != 0)
	{
    
    
		char buffer[1024];
		//这个函数在avutil.lib中 ,则要添加comment这个库
		av_strerror(ret, buffer, sizeof(buffer) - 1);//将记录错误的返回值解析成字符串
		cout << "avcodec_open2 failed! : " << buffer << endl;
		getchar();
		return -1;
	}
	cout << "audio avcodec_open2 success!" << endl;

第三步;获取了视频流/音频流的解码器以及解码器参数的上下文后我们就可以从数据流中取包然后解码成帧了。

主要步骤就是
av_packet_alloc、av_frame_alloc调用接口定义AVPacket 和AVFrame ,
for (; ;)循环读取解压
	av_read_frame读取包AVPacket 
	avcodec_send_packet将读取到的包AVPacket 发送到对应解码器的解码线程
	av_packet_unref 因为AVPacket 变量重复使用,但是其data数据区是用完一次就可以释放的,调用unref进行清空,引用计数-1.
	for循环  循环读取解压过来的帧。
		avcodec_receive_frame读取解压过来返回的帧,注意解压一包可能返回多帧因此需要for循环来循环读取解压过来的帧。

最后再释放内存
av_frame_free(&frame);
av_packet_free(&pkt);//释放要传入地址

详细代码如下;

//读取视频帧
	//调用接口申请内存比较方便,因为后续维护pkt需要放到队列里面,用指针比较好,比较指针可以转为void类型
	//并且在调用接口处也方便,只需要类的前置声明即可,不需要全部声明出来,架构更加清晰
	AVPacket *pkt = av_packet_alloc();//调用接口申请空间,那么就要调用接口释放
	AVFrame *frame = av_frame_alloc();
	for (; ;)
	{
    
    
		//int av_read_frame(AVFormatContext *s, AVPacket *pkt);
		int re = av_read_frame(acformatCon, pkt);
		if (re != 0)//失败的或者读到文件结尾
		{
    
    
			//文件读取结束后又移动到对应位置循环播放
			cout << "--------------end---------------" << endl;
			int ms = 3000;//三秒的位置,再根据时间基数(分数)转换
			long long pos = (double)ms / (double)1000 * r2b(acformatCon->streams[pkt->stream_index]->time_base);
			av_seek_frame(acformatCon, i_videoStream,pos, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME);//往后对齐,对齐到关键帧
			//break;//空间不需要释放的,因为内部data空间没有成功
			continue;
		}
		cout << "packet size = " << pkt->size << endl;
		//显示时间
		cout << "packet pts(time_base) = " << pkt->pts<< endl;
		//编码时间
		cout << "packet dts(time_base) = " << pkt->dts << endl;
		//音视频的time_base是可能不一样的,因此要做成相同单位方便做同步
		cout << "packet pts(ms) = " << pkt->pts * r2b(acformatCon->streams[pkt->stream_index]->time_base) *1000;//这里的pts,dts都是以这个时间为基数的也是一个分数
		//负数就是表示预编码
		
		AVCodecContext *cc = NULL;
		if (pkt->stream_index == i_videoStream)
		{
    
    
			cout << "picture" << endl;
			cc = vc;
		}
		else if (pkt->stream_index == i_audioStream)
		{
    
    
			cout << "audio" << endl;
			cc = ac;
		}

		//解码视频  因为音视频涉及相同的接口
		ret = avcodec_send_packet(cc, pkt);//发送packet到解码线程
		//XSleep(500);//500ms
		//pkt有内部的空间,已经上个pkt已经发到解码线程了则可以释放其内部data
		//但其pkt这个变量还是可以使用的,则使用内存引用计数-1的方法,为0则释放空间
		av_packet_unref(pkt);
		if (ret != 0)
		{
    
    
			char buffer[1024];
			//这个函数在avutil.lib中 ,则要添加comment这个库
			av_strerror(ret, buffer, sizeof(buffer) - 1);//将记录错误的返回值解析成字符串
			cout << "avcodec_open2 failed! : " << buffer << endl;
			continue;
		}
		//接收,这里要注意发送是不占用cpu时间的采用多线程的方法
		//发1可以会接收多个,因此需要循环的,并且最后需要传入null来把后面的缓冲帧都接收
		for (; ; )
		{
    
    
			re = avcodec_receive_frame(cc, frame);
			if (re != 0) break;
			cout << "recv frame :" << frame->format << "  " << frame->linesize[0] << endl;
		}
	}
	av_frame_free(&frame);
	av_packet_free(&pkt);//释放要传入地址

2.2.3、视频像素和尺寸转换 swscale

包含的头文件libswscale/swscale.h和库"swscale.lib"

注意这一部分使用显卡来做效率更高,但是ffmpeg也提供了这类型的库给我们,ffmpeg使用简单,但是性能开销还是比较大的,因为视频像素涉及的内存空间太大了,一张图片这么多个像素点一个像素点有需要几个字节来存储,因此视频像素的尺寸转换是十分影响效率的。

转换格式参数上下文的创建和释放
获取它的两个API函数,区别就是第一个参数区别,
第一个sws_getContext,就是直接创建一个新的上下文空间给你,
第二个sws_getCachedContext,就是你可以把之前创建好的上下文传入进去,他会缓冲里面去找输入输出的格式是否一样,因为都是一套设定的,如果存在这一套机制,那么就会返回相同的指针,来给你使用,因此第一次是可以传入NULL的,但是如果之后再次传入SwsContext ,并且与之前的不一致的话,那么他就会把之前的SwsContext 空间释放掉重新分配返回回来的,因此需要注意多线程同时访问的时候会存在问题,要上锁的,所以在多线程当中最好还是独立建一个SwsContext ,进行使用。

struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,  int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param);
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);
int srcW, int srcH, int dstW, int dstH, 原的宽高,目的宽高
enum AVPixelFormat srcFormat,     dstFormat 原像素格式,目的像素格式
int flags   SWScale库本身提供了很多套算法,flags就是选择对应的尺寸转换算法,不同的算法会有不同的性能和尺寸转换后的差异。
SwsFilter *srcFilter  过滤器暂时不考虑,直接设置null即可
double *param 是与flags设置的算法传参有关的,可以不用管直接使用默认的即可

释放视频转换上下文空间,但是注意这个只是传入指针,因此要注意释放后这个变量指针没有置0,需要后面手动置0,因为上面有创建函数是直接判断是否为空进行处理的,如果这里释放了内存不置空那边调用就会崩溃的。
void sws_freeContext(struct SwsContext *swsContext);

2.2.4、具体尺寸格式转换的函数 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[]);
const uint8_t *const srcSlice[];原数据存放的地址,一个数组至于取几个要看前面像素格式决定的,如果是YUV平面格式的就要取3位分别对应YUV,如果是RGB打包格式的那么就只取一位。
const int srcStride[], 一行字节数的大小,就是之前frame里面的linesize。
int srcSliceY  不用的
int srcSliceH 高度,不用宽度,因为使用的是内存对齐后的linesize了。
uint8_t *const dst[];转换后的目的数据存放的空间,是一个指针数组,因此要提前分配空间,
const int dstStride[] 输出的一行的大小

swscale实践

	//像素格式和尺寸转换上下文
	SwsContext *vctx = NULL;
	unsigned char *rgb = NULL;
//在解码后得到解码帧后进行转换的,注意内存的申请和格式转换
	//视频
	if (cc == vc)
	{
    
    
		vctx = sws_getCachedContext(
			vctx,	//传NULL会新创建
			frame->width, frame->height,		//输入的宽高
			(AVPixelFormat)frame->format,	//输入格式 YUV420p
			frame->width, frame->height,	//输出的宽高
			AV_PIX_FMT_RGBA,				//输入格式RGBA
			SWS_BILINEAR,					//尺寸变化的算法
			0, 0, 0);
		//if(vctx)
		//cout << "像素格式尺寸转换上下文创建或者获取成功!" << endl;
		//else
		//	cout << "像素格式尺寸转换上下文创建或者获取失败!" << endl;
		if (vctx)
		{
    
    
			//申请转换后数据存放的内存
			if (!rgb) rgb = new unsigned char[frame->width*frame->height * 4];
			//转换为对应存放格式,因为是要转换为打包的RGBA格式因此只需要一维,如果是YUV420P的则需要三维都需要内存地址的,否则会奔溃
			uint8_t *data[2] = {
    
     0 };
			data[0] = rgb;
			int lines[2] = {
    
     0 };
			lines[0] = frame->width * 4;//存放一行数据大小,
			re = sws_scale(vctx,
				frame->data,		//输入数据
				frame->linesize,	//输入行大小
				0,
				frame->height,		//输入高度
				data,				//输出数据和大小
				lines				//一行的大小
			);
			cout << "sws_scale = " << re << endl;
		}
	}

2.4、音频重采样 Swresample

包含的头文件libswresample/swresample.h和库swresample.lib
总所周知,封装解码出来的音频帧一般都是不能播放的,因此存放的时候都是以32位存放的,而声卡一般都只支持S16位以下的,因此声音播放需要重采样这个过程,ffmpeg提供了专门重采样的模块

同样对于一个设备,在ffmpeg中都会存在一个上下文修饰这个设备,存储他的相关参数。对于音频重采样就有重采样的上下文SwrContext。

重采样的上下文的创建和释放

SwrContext *swr_alloc(void);//创建
int swr_init(struct SwrContext *s);//初始化
void swr_free(struct SwrContext **s);//释放,存入双重指针

设置重采样上下文的参数

struct SwrContext *swr_alloc_set_opts(struct SwrContext *s,
                                      int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate,
                                      int64_t  in_ch_layout, enum AVSampleFormat  in_sample_fmt, int  in_sample_rate,
                                      int log_offset, void *log_ctx);

struct SwrContext *s;设置哪个重采样上下文,传入指针
int64_t out_ch_layout 输出的声道样式,左右声道,立体声 1.5倍声道,有对应的宏在\include\libavutil\channel_layout.h里面
enum AVSampleFormat out_sample_fmt,输出的样本格式,FLTP,s16,s14的
int out_sample_rate,输出的样本率,重采样的样本率是可以不变的
int64_t  in_ch_layout, enum AVSampleFormat  in_sample_fmt, int  in_sample_rate, 输入的声道样式,样本格式,采用率,注意可以修改输入输出的采样率进行快进等操作,但是存在声音失真
int log_offset, void *log_ctx  不用管直接赋值0

一帧帧的音频转换进行重采样swr_convert

int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);
                               
struct SwrContext *s  操作的重采样上下文
uint8_t **out  输出的地址,传入的是双重指针,就是一个数组指针,
int out_count  单通道的样本数量就是解码器参数中的frame_size 如果是双通道格式要乘以2const uint8_t **in ,输入的数据地址,可以把解码出来的data直接传入
 int in_count,单通道样本的数量

代码实践

#include "libswresample/swresample.h"//包含头文件
#pragma comment(lib,"swresample.lib")//包含指定的库文件

//音频重采样
SwrContext *actx = NULL;

//通过返回值方式创建内存,就不需要像前面上下文一样传入双重指针了
actx = swr_alloc_set_opts(
	actx,
	av_get_default_channel_layout(2),//输出格式 2通道
	AV_SAMPLE_FMT_S16,//输出的样本格式  16位两个字节表示
	ac->sample_rate, //输出采样率
	av_get_default_channel_layout(ac->channels),//输入格式
	ac->sample_fmt,
	ac->sample_rate,
	0,0
);
//注意要先设置属性,再初始化,就是要先创建内存
re = swr_init(actx);
if (re != 0)
{
    
    
	char buf[1024] = {
    
     0 };
	av_strerror(re, buf, sizeof(buf) - 1);
	cout << "swr_init  failed! :" << buf << endl;
	getchar();
	return -1;
}
unsigned char *pcm = NULL;//代码处理音视频 要定义位无符号类型

//然后在解码帧出来之后对帧进行判断,如果是音频则做重采样,视频就做像素尺寸转换
else if (cc == ac)//音频
{
    
    
	//注意数据的转换,
	uint8_t *data[2] = {
    
    0};
	//乘以样本大小S16的2个字节,乘以输出的通道数2,则一帧存放需要这么多字节
	if (!pcm) pcm = new uint8_t[frame->nb_samples * 2 * 2];
	data[0] = pcm;
	//返回单通道样本数的数量
	int len = swr_convert(
		actx,
		data,frame->nb_samples,//输出数据的存放地址,样本数量
		(const uint8_t**)frame->data,frame->nb_samples//输入数据的存放地址,样本数量
	);
	cout << "swr_convert = " << len << endl;
}

FFmepg-编解码及视频像素格式尺寸转换和音频重采样 整体代码流程

//代码参考自夏曹俊老师ffmpeg与QT开发课程
#include <iostream>
#include <thread>
using namespace std;

/*
	文件解封装测试
*/
#ifdef __cplusplus
extern "C"
{
    
    
#endif // !__cplusplus
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include "libswresample/swresample.h"//包含头文件
#ifdef __cplusplus
}
#endif // !__cplusplus
#pragma comment(lib,"avformat.lib")//添加库文件,也可以在属性处添加
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"swresample.lib")
#pragma comment(lib,"swscale.lib")


//参考opencv里面的代码的,因为opencv也是用到ffmpeg的
static double r2d(AVRational r)
{
    
    
	return r.den == 0 ? 0 : (double)r.num / (double)r.den;
}
void XSleep(int ms)
{
    
    
	//c++11支持的
	chrono::milliseconds du(ms);
	this_thread::sleep_for(du);
}
AVPixelFormat ConvertDeprecatedFormat(enum AVPixelFormat format)
{
    
    
	switch (format)
	{
    
    
	case AV_PIX_FMT_YUVJ420P:
		return AV_PIX_FMT_YUV420P;
		break;
	case AV_PIX_FMT_YUVJ422P:
		return AV_PIX_FMT_YUV422P;
		break;
	case AV_PIX_FMT_YUVJ444P:
		return AV_PIX_FMT_YUV444P;
		break;
	case AV_PIX_FMT_YUVJ440P:
		return AV_PIX_FMT_YUV440P;
		break;
	default:
		return format;
		break;
	}
}
int main()
{
    
    
	cout << "Test Demux FFmpeg.club" << endl;
	const char *path = "wx_camera_1615072849946.mp4";
	//初始化封装库
	av_register_all();

	//初始化网络库 (可以打开rtsp rtmp http 协议的流媒体视频)
	avformat_network_init();

	//注册解码器
	avcodec_register_all();

	//参数设置
	AVDictionary *opts = NULL;
	//设置rtsp流已tcp协议打开
	av_dict_set(&opts, "rtsp_transport", "tcp", 0);

	//网络延时时间
	av_dict_set(&opts, "max_delay", "500", 0);


	//解封装上下文
	AVFormatContext *ic = NULL;
	int re = avformat_open_input(
		&ic,
		path,
		0,  // 0表示自动选择解封器
		&opts //参数设置,比如rtsp的延时时间
	);
	if (re != 0)
	{
    
    
		char buf[1024] = {
    
     0 };
		av_strerror(re, buf, sizeof(buf) - 1);
		cout << "open " << path << " failed! :" << buf << endl;
		getchar();
		return -1;
	}
	cout << "open " << path << " success! " << endl;

	//获取流信息 
	re = avformat_find_stream_info(ic, 0);

	//总时长 毫秒
	int totalMs = ic->duration / (AV_TIME_BASE / 1000);
	cout << "totalMs = " << totalMs << endl;

	//打印视频流详细信息
	av_dump_format(ic, 0, path, 0);

	//音视频索引,读取时区分音视频
	int videoStream = 0;
	int audioStream = 1;

	//获取音视频流信息 (遍历,函数获取)
	for (int i = 0; i < ic->nb_streams; i++)
	{
    
    
		AVStream *as = ic->streams[i];
		cout << "codec_id = " << as->codecpar->codec_id << endl;
		cout << "format = " << as->codecpar->format << endl;

		//音频 AVMEDIA_TYPE_AUDIO
		if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
		{
    
    
			audioStream = i;
			cout << i << "音频信息" << endl;
			cout << "sample_rate = " << as->codecpar->sample_rate << endl;
			//AVSampleFormat;
			cout << "channels = " << as->codecpar->channels << endl;
			//一帧数据?? 单通道样本数 
			cout << "frame_size = " << as->codecpar->frame_size << endl;
			//1024 * 2 * 2 = 4096  fps = sample_rate/frame_size

		}
		//视频 AVMEDIA_TYPE_VIDEO
		else if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
    
    
			videoStream = i;
			cout << i << "视频信息" << endl;
			cout << "width=" << as->codecpar->width << endl;
			cout << "height=" << as->codecpar->height << endl;
			//帧率 fps 分数转换
			cout << "video fps = " << r2d(as->avg_frame_rate) << endl;
		}
	}

	//获取视频流
	videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);

	//
	///视频解码器打开
	///找到视频解码器
	AVCodec *vcodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);
	if (!vcodec)
	{
    
    
		cout << "can't find the codec id " << ic->streams[videoStream]->codecpar->codec_id;
		getchar();
		return -1;
	}
	cout << "find the AVCodec " << ic->streams[videoStream]->codecpar->codec_id << endl;

	AVCodecContext *vc = avcodec_alloc_context3(vcodec);

	///配置解码器上下文参数
	avcodec_parameters_to_context(vc, ic->streams[videoStream]->codecpar);
	//八线程解码
	vc->thread_count = 8;

	///打开解码器上下文
	re = avcodec_open2(vc, 0, 0);
	if (re != 0)
	{
    
    
		char buf[1024] = {
    
     0 };
		av_strerror(re, buf, sizeof(buf) - 1);
		cout << "avcodec_open2  failed! :" << buf << endl;
		getchar();
		return -1;
	}
	cout << "video avcodec_open2 success!" << endl;


	//
	///音频解码器打开
	AVCodec *acodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id);
	if (!acodec)
	{
    
    
		cout << "can't find the codec id " << ic->streams[audioStream]->codecpar->codec_id;
		getchar();
		return -1;
	}
	cout << "find the AVCodec " << ic->streams[audioStream]->codecpar->codec_id << endl;
	///创建解码器上下文呢
	AVCodecContext *ac = avcodec_alloc_context3(acodec);

	///配置解码器上下文参数
	avcodec_parameters_to_context(ac, ic->streams[audioStream]->codecpar);
	//八线程解码
	ac->thread_count = 8;

	///打开解码器上下文
	re = avcodec_open2(ac, 0, 0);
	if (re != 0)
	{
    
    
		char buf[1024] = {
    
     0 };
		av_strerror(re, buf, sizeof(buf) - 1);
		cout << "avcodec_open2  failed! :" << buf << endl;
		getchar();
		return -1;
	}
	cout << "audio avcodec_open2 success!" << endl;

	///ic->streams[videoStream]
	//malloc AVPacket并初始化
	AVPacket *pkt = av_packet_alloc();
	AVFrame *frame = av_frame_alloc();
	//像素格式和尺寸转换上下文
	SwsContext *vctx = NULL;
	unsigned char *rgb = NULL;
	//音频重采样
	SwrContext *actx = NULL;
	
	//通过返回值方式创建内存,就不需要像前面上下文一样传入双重指针了
	actx = swr_alloc_set_opts(
		actx,
		av_get_default_channel_layout(2),//输出格式 2通道
		AV_SAMPLE_FMT_S16,//输出的样本格式  16位两个字节表示
		ac->sample_rate, //输出采样率
		av_get_default_channel_layout(ac->channels),//输入格式
		ac->sample_fmt,
		ac->sample_rate,
		0,0
	);
	//注意要先设置属性,再初始化,就是要先创建内存
	re = swr_init(actx);
	if (re != 0)
	{
    
    
		char buf[1024] = {
    
     0 };
		av_strerror(re, buf, sizeof(buf) - 1);
		cout << "swr_init  failed! :" << buf << endl;
		getchar();
		return -1;
	}
	unsigned char *pcm = NULL;//代码处理音视频 要定义位无符号类型
	for (;;)
	{
    
    
		int re = av_read_frame(ic, pkt);
		if (re != 0)
		{
    
    
			//循环播放
			cout << "==============================end==============================" << endl;
			int ms = 3000; //三秒位置 根据时间基数(分数)转换
			long long pos = (double)ms / (double)1000 * r2d(ic->streams[pkt->stream_index]->time_base);
			av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
			continue;
		}
		cout << "pkt->size = " << pkt->size << endl;
		//显示的时间
		cout << "pkt->pts = " << pkt->pts << endl;

		//转换为毫秒,方便做同步
		cout << "pkt->pts ms = " << pkt->pts * (r2d(ic->streams[pkt->stream_index]->time_base) * 1000) << endl;



		//解码时间
		cout << "pkt->dts = " << pkt->dts << endl;

		AVCodecContext *cc = 0;
		if (pkt->stream_index == videoStream)
		{
    
    
			cout << "图像" << endl;
			cc = vc;


		}
		if (pkt->stream_index == audioStream)
		{
    
    
			cout << "音频" << endl;
			cc = ac;
		}

		///解码视频
		//发送packet到解码线程  send传NULL后调用多次receive取出所有缓冲帧
		re = avcodec_send_packet(cc, pkt);
		//释放,引用计数-1 为0释放空间
		av_packet_unref(pkt);

		if (re != 0)
		{
    
    
			char buf[1024] = {
    
     0 };
			av_strerror(re, buf, sizeof(buf) - 1);
			cout << "avcodec_send_packet  failed! :" << buf << endl;
			continue;
		}

		for (;;)
		{
    
    
			//从线程中获取解码接口,一次send可能对应多次receive
			re = avcodec_receive_frame(cc, frame);
			if (re != 0) break;
			cout << "recv frame " << frame->format << " " << frame->linesize[0] << endl;

			//视频
			if (cc == vc)
			{
    
    
				vctx = sws_getCachedContext(
					vctx,	//传NULL会新创建
					frame->width, frame->height,		//输入的宽高
					(AVPixelFormat)frame->format,	//输入格式 YUV420p
					frame->width, frame->height,	//输出的宽高
					AV_PIX_FMT_RGBA,				//输入格式RGBA
					SWS_BILINEAR,					//尺寸变化的算法
					0, 0, 0);
				//if(vctx)
				//cout << "像素格式尺寸转换上下文创建或者获取成功!" << endl;
				//else
				//	cout << "像素格式尺寸转换上下文创建或者获取失败!" << endl;
				if (vctx)
				{
    
    
					//申请转换后数据存放的内存
					if (!rgb) rgb = new unsigned char[frame->width*frame->height * 4];
					//转换为对应存放格式,因为是要转换为打包的RGBA格式因此只需要一维
					uint8_t *data[2] = {
    
     0 };
					data[0] = rgb;
					int lines[2] = {
    
     0 };
					lines[0] = frame->width * 4;//存放一行数据大小,
					re = sws_scale(vctx,
						frame->data,		//输入数据
						frame->linesize,	//输入行大小
						0,
						frame->height,		//输入高度
						data,				//输出数据和大小
						lines				//一行的大小
					);
					cout << "sws_scale = " << re << endl;
				}
			}
			else if (cc == ac)//音频
			{
    
    
				uint8_t *data[2] = {
    
    0};
				//乘以样本大小S16的2个字节,乘以输出的通道数2,则一帧存放需要这么多字节
				if (!pcm) pcm = new uint8_t[frame->nb_samples * 2 * 2];
				data[0] = pcm;
				//返回单通道样本数的数量
				int len = swr_convert(
					actx,
					data,frame->nb_samples,//输出数据的存放地址,样本数量
					(const uint8_t**)frame->data,frame->nb_samples//输入数据的存放地址,样本数量
				);
				cout << "swr_convert = " << len << endl;
			}
		}



		//XSleep(500);
	}
	av_frame_free(&frame);
	av_packet_free(&pkt);



	if (ic)
	{
    
    
		//释放封装上下文,并且把ic置0
		avformat_close_input(&ic);
	}

	getchar();
	return 0;
}

源码:Git地址

猜你喜欢

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