ffplay源码分析-日志分析
基于ffmpeg6.0源码分析。
简介
本文主要介绍ffplay通过命令行参数设置日志flags日志级别、以及是如何打印日志内容的。
流程
main函数
在main函数中的2行代码进行配置日志。
ffplay.c
int main(int argc, char **argv)
{
...
av_log_set_flags(AV_LOG_SKIP_REPEATED); // 设置日志如果是重复文案是否跳过
parse_loglevel(argc, argv, options); // 从参数中解析日志级别、日志是否要落文件、是否要输出banner信息。
...
}
1. av_log_set_flags
av_log_set_flags是用来设置日志的flags,在main函数中首先将它设置为了AV_LOG_SKIP_REPEATED
,ffplay中的日志是按行进行打印的,这个flag的意思是:如果当前的日子与上一行日志发生了重复会进行折叠(不重复打印)。但折叠后会打印一句"Last message repeated n times"。
对于这个flag的处理可以参考:log.c文件中av_log_default_callback函数。
log.c
void av_log_default_callback(void* ptr, int level, const char* fmt, va_list vl)
{
...
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;
}
...
}
2. parse_loglevel
从命令行参数中解析日志的配置。
parse_loglevel(argc, argv, options); // 从参数中解析日志级别、日志是否要落文件、是否要输出banner信息。
cmdutils.c
void parse_loglevel(int argc, char **argv, const OptionDef *options)
{
// 1.先根据loglevel在命令行参数中查找日志配置
int idx = locate_option(argc, argv, options, "loglevel");
char *env;
check_options(options);
if (!idx)
idx = locate_option(argc, argv, options, "v"); // 2. 如果没有找到loglevel,则在参数中查找v,如果找到则idx不为0。
if (idx && argv[idx + 1])
opt_loglevel(NULL, "loglevel", argv[idx + 1]); // 根据参数设置日志级别,flags等信息。
idx = locate_option(argc, argv, options, "report");
env = getenv_utf8("FFREPORT"); // 读取环境变量FFREPORT
if (env || idx) {
FILE *report_file = NULL;
init_report(env, &report_file); // 初始化上报文件
if (report_file) {
int i;
fprintf(report_file, "Command line:\n"); // 打印用户的命令行参数
for (i = 0; i < argc; i++) {
dump_argument(report_file, argv[i]);
fputc(i < argc - 1 ? ' ' : '\n', report_file);
}
fflush(report_file);
}
}
freeenv_utf8(env); // 空实现
idx = locate_option(argc, argv, options, "hide_banner"); // 是否要不显示banner.
if (idx)
hide_banner = 1;
}
- 先根据loglevel在命令行参数中查找日志相关的设置。
- 如果找不到loglevel则继续查找v。
v和loglevel的参数用途是一样的。可以从opt_common.h的宏定义CMDUTILS_COMMON_OPTIONS中看出。
opt_common.h
{
"loglevel", HAS_ARG, {
.func_arg = opt_loglevel }, "set logging level","loglevel" }, \
{
"v", HAS_ARG, {
.func_arg = opt_loglevel }, "set logging level", "loglevel" }, \
- 根据参数设置配置日志模块flags和日志级别。
opt_loglevel。
int opt_loglevel(void *optctx, const char *opt, const char *arg)
{
const struct {
const char *name; int level; } log_levels[] = {
{
"quiet" , AV_LOG_QUIET },
{
"panic" , AV_LOG_PANIC },
{
"fatal" , AV_LOG_FATAL },
{
"error" , AV_LOG_ERROR },
{
"warning", AV_LOG_WARNING },
{
"info" , AV_LOG_INFO },
{
"verbose", AV_LOG_VERBOSE },
{
"debug" , AV_LOG_DEBUG },
{
"trace" , AV_LOG_TRACE },
};
const char *token;
char *tail;
int flags = av_log_get_flags(); // 获取当前日志模块的flags
int level = av_log_get_level(); // 获取当前日志模块的级别
int cmd, i = 0;
av_assert0(arg);
while (*arg) {
token = arg;
if (*token == '+' || *token == '-') {
cmd = *token++;
} else {
cmd = 0;
}
if (!i && !cmd) {
flags = 0; /* missing relative prefix, build absolute value */
}
// 解析日志参数中的flag信息
// Flags can also be used alone by adding a ’+’/’-’ prefix to set/reset a single flag without affecting other
// flags or changing loglevel. When setting both flags and loglevel, a ’+’ separator is expected between the
// last flags value and before loglevel
if (av_strstart(token, "repeat", &arg)) {
if (cmd == '-') {
flags |= AV_LOG_SKIP_REPEATED;
} else {
flags &= ~AV_LOG_SKIP_REPEATED; // Indicates that repeated log output should not be compressed to the first line and the "Last message repeated n times" line will be omitted.
}
} else if (av_strstart(token, "level", &arg)) {
if (cmd == '-') {
flags &= ~AV_LOG_PRINT_LEVEL;
} else {
flags |= AV_LOG_PRINT_LEVEL; // Indicates that log output should add a [level] prefix to each message line. This can be used as an alternative to log coloring, e.g. when dumping the log to file.
}
} else {
break;
}
i++;
}
if (!*arg) {
goto end; //如果没有参数,则不用设置什么
} else if (*arg == '+') {
arg++; // 跳过'+'
} else if (!i) {
// 如果配置的level前面没有前缀"+"则会重置flags
flags = av_log_get_flags(); /* level value without prefix, reset flags */
}
for (i = 0; i < FF_ARRAY_ELEMS(log_levels); i++) {
if (!strcmp(log_levels[i].name, arg)) {
// 寻找参数中的日志级别
level = log_levels[i].level;
goto end; // 如果寻找到就结束了。
}
}
// 如果用户设置的是数字,根据数字寻找级别。
level = strtol(arg, &tail, 10);
if (*tail) {
av_log(NULL, AV_LOG_FATAL, "Invalid loglevel \"%s\". "
"Possible levels are numbers or:\n", arg);
for (i = 0; i < FF_ARRAY_ELEMS(log_levels); i++)
av_log(NULL, AV_LOG_FATAL, "\"%s\"\n", log_levels[i].name);
exit_program(1);
}
end:
av_log_set_flags(flags); // 设置flags
av_log_set_level(level); // 设置日志级别
return 0;
}
- 日志定义了9个级别:quiet,panic,error,warning,info,verbose,debug,trace。
- 首先会获取原本的日志flags和日志level,然后再去解析命令行参数。解析的时候会优先处理flags,这里为什么处理"+/-“?是因为这个日志配置支持用户使用“+”拼接日志的flags和level配置。比如我配置为:
ffplay -v level+info -report xxxx.mp4
其中代表我将flags设置为了打印level并且将日志级别设置为了info。”+/-"也可以单独为flag进行设置,+代表设置,-代表恢复默认。具体可参考:ffmpeg-doc。这里的日志flags只识别"repeat"和"level"其中repeat代表是否要折叠重复的日志,level代表是否要再每行日志前增加level信息。 - for循环根据用户配置的日志level名称信息,寻找对应的level数字级别。如果用户设置的直接是数字,那么就不用转换。
- 通过av_log_set_flags、av_log_set_level设置日志flags和日志级别。
- 查看用户命令行是否有配置report,决定日志是否要落文件。
也会读取用户的环境变量:FFREPORT
,如果用户设置了这个环境变量,或者用户命令行参数配置了report,将会初始化日志文件。
opt_common.c
int init_report(const char *env, FILE **file)
{
char *filename_template = NULL;
char *key, *val;
int ret, count = 0;
int prog_loglevel, envlevel = 0;
time_t now;
struct tm *tm;
AVBPrint filename;
if (report_file) /* already opened */
return 0;
time(&now);
tm = localtime(&now); // 获取当前时间,用来生成文件名
while (env && *env) {
// exp: FFREPORT=file=ffreport.log:level=32
// FFREPORT中的参数是以'='分割,每个键值对之间使用':'分割。
if ((ret = av_opt_get_key_value(&env, "=", ":", 0, &key, &val)) < 0) {
if (count)
av_log(NULL, AV_LOG_ERROR,
"Failed to parse FFREPORT environment variable: %s\n",
av_err2str(ret));
break;
}
if (*env)
env++;
count++;
if (!strcmp(key, "file")) {
av_free(filename_template);
filename_template = val; // 获得用户设置的文件名模版
val = NULL;
} else if (!strcmp(key, "level")) {
// 这个环境变量中还可以设置level, 如:FFREPORT=file=ffreport.log:level=32 ffmpeg -i input output
char *tail;
report_file_level = strtol(val, &tail, 10); // 解析到日志level
if (*tail) {
av_log(NULL, AV_LOG_FATAL, "Invalid report file level\n");
exit_program(1);
}
envlevel = 1;
} else {
av_log(NULL, AV_LOG_ERROR, "Unknown key '%s' in FFREPORT\n", key);
}
av_free(val);
av_free(key);
}
av_bprint_init(&filename, 0, AV_BPRINT_SIZE_AUTOMATIC);
// 如果没有配置文件名称,则默认使用%p-%t.log这个文件名模版
expand_filename_template(&filename,
av_x_if_null(filename_template, "%p-%t.log"), tm);
av_free(filename_template);
if (!av_bprint_is_complete(&filename)) {
av_log(NULL, AV_LOG_ERROR, "Out of memory building report file name\n");
return AVERROR(ENOMEM);
}
prog_loglevel = av_log_get_level();
if (!envlevel)
report_file_level = FFMAX(report_file_level, prog_loglevel); // 重新设置日志级别。
report_file = fopen(filename.str, "w"); // 打开文件
if (!report_file) {
int ret = AVERROR(errno);
av_log(NULL, AV_LOG_ERROR, "Failed to open report \"%s\": %s\n",
filename.str, strerror(errno));
return ret;
}
av_log_set_callback(log_callback_report);
// 文件开头写一下程序名,日期,日志级别信息。
av_log(NULL, AV_LOG_INFO,
"%s started on %04d-%02d-%02d at %02d:%02d:%02d\n"
"Report written to \"%s\"\n"
"Log level: %d\n",
program_name,
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec,
filename.str, report_file_level);
av_bprint_finalize(&filename, NULL);
if (file)
*file = report_file;
return 0;
}
- 解析FFREPORT环境变量中配置的参数,比如我配置为:FFREPORT=file=ffreport.log:level=32,其中键值对之间是使用":“进行拼接,key和value之间使用”="拼接。
- file对应的文件名,决定了日志文件名的模版。
- level对应的是日志界别,它会根据目前日志的级别设置文件的日志级别,默认的文件日志界别是DEBUG。
- 设置日志模块的callback,使它指
log_callback_report
函数。 - 回到
parse_loglevel
函数,初始化完report_file
后会将用户的命令行参数写入文件。
void parse_loglevel(int argc, char **argv, const OptionDef *options)
{
...
if (env || idx) {
FILE *report_file = NULL;
init_report(env, &report_file); // 初始化上报文件
if (report_file) {
int i;
fprintf(report_file, "Command line:\n"); // 打印用户的命令行参数
for (i = 0; i < argc; i++) {
dump_argument(report_file, argv[i]);
fputc(i < argc - 1 ? ' ' : '\n', report_file);
}
fflush(report_file);
}
}
freeenv_utf8(env); // 空实现
idx = locate_option(argc, argv, options, "hide_banner"); // 是否要不显示banner.
if (idx)
hide_banner = 1;
}
- 寻找用户是否配置了
hide_banner
,用来设置决定是否打印banner信息。parse_loglevel
函数的最后进行了读取命令行参数hide_banner
。
opt_common.c中的show_banner
会根据是否hide_banner
来决定是否打印banner信息,同时如果用户的命令行参数中有’-version’也会不打印banner信息,因为’-vesion’这个命令本身的执行函数就会打印程序版权、库版本等信息。
opt_common.c
void show_banner(int argc, char **argv, const OptionDef *options)
{
int idx = locate_option(argc, argv, options, "version");
if (hide_banner || idx)
return;
// 打印ffmpeg的版本,版权信息、配置信息
print_program_info (INDENT|SHOW_COPYRIGHT, AV_LOG_INFO);
// 打印子库的配置信息
print_all_libs_info(INDENT|SHOW_CONFIG, AV_LOG_INFO);
// 打印字库的版本信息
print_all_libs_info(INDENT|SHOW_VERSION, AV_LOG_INFO);
}
// 处理 -vesion
int show_version(void *optctx, const char *opt, const char *arg)
{
av_log_set_callback(log_callback_help);
print_program_info (SHOW_COPYRIGHT, AV_LOG_INFO);
print_all_libs_info(SHOW_VERSION, AV_LOG_INFO);
return 0;
}
日志打印回调
report文件回调
当我们命令行或环境变量配置了report,程序在初始化日志模块的时候,会使用av_log_set_callback
设置日志回调为log_callback_report
函数。如果不设置的话,日志默认回调为av_log_default_callback
。
- 文件日志callback。
log.c
static void (*av_log_callback)(void*, int, const char*, va_list) =
av_log_default_callback;
static void log_callback_report(void *ptr, int level, const char *fmt, va_list vl)
{
va_list vl2;
char line[1024];
static int print_prefix = 1;
va_copy(vl2, vl);
av_log_default_callback(ptr, level, fmt, vl); // 也调用默认的打印,打印一次。
av_log_format_line(ptr, level, fmt, vl2, line, sizeof(line), &print_prefix);
va_end(vl2);
if (report_file_level >= level) {
// 判断日志级别
fputs(line, report_file); // 将日志输出到文件
fflush(report_file);
}
}
先使用日志模块默认的回调打印了一次日志,然后使用av_log_format_line
将日志格式化为一行,然后判断当前需要打印的日志级别,如果大于文件设置的打印日志级别,则进行输出文件。
- av_log_format_line。
log.c
void av_log_format_line(void *ptr, int level, const char *fmt, va_list vl,
char *line, int line_size, int *print_prefix)
{
av_log_format_line2(ptr, level, fmt, vl, line, line_size, 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)
{
AVBPrint part[4];
int ret;
format_line(ptr, level, fmt, vl, part, print_prefix, NULL);
ret = snprintf(line, line_size, "%s%s%s%s", part[0].str, part[1].str, part[2].str, part[3].str); // 把四个部分整合在一起
av_bprint_finalize(part+3, NULL); // 释放内存
return ret;
}
av_log_foramt_line
它分配了4个AVBPrint,这个结构体相当于ffmpeg封装了一个类似c++中std::string的结构,它可用动态扩展内存、方便管理字符串。它最终会调用format_line
进行格式化日志,然后使用snprintf
将日志4个AVBPrint中的str整理为1行。
log.c
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; // 初始化4个AVBPrint
av_bprint_init(part+0, 0, AV_BPRINT_SIZE_AUTOMATIC); // parent信息
av_bprint_init(part+1, 0, AV_BPRINT_SIZE_AUTOMATIC); // 自身信息
av_bprint_init(part+2, 0, AV_BPRINT_SIZE_AUTOMATIC); // level信息
av_bprint_init(part+3, 0, 65536); // 用户参数
if(type) type[0] = type[1] = AV_CLASS_CATEGORY_NA + 16;
if (*print_prefix && avc) {
// avc不为空的情况
if (avc->parent_log_context_offset) {
// 获取parent
AVClass** parent = *(AVClass ***) (((uint8_t *) avcl) +
avc->parent_log_context_offset);
if (parent && *parent) {
// 将parent打印到 part0中。
av_bprintf(part+0, "[%s @ %p] ", // %p 打印地址
(*parent)->item_name(parent), parent);
if(type) type[0] = get_category(parent); // 获取parent的分类信息。
}
}
av_bprintf(part+1, "[%s @ %p] ",
avc->item_name(avcl), avcl); // 打印自身的信息
if(type) type[1] = get_category(avcl); // 获取自身的分类信息。
}
if (*print_prefix && (level > AV_LOG_QUIET) && (flags & AV_LOG_PRINT_LEVEL))
av_bprintf(part+2, "[%s] ", get_level_str(level)); // 是否打印日志的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'; // 如果最后一个字符是\n或\r则下一行需要打印level信息。
}
}
format_line
- 首先会初始化4个AVBPrint,他们分别存储avcl结构parent的信息、avcl结构本身的信息、日志级别信息、用户打印日志时传入的参数。
- 打印avcl的parent结构时会使用
avc->parent_log_context_offset
+本身的地址,获取到parent的地址。然后使用parent中的item_name
函数获取需要打印的字符传,并打印自身的地址。 - 打印avcl本身,也是调用自身的
item_name
函数获取需要打印的字符传,并打印自身的地址。 - 打印level信息,这里需要判断是每一行的开头,才增加level信息。
- 打印调用方传的参数。
- ffmpeg中一些结构的开始是一个AVClass指针,那么这种结构就可以转换为AVClass*,提供给日志打印模块。日志打印模块则可以跟进AVClass内部的信息进行打印。
log.h
typedef struct AVClass {
/**
* The name of the class; usually it is the same name as the
* context structure type to which the AVClass is associated.
*/
const char* class_name;
/**
* A pointer to a function which returns the name of a context
* instance ctx associated with the class.
*/
const char* (*item_name)(void* ctx);
/**
* a pointer to the first option specified in the class if any or NULL
*
* @see av_set_default_options()
*/
const struct AVOption *option;
/**
* LIBAVUTIL_VERSION with which this structure was created.
* This is used to allow fields to be added without requiring major
* version bumps everywhere.
*/
int version;
/**
* Offset in the structure where log_level_offset is stored.
* 0 means there is no such variable
*/
int log_level_offset_offset;
...
}
如AVCodecContext
的首个字段就是AVClass*
avcodec.h
typedef struct AVCodecContext {
/**
* information on struct for av_log
* - set by avcodec_alloc_context3
*/
const AVClass *av_class;
int log_level_offset;
...
}
默认打印
log.c
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];
static int is_atty;
int type[2];
unsigned tint = 0;
if (level >= 0) {
tint = level & 0xff00;
level &= 0xff;
}
if (level > av_log_level)
return;
ff_mutex_lock(&mutex);
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'){
// 如果当前行与上一行打印的日志内容相同,并且AV_LOG_SKIP_REPEATED是开启的状态。
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);
ff_mutex_unlock(&mutex);
}
- 默认的日志打印前面的部分跟打印文件日志类似,如format_line。
- 增加了处理重复日志的逻辑,如果日志和上一行重复则跟进日志的flags中的AV_LOG_SKIP_REPEATED则不打印这行日志。但如果用户设置了要打印日志的level信息,则会打印一句
Last message repeated %d times
。注意这了如果是命令行的话输出的Last message repeated %d times\r
,如果是非命令行的话输出的是Last message repeated %d times\n
,'\r’是回车符号它可以使我们看到的是最后一个Last message repeated %d times
可以避免重复。 - 将命令行不方便显示的字符替换为’?'。
- 为打印字符串增加颜色。
- 通过给控制输出格式给字符增加\033前缀,可以字符在命令行的输出颜色等。“\033[%u;3%um%s\033[0m”。
- 可以参考:color
static void ansi_fputs(int level, int tint, const char *str, int local_use_color)
{
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);
}