ffmpeg av_parser_parse2函数分析各种码流测试程序

ffmpeg av_parser_parse2函数分析各种码流测试程序


author: hjjdebug

date: 2023年 07月 14日 星期五 16:14:05 CST


测试程序见后, 可编译运行(ffmpeg v4.4)

我需要一个简单的程序,实现能跟入ffmpeg 库.了解av_parser_parse2的工作原理.

下面是跟踪调试部分记录.

关于测流码流的获取,通常我们见到的是音视频在一起的ts流文件,你可以用ffmpeg 工具提取.

例如: 找一个或录一个h264的音视频码率,存为1.ts, 用下面命令抽取出视频扔掉音频.

ffmpeg -i 1.ts -c:v copy -an 1.h264

如果不是h264的码流,也可以转换,方法:

ffmpeg -i not_ffmpeg.ts -c:v h264 -an 1.h264


  1. avcodec_find_decoder(id);

id: AV_CODEC_ID_H264,

根据id来查找AVCodec, 所有的codec 用一个列表指针来管理,依次枚举这个列表,在libavcodec/codec_list.c中定义

看看它们的id与所查的是否一致就可以了,这样就找到了 一个对象指针 ff_h264_decoder, 在libavcodec/h264dec.c中定义

一些关键函数如.init,.decode,.close等自然都有了着落.

AVCodec ff_h264_decoder = {

.name = "h264",

.long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),

.type = AVMEDIA_TYPE_VIDEO,

.id = AV_CODEC_ID_H264,

.priv_data_size = sizeof(H264Context),

.init = h264_decode_init,

.close = h264_decode_end,

.decode = h264_decode_frame,

.capabilities = /*AV_CODEC_CAP_DRAW_HORIZ_BAND |*/ AV_CODEC_CAP_DR1 |

AV_CODEC_CAP_DELAY | AV_CODEC_CAP_SLICE_THREADS |

AV_CODEC_CAP_FRAME_THREADS,

.... //忽略若干硬件加速项

.caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_EXPORTS_CROPPING |

FF_CODEC_CAP_ALLOCATE_PROGRESS | FF_CODEC_CAP_INIT_CLEANUP,

.flush = h264_decode_flush,

.update_thread_context = ONLY_IF_THREADS_ENABLED(ff_h264_update_thread_context),

.profiles = NULL_IF_CONFIG_SMALL(ff_h264_profiles),

.priv_class = &h264_class,

};


  1. AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);

依据pCodec分配一个AVCodecContext, 并进行必要的初始化

AVCodecContext, 我看了一眼,1800行的结构定义令人镗目结舌!

可见codec 结构不大,但ctx 结构很大!!

它的第一个重要单元是

typedef struct AVCodecContext { //1800 行的结构定义

const AVClass *av_class; // 它是一个AVClass, 在这个类指针中,会描述该Context类选项信息

...

}

这里介绍几个概念,默认值

没有codec 的ctx;

s->codec 时基 time_base = {0,1}

s->framerate = {0,1}

s->pkg_timebase = {0,1}

s->sample_aspect_ratio = {0,1}

s->pix_fmt = AV_PIX_FMT_NONE;

s->sample_fmt = AV_SAMPLE_FMT_NONE;

s->av_class = &av_codec_context_class; //重要!! 由这个指针可以找到这个类叫什么名称,有什么选项等信息

这里,所有的codecCtx 都指向一个av_codec_context_class 对象地址. 这是一个AVClass 对象

av_codec_context_class 定义在libavcodec/options.c中

看一下它的定义:

static const AVClass av_codec_context_class = {

.class_name = "AVCodecContext",

.item_name = context_to_name,

.option = avcodec_options,

.version = LIBAVUTIL_VERSION_INT,

.log_level_offset_offset = offsetof(AVCodecContext, log_level_offset),

.child_next = codec_child_next,

#if FF_API_CHILD_CLASS_NEXT

.child_class_next = codec_child_class_next,

#endif

.child_class_iterate = codec_child_class_iterate,

.category = AV_CLASS_CATEGORY_ENCODER,

.get_category = get_category,

};

类名称"AVCodecContext"

codec名称函数:根据codecCtx指针,返回codecCtx->codec->name

查找codec私有数据函数, 返回s->priv_data

查找codec私有类函数, 返回s->priv_class 等函数

通用选项定义表: avcodec_options

由于定义了选项表,一个函数调用就能填充AVCodecCtx对象的一些默认选项. av_opt_set_defaults2(void *s)

opt = av_opt_next(s,opt); //获取s对象的下一个选项地址. s对象是分配的ctx对象,

根据opts 的定义把其值填入s 所在的成员中. 例如填整数值

write_number(s, opt, dst, 1, 1, opt->default_val.i64);

单纯的写num, s指针是不需要的,但出错时s指针可以传递给av_log,故保留其指针

小知识: AVClass 类是什么? (最抽象的类)

