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);
		}

	}   
}
相关推荐
l***77524 小时前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
ZouZou老师10 小时前
FFmpeg性能优化经典案例
性能优化·ffmpeg
aqi0012 小时前
FFmpeg开发笔记(九十)采用FFmpeg套壳的音视频转码百宝箱FFBox
ffmpeg·音视频·直播·流媒体
齐齐大魔王14 小时前
FFmpeg
ffmpeg
你好音视频16 小时前
FFmpeg RTSP拉流流程深度解析
ffmpeg
IFTICing1 天前
【环境配置】ffmpeg下载、安装、配置(Windows环境)
windows·ffmpeg
haiy20111 天前
FFmpeg 编译
ffmpeg
aqi001 天前
FFmpeg开发笔记(八十九)基于FFmpeg的直播视频录制工具StreamCap
ffmpeg·音视频·直播·流媒体
八月的雨季 最後的冰吻1 天前
FFmepg--28- 滤镜处理 YUV 视频帧:实现上下镜像效果
ffmpeg·音视频
ganqiuye1 天前
向ffmpeg官方源码仓库提交patch
大数据·ffmpeg·video-codec