ffplay源码分析-日志分析

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;
}
  1. 先根据loglevel在命令行参数中查找日志相关的设置。
  2. 如果找不到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" },         \
  1. 根据参数设置配置日志模块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和日志级别。
  1. 查看用户命令行是否有配置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;
}
  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

  1. 文件日志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将日志格式化为一行,然后判断当前需要打印的日志级别,如果大于文件设置的打印日志级别,则进行输出文件。

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

猜你喜欢

转载自blog.csdn.net/a992036795/article/details/129768149