当前位置: 首页 > news >正文

ffmpeg4.1 源码学习之-转封装

前言


  • ffmpeg 的源码量非常的多,而且非常繁杂,非常多的函数,如果一个函数一个函数看的话要花费比较多的时间。所以本文通过跟踪ffmpeg转封装的过程来学习ffmpeg的源码
  • 具体转封装的命令:ffmpeg -i 1_cut.flv -c copy -f mp4 1.mp4
  • 在学习过程中,如果遇到libavformat、libavcodec、libavutils等库的主要函数,将单独写一篇文章进行分析
  • 为了减少篇幅,源码的异常处理都删除了

源码之旅

1.1 main 函数分析

  • 源文件:fftools/ffmpeg.c

  • 功能:完成动态库加载、退出函数注册、日志输出、以及avdevice、avformat、network等模块的初始化,命令行参数解析,转码转封装等操作

  • 下面开启main函数源码分析

  • init_dynload函数,在window系统下调用了SetDllDirectory("")函数将当前路径从动态库搜索中删除,这么做的目的是为了避免动态库恶意副本供给,详细可参考动态库链接安全性

  • register_exit函数,将退出函数注册给了一个静态全局函数指针program_exit,当程序需要退出时会调用该函数指针指向的函数

int main(int argc, char **argv)
{int i, ret;BenchmarkTimeStamps ti;init_dynload();register_exit(ffmpeg_cleanup);setvbuf(stderr,NULL,_IONBF,0); /* win32 runtime needs this */// 跳过重复的日志,避免打印出一大堆的重复日志来av_log_set_flags(AV_LOG_SKIP_REPEATED);// 从命令行中解析日志级别,为后续日志输出提供输出级别parse_loglevel(argc, argv, options);// 判断第一个参数是不是"-d",如果是,则作为守护进程if(argc>1 && !strcmp(argv[1], "-d")){run_as_daemon=1;av_log_set_callback(log_callback_null);argc--;argv++;}#if CONFIG_AVDEVICE// 初始化libavdevice,并且注册所有的输入输出设备// 这个函数单独写文章分析,这里不再赘述avdevice_register_all();
#endif// 完成网络初始化,这里主要是针对windows平台,因为windows// 使用网络首先需要调用WSAStartup函数去初始化// 此外还对TLS协议进行了初始化avformat_network_init();// 显示版权等相关信息show_banner(argc, argv, options);/* parse options and open all input/output files */// 如英文注释所示:这里开始解析参数,并且打开输入输出文件ret = ffmpeg_parse_options(argc, argv);if (ret < 0)exit_program(1);// 如果没有输出文件,并且输入文件也没有,则显示帮助信息if (nb_output_files <= 0 && nb_input_files == 0) {show_usage();av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name);exit_program(1);}// 如果没有输出文件,则提示必须要有一个输出文件/* file converter / grab */if (nb_output_files <= 0) {av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");exit_program(1);}// 查看输出url中是否有rtp协议for (i = 0; i < nb_output_files; i++) {if (strcmp(output_files[i]->ctx->oformat->name, "rtp"))want_sdp = 0;}current_time = ti = get_benchmark_time_stamps();// 开始转码,这个函数将单独讲解if (transcode() < 0)exit_program(1);// 后面是一些相关异常的处理if (do_benchmark) {int64_t utime, stime, rtime;current_time = get_benchmark_time_stamps();utime = current_time.user_usec - ti.user_usec;stime = current_time.sys_usec  - ti.sys_usec;rtime = current_time.real_usec - ti.real_usec;av_log(NULL, AV_LOG_INFO,"bench: utime=%0.3fs stime=%0.3fs rtime=%0.3fs\n",utime / 1000000.0, stime / 1000000.0, rtime / 1000000.0);}av_log(NULL, AV_LOG_DEBUG, "%"PRIu64" frames successfully decoded, %"PRIu64" decoding errors\n",decode_error_stat[0], decode_error_stat[1]);if ((decode_error_stat[0] + decode_error_stat[1]) * max_error_rate < decode_error_stat[1])exit_program(69);exit_program(received_nb_signals ? 255 : main_return_code);return main_return_code;
}
1.2 ffmpeg_parse_options 源码分析
  • 该函数中调用的Option相关结构体将在单独的文章列举

  • split_commandline函数重点参数说明

    • options:全局常量OptionDef结构体数组,里面预先定义了好多命令参数,比如'c','codec','ss'等,并且包含相关参数的说明和解释
    • 下面是其中一条option参考数据

    { “ss”, HAS_ARG | OPT_TIME | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(start_time) }, “set the start time offset”, “time_off” }

  • groups:全局常量OptionGroupDef结构体数组,预先定义了需要匹配的分离器

  • 下面是初始化数据

    [GROUP_OUTFILE] = { “output url”, NULL, OPT_OUTPUT },
    [GROUP_INFILE] = { “input url”, “i”, OPT_INPUT },

  • 下面是该函数的源码

    int ffmpeg_parse_options(int argc, char **argv)
    {
    OptionParseContext octx;
    uint8_t error[128];
    int ret;

    memset(&octx, 0, sizeof(octx));/* split the commandline into an internal representation */
    // 解析命令行参数,解析成内部需要的形式,这个函数后面将讲解
    ret = split_commandline(&octx, argc, argv, options, groups,FF_ARRAY_ELEMS(groups));/* apply global options */
    // 解析ctx中全局group
    ret = parse_optgroup(NULL, &octx.global_opts);/* configure terminal and setup signal handlers */
    // 配置信号句柄,详细的就不看了
    // 同时也枚举ctrl-c信号
    term_init();/* open input files */
    // 打开输入文件,主要是通过open_input_file来打开
    ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);/* create the complex filtergraphs */
    ret = init_complex_filters();/* open output files */
    // 打开输出文件
    ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);check_filter_outputs();
    

    fail:
    uninit_parse_context(&octx);
    return ret;
    }

