FFmpeg庖丁解牛系列之log模块

引子

注:本文代码基于FFmpeg-3.4.2

av_log日志系统是FFmpeg里最基本的一个模块,主要提供控制台打印输出,与此相关的源文件主要有如下几个:

libavutil/log.c
libavutil/log.h
libavutil/bprint.c
libavutil/bprint.h

其中,bprint是底层打印buffer管理,通过av_bprint相关接口将待打印的内容(有可能是变参列表)转换成字符串,并存储到底层打印buffer里。
文件log.c里则实现了对bprint的封装,并为整个FFmpeg提供API接口调用,log.h里包含了相关的头文件。

bprint模块

首先,在bprint.h头文件里,通过宏定义FF_PAD_STRUCTURE定义了一个结构体AVBPrint,并且,为了后期扩展结构体兼容,按1024字节对齐结构体(padding),

typedef struct AVBPrint{ \
    char *str;         /**< string so far */
    unsigned len;      /**< length so far */
    unsigned size;     /**< allocated memory */
    unsigned size_max; /**< maximum allocated memory */
    char reserved_internal_buffer[1];
    char reserved_padding[size - sizeof(struct ff_pad_helper_AVBPrint)]; \
} AVBPrint;

下面逐一介绍bprint的接口函数:

void av_bprint_init(AVBPrint *buf, unsigned size_init, unsigned size_max);

以上函数完成一个AVBPrint 类型打印buffer的初始化,内存由内部的av_bprint_alloc函数分配,初始化大小为size_init,而第三个参数size_max表示此buffer最大尺寸,根据值的不同有不同的意义:0表示此buffer不写入任何内容,仅进行长度累加计数;1是自动模式,大小可以取最大值;-1则是最大整数,UINT_MAX

void av_bprint_init_for_buffer(AVBPrint *buf, char *buffer, unsigned size);

以上函数,基于参数提供的buffer缓冲区,初始化一个AVBPrint 类型的buffer,内存分配由调用者完成。

void av_bprintf(AVBPrint *buf, const char *fmt, ...) av_printf_format(2, 3);

将一个格式化后的字符串输出到打印buffer,这里需要注意函数声明里有语句av_printf_format(2, 3),其对应的宏定义如下,

#define av_printf_format(fmtpos, attrpos) _attribute__((__format__(__printf__, fmtpos, attrpos)))