从AVClass 的一个实例av_codec_context_class 中, 我们也大概知道了AVClass 类是干什么的了,它是描述一个

Context类的类,说明了被描述者的名称,选项等信息.

//跟codec相关的信息要保留

s->codec = codec;

s->codec_id = codec->id;

s->codec_type = codec->type;

if(codec->priv_data_size)

{

s->priv_data=av_mallocz(codec->priv_data_size); //H264Context ,大小53k, 看前面定义

if(codec->priv_class) //codec 的私有类 h264_class,参加前面decoder定义

{

*(const AVClass**)s->priv_data = codec->priv_class; //私有类指针填充到私有数据第一项

av_opt_set_defaults(s->priv_data); // 这个priv_data 是一个含av_optios的AVClass 对象,所以可以对其成员进行初始化.

}

}

h264_class 定义很简单. 如下,是一个AVClass 对象: 这样可以对H264Context 进行一些默认选项设置.

static const AVClass h264_class = {

.class_name = "H264 Decoder",

.item_name = av_default_item_name,

.option = h264_options,

.version = LIBAVUTIL_VERSION_INT,

};

H264Context 定义有200多行就不copy了.

H264Context 与 AVCodecContext 是什么关系, 前者是被包在后者之中的.

这样我们认为codecCtx 准备好了

小知识:AVCodec 类是什么? 以ff_h264_decoder 为例

AVCodec 是针对某种数据类型而定义的解码或编码框架,定义了codec的ID,TYPE,定义了初始化,打开,关闭函数等.

小知识:AVCodecContext 是什么数据类型?

答:对于某种给定的Codec,可以认为它是一个单实例类,定义了对数据的操作,可以用全局变量定义,而ConText类,则是包含该单实例对象的

普通实例类,可以有多个Context对象,每个对象对应于具体的一个文件或数据流,保留了流中具体的宽,高,比特率等属性信息. codec的操作结果

往往就保存在Context中, 所以Context 类往往有很大的数据结构.


AVCodecParserContext* pCodecParserCtx = av_parser_init(vDecoderID);//根据解码器类型获取解析器上下文


该函数返回一个 AVCodecParserContext, 大小352字节.

typedef struct AVCodecParserContext {

void *priv_data;

struct AVCodecParser *parser;

int64_t pts;

int64_t dts;

....

};

p sizeof(AVCodecParserContext)

$9 = 352

parseContext 有一个重要成员,那就是parser(AVCodecParser 类型).

parser 类有一个list ,在avcodec/parser_list.c 中定义,我们遍历这张表,根据 AV_CODEC_ID_H264 找到了ff_h264_parser

AVCodecParser ff_h264_parser = {

.codec_ids = { AV_CODEC_ID_H264 },

.priv_data_size = sizeof(H264ParseContext), //大小3144字节

.parser_init = init, //分配完私有数据,调用这个init,当然对这个私有数据进行一定的初始化了.

.parser_parse = h264_parse, //这个函数很关键,是264数据的分析函数入口

.parser_close = h264_close,

.split = h264_split,

};

typedef struct H264ParseContext {

ParseContext pc;

H264ParamSets ps;

H264DSPContext h264dsp;

H264POCContext poc;

H264SEIContext sei;

int is_avc;

int nal_length_size;

int got_first;

int picture_structure;

uint8_t parse_history[6];

int parse_history_count;

int parse_last_mb;

int64_t reference_dts;

int last_frame_num, last_picture_structure;

} H264ParseContext;

可以理解为AVCodecParserContext 是接口类

H264ParseContext 是实现类

(gdb) p parser->priv_data_size

$10 = 3144

(gdb)


//解析处理,avPacket.size不为0,说明解析出完整的一帧数据

int len = av_parser_parse2(pCodecParserCtx, pCodecCtx,

&avPacket.data, &avPacket.size, //输出

pCurBuf, dwCurBufSize, //输入

AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);


由于pCodecParserCtx 被AV_CODEC_ID_H264 初始化过,

s->parser = (AVCodecParser*)parser; //这个被枚举到的parser就是ff_h264_parser, 想当于把子类对象指针付给了父类指针

所以 s->parser->parser_parse 会调用到h264_parse()

此函数会调用

next = h264_find_frame_end(p, buf, buf_size, avctx); //annex-b 模式,找头000001,并且要包含图像帧

parse_nal_units(s, avctx, buf, buf_size); //对各单元的解析,用到了bit 操作函数及 哥伦布解码

cpp 复制代码
$ cat main.cpp
/*
 * 了解av_parser_parse2 的工作原理 测试代码
 * 跟踪调试进入ffmpeg 内部才会有收获!
 */
#include <stdio.h>
extern "C"
{
#include "libavcodec/avcodec.h"
};