1.3 split_commandline 函数分析

  • 源文件:fftools\cmdutils.c

  • 功能:分离命令行命令

  • 下面是源码

    int split_commandline(OptionParseContext *octx, int argc, char *argv[],
    const OptionDef *options,
    const OptionGroupDef *groups, int nb_groups)
    {
    int optindex = 1;
    int dashdash = -2;

    /* perform system-dependent conversions for arguments list */
    // 在windows平台下,这个函数负责将命令行参数从宽字符转换成UTF8,
    // 这样就可以解决中文的问题
    prepare_app_arguments(&argc, &argv);// 初始化octx,根据groups的内容来初始化octx里的groups
    // 同时初始化global_opts
    init_parse_context(octx, groups, nb_groups);
    av_log(NULL, AV_LOG_DEBUG, "Splitting the commandline.\n");// 下面开始解析命令行参数
    while (optindex < argc) {// 从命令行中获取参数const char *opt = argv[optindex++], *arg;const OptionDef *po;int ret;av_log(NULL, AV_LOG_DEBUG, "Reading option '%s' ...", opt);// 如果获取到的参数是“--”,则记录位置,并且continueif (opt[0] == '-' && opt[1] == '-' && !opt[2]) {dashdash = optindex;continue;}// 如果是没有分隔符的参数,或者只有一个字符,或者是dashdash// 后面对应的参数则直接保存,并且统一将这个参数保存到ctx中groups中的第一个OptionGroupList中// 后面将单独分析这个函数,或者可以做一个表这样更明显一些/* unnamed group separators, e.g. output filename */if (opt[0] != '-' || !opt[1] || dashdash+1 == optindex) {finish_group(octx, 0, opt);av_log(NULL, AV_LOG_DEBUG, " matched as %s.\n", groups[0].name);continue;}opt++;
    

    // 这是一个宏,可以从argv中获取参数
    #define GET_ARG(arg)
    do {
    arg = argv[optindex++];
    if (!arg) {
    av_log(NULL, AV_LOG_ERROR, “Missing argument for option ‘%s’.\n”, opt);
    return AVERROR(EINVAL);
    }
    } while (0)

        /* named group separators, e.g. -i */// 是否匹配具名的分隔符,简单来说就是将输入的分隔符or关键字在// 默认的groups数组中查找,若能查找到则保存到octx中,这里主要是-i选项// 按照当前的配置 groups中只有两个成员if ((ret = match_group_separator(groups, nb_groups, opt)) >= 0) {GET_ARG(arg);finish_group(octx, ret, arg);av_log(NULL, AV_LOG_DEBUG, " matched as %s with argument '%s'.\n",groups[ret].name, arg);continue;}/* normal options */// 将输入的命令行参数关键字在options中开始查找,options保存预先定义好的// 参数关键字,比如:“-c”等po = find_option(options, opt);// 如果从预先定义的选项中查找到了,则保存起来if (po->name) {if (po->flags & OPT_EXIT) {/* optional argument, e.g. -h */arg = argv[optindex++];} else if (po->flags & HAS_ARG) {GET_ARG(arg);} else {arg = "1";}// 根据相应的条件,将参数保存到octx中的cur_group或者global_opts中add_opt(octx, po, opt, arg);av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with ""argument '%s'.\n", po->name, po->help, arg);continue;}/* AVOptions */// 没有明确被处理的选项将通过opt_default函数作为AVOption来处理if (argv[optindex]) {// 下面将讲解opt_default()函数ret = opt_default(NULL, opt, argv[optindex]);if (ret >= 0) {av_log(NULL, AV_LOG_DEBUG, " matched as AVOption '%s' with ""argument '%s'.\n", opt, argv[optindex]);optindex++;continue;} else if (ret != AVERROR_OPTION_NOT_FOUND) {av_log(NULL, AV_LOG_ERROR, "Error parsing option '%s' ""with argument '%s'.\n", opt, argv[optindex]);return ret;}}/* boolean -nofoo options */// 如果起始两个字符是no,除去no之后剩余的字符能够在options中匹配到// 并且flag中有`OPT_BOOL`标记。比如if (opt[0] == 'n' && opt[1] == 'o' &&(po = find_option(options, opt + 2)) &&po->name && po->flags & OPT_BOOL) {// 选项添加到octx中的cur_group或者global_opts中add_opt(octx, po, opt, "0");av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with ""argument 0.\n", po->name, po->help);continue;}av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'.\n", opt);return AVERROR_OPTION_NOT_FOUND;
    }if (octx->cur_group.nb_opts || codec_opts || format_opts || resample_opts)av_log(NULL, AV_LOG_WARNING, "Trailing options were found on the ""commandline.\n");av_log(NULL, AV_LOG_DEBUG, "Finished splitting the commandline.\n");return 0;
    

    }

1.4 finish_group()函数源码分析

  • finish_group()负责将和输入url、输出url相关的参数保存到OptionParseContext.groups

    /*

    • Finish parsing an option group.

    • @param group_idx which group definition should this group belong to

    • @param arg argument of the group delimiting option
      */
      static void finish_group(OptionParseContext *octx, int group_idx,
      const char *arg)
      {
      OptionGroupList *l = &octx->groups[group_idx];
      OptionGroup *g;

      // 在groups的基础上重新分配一个array。
      GROW_ARRAY(l->groups, l->nb_groups);
      g = &l->groups[l->nb_groups - 1];

      // 从octx的cur_group中获取到内容
      // 然后开始初始化
      // -i filename之前配置的参数,如果不是保存在octx->global_group中,就会暂时保存在octx->cur_group中,
      //这个时候有输入或者输出文件了,我们将cur_group中的数据保存到octx->optionGroupList中的optionGroup中,和输入文件挂靠在一起
      *g = octx->cur_group;
      g->arg = arg;
      g->group_def = l->group_def;
      g->sws_dict = sws_dict;
      g->swr_opts = swr_opts;
      g->codec_opts = codec_opts;
      g->format_opts = format_opts;
      g->resample_opts = resample_opts;

      codec_opts = NULL;
      format_opts = NULL;
      resample_opts = NULL;
      sws_dict = NULL;
      swr_opts = NULL;
      init_opts();

      memset(&octx->cur_group, 0, sizeof(octx->cur_group));
      }