attribute((format(printf, a, b))) 是一种请求编译器对参数格式进行检查的声明,其中,printf 指定按什么风格检查,这里是printf的风格;a 指定传入函数的第几个参数是格式化字符串;b 指定从函数的第几个参数开始按上述规则检查。所以av_printf_format(2, 3) 表示基于printf风格,第2个参数为格式化字符串,从第3个参数开始检查。(具体介绍可以参考文章 https://blog.csdn.net/huangjh2017/article/details/76944564

void av_vbprintf(AVBPrint *buf, const char *fmt, va_list vl_arg);

此函数功能与前一个函数类似,只是参数由可变参数列表变成已经转化过的 va_list 了。

void av_bprint_chars(AVBPrint *buf, char c, unsigned n);

此函数将n个字符c打印输出到打印buffer(AVBPrint 结构体的buffer);

void av_bprint_append_data(AVBPrint *buf, const char *data, unsigned size);

此函数将从data指针传过来的size大小的字符串附加到打印buffer(AVBPrint 结构体的buffer);

void av_bprint_strftime(AVBPrint *buf, const char *fmt, const struct tm *tm);

此函数将参数tm时间,按照参数fmt格式输出,并存储到打印buffer(AVBPrint结构体的buffer),以代替C库函数strftime,注释说标准strftime 函数是 poor design,哈哈;

void av_bprint_get_buffer(AVBPrint *buf, unsigned size, unsigned char **mem, unsigned *actual_size);

此函数获取 size 大小的打印buffer空闲区,如果空闲大小不够,内部会自动分配,mem 返回空闲内存的起始地址,actual_size 返回实际分配的空间大小;

void av_bprint_clear(AVBPrint *buf);

此函数清除打印buffer里的所有内容;

static inline int av_bprint_is_complete(const AVBPrint *buf)

此函数返回打印buffer里是否有内容;

int av_bprint_finalize(AVBPrint *buf, char **ret_str);

此函数销毁一个打印buffer,并可以通过ret_str获取销毁前buffer里的字符串内容,但是打印buffer里的内存需要调用者主动释放。如果ret_str传入的是NULL,则会自动释放打印buffer的内存。这样做的目的应该是为了不同需求,以满足灵活的调用吧。

以下是一些内部函数的介绍,

static int av_bprint_alloc(AVBPrint *buf, unsigned room)

给打印buffer分配 room 大小的空间。

static void av_bprint_grow(AVBPrint *buf, unsigned extra_len)

此函数完成打印buffer增加 extra_len 长度的字节,内部变量buf->len会增加extra_len

#define av_bprint_room(buf) ((buf)->size - FFMIN((buf)->len, (buf)->size))

此宏定义返回打印buffer里空闲内存空间的大小。

log模块结构定义

通过log.h头文件可以了解log模块主要的对外API,

typedef enum {
    AV_CLASS_CATEGORY_NA = 0,
    AV_CLASS_CATEGORY_INPUT,
    AV_CLASS_CATEGORY_OUTPUT,
    AV_CLASS_CATEGORY_MUXER,
    AV_CLASS_CATEGORY_DEMUXER,
    AV_CLASS_CATEGORY_ENCODER,
    AV_CLASS_CATEGORY_DECODER,
    AV_CLASS_CATEGORY_FILTER,
    AV_CLASS_CATEGORY_BITSTREAM_FILTER,
    AV_CLASS_CATEGORY_SWSCALER,
    AV_CLASS_CATEGORY_SWRESAMPLER,
    AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT = 40,
    AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT,
    AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,
    AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT,
    AV_CLASS_CATEGORY_DEVICE_OUTPUT,
    AV_CLASS_CATEGORY_DEVICE_INPUT,
    AV_CLASS_CATEGORY_NB  ///< not part of ABI/API
}AVClassCategory;

此枚举将FFmpeg里的功能模块进行分类,这样在打印时可以区别打印,显示类别名称等。

typedef struct AVClass {
    const char* class_name;  /*class名,如AC3 decoder*/
    const char* (*item_name)(void* ctx);  /*获取class name的注册回调函数*/
    const struct AVOption *option; /*option选项信息,模块参数配置相关的,我们下一篇文章详细介绍*/
    int version; /*FFmpeg代码里avutil模块的版本号,用于版本号不同时的一些差异化处理*/
    int log_level_offset_offset; /*打印级别成员变量在结构体中的偏移地址,0的话表示没有这个偏移成员变量*/
    int parent_log_context_offset; /*保存父类log context(也就是av_log函数的第一个参数)的偏移地址*/
    void* (*child_next)(void *obj, void *prev); /*下一个带option的子链接对象*/
    const struct AVClass* (*child_class_next)(const struct AVClass *prev); /*下一个带option的子链接对象class*/
    AVClassCategory category; /*class所属的类别,前面宏定义*/
    AVClassCategory (*get_category)(void* ctx); /*获取context的类别*/
    int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags); /*注册的回调函数,获取key键option的范围*/
} AVClass;

child_nextchild_class_next的作用,主要用于链表式的音视频处理结构中(如libavfilter),通过这两个接口获取下一处理单元以及类别class 。

以下是打印级别的宏定义:

#define AV_LOG_QUIET    -8
#define AV_LOG_PANIC     0
#define AV_LOG_FATAL     8
#define AV_LOG_ERROR    16
#define AV_LOG_WARNING  24
#define AV_LOG_INFO     32
#define AV_LOG_VERBOSE  40
#define AV_LOG_DEBUG    48
#define AV_LOG_TRACE    56

log模块API

void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);

