终于开始了自己的处女作,对于ffmpeg本人还是个小学生,如果有写的不对的地方,还请不吝赐教,再次感谢!
ffmpeg的版本为3.1.3,是当前的最新版。
涉及到的文件有:
其它文件如:
ffmpeg_cuvid.c ffmpeg_dxva2.c ffmpeg_qsv.c ffmpeg_vaapi.c ffmpeg_vdpau.c ffmpeg_videotoolbox.c
都是与硬件相关的,在编译时需要加对应的参数。不在此讨论范围之内。参考如下:
ffmpeg.h中
enum HWAccelID { HWACCEL_NONE = 0, HWACCEL_AUTO, HWACCEL_VDPAU, HWACCEL_DXVA2, HWACCEL_VDA, HWACCEL_VIDEOTOOLBOX, HWACCEL_QSV, HWACCEL_VAAPI, HWACCEL_CUVID, }; typedef struct HWAccel { const char *name; int (*init)(AVCodecContext *s); enum HWAccelID id; enum AVPixelFormat pix_fmt; } HWAccel;ffmpeg_opt.c中
const HWAccel hwaccels[] = { #if HAVE_VDPAU_X11 { "vdpau", vdpau_init, HWACCEL_VDPAU, AV_PIX_FMT_VDPAU }, #endif #if HAVE_DXVA2_LIB { "dxva2", dxva2_init, HWACCEL_DXVA2, AV_PIX_FMT_DXVA2_VLD }, #endif #if CONFIG_VDA { "vda", videotoolbox_init, HWACCEL_VDA, AV_PIX_FMT_VDA }, #endif #if CONFIG_VIDEOTOOLBOX { "videotoolbox", videotoolbox_init, HWACCEL_VIDEOTOOLBOX, AV_PIX_FMT_VIDEOTOOLBOX }, #endif #if CONFIG_LIBMFX { "qsv", qsv_init, HWACCEL_QSV, AV_PIX_FMT_QSV }, #endif #if CONFIG_VAAPI { "vaapi", vaapi_decode_init, HWACCEL_VAAPI, AV_PIX_FMT_VAAPI }, #endif #if CONFIG_CUVID { "cuvid", cuvid_init, HWACCEL_CUVID, AV_PIX_FMT_CUDA }, #endif { 0 }, }; int hwaccel_lax_profile_check = 0; AVBufferRef *hw_device_ctx;
进入主题。
在main函数中,如下函数解析参数和处理输入和输出文件
/* parse options and open all input/output files */ ret = ffmpeg_parse_options(argc, argv); if (ret < 0) exit_program(1);
int Cffmpeg::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将命令行参数进行分析,并存入octx中 */ ret = split_commandline(&octx, argc, argv, options, groups, FF_ARRAY_ELEMS(groups)); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error splitting the argument list: "); goto fail; } /* apply global options 设置全局参数*/ ret = parse_optgroup(NULL, &octx.global_opts); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error parsing global options: "); goto fail; } /* open input files设置输入文件相关的参数,并打开输入文件 */ ret = open_files(&octx.groups[GROUP_INFILE], "input"); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error opening input files: "); goto fail; } /* create the complex filtergraphs如果参数中设置了滤镜相关的内容,则对滤镜初始化 */ ret = init_complex_filters(); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n"); goto fail; } /* open output files 设置输出文件相关参数,并打开输出文件*/ ret = open_files(&octx.groups[GROUP_OUTFILE], "output"); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error opening output files: "); goto fail; } /* configure the complex filtergraphs 配置滤镜*/ ret = configure_complex_filters(); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error configuring complex filters.\n"); goto fail; } fail: uninit_parse_context(&octx);//释放octx if (ret < 0) { av_strerror(ret, (char*)error, sizeof(error)); av_log(NULL, AV_LOG_FATAL, "%s\n", error); } return ret; }split_commandline()的参数解析如下:
1.OptionParseContext octx;用来将命令行中的参数通过分析分别存入此结构体的对应变量中。之后argc, argv就没用了。
2.options,定义了所有的参数、参数类型、参数是否有值、是放到全局变量中还是放到OptionsContext结构的某个变量中,或者是调用一个函数来处理参数的值。
定义如下:
#define OFFSET(x) offsetof(OptionsContext, x) const OptionDef options[] = { /* main options */ #include "cmdutils_common_opts.h" { "f", HAS_ARG | OPT_STRING | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(format) }, "force format", "fmt" }, { "y", OPT_BOOL, { &file_overwrite }, "overwrite output files" }, { "n", OPT_BOOL, { &no_file_overwrite }, "never overwrite output files" }, { "ignore_unknown", OPT_BOOL, { &ignore_unknown_streams }, "Ignore unknown stream types" }, { "copy_unknown", OPT_BOOL | OPT_EXPERT, { ©_unknown_streams }, "Copy unknown stream types" }, { "c", HAS_ARG | OPT_STRING | OPT_SPEC | OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(codec_names) }, "codec name", "codec" }, { "codec", HAS_ARG | OPT_STRING | OPT_SPEC | OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(codec_names) }, "codec name", "codec" }, { "pre", HAS_ARG | OPT_STRING | OPT_SPEC | OPT_OUTPUT, { .off = OFFSET(presets) }, "preset name", "preset" }, { "map", HAS_ARG | OPT_EXPERT | OPT_PERFILE | OPT_OUTPUT, { .func_arg = opt_map }, "set input stream mapping", "[-]input_file_id[:stream_specifier][,sync_file_id[:stream_specifier]]" },
类似.off = OFFSET(format) 参数的值放到结构体OptionsContext的format中。这个结构体变量将在open_files中定义。
类似.func_arg = opt_map表示要调用opt_map函数处理此参数的值。
3.参数groups
typedef struct OptionGroupDef { /**< group name */ const char *name; /** * Option to be used as group separator. Can be NULL for groups which * are terminated by a non-option argument (e.g. ffmpeg output files) */ const char *sep; /** * Option flags that must be set on each option that is * applied to this group */ int flags; } OptionGroupDef; static const OptionGroupDef groups[] = { [GROUP_OUTFILE] = { "output file", NULL, OPT_OUTPUT }, [GROUP_INFILE] = { "input file", "i", OPT_INPUT }, };
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 */ prepare_app_arguments(&argc, &argv); 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); /*如果使“--”记下,后面没值。继续循环*/ if (opt[0] == '-' && opt[1] == '-' && !opt[2]) { dashdash = optindex; continue; } /* 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++; #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 看是不是-i如果使的话输入文件组参数结束*/ 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中找是否存在*/ po = find_option(options, opt); if (po->name) { if (po->flags & OPT_EXIT) { /* optional argument, e.g. -h 遇到这个参数是要直接退出程序的,在write_option()中在最后有判断*/ arg = argv[optindex++]; } else if (po->flags & HAS_ARG) {//如果需要有参数的就获取参数 GET_ARG(arg); } else {//不需要参数的就设置默认值为“1” arg = "1"; } /*根据先前在options中对参数的定义判断,是加到全局队列global_opts中,还是加到临时队列cur_group中。*/ 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 如果没有在options中找到,那可能是某个库独特的参数在此处理*/ if (argv[optindex]) { 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 在options中所列的参数前加上-no的处理在此,但是得是OPT_BOOL的属性。值设为0.*/ if (opt[0] == 'n' && opt[1] == 'o' && (po = find_option(options, opt + 2)) && po->name && po->flags & OPT_BOOL) { 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; }
init_parse_context()函数的作用是将groups放到octx中的OptionGroupList *groups;中。nb_groups;设置为2.global_opts初始化。
其中global_opts是用来存放与输入输出文件无关的变量。
groups[0]存放于输出文件有关的参数
groups[1]存放于输入文件有关的参数
由于ffmpeg的命令行参数规则如下:
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...
所以全局参数,输入参数,输出参数是相对分离开的。
在OptionParseContext中OptionGroup cur_group;变量就充当了临时存放一组数据的作用。会在finish_group()中移动到对应的队列中。可能是输入文件队列也可能是输出文件队列。全局的队列直接存入。是存入cur_group还是存入global_opts中在函数add_opt()中决定。
/* * 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; GROW_ARRAY(l->groups, l->nb_groups); g = &l->groups[l->nb_groups - 1]; *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)); }
这些类型的参数在opt_default()中处理。
#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; const AVClass *cc = avcodec_get_class(), *fc = avformat_get_class(); #if CONFIG_AVRESAMPLE const AVClass *rc = avresample_get_class(); #endif #if CONFIG_SWSCALE const AVClass *sc = sws_get_class(); #endif #if CONFIG_SWRESAMPLE const AVClass *swr_class = swr_get_class(); #endif if (!strcmp(opt, "debug") || !strcmp(opt, "fdebug")) av_log_set_level(AV_LOG_DEBUG); if (!(p = strchr(opt, ':'))) p = opt + strlen(opt); av_strlcpy(opt_stripped, opt, FFMIN(sizeof(opt_stripped), p - opt + 1)); 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)))) { av_dict_set(&codec_opts, opt, arg, FLAGS); consumed = 1; } 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_SWSCALE if (!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; } #else if (!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_SWRESAMPLE if (!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_AVRESAMPLE if ((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; } #endif if (consumed) return 0; return AVERROR_OPTION_NOT_FOUND; }
swr_opts:在libswresample\options.c中
codec_opts:在libavcodec\options.c中
format_opts:在libavformat\options.c中
resample_opts:在libavresample\options.c中
从如下声明中就可以看出来
const AVClass *cc = avcodec_get_class(), *fc = avformat_get_class(); #if CONFIG_AVRESAMPLE const AVClass *rc = avresample_get_class(); #endif #if CONFIG_SWSCALE const AVClass *sc = sws_get_class(); #endif #if CONFIG_SWRESAMPLE const AVClass *swr_class = swr_get_class(); #endif
parse_optgroup()用来处理已经放到octx中的参数。在ffmpeg_parse_options()中和open_files()中都有调用。分别处理octx.global_opts全局参数、octx.groups[GROUP_INFILE]输入文件参数、octx.groups[GROUP_OUTFILE]输出文件参数。
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]; 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); 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; }
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*/ void *dst = po->flags & (OPT_OFFSET | OPT_SPEC) ? (uint8_t *)optctx + po->u.off : po->u.dst_ptr; int *dstcount; if (po->flags & OPT_SPEC) { SpecifierOpt **so = dst; char *p = strchr(opt, ':'); char *str; dstcount = (int *)(so + 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 = &(*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; }
之前提到过的。如果参数预制了OPT_EXIT标识,在write_option()中会退出程序。如上代码可知。
exit_program()很简单。如下
static void (*program_exit)(int ret); void register_exit(void (*cb)(int ret)) { program_exit = cb; } void exit_program(int ret) { if (program_exit) program_exit(ret); exit(ret); }就是执行一个函数指针指向的函数。此指针由register_exit()在main函数中设置。
int main(int argc, char **argv) { int ret; int64_t ti; init_dynload(); register_exit(ffmpeg_cleanup);
之后ffmpeg_parse_options()开始打开输入文件和输出文件,避免返回到上面看此函数代码。将部分代码贴于此。
/* open input files */ ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error opening input files: "); goto fail; } /* create the complex filtergraphs */ ret = init_complex_filters(); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n"); goto fail; } /* open output files */ ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error opening output files: "); goto fail; } /* configure the complex filtergraphs */ ret = configure_complex_filters(); if (ret < 0) { av_log(NULL, AV_LOG_FATAL, "Error configuring complex filters.\n"); goto fail; }有两个open_files()分别将open_input_file()和open_output_file()作为回调传入。意思很明显,就是要进行输入文件和输出文件的初始化。
static int open_files(OptionGroupList *l, const char *inout, int (*open_file)(OptionsContext*, const char*)) { int i, ret; for (i = 0; i < l->nb_groups; i++) { OptionGroup *g = &l->groups[i]; OptionsContext o; init_options(&o); o.g = g; 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); ret = open_file(&o, g->arg); uninit_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; }就是调用了parse_optgroup()、open_input_file()和open_output_file()函数。后两个函数不在此讨论。
{ "filter_complex", HAS_ARG | OPT_EXPERT, { .func_arg = opt_filter_complex }, "create a complex filtergraph", "graph_description" }, { "lavfi", HAS_ARG | OPT_EXPERT, { .func_arg = opt_filter_complex }, "create a complex filtergraph", "graph_description" }, { "filter_complex_script", HAS_ARG | OPT_EXPERT, { .func_arg = opt_filter_complex_script }, "read complex filtergraph description from a file", "filename" },
static int opt_filter_complex(void *optctx, const char *opt, const char *arg) { GROW_ARRAY(filtergraphs, nb_filtergraphs); if (!(filtergraphs[nb_filtergraphs - 1] = av_mallocz(sizeof(*filtergraphs[0])))) return AVERROR(ENOMEM); filtergraphs[nb_filtergraphs - 1]->index = nb_filtergraphs - 1; filtergraphs[nb_filtergraphs - 1]->graph_desc = av_strdup(arg); if (!filtergraphs[nb_filtergraphs - 1]->graph_desc) return AVERROR(ENOMEM); input_stream_potentially_available = 1; return 0; } static int opt_filter_complex_script(void *optctx, const char *opt, const char *arg) { uint8_t *graph_desc = read_file(arg); if (!graph_desc) return AVERROR(EINVAL); GROW_ARRAY(filtergraphs, nb_filtergraphs); if (!(filtergraphs[nb_filtergraphs - 1] = av_mallocz(sizeof(*filtergraphs[0])))) return AVERROR(ENOMEM); filtergraphs[nb_filtergraphs - 1]->index = nb_filtergraphs - 1; filtergraphs[nb_filtergraphs - 1]->graph_desc = graph_desc; input_stream_potentially_available = 1; return 0; }
ffmpeg_parse_options()最后要调用uninit_parse_context(&octx);释放存入octx的参数。命令行参数完成了它这一阶段的使命。
void uninit_parse_context(OptionParseContext *octx) { int i, j; for (i = 0; i < octx->nb_groups; i++) { OptionGroupList *l = &octx->groups[i]; for (j = 0; j < l->nb_groups; j++) { av_freep(&l->groups[j].opts); av_dict_free(&l->groups[j].codec_opts); av_dict_free(&l->groups[j].format_opts); av_dict_free(&l->groups[j].resample_opts); av_dict_free(&l->groups[j].sws_dict); av_dict_free(&l->groups[j].swr_opts); } av_freep(&l->groups); } av_freep(&octx->groups); av_freep(&octx->cur_group.opts); av_freep(&octx->global_opts.opts); uninit_opts(); }