1.5 add_opt()函数解析

  • add_opt()将和options中相关的参数保存到OptionParseContext.cur_groups或者OptionParseContext.global_groups

  • 下面是源码

    /*

    • Add an option instance to currently parsed group.
      */
      static void add_opt(OptionParseContext *octx, const OptionDef *opt,
      const char *key, const char val)
      {
      // 如果选项中的flags标记为有下列 OPT_
      三个中的一个,就不是一个全局选项,比如codec或者c参数就包含标记OPT_SPEC
      int global = !(opt->flags & (OPT_PERFILE | OPT_SPEC | OPT_OFFSET));
      OptionGroup *g = global ? &octx->global_opts : &octx->cur_group;

      // 将选项存放到对应的OptionGroup中
      GROW_ARRAY(g->opts, g->nb_opts);
      g->opts[g->nb_opts - 1].opt = opt;
      g->opts[g->nb_opts - 1].key = key;
      g->opts[g->nb_opts - 1].val = val;
      }

1.6 opt_default()函数源码解析

  • 该函数负责解析:与输入输出参数(-i)、options中的参数、dashdash(–)参数不匹配的其他参数
  • 下面是该函数的源码
#define FLAGS (o->type == AV_OPT_TYPE_FLAGS && (arg[0]=='-' || arg[0]=='+')) ? AV_DICT_APPEND : 0
int opt_default(void *optctx, const char *opt, const char *arg)
{const AVOption *o;int consumed = 0;char opt_stripped[128];const char *p;// avcodec_get_class()函数返回静态全局常量 static const AVClass av_codec_context_class的指针const AVClass *cc = avcodec_get_class(), *fc = avformat_get_class();
#if CONFIG_AVRESAMPLE// 下列函数已经被弃用了,暂时不分析,猜测也是返回类似codec的 AVClass类型静态全局变量指针const AVClass *rc = avresample_get_class();
#endif
#if CONFIG_SWSCALE// 返回全局常量 const AVClass ff_sws_context_class的指针const AVClass *sc = sws_get_class();
#endif
#if CONFIG_SWRESAMPLE// 返回静态全局常量 static const AVClass av_class的指针const AVClass *swr_class = swr_get_class();
#endifif (!strcmp(opt, "debug") || !strcmp(opt, "fdebug"))av_log_set_level(AV_LOG_DEBUG);// 获取':'首次在opt中出现的位置if (!(p = strchr(opt, ':')))// 若没有p指针设置为opt的字符串尾部p = opt + strlen(opt);// 将opt中的字符串copy到opt_stripped中av_strlcpy(opt_stripped, opt, FFMIN(sizeof(opt_stripped), p - opt + 1));// 从av_codec_context_class.option查找匹配的选项if ((o = opt_find(&cc, opt_stripped, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ)) ||((opt[0] == 'v' || opt[0] == 'a' || opt[0] == 's') &&(o = opt_find(&cc, opt + 1, NULL, 0, AV_OPT_SEARCH_FAKE_OBJ)))) {// 如果找到了则将其设置进入全局变量codec_opts中av_dict_set(&codec_opts, opt, arg, FLAGS);consumed = 1;}// 同上,从av_format_context_class.option查找匹配的选项// 后面的代码与这个类似就不再细看了if ((o = opt_find(&fc, opt, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) {av_dict_set(&format_opts, opt, arg, FLAGS);if (consumed)av_log(NULL, AV_LOG_VERBOSE, "Routing option %s to both codec and muxer layer\n", opt);consumed = 1;}
#if CONFIG_SWSCALEif (!consumed && (o = opt_find(&sc, opt, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) {struct SwsContext *sws = sws_alloc_context();int ret = av_opt_set(sws, opt, arg, 0);sws_freeContext(sws);if (!strcmp(opt, "srcw") || !strcmp(opt, "srch") ||!strcmp(opt, "dstw") || !strcmp(opt, "dsth") ||!strcmp(opt, "src_format") || !strcmp(opt, "dst_format")) {av_log(NULL, AV_LOG_ERROR, "Directly using swscale dimensions/format options is not supported, please use the -s or -pix_fmt options\n");return AVERROR(EINVAL);}if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error setting option %s.\n", opt);return ret;}av_dict_set(&sws_dict, opt, arg, FLAGS);consumed = 1;}
#elseif (!consumed && !strcmp(opt, "sws_flags")) {av_log(NULL, AV_LOG_WARNING, "Ignoring %s %s, due to disabled swscale\n", opt, arg);consumed = 1;}
#endif
#if CONFIG_SWRESAMPLEif (!consumed && (o=opt_find(&swr_class, opt, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) {struct SwrContext *swr = swr_alloc();int ret = av_opt_set(swr, opt, arg, 0);swr_free(&swr);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error setting option %s.\n", opt);return ret;}av_dict_set(&swr_opts, opt, arg, FLAGS);consumed = 1;}
#endif
#if CONFIG_AVRESAMPLEif ((o=opt_find(&rc, opt, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) {av_dict_set(&resample_opts, opt, arg, FLAGS);consumed = 1;}
#endifif (consumed)return 0;return AVERROR_OPTION_NOT_FOUND;
}
1.7 parse_optgroup()函数分析

  • 解析OptionGroup结构体
  • 下面是源码
