ffmpeg中av_log的实现分析
[时间:2017-10] [状态:Open]
[关键词:ffmpeg,avutil,av_log, 日志输出]
0 引言
FFmpeg的libavutil中的日志输出的接口整体比较少,但是功能还是不错的,对于后续自己实现日志模块还是值得参考的。本文就libavutil中的日志模块部分的实现做一个简要的整理。希望可以达到解释清楚的目的。
注意:本部分的主要代码位于libavtuil/log.h、libavutil/log.c。
1 AVClass的定义部分
log接口的输出是依赖于AVClass的,所以我们在log.h头文件中首先看到的是AVClass的定义,具体如下:
/* 这是一个使用c的结构体模拟c++的类的实现逻辑 */
typedef struct AVClass {
/* 类名字 */
const char* class_name;
/* 返回相关结构体的名称的回调函数 */
const char* (*item_name)(void* ctx);
const struct AVOption *option;
/* 该结构创建时的LIBAVUTIL_VERSION */
int version;
/* log_level_offset在结构体的偏移量 */
int log_level_offset_offset;
/* 父context指针保存位置的偏移量,可以为0 */
int parent_log_context_offset;
/* 返回支持AVOption的下一个子对象的函数指针 */
void* (*child_next)(void *obj, void *prev);
/* 返回下一个支持AVOption的子对象对应的AVClass的函数指针 */
const struct AVClass* (*child_class_next)(const struct AVClass *prev);
/* 用于区分AVClass的类别,比如muxer、demuxer、encoder、decoder等 */
AVClassCategory category;
/* 用于返回AVClassCategory的函数指针 */
AVClassCategory (*get_category)(void* ctx);
/* 返回所支持范围的函数指针 */
int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags);
} AVClass;
这里举个例子,以mp3的demuxer为例,我们看看代码中是如何初始化的:
/* 这里只是初始化了部分AVClass成员 */
static const AVClass demuxer_class = {
.class_name = "mp3",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
.category = AV_CLASS_CATEGORY_DEMUXER,
};
/* AVInputFormat拥有一个AVClass数据域priv_class */
AVInputFormat ff_mp3_demuxer = {
.name = "mp3",
.long_name = NULL_IF_CONFIG_SMALL("MP2/3 (MPEG audio layer 2/3)"),
.read_probe = mp3_read_probe,
.read_header = mp3_read_header,
.read_packet = mp3_read_packet,
.read_seek = mp3_seek,
.priv_data_size = sizeof(MP3DecContext),
.flags = AVFMT_GENERIC_INDEX,
.extensions = "mp2,mp3,m2a,mpa", /* XXX: use probe */
.priv_class = &demuxer_class,
};
2 对外接口部分的实现
libavutil中的日志模块主要有以下几个接口及其实现如下:
// 这是c里面的可变参数列表的标准用法
void av_log(void* avcl, int level, const char *fmt, ...)
{
AVClass* avc = avcl ? *(AVClass **) avcl : NULL;
va_list vl;
va_start(vl, fmt);
if (avc && avc->version >= (50 << 16 | 15 << 8 | 2) &&
avc->log_level_offset_offset && level >= AV_LOG_FATAL)
level += *(int *) (((uint8_t *) avcl) + avc->log_level_offset_offset);
av_vlog(avcl, level, fmt, vl);
va_end(vl);
}
void av_vlog(void* avcl, int level, const char *fmt, va_list vl)
{
void (*log_callback)(void*, int, const char*, va_list) = av_log_callback;
if (log_callback)
log_callback(avcl, level, fmt, vl);
}
int av_log_get_level(void)
{
return av_log_level;
}
void av_log_set_level(int level)
{
av_log_level = level;
}
void av_log_set_flags(int arg)
{
flags = arg;
}
int av_log_get_flags(void)
{
return flags;
}
void av_log_set_callback(void (*callback)(void*, int, const char*, va_list))
{
av_log_callback = callback;
}
从上面代码中可以看到这几个对外接口实现都是比较简单,主要是设置几个全局的参数,并调用av_log_callback输出日志。我们先看看其中几个全局变量的定义:
// 默认日志输出的访问锁
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 当前的日志输出级别及标志
static int av_log_level = AV_LOG_INFO;
static int flags;
// 默认的日志输出回调函数
static void (*av_log_callback)(void*, int, const char*, va_list) =
av_log_default_callback;
3 后台隐藏的部分代码
日志模块提供了标准的日志输出格式,同时可以设置输出日志的颜色,并通过AVClass获得更多对象的属性。本部分将主要围绕av_log_default_callback的实现分析。其代码如下:
void av_log_default_callback(void* ptr, int level, const char* fmt, va_list vl)
{
static int print_prefix = 1;
static int count;
static char prev[LINE_SZ]; // 注意这里的所有的静态变量是共享的
AVBPrint part[4];
char line[LINE_SZ]; // LINE_SZ = 1024
static int is_atty;
int type[2];
unsigned tint = 0;
if (level >= 0) {
tint = level & 0xff00;
level &= 0xff;
}
// 对于日志级别较高的不输出
if (level > av_log_level)
return;
// 为了保证日志输出的线程安全,这里加了锁
#if HAVE_PTHREADS
pthread_mutex_lock(&mutex);
#endif
// 格式化数据,最终保存在line中
format_line(ptr, level, fmt, vl, part, &print_prefix, type);
snprintf(line, sizeof(line), "%s%s%s%s", part[0].str, part[1].str, part[2].str, part[3].str);
#if HAVE_ISATTY
if (!is_atty)
is_atty = isatty(2) ? 1 : -1;
#endif
// 打印日志前缀,并过滤重复日志
if (print_prefix && (flags & AV_LOG_SKIP_REPEATED) && !strcmp(line, prev) &&
*line && line[strlen(line) - 1] != '\r'){
count++;
if (is_atty == 1)
fprintf(stderr, " Last message repeated %d times\r", count);
goto end;
}
// 输出重复日志的次数,并重置计数
if (count > 0) {
fprintf(stderr, " Last message repeated %d times\n", count);
count = 0;
}
strcpy(prev, line);
// 分部分日志输出
sanitize(part[0].str);
colored_fputs(type[0], 0, part[0].str);
sanitize(part[1].str);
colored_fputs(type[1], 0, part[1].str);
sanitize(part[2].str);
colored_fputs(av_clip(level >> 3, 0, NB_LEVELS - 1), tint >> 8, part[2].str);
sanitize(part[3].str);
colored_fputs(av_clip(level >> 3, 0, NB_LEVELS - 1), tint >> 8, part[3].str);
#if CONFIG_VALGRIND_BACKTRACE
if (level <= BACKTRACE_LOGLEVEL)
VALGRIND_PRINTF_BACKTRACE("%s", "");
#endif
end:
// 输出完成的资源释放及锁释放
av_bprint_finalize(part+3, NULL);
#if HAVE_PTHREADS
pthread_mutex_unlock(&mutex);
#endif
}
从上面代码可以看出,av_log_default_callback的关键是通过AVBPrint、format_line、sanitize、colored_fputs实现的。下面将依次介绍这几个函数和结构体。
AVBPrint结构体
该结构体定义在libavutil/bprint.h,定义如下:
/* 为了兼容性添加了填充字节的结构体 */
#define FF_PAD_STRUCTURE(name, size, ...) \
struct ff_pad_helper_##name { __VA_ARGS__ }; \
typedef struct name { \
__VA_ARGS__ \
char reserved_padding[size - sizeof(struct ff_pad_helper_##name)]; \
} name;
FF_PAD_STRUCTURE(AVBPrint, 1024,
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];
)
其中提供的接口函数如下:
#define AV_BPRINT_SIZE_UNLIMITED ((unsigned)-1)
#define AV_BPRINT_SIZE_AUTOMATIC 1
#define AV_BPRINT_SIZE_COUNT_ONLY 0
/* 初始化print buffer,类似构造函数 */
void av_bprint_init(AVBPrint *buf, unsigned size_init, unsigned size_max);
/* 使用给定buffer初始化print buffer */
void av_bprint_init_for_buffer(AVBPrint *buf, char *buffer, unsigned size);
/* 向print buffer中添加格式化字符串(字符、二进制) */
void av_bprintf(AVBPrint *buf, const char *fmt, ...) av_printf_format(2, 3);
void av_vbprintf(AVBPrint *buf, const char *fmt, va_list vl_arg);
void av_bprint_chars(AVBPrint *buf, char c, unsigned n);
void av_bprint_append_data(AVBPrint *buf, const char *data, unsigned size);
/* 为了外部使用而分配存储空间 */
void av_bprint_get_buffer(AVBPrint *buf, unsigned size,
unsigned char **mem, unsigned *actual_size);
/* 清空print buffer,但保留已分配的空间 */
void av_bprint_clear(AVBPrint *buf);
/* 查看print buffer是否完整(非截断) */
static inline int av_bprint_is_complete(const AVBPrint *buf)
{
return buf->len < buf->size;
}
/* 反初始化print buffer,调用之后该结构无法使用 */
int av_bprint_finalize(AVBPrint *buf, char **ret_str);
/* 转义src中字符,并保存到dstbuff中 */
void av_bprint_escape(AVBPrint *dstbuf, const char *src, const char *special_chars,
enum AVEscapeMode mode, int flags);
这里说明下av_printf_format(2, 3)的含义,这是GCC扩展的语法格式,ffmpeg中该宏的定义如下:#define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos)))
。
它的主要作用是提示编译器,对这个函数的调用需要像printf一样,用对应的format字符串来check可变参数的数据类型。例如:
extern int my_printf (void *my_object, const char *my_format, ...) __attribute__ ((format (printf, 2, 3)));
format (printf, 2, 3)
告诉编译器,其第二个参数my_format相当于printf函数的format,而可变参数是从my_printf的第3个参数开始。这样编译器就会在编译时用和printf一样的check法则来确认可变参数是否正确了。
下面是以上关于AVBPrint接口的实现:
// AVBPrint中可用空间字节数
#define av_bprint_room(buf) ((buf)->size - FFMIN((buf)->len, (buf)->size))
#define av_bprint_is_allocated(buf) ((buf)->str != (buf)->reserved_internal_buffer)
static int av_bprint_alloc(AVBPrint *buf, unsigned room)
{
char *old_str, *new_str;
unsigned min_size, new_size;
if (buf->size == buf->size_max)// 已达到最大可使用的长度
return AVERROR(EIO);
if (!av_bprint_is_complete(buf))// 实际长度已经超过存储区域长度
return AVERROR_INVALIDDATA; /* it is already truncated anyway */
min_size = buf->len + 1 + FFMIN(UINT_MAX - buf->len - 1, room);
new_size = buf->size > buf->size_max / 2 ? buf->size_max : buf->size * 2;
if (new_size < min_size)
new_size = FFMIN(buf->size_max, min_size);
old_str = av_bprint_is_allocated(buf) ? buf->str : NULL;
new_str = av_realloc(old_str, new_size);
if (!new_str)
return AVERROR(ENOMEM);
if (!old_str)
memcpy(new_str, buf->str, buf->len + 1);
buf->str = new_str;
buf->size = new_size;
return 0;
}
static void av_bprint_grow(AVBPrint *buf, unsigned extra_len)
{
/* arbitrary margin to avoid small overflows */
extra_len = FFMIN(extra_len, UINT_MAX - 5 - buf->len);
buf->len += extra_len;
if (buf->size)
buf->str[FFMIN(buf->len, buf->size - 1)] = 0;
}
void av_bprint_init(AVBPrint *buf, unsigned size_init, unsigned size_max)
{
unsigned size_auto = (char *)buf + sizeof(*buf) -
buf->reserved_internal_buffer;
if (size_max == 1)
size_max = size_auto;
buf->str = buf->reserved_internal_buffer;
buf->len = 0;
buf->size = FFMIN(size_auto, size_max);
buf->size_max = size_max;
*buf->str = 0;
if (size_init > buf->size)
av_bprint_alloc(buf, size_init - 1);
}
void av_bprint_init_for_buffer(AVBPrint *buf, char *buffer, unsigned size)
{
buf->str = buffer;
buf->len = 0;
buf->size = size;
buf->size_max = size;
*buf->str = 0;
}
void av_bprintf(AVBPrint *buf, const char *fmt, ...)
{
unsigned room;
char *dst;
va_list vl;
int extra_len;
while (1) {
room = av_bprint_room(buf);
dst = room ? buf->str + buf->len : NULL;
va_start(vl, fmt);
extra_len = vsnprintf(dst, room, fmt, vl);
va_end(vl);
if (extra_len <= 0)
return;
if (extra_len < room)
break;
if (av_bprint_alloc(buf, extra_len))
break;
}
av_bprint_grow(buf, extra_len);
}
void av_vbprintf(AVBPrint *buf, const char *fmt, va_list vl_arg)
{
unsigned room;
char *dst;
int extra_len;
va_list vl;
while (1) {
room = av_bprint_room(buf);
dst = room ? buf->str + buf->len : NULL;
va_copy(vl, vl_arg);
extra_len = vsnprintf(dst, room, fmt, vl);
va_end(vl);
if (extra_len <= 0)
return;
if (extra_len < room)
break;
if (av_bprint_alloc(buf, extra_len))
break;
}
av_bprint_grow(buf, extra_len);
}
void av_bprint_chars(AVBPrint *buf, char c, unsigned n)
{
unsigned room, real_n;
while (1) {
room = av_bprint_room(buf);
if (n < room)
break;
if (av_bprint_alloc(buf, n))
break;
}
if (room) {
real_n = FFMIN(n, room - 1);
memset(buf->str + buf->len, c, real_n);
}
av_bprint_grow(buf, n);
}
void av_bprint_append_data(AVBPrint *buf, const char *data, unsigned size)
{
unsigned room, real_n;
while (1) {
room = av_bprint_room(buf);
if (size < room)
break;
if (av_bprint_alloc(buf, size))
break;
}
if (room) {
real_n = FFMIN(size, room - 1);
memcpy(buf->str + buf->len, data, real_n);
}
av_bprint_grow(buf, size);
}
void av_bprint_get_buffer(AVBPrint *buf, unsigned size,
unsigned char **mem, unsigned *actual_size)
{
if (size > av_bprint_room(buf))
av_bprint_alloc(buf, size);
*actual_size = av_bprint_room(buf);
*mem = *actual_size ? buf->str + buf->len : NULL;
}
void av_bprint_clear(AVBPrint *buf)
{
if (buf->len) {
*buf->str = 0;
buf->len = 0;
}
}
int av_bprint_finalize(AVBPrint *buf, char **ret_str)
{
unsigned real_size = FFMIN(buf->len + 1, buf->size);
char *str;
int ret = 0;
if (ret_str) {
if (av_bprint_is_allocated(buf)) {
str = av_realloc(buf->str, real_size);
if (!str)
str = buf->str;
buf->str = NULL;
} else {
str = av_malloc(real_size);
if (str)
memcpy(str, buf->str, real_size);
else
ret = AVERROR(ENOMEM);
}
*ret_str = str;
} else {
if (av_bprint_is_allocated(buf))
av_freep(&buf->str);
}
buf->size = real_size;
return ret;
}
#define WHITESPACES " \n\t\r"
void av_bprint_escape(AVBPrint *dstbuf, const char *src, const char *special_chars,
enum AVEscapeMode mode, int flags)
{
const char *src0 = src;
if (mode == AV_ESCAPE_MODE_AUTO)
mode = AV_ESCAPE_MODE_BACKSLASH; /* TODO: implement a heuristic */
switch (mode) {
case AV_ESCAPE_MODE_QUOTE:
/* enclose the string between '' */
av_bprint_chars(dstbuf, '\'', 1);
for (; *src; src++) {
if (*src == '\'')
av_bprintf(dstbuf, "'\\''");
else
av_bprint_chars(dstbuf, *src, 1);
}
av_bprint_chars(dstbuf, '\'', 1);
break;
/* case AV_ESCAPE_MODE_BACKSLASH or unknown mode */
default:
/* \-escape characters */
for (; *src; src++) {
int is_first_last = src == src0 || !*(src+1);
int is_ws = !!strchr(WHITESPACES, *src);
int is_strictly_special = special_chars && strchr(special_chars, *src);
int is_special =
is_strictly_special || strchr("'\\", *src) ||
(is_ws && (flags & AV_ESCAPE_FLAG_WHITESPACE));
if (is_strictly_special ||
(!(flags & AV_ESCAPE_FLAG_STRICT) &&
(is_special || (is_ws && is_first_last))))
av_bprint_chars(dstbuf, '\\', 1);
av_bprint_chars(dstbuf, *src, 1);
}
break;
}
}
这一部分实现代码逻辑比较简单。这里不做过多解释。
format_line
static void format_line(void *avcl, int level, const char *fmt, va_list vl,
AVBPrint part[4], int *print_prefix, int type[2])
{
AVClass* avc = avcl ? *(AVClass **) avcl : NULL;
av_bprint_init(part+0, 0, 1);// part[0]中保存ffmpeg内部的前缀格式,比如hls@12345678
av_bprint_init(part+1, 0, 1);// part[1]中保存AVClass所在对象的category
av_bprint_init(part+2, 0, 1);// part[2]中保存日志的级别
av_bprint_init(part+3, 0, 65536);// part[3]中保存实际输出的日志
if(type) type[0] = type[1] = AV_CLASS_CATEGORY_NA + 16;
if (*print_prefix && avc) {
if (avc->parent_log_context_offset) {
AVClass** parent = *(AVClass ***) (((uint8_t *) avcl) +
avc->parent_log_context_offset);
if (parent && *parent) {
av_bprintf(part+0, "[%s @ %p] ",
(*parent)->item_name(parent), parent);
if(type) type[0] = get_category(parent);
}
}
av_bprintf(part+1, "[%s @ %p] ",
avc->item_name(avcl), avcl);
if(type) type[1] = get_category(avcl);
if (flags & AV_LOG_PRINT_LEVEL)
av_bprintf(part+2, "[%s] ", get_level_str(level));
}
av_vbprintf(part+3, fmt, vl);
if(*part[0].str || *part[1].str || *part[2].str || *part[3].str) {
char lastc = part[3].len && part[3].len <= part[3].size ? part[3].str[part[3].len - 1] : 0;
*print_prefix = lastc == '\n' || lastc == '\r';
}
}
sanitize
这个函数对符号进行检查,如果是不可显示的字符直接替换为'?'。
static void sanitize(uint8_t *line){
while(*line){
if(*line < 0x08 || (*line > 0x0D && *line < 0x20))
*line='?';
line++;
}
}
colored_fputs
这个函数真正完成字符串的输出。输出到命令行中。
static void colored_fputs(int level, int tint, const char *str)
{
int local_use_color;
if (!*str)
return;
if (use_color < 0)
check_color_terminal();
if (level == AV_LOG_INFO/8) local_use_color = 0;
else local_use_color = use_color;
// windows下使用SetConsoleTextAttribute设置输出字体颜色
#if defined(_WIN32) && !defined(__MINGW32CE__) && HAVE_SETCONSOLETEXTATTRIBUTE
if (local_use_color)
SetConsoleTextAttribute(con, background | color[level]);
fputs(str, stderr);
if (local_use_color)
SetConsoleTextAttribute(con, attr_orig);
#else
// *nix使用fprintf的颜色输出控制
if (local_use_color == 1) {
fprintf(stderr,
"\033[%"PRIu32";3%"PRIu32"m%s\033[0m",
(color[level] >> 4) & 15,
color[level] & 15,
str);
} else if (tint && use_color == 256) {
fprintf(stderr,
"\033[48;5;%"PRIu32"m\033[38;5;%dm%s\033[0m",
(color[level] >> 16) & 0xff,
tint,
str);
} else if (local_use_color == 256) {
fprintf(stderr,
"\033[48;5;%"PRIu32"m\033[38;5;%"PRIu32"m%s\033[0m",
(color[level] >> 16) & 0xff,
(color[level] >> 8) & 0xff,
str);
} else
fputs(str, stderr);
#endif
}
这里调用了一个函数check_color_terminal
。代码如下。这部分代码严重依赖于平台,感兴趣的可以对应下。
static void check_color_terminal(void)
{
#if defined(_WIN32) && !defined(__MINGW32CE__) && HAVE_SETCONSOLETEXTATTRIBUTE
CONSOLE_SCREEN_BUFFER_INFO con_info;
con = GetStdHandle(STD_ERROR_HANDLE);
use_color = (con != INVALID_HANDLE_VALUE) && !getenv("NO_COLOR") &&
!getenv("AV_LOG_FORCE_NOCOLOR");
if (use_color) {
GetConsoleScreenBufferInfo(con, &con_info);
attr_orig = con_info.wAttributes;
background = attr_orig & 0xF0;
}
#elif HAVE_ISATTY
char *term = getenv("TERM");
use_color = !getenv("NO_COLOR") && !getenv("AV_LOG_FORCE_NOCOLOR") &&
(getenv("TERM") && isatty(2) || getenv("AV_LOG_FORCE_COLOR"));
if ( getenv("AV_LOG_FORCE_256COLOR")
|| (term && strstr(term, "256color")))
use_color *= 256;
#else
use_color = getenv("AV_LOG_FORCE_COLOR") && !getenv("NO_COLOR") &&
!getenv("AV_LOG_FORCE_NOCOLOR");
#endif
}
4 小结
到此,我们基本梳理了全部FFmpeg中libavutil所提供的日志输出机制,从上面代码来看,整体思路比较清晰,但是涉及代码部分很多,有些内容是很值得参考的,比如字符输出颜色控制、c变长参数列表使用等等。
本文可能代码比较多,如果不感兴趣可以快速了解下。
----------------------------------------------------------------------------------------------------------------------------
本文作者:Tocy e-mail: zyvj@qq.com
版权所有@2015-2020,请勿用于商业用途,转载请注明原文地址。本人保留所有权利。