这是核心log输出函数,第一个参数是av context结构,第二个参数是打印级别,第三个参数是格式字符串,第四个参数可变参数列表。同样,编译器需要进行类似printf的格式检查。需要说明的是第一个参数指向的结构体类型可能不大一样,如AVFilterContextAVCodecContext都可以作为指针传递进来,但是这些结构体的第一个成员变量都是const AVClass *av_class; 类型,所以在av_log函数实现里,会直接取这个class,从而根据不同的class得到class_name,打印输出,提供更加丰富的日志输出。

void av_vlog(void *avcl, int level, const char *fmt, va_list vl);

此函数与前述接口类似,只是可变参数已由整理后的va_list代替。

int av_log_get_level(void);
void av_log_set_level(int level);

获取和设置当前的整体打印级别(类似于打印门槛),当调用语句的打印级别小于或者等于设置的整体打印级别时,输出打印结果,否则不予输出。由前述打印级别宏定义看出,打印级别值越小,问题严重程度越高,越是应该输出日志信息。

void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));

此函数注册一个回调函数到log模块,从而所有的log输出最终都会转到用户注册的这个回调函数里执行。比如在Android平台上,我们就可以注册一个类似的函数,将所有日志通过logcat输出。
特别需要注意的是,用户函数必须是线程安全的,也就是说可能多个线程同时调用此函数,可能需要添加互斥量保护临界区。

void av_log_default_callback(void *avcl, int level, const char *fmt, va_list vl);

这是默认的log日志打印函数,如果用户没有注册自己的回调函数,那么所有日志将通过此函数执行输出。稍后我们分析此函数的实现代码。

const char* av_default_item_name(void* ctx);
AVClassCategory av_default_get_category(void *ptr);

这两个函数分别获取class_namecategory类别。

void av_log_format_line(void *ptr, int level, const char *fmt, va_list vl, char *line, int line_size, int *print_prefix);
int av_log_format_line2(void *ptr, int level, const char *fmt, va_list vl, char *line, int line_size, int *print_prefix);

两个函数功能一样,内部av_log_format_line通过调用av_log_format_line2实现。
此函数将待打印的字符串信息进行格式化,并加上相关头部信息,如class_name,category类别名,错误打印级别字符串(如果flag设置了AV_LOG_PRINT_LEVEL宏定义来打印这部分信息)等组合输出。

void av_log_set_flags(int arg);
int av_log_get_flags(void);

设置和获取 flags 标记,注意不要和打印级别混淆。现在 flags 标记只有两种选项,一个是对于重复的打印,是否逐条输出,由宏定义AV_LOG_SKIP_REPEATED 配置,另一个就是前述的是否在打印前加上打印级别字符串信息,由宏定义 AV_LOG_PRINT_LEVEL 配置。

log模块核心实现

av_log_default_callback 函数是默认的日志输出实现,它赋值给了如下静态变量,作为 av_log_callback 的初始值,如果没有用户注册新的打印回调函数,则调用此函数输出日志。

static void (*av_log_callback)(void*, int, const char*, va_list) = av_log_default_callback;

首先判断当前打印级别,如果高于整体设置级别,就不输出日志,

if (level > av_log_level)
        return;

由于打印要求是线程安全的,所以这里引入互斥量对,做保护,

    pthread_mutex_lock(&mutex);
    pthread_mutex_unlock(&mutex);

format_line函数,完成待打印内容的格式化,主要依赖bprint模块来实现,并将四部分(class_namecategory,打印级别字符串,打印内容)分别存储到四个不同的打印buffer 。接下来,通过c库函数snprintf,将这四部分组合成一个字符串,存储到静态变量 prev 里,以便下次调用此函数时判断是否是重复打印,再根据打印配置标记 flags 做对应处理。
最后,这四部分打印信息通过 sanitize 函数完成不可打印字符(如控制字符)的过滤处理,并用“?”代替。通过 colored_fputs 函数以不同颜色输出到标准输出stderr。关于颜色打印,可以参考文档 https://blog.csdn.net/ericbar/article/details/79652086

收尾

log模块是FFmpeg的基础模块,主要是对基本log输出进行封装,添加了class name,category,打印级别等信息,并按照不同等级进行颜色输出管理,方便我们在模块调试时使用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值