int parse_optgroup(void *optctx, OptionGroup *g)
{int i, ret;av_log(NULL, AV_LOG_DEBUG, "Parsing a group of options: %s %s.\n",g->group_def->name, g->arg);for (i = 0; i < g->nb_opts; i++) {Option *o = &g->opts[i];// 如果OptionGroup.group_def.flags和OptionGroup.opts[i].flags不匹配,则返回错误if (g->group_def->flags &&!(g->group_def->flags & o->opt->flags)) {av_log(NULL, AV_LOG_ERROR, "Option %s (%s) cannot be applied to ""%s %s -- you are trying to apply an input option to an ""output file or vice versa. Move this option before the ""file it belongs to.\n", o->key, o->opt->help,g->group_def->name, g->arg);return AVERROR(EINVAL);}av_log(NULL, AV_LOG_DEBUG, "Applying option %s (%s) with argument %s.\n",o->key, o->opt->help, o->val);// 若匹配则将其写入到option中ret = write_option(optctx, o->opt, o->key, o->val);if (ret < 0)return ret;}av_log(NULL, AV_LOG_DEBUG, "Successfully parsed a group of options.\n");return 0;
}
1.8 write_option()函数源码分析

  • write_option可以判断参数对应的arg是不是合法的
  • 下面是源码
static int write_option(void *optctx, const OptionDef *po, const char *opt,const char *arg)
{/* new-style options contain an offset into optctx, old-style address of* a global var*/// 获取存储位置// 这里分两种情况,OptionDef.flags中含有OPT_OFFSET和OPT_SPEC标记时,OptionDef.u.off保存的// 是该参数对应在 OptionContext结构体中的偏移,如果不包含这些标记的话,就是使用// OptionDef.u.dst_ptr保存的变量指针// 下面举个例子吧: { "f",HAS_ARG | OPT_STRING | OPT_OFFSET |OPT_INPUT | OPT_OUTPUT,{ .off = OFFSET(format)},// 对于option ‘f’来说,它flag含有OPT_OFFSET所以需要使用OptionDef.u.off中保存的指针,在上面这个例子就是OFFSET(format)// 而这个宏的定义是这样的:#define OFFSET(x) offsetof(OptionsContext, x),就是获取format在OptionContext结构体中的偏移// 另外一个例子: { "y",OPT_BOOL,{&file_overwrite },"overwrite output files" }// 对弈option ‘y’来说,它的flag不包含OPT_OFFSET、OPT_SPEC,所以使用OptionDef.u.dst_ptr的值,也就是&file_overwritevoid *dst = po->flags & (OPT_OFFSET | OPT_SPEC) ?(uint8_t *)optctx + po->u.off : po->u.dst_ptr;int *dstcount;// 根据不同的flags,dst是一个不同的类型的指针,后续的代码就不在看了,理解上面那行代码就ok了// 这里需要单独讲一下:在OptionsContext结构体的成员中每一个SpecifierOpt* 成员后面都有一个int型的成员// 比如: SpecifierOpt *audio_channels;//        int        nb_audio_channels;//        SpecifierOpt *audio_sample_rate;//        int        nb_audio_sample_rate;// 那个int成员用来表明上面那个指针指向的数组有多少个成员if (po->flags & OPT_SPEC) {SpecifierOpt **so = dst;char *p = strchr(opt, ':');char *str;dstcount = (int *)(so + 1);// 这个函数里面,如果grow_array()函数调用成功,则dstcount的值会变成`discount+1的值*so = grow_array(*so, sizeof(**so), dstcount, *dstcount + 1);str = av_strdup(p ? p + 1 : "");if (!str)return AVERROR(ENOMEM);(*so)[*dstcount - 1].specifier = str;// 对应的参数需要保存在这个新的dst处dst = &(*so)[*dstcount - 1].u;}if (po->flags & OPT_STRING) {char *str;str = av_strdup(arg);av_freep(dst);if (!str)return AVERROR(ENOMEM);*(char **)dst = str;} else if (po->flags & OPT_BOOL || po->flags & OPT_INT) {*(int *)dst = parse_number_or_die(opt, arg, OPT_INT64, INT_MIN, INT_MAX);} else if (po->flags & OPT_INT64) {*(int64_t *)dst = parse_number_or_die(opt, arg, OPT_INT64, INT64_MIN, INT64_MAX);} else if (po->flags & OPT_TIME) {*(int64_t *)dst = parse_time_or_die(opt, arg, 1);} else if (po->flags & OPT_FLOAT) {*(float *)dst = parse_number_or_die(opt, arg, OPT_FLOAT, -INFINITY, INFINITY);} else if (po->flags & OPT_DOUBLE) {*(double *)dst = parse_number_or_die(opt, arg, OPT_DOUBLE, -INFINITY, INFINITY);} else if (po->u.func_arg) {int ret = po->u.func_arg(optctx, opt, arg);if (ret < 0) {av_log(NULL, AV_LOG_ERROR,"Failed to set value '%s' for option '%s': %s\n",arg, opt, av_err2str(ret));return ret;}}if (po->flags & OPT_EXIT)exit_program(0);return 0;
}
1.9 open_files函数分析

  • 源文件:fftools/ffmpeg_opt.c

  • 下面直接分析源码吧

    static int open_files(OptionGroupList *l, const char inout,
    int (open_file)(OptionsContext
    , const char))
    {
    int i, ret;

    // 循环遍历OptionGroupList中的OptionGroup
    for (i = 0; i < l->nb_groups; i++) {OptionGroup *g = &l->groups[i];OptionsContext o;// 初始化OptionsContextinit_options(&o);o.g = g;// 解析OptionGroup,并且存储数据到o中ret = parse_optgroup(&o, g);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error parsing options for %s file ""%s.\n", inout, g->arg);return ret;}av_log(NULL, AV_LOG_DEBUG, "Opening an %s file: %s.\n", inout, g->arg);// 调用传入的open_file函数指针打开文件// open_file主要的两个可能的值是:open_input_file()函数指针和 open_output_file()函数指针// 这两个函数下面将开始讲解ret = open_file(&o, g->arg);// 反初始化OptionsContextuninit_options(&o);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Error opening %s file %s.\n",inout, g->arg);return ret;}av_log(NULL, AV_LOG_DEBUG, "Successfully opened the file.\n");
    }return 0;
    

    }

1.10 open_input_file函数解析

  • 这个函数是用来打开输入文件

  • 下面是源码

    static int open_input_file(OptionsContext *o, const char *filename)
    {
    InputFile *f;
    AVFormatContext *ic;
    AVInputFormat *file_iformat = NULL;
    int err, i, ret;
    int64_t timestamp;
    AVDictionary *unused_opts = NULL;
    AVDictionaryEntry *e = NULL;
    char * video_codec_name = NULL;
    char * audio_codec_name = NULL;
    char *subtitle_codec_name = NULL;
    char * data_codec_name = NULL;
    int scan_all_pmts_set = 0;

    // 相应参数的修正
    if (o->stop_time != INT64_MAX && o->recording_time != INT64_MAX) {o->stop_time = INT64_MAX;av_log(NULL, AV_LOG_WARNING, "-t and -to cannot be used together; using -t.\n");
    }if (o->stop_time != INT64_MAX && o->recording_time == INT64_MAX) {int64_t start_time = o->start_time == AV_NOPTS_VALUE ? 0 : o->start_time;if (o->stop_time <= start_time) {av_log(NULL, AV_LOG_ERROR, "-to value smaller than -ss; aborting.\n");exit_program(1);} else {o->recording_time = o->stop_time - start_time;}
    }// 如果指定了格式,则直接调用av_find_input_format()函数而不需要去探测输入文件的格式了
    // 比如通过这个命令就可以指定输入文件的封装格式:ffmpeg -f flv -i G:/1.flv -c copy -f mp4 G:/2.mp4
    // -f flv 就指定了输入文件的格式为flv格式
    if (o->format) {// 根据用户指定的输入格式来查找对应的处理类,如果没有找到则报错,退出应用程序if (!(file_iformat = av_find_input_format(o->format))) {av_log(NULL, AV_LOG_FATAL, "Unknown input format: '%s'\n", o->format);exit_program(1);}
    }// 判断输入文件的格式是不是'-',如果是则认为是pipe输入
    // 输入文件
    if (!strcmp(filename, "-"))filename = "pipe:";
    // 如果文件名是pipe或者/dev/stdin,则认为是通过标准输入来进行互动
    stdin_interaction &= strncmp(filename, "pipe:", 5) &&strcmp(filename, "/dev/stdin");/* get default parameters from command line */
    // 获取AVFormatContext实例,这个会有单独的文章分析
    ic = avformat_alloc_context();
    if (!ic) {print_error(filename, AVERROR(ENOMEM));exit_program(1);
    }
    // 如果指定了音频采样率的,则将采样率保存到octx中的OptionGroup中的format_opts中
    if (o->nb_audio_sample_rate) {av_dict_set_int(&o->g->format_opts, "sample_rate", o->audio_sample_rate[o->nb_audio_sample_rate - 1].u.i, 0);
    }
    if (o->nb_audio_channels) {/* because we set audio_channels based on both the "ac" and* "channel_layout" options, we need to check that the specified* demuxer actually has the "channels" option before setting it */if (file_iformat && file_iformat->priv_class &&av_opt_find(&file_iformat->priv_class, "channels", NULL, 0,AV_OPT_SEARCH_FAKE_OBJ)) {av_dict_set_int(&o->g->format_opts, "channels", o->audio_channels[o->nb_audio_channels - 1].u.i, 0);}
    }
    if (o->nb_frame_rates) {/* set the format-level framerate option;* this is important for video grabbers, e.g. x11 */if (file_iformat && file_iformat->priv_class &&av_opt_find(&file_iformat->priv_class, "framerate", NULL, 0,AV_OPT_SEARCH_FAKE_OBJ)) {av_dict_set(&o->g->format_opts, "framerate",o->frame_rates[o->nb_frame_rates - 1].u.str, 0);}
    }
    if (o->nb_frame_sizes) {av_dict_set(&o->g->format_opts, "video_size", o->frame_sizes[o->nb_frame_sizes - 1].u.str, 0);
    }
    if (o->nb_frame_pix_fmts)av_dict_set(&o->g->format_opts, "pixel_format", o->frame_pix_fmts[o->nb_frame_pix_fmts - 1].u.str, 0);// 这是一个宏,这个宏从OptionsContext(o).codec_names中查找有没有后最后一个参数匹配的媒体类型
    // 如果有就将里面的值赋值给第三个参数
    MATCH_PER_TYPE_OPT(codec_names, str,    video_codec_name, ic, "v");
    MATCH_PER_TYPE_OPT(codec_names, str,    audio_codec_name, ic, "a");
    MATCH_PER_TYPE_OPT(codec_names, str, subtitle_codec_name, ic, "s");
    MATCH_PER_TYPE_OPT(codec_names, str,     data_codec_name, ic, "d");// 如果指定了编码,则直接查找编码对应的处理类,如果没有找到则直接退出应用程序了
    // 相关查找函数稍后分析
    if (video_codec_name)ic->video_codec    = find_codec_or_die(video_codec_name   , AVMEDIA_TYPE_VIDEO   , 0);
    if (audio_codec_name)ic->audio_codec    = find_codec_or_die(audio_codec_name   , AVMEDIA_TYPE_AUDIO   , 0);
    if (subtitle_codec_name)ic->subtitle_codec = find_codec_or_die(subtitle_codec_name, AVMEDIA_TYPE_SUBTITLE, 0);
    if (data_codec_name)ic->data_codec     = find_codec_or_die(data_codec_name    , AVMEDIA_TYPE_DATA    , 0);ic->video_codec_id     = video_codec_name    ? ic->video_codec->id    : AV_CODEC_ID_NONE;
    ic->audio_codec_id     = audio_codec_name    ? ic->audio_codec->id    : AV_CODEC_ID_NONE;
    ic->subtitle_codec_id  = subtitle_codec_name ? ic->subtitle_codec->id : AV_CODEC_ID_NONE;
    ic->data_codec_id      = data_codec_name     ? ic->data_codec->id     : AV_CODEC_ID_NONE;ic->flags |= AVFMT_FLAG_NONBLOCK;
    if (o->bitexact)ic->flags |= AVFMT_FLAG_BITEXACT;// 指定中断callback函数
    ic->interrupt_callback = int_cb;if (!av_dict_get(o->g->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {av_dict_set(&o->g->format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);scan_all_pmts_set = 1;
    }
    /* open the input file with generic avformat function */
    // 打开输入文件,这个函数单独写一篇文章分析
    err = avformat_open_input(&ic, filename, file_iformat, &o->g->format_opts);
    if (err < 0) {print_error(filename, err);if (err == AVERROR_PROTOCOL_NOT_FOUND)av_log(NULL, AV_LOG_ERROR, "Did you mean file:%s?\n", filename);exit_program(1);
    }
    if (scan_all_pmts_set)av_dict_set(&o->g->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);
    remove_avoptions(&o->g->format_opts, o->g->codec_opts);
    assert_avoptions(o->g->format_opts);/* apply forced codec ids */
    for (i = 0; i < ic->nb_streams; i++)choose_decoder(o, ic, ic->streams[i]);if (find_stream_info) {AVDictionary **opts = setup_find_stream_info_opts(ic, o->g->codec_opts);int orig_nb_streams = ic->nb_streams;/* If not enough info to get the stream parameters, we decode thefirst frames to get it. (used in mpeg case for example) */ret = avformat_find_stream_info(ic, opts);for (i = 0; i < orig_nb_streams; i++)av_dict_free(&opts[i]);av_freep(&opts);if (ret < 0) {av_log(NULL, AV_LOG_FATAL, "%s: could not find codec parameters\n", filename);if (ic->nb_streams == 0) {avformat_close_input(&ic);exit_program(1);}}
    }if (o->start_time != AV_NOPTS_VALUE && o->start_time_eof != AV_NOPTS_VALUE) {av_log(NULL, AV_LOG_WARNING, "Cannot use -ss and -sseof both, using -ss for %s\n", filename);o->start_time_eof = AV_NOPTS_VALUE;
    }if (o->start_time_eof != AV_NOPTS_VALUE) {if (o->start_time_eof >= 0) {av_log(NULL, AV_LOG_ERROR, "-sseof value must be negative; aborting\n");exit_program(1);}if (ic->duration > 0) {o->start_time = o->start_time_eof + ic->duration;if (o->start_time < 0) {av_log(NULL, AV_LOG_WARNING, "-sseof value seeks to before start of file %s; ignored\n", filename);o->start_time = AV_NOPTS_VALUE;}} elseav_log(NULL, AV_LOG_WARNING, "Cannot use -sseof, duration of %s not known\n", filename);
    }
    timestamp = (o->start_time == AV_NOPTS_VALUE) ? 0 : o->start_time;
    /* add the stream start time */
    if (!o->seek_timestamp && ic->start_time != AV_NOPTS_VALUE)timestamp += ic->start_time;/* if seeking requested, we execute it */
    if (o->start_time != AV_NOPTS_VALUE) {int64_t seek_timestamp = timestamp;if (!(ic->iformat->flags & AVFMT_SEEK_TO_PTS)) {int dts_heuristic = 0;for (i=0; i<ic->nb_streams; i++) {const AVCodecParameters *par = ic->streams[i]->codecpar;if (par->video_delay) {dts_heuristic = 1;break;}}if (dts_heuristic) {seek_timestamp -= 3*AV_TIME_BASE / 23;}}ret = avformat_seek_file(ic, -1, INT64_MIN, seek_timestamp, seek_timestamp, 0);if (ret < 0) {av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",filename, (double)timestamp / AV_TIME_BASE);}
    }/* update the current parameters so that they match the one of the input stream */
    add_input_streams(o, ic);/* dump the file content */
    av_dump_format(ic, nb_input_files, filename, 0);GROW_ARRAY(input_files, nb_input_files);
    f = av_mallocz(sizeof(*f));
    if (!f)exit_program(1);
    input_files[nb_input_files - 1] = f;f->ctx        = ic;
    f->ist_index  = nb_input_streams - ic->nb_streams;
    f->start_time = o->start_time;
    f->recording_time = o->recording_time;
    f->input_ts_offset = o->input_ts_offset;
    f->ts_offset  = o->input_ts_offset - (copy_ts ? (start_at_zero && ic->start_time != AV_NOPTS_VALUE ? ic->start_time : 0) : timestamp);
    f->nb_streams = ic->nb_streams;
    f->rate_emu   = o->rate_emu;
    f->accurate_seek = o->accurate_seek;
    f->loop = o->loop;
    f->duration = 0;
    f->time_base = (AVRational){ 1, 1 };
    

    #if HAVE_THREADS
    f->thread_queue_size = o->thread_queue_size > 0 ? o->thread_queue_size : 8;
    #endif

    /* check if all codec options have been used */
    unused_opts = strip_specifiers(o->g->codec_opts);
    for (i = f->ist_index; i < nb_input_streams; i++) {e = NULL;while ((e = av_dict_get(input_streams[i]->decoder_opts, "", e,AV_DICT_IGNORE_SUFFIX)))av_dict_set(&unused_opts, e->key, NULL, 0);
    }e = NULL;
    while ((e = av_dict_get(unused_opts, "", e, AV_DICT_IGNORE_SUFFIX))) {const AVClass *class = avcodec_get_class();const AVOption *option = av_opt_find(&class, e->key, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ);const AVClass *fclass = avformat_get_class();const AVOption *foption = av_opt_find(&fclass, e->key, NULL, 0,AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ);if (!option || foption)continue;if (!(option->flags & AV_OPT_FLAG_DECODING_PARAM)) {av_log(NULL, AV_LOG_ERROR, "Codec AVOption %s (%s) specified for ""input file #%d (%s) is not a decoding option.\n", e->key,option->help ? option->help : "", nb_input_files - 1,filename);exit_program(1);}av_log(NULL, AV_LOG_WARNING, "Codec AVOption %s (%s) specified for ""input file #%d (%s) has not been used for any stream. The most ""likely reason is either wrong type (e.g. a video option with ""no video streams) or that it is a private option of some decoder ""which was not actually used for any stream.\n", e->key,option->help ? option->help : "", nb_input_files - 1, filename);
    }
    av_dict_free(&unused_opts);for (i = 0; i < o->nb_dump_attachment; i++) {int j;for (j = 0; j < ic->nb_streams; j++) {AVStream *st = ic->streams[j];if (check_stream_specifier(ic, st, o->dump_attachment[i].specifier) == 1)dump_attachment(st, o->dump_attachment[i].u.str);}
    }input_stream_potentially_available = 1;return 0;
    

    }

相关文章:

ffmpeg4.1 源码学习之-转封装

前言 ffmpeg 的源码量非常的多&#xff0c;而且非常繁杂&#xff0c;非常多的函数&#xff0c;如果一个函数一个函数看的话要花费比较多的时间。所以本文通过跟踪ffmpeg转封装的过程来学习ffmpeg的源码具体转封装的命令&#xff1a;ffmpeg -i 1_cut.flv -c copy -f mp4 1.mp4在…...

ChatGPT写作文章-快速使用ChatGPT不用注册方式

如何更好地使用ChatGPT批量生成文章&#xff1a;详细教程 作为一款强大的文本生成器&#xff0c;ChatGPT可以帮助您快速、高效地批量生成文章。但如果您还不知道如何更好地使用ChatGPT&#xff0c;那么这篇详细的列表教程将会指导您如何使用它来生成高质量的文章&#xff0c;提…...

Nginx配置ip白名单(服务权限控制)

Nginx服务器权限控制&#xff1a;Nginx 是一款高性能的 HTTP 和反向代理服务器。它可以通过配置文件实现权限控制&#xff0c;从而限制或允许特定的 IP 地址、网络或用户访问指定的资源。这里是一些基本的 Nginx 权限控制方法&#xff1a; 1. 基于 IP 地址的访问控制 在 Ngin…...

Vue 核心(二)

文章目录Vue 核心&#xff08;二&#xff09;八、 绑定样式1、 class2、 style九、 条件渲染1、 渲染指令2、 使用示例十、 列表渲染1、 基本语法2、 key原理3、 列表过滤4、 列表排序5、 数组更新检测6、 大总结十一、 收集表单数据十二、 内置指令1、 v-text2、 v-html3、 v-…...

犯罪现场还原虚拟vr训练平台突破各种教学限制

在当今社会矛盾日益凸显、各式犯罪层出不穷的背景下&#xff0c;创新改革公安院校实战化教学方式&#xff0c;强化对基层公安队伍实战化应用型人才的供给能力具有重要性、紧迫性。 案件现场勘查是门技术活&#xff0c;如何让民警快速有效提升技能、服务实战? 常规刑侦专业现场…...

LeetCode 617. 合并二叉树 | C++语言版

LeetCode 617. 合并二叉树 | C语言版LeetCode 617. 合并二叉树题目描述解题思路思路一&#xff1a;使用递归代码实现运行结果参考文章&#xff1a;思路二&#xff1a;减少遍历节点数代码实现运行结果参考文章&#xff1a;LeetCode 617. 合并二叉树 题目描述 题目地址&#xf…...

Python量化交易08——利用Tushare获取日K数据

总有同学问我要数据...很多第三方接口都能直接获取数据的&#xff0c;本次介绍怎么用Tushare库获取A股日k线行情的数据。 Tushare有两个版本&#xff0c;老版现在快弃用了。 新版的官网链接在这&#xff1a;Tushare数据 需要注册一个账号&#xff0c;然后改一下资料&#xff0…...

30张精美可视化大屏,无需代码直接套用,解决你95%的大屏需求!

通常制作可视化大屏&#xff0c;一般有两种方式 代码开发&#xff1a;写代码调用数据和图表库&#xff0c;比如写JSEcharts &#xff1b; 可视化工具&#xff1a;用现成的数据可视化工具 前者简单直接&#xff0c;直接写代码就完事。但对于大部分人来说门槛较高&#xff0c;维…...

TCP网络事件模型的封装1.0

TCP网络模型的封装 最近学习了TCP网络模型的封装&#xff0c;其中运用的封装技术个人感觉有点绕 在反复读代码、做思维导图下初步理解了这套封装模型&#xff0c;不禁感叹原来代码还能这样写&#xff1f;神奇&#xff01; 为此将源码分享出来并将流程图画出&#xff0c;方便…...

NC271.二叉搜索树的后序遍历序列

文章目录一、题目描述二、示例三、主要思路一、题目描述 输入一个整数数组&#xff0c;判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回 true ,否则返回 false 。假设输入的数组的任意两个数字都互不相同。 提示&#xff1a; 1.二叉搜索树是指父亲节点大于左子树中…...

研究fastdds v2.8.0 1之 基础模块

阅读 dds 协议 1.4 版本 &#xff0c; 结合fastdds 2.8 的代码理解dds。 Entity 理解 DCPS基础设施模块由以下类组成&#xff1a; Entity DomainEntity QosPolicy Listener Status WaitSet Condition GuardCondition StatusCondition1、Entity 是所有DCPS 对象的基础类 virt…...

ElasticSearch系列 - SpringBoot整合ES:精确值查询 term

文章目录01. ElasticSearch term 查询&#xff1f;02. ElasticSearch term 查询数值型数据&#xff1f;03. ElasticSearch term 查询字符串类型数据&#xff1f;04. ElasticSearch term 查询日期型数据&#xff1f;05. ElasticSearch term 查询日期型数据的注意事项&#xff1f…...

关于async/await、promise和setTimeout执行顺序

关于async/await、promise和setTimeout执行顺序 async function async1() {console.log(async1 start);await async2();console.log(asnyc1 end); } async function async2() {console.log(async2); } console.log(script start); setTimeout(() > {console.log(setTimeOut…...

2023-03-31:如何计算字符串中不同的非空回文子序列个数?

2023-03-31&#xff1a;给定一个字符串 s&#xff0c;返回 s 中不同的非空 回文子序列 个数&#xff0c; 通过从 s 中删除 0 个或多个字符来获得子序列。 如果一个字符序列与它反转后的字符序列一致&#xff0c;那么它是 回文字符序列。 如果有某个 i , 满足 ai ! bi &#xff…...

D. The Number of Imposters(二分图染色)

Problem - D - Codeforces Theofanis开始玩名为“Among them”的新网络游戏。然而&#xff0c;他总是和塞浦路斯球员一起踢球&#xff0c;他们都有一个相同的名字:“安德烈亚斯”(塞浦路斯最常见的名字)。在每个游戏中&#xff0c;Theofanis和n个其他玩家一起玩。因为它们都有相…...

图片太大怎么改小kb?简单的图片压缩方法分享

平时当我们在朋友圈分享一些有趣的照片或者使用图片素材进行上传的时候&#xff0c;经常遇到图片大小kb超出平台限制的情况&#xff0c;这时就无法正常上传了&#xff0c;遇到这种情况我们就需要想办法降低图片大小kb&#xff0c;那么有什么办法能够压缩图片大小呢&#xff1f;…...

【python-leecode刷题】动态规划类问题----以53. 最大子数组和为例

作者&#xff1a;20岁爱吃必胜客&#xff08;坤制作人&#xff09;&#xff0c;近十年开发经验, 跨域学习者&#xff0c;目前于海外某世界知名高校就读计算机相关专业。荣誉&#xff1a;阿里云博客专家认证、腾讯开发者社区优质创作者&#xff0c;在CTF省赛校赛多次取得好成绩。…...

Idea常用快捷键设置

设置来源于尚硅谷宋红康老师 第1组&#xff1a;通用型 说明 快捷键 复制代码-copy ctrl c 粘贴-paste ctrl v 剪切-cut ctrl x 撤销-undo ctrl z 反撤销-redo ctrl shift z 保存-save all ctrl s 全选-select all ctrl a 第2组&#xff1a;提高编写速度&#xff08;上…...

【新2023Q2模拟题JAVA】华为OD机试 - 分苹果

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:分苹果 题目 AB两个人把苹果…...

【博学谷学习记录】超强总结,用心分享丨人工智能 自然语言处理 BERT、GPT、ELMO对比学习简记

目录三模型架构BERTGPTELMO三者差异点三模型架构 BERT 优点 在11个NLP任务上取得SOAT成绩.利用了Transformer的并行化能力以及长语句捕捉语义依赖和结构依赖.BERT实现了双向Transformer并为后续的微调任务留出足够的空间. 缺点 BERT模型太大, 太慢.BERT模型中的中文模型是以…...

【嵌入式Bluetooth应用开发笔记】第四篇:初探蓝牙HOST及应用开发(持续更新ing)

概念 蓝牙HOST(Bluetooth Host)是指能够连接到其他蓝牙设备并控制它们的设备。在蓝牙技术中,通常有两种类型的设备:蓝牙HOST和蓝牙SLAVE。蓝牙HOST通常是指拥有控制权的设备,它可以主动连接其他蓝牙设备并向其发送命令。相反,蓝牙SLAVE则是指被动连接的设备,它接受来自…...

GORM 基础 -- CRUD 接口

1、Create 1.1 创建纪录 user : User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}result : db.Create(&user) // pass pointer of data to Createuser.ID // 回填插入数据的主键 result.Error // 返回的 error 信息 result.RowsAffect…...

为什么0代码自动化测试越来越受欢迎?一文2000字解析

目录 01、什么是零代码自动化测试 02、为什么零代码自动化测试越来越受欢迎 03、有代码和零代码自动化有什么区别 04、零代码自动化测试可以帮助你做什么 05、零代码自动化测试方法&#xff1a;NLP&#xff08;自然语言处理&#xff09; 06、为什么我们需要零代码自动化测…...

cleanmymac最新2023版 mac清理软件CleanMyMac X4.12.5 中文版功能介绍

CleanMyMac X4.12.5 中文版只需两个简单步骤就可以把系统里那些乱七八糟的无用文件统统清理掉&#xff0c;节省宝贵的磁盘空间。cleanmymac x个人认为X代表界面上的最大升级&#xff0c;功能方面有更多增加&#xff0c;与最新macOS系统更加兼容&#xff0c;流畅地与系统性能更加…...

pyhon部署注意事项

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…...

宣城x移动云,打造“城市级物联感知平台”

随着新一代信息技术与城市现代化的深度融合&#xff0c;智慧城市建设的重要性也愈发凸显。而在智慧城市建设中&#xff0c;物联网感知体系扮演着中枢神经系统的角色。 安徽宣城紧抓长三角城市群一体化发展机遇&#xff0c;为构建“数字宣城”建设发展新模式&#xff0c;携手移…...

英伟达Jetson NX套件刷机,配置Ubuntu20。

0. 前言 人并没有眼见得那么光鲜亮丽&#xff0c;博客也是。 今天推荐一本书《一百个人的十年》&#xff0c;没错就是我们的那十年&#xff08;60年代&#xff09;。写得很真实&#xff0c;牛棚猪圈&#xff0c;确实如此。 1. SdkManager安装 官网下载。 打开终端 执行命令sud…...

Vue计算属性

计算属性 ​ 计算属性的重点突出在属性两个字上(属性是名词)&#xff0c;首先它是个属性其次这个属性有计算的能力(计算是动词)&#xff0c;这里的计算就是个函数;简单点说&#xff0c;它就是一个能够将计算结果缓存起来的属性(将行为转化成了静态的属性)&#xff0c;仅此而已…...

代码随想录刷题-字符串-反转字符串

文章目录反转字符串习题双指针swap 的两种方式反转字符串 本节对应代码随想录中&#xff1a;代码随想录&#xff0c;讲解视频&#xff1a;字符串基础操作&#xff01; | LeetCode&#xff1a;344.反转字符串_哔哩哔哩_bilibili 习题 题目链接&#xff1a;344. 反转字符串 - …...

14-链表练习-剑指 Offer II 021. 删除链表的倒数第 n 个结点

题目 给定一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[] 示例 3&…...