#define _READ_BUF_SIZE 40960
int main()
{
	uint8_t* pReadBuf = new uint8_t[_READ_BUF_SIZE];//每次从文件中读取数据的缓冲
	memset(pReadBuf, 0, _READ_BUF_SIZE);
	const char* iFileName = "1.h264";//"1.mpeg4";"1.aac"; 裸码流的获取,可通过ffmpeg 获取
	const char* oFileName = "t1.data";//
	FILE* fp = fopen(iFileName, "rb");
	if (!fp) 
	{
		return -1;
	}

	FILE* fp_out = fopen(oFileName, "wb");
	if (!fp_out) return -1;
	
//	enum AVCodecID vDecoderID = AV_CODEC_ID_MP3;//AV_CODEC_ID_MPEG4;//AV_CODEC_ID_H264;
	enum AVCodecID vDecoderID = AV_CODEC_ID_H264;  //还可以指定mp3,mpeg4类型,看文件数据类型
	AVCodec *pCodec = avcodec_find_decoder(vDecoderID);//获取指定类型的解码器, parser 需要codec_ctx
	if(!pCodec)
	{
		return -1;
	}
	AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);//根据解码器分配一个解码器上下文
	if (!pCodecCtx)
	{
		return -1;
	}
	AVCodecParserContext* pCodecParserCtx = av_parser_init(vDecoderID);//根据解码器类型获取解析器上下文
	if (!pCodecParserCtx)
	{
		return -1;
	}

	uint8_t* pCurBuf = NULL;              //记录缓冲读指针位置
	AVPacket avPacket;
	int dwCurBufSize = 0;           //记录当前缓冲中剩余有效数据长度
	for (;;)
	{
		dwCurBufSize = fread(pReadBuf, 1, _READ_BUF_SIZE, fp);
		if (feof(fp))
		{
			printf("end of file\n");
			break;
		}
		if (dwCurBufSize == 0)
		{
			return -1;
		}
		pCurBuf = pReadBuf;
		while (dwCurBufSize > 0)
		{
			//解析处理,avPacket.size不为0,说明解析出完整的一帧数据
			int len = av_parser_parse2(pCodecParserCtx, pCodecCtx, 
					&avPacket.data, &avPacket.size,      //输出
					pCurBuf, dwCurBufSize,                //输入
					AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);

			pCurBuf += len; //移动缓冲读指针
			dwCurBufSize -= len;//剩余缓冲数据长度
			if (avPacket.size == 0)
			{
				//如果输出size为0 说明输入长度不够解析为完整的一帧,需要继续输入数据
				continue;
			}
			//打印输出帧信息
			printf("Packet Seq Num:%d\t", pCodecParserCtx->output_picture_number);
			printf("KeyFrame:%d pts:%ld, dts:%ld, duration:%d\t", pCodecParserCtx->key_frame, 
					pCodecParserCtx->pts, pCodecParserCtx->dts, pCodecParserCtx->duration);
			switch(pCodecParserCtx->pict_type)
			{
				case AV_PICTURE_TYPE_I:
					printf("\tPacket Type:I\t");
					break;
				case AV_PICTURE_TYPE_P:
					printf("\tPacket Type:P\t");
					break;
				case AV_PICTURE_TYPE_B:
					printf("\tPacket Type:B\t");
					break;
				default:
					printf("\tPacket Type:error:%d\t", pCodecParserCtx->pict_type);
					break;
			}

			printf("\tPacket Size:%d\n", avPacket.size);
			//将长度信息保存到文件中
			fprintf(fp_out, "%d\n", avPacket.size);
		}

	}   
}
相关推荐
yunhuibin6 小时前
ffmpeg面向对象——拉流协议匹配机制探索
学习·ffmpeg
cuijiecheng201812 小时前
音视频入门基础:FLV专题(13)——FFmpeg源码中,解析任意Type值的SCRIPTDATAVALUE类型的实现
ffmpeg·音视频
小神.Chen1 天前
YouTube音视频合并批处理基于 FFmpeg的
ffmpeg·音视频
昱禹3 天前
记一次因视频编码无法在浏览器播放、编码视频报错问题
linux·python·opencv·ffmpeg·音视频
寻找09之夏3 天前
【FFmpeg 深度解析】:全方位视频合成
ffmpeg·音视频
zanglengyu3 天前
ffmpeg取rtsp流音频数据保存声音为wav文件
ffmpeg·音视频
cuijiecheng20183 天前
音视频入门基础:FLV专题(11)——FFmpeg源码中,解析SCRIPTDATASTRING类型的ScriptDataValue的实现
ffmpeg·音视频
汪子熙3 天前
什么是 LDAC、SBC 和 AAC 音频编码技术
ffmpeg·音视频·aac
cpp_learners4 天前
Windows环境 源码编译 FFmpeg
windows·ffmpeg·源码编译·ffmpeg源码编译
cuijiecheng20184 天前
音视频入门基础:FLV专题(8)——FFmpeg源码中,解码Tag header的实现
ffmpeg·音视频