ffmpeg4.0.4 ffmpeg.c 讲解

ffmpeg.c 是 FFmpeg 中的一个核心文件,负责实现 FFmpeg 命令行工具的主要功能。这个文件包含了 FFmpeg 命令行工具的入口函数 main(),以及与命令行参数解析、多媒体处理、编解码、封装格式处理等相关的功能实现。

int main(int argc, char **argv)
{
    int i, ret;
    int64_t ti;

    init_dynload(); // 初始化动态加载库

    register_exit(ffmpeg_cleanup); // 注册退出时的清理函数

    setvbuf(stderr,NULL,_IONBF,0); // 设置标准错误流的缓冲区

    av_log_set_flags(AV_LOG_SKIP_REPEATED); // 设置日志标志,跳过重复日志

    parse_loglevel(argc, argv, options); // 解析命令行参数中的日志级别选项

    if(argc>1 && !strcmp(argv[1], "-d")){
        run_as_daemon=1; // 如果命令行参数中包含 -d 选项,则运行为守护进程模式
        av_log_set_callback(log_callback_null); // 设置日志回调函数为空
        argc--;
        argv++;
    }

#if CONFIG_AVDEVICE
    avdevice_register_all(); // 注册所有设备
#endif
    avformat_network_init(); // 初始化网络功能

    show_banner(argc, argv, options); // 显示 FFmpeg 的版本信息和命令行参数

    /* 解析命令行参数并打开所有的输入输出文件 */
    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); // 如果没有输入输出文件,则退出程序
    }

    /* 如果没有输出文件,则输出错误信息并退出 */
    if (nb_output_files <= 0) {
        av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");
        exit_program(1);
    }

    for (i = 0; i < nb_output_files; i++) {
        if (strcmp(output_files[i]->ctx->oformat->name, "rtp"))
            want_sdp = 0; // 如果输出文件不是 RTP 格式,则设置 want_sdp 标志为 0
    }

    current_time = ti = getutime(); // 获取当前时间
    if (transcode() < 0) // 执行音视频转码
        exit_program(1); // 如果转码失败则退出程序
    ti = getutime() - ti; // 计算转码时间
    if (do_benchmark) { // 如果需要进行性能测试
        av_log(NULL, AV_LOG_INFO, "bench: utime=%0.3fs\n", ti / 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; // 返回主函数的返回值
}

ffmpeg_cleanup

filtergraphs 是一个指针数组,它存储了多个 FilterGraph 结构体的指针。在函数中,会对 filtergraphs 数组中
的每个元素进行释放操作,包括释放 FilterGraph 结构体内部的资源,并最终释放整个数组的内存空间。

在代码中,filtergraphs 是一个指向 FilterGraph* 类型的指针,它指向了一组 FilterGraph 结构体的指针。
在循环中,通过对 filtergraphs 数组进行遍历,可以依次访问每个 FilterGraph 结构体的指针,并对其进行操作。

在这个函数中,filtergraphs 数组中的每个元素都指向一个 FilterGraph 结构体,而每个 FilterGraph 结构
体又包含了一系列的输入输出流以及相应的滤镜等资源。在释放过程中,需要逐个释放这些资源,并最
终释放 filtergraphs 数组本身所占用的内存空间。

因此,对 filtergraphs 数组进行释放操作的目的是确保在程序结束时能够正确地释放所有与滤镜图相关
的资源,避免内存泄漏等问题。

static void ffmpeg_cleanup(int ret)
{
    int i, j; /* 循环变量 */

    if (do_benchmark)
    {
        /* 如果开启了性能测试,则打印最大占用物理内存 */
        int maxrss = getmaxrss() / 1024;
        av_log(NULL, AV_LOG_INFO, "bench: maxrss=%ikB\n", maxrss);
    }

    for (i = 0; i < nb_filtergraphs; i++)
    {
        /* 释放滤镜图的资源 */
        FilterGraph *fg = filtergraphs[i];
        avfilter_graph_free(&fg->graph);
        for (j = 0; j < fg->nb_inputs; j++)
        {
            /* 遍历当前滤镜图的所有输入 */

            while (av_fifo_size(fg->inputs[j]->frame_queue))
            {
                /* 当输入的帧队列中还有帧时,循环释放帧资源 */

                AVFrame *frame;
                av_fifo_generic_read(fg->inputs[j]->frame_queue, &frame,
                                     sizeof(frame), NULL);
                /* 从帧队列中读取帧的指针 */

                av_frame_free(&frame);
                /* 释放帧资源 */
            }

            av_fifo_freep(&fg->inputs[j]->frame_queue);
            /* 释放输入的帧队列 */

            if (fg->inputs[j]->ist->sub2video.sub_queue)
            {
                /* 如果输入的子流到视频流的队列存在 */

                while (av_fifo_size(fg->inputs[j]->ist->sub2video.sub_queue))
                {
                    /* 当子流到视频流的队列中还有数据时,循环释放子流资源 */

                    AVSubtitle sub;
                    av_fifo_generic_read(fg->inputs[j]->ist->sub2video.sub_queue,
                                         &sub, sizeof(sub), NULL);
                    /* 从子流到视频流的队列中读取子流的指针 */

                    avsubtitle_free(&sub);
                    /* 释放子流资源 */
                }

                av_fifo_freep(&fg->inputs[j]->ist->sub2video.sub_queue);
                /* 释放子流到视频流的队列 */
            }

            av_buffer_unref(&fg->inputs[j]->hw_frames_ctx);
            /* 释放输入的硬件帧上下文 */

            av_freep(&fg->inputs[j]->name);
            /* 释放输入的名称字符串 */

            av_freep(&fg->inputs[j]);
            /* 释放输入本身 */
        }

        av_freep(&fg->inputs);
        // 释放输入资源数组

        for (j = 0; j < fg->nb_outputs; j++)
        {
            // 遍历当前滤镜图的所有输出

            av_freep(&fg->outputs[j]->name);
            // 释放输出的名称字符串

            av_freep(&fg->outputs[j]->formats);
            // 释放输出的格式数组

            av_freep(&fg->outputs[j]->channel_layouts);
            // 释放输出的声道布局数组

            av_freep(&fg->outputs[j]->sample_rates);
            // 释放输出的采样率数组

            av_freep(&fg->outputs[j]);
            // 释放输出本身
        }

        av_freep(&fg->outputs);
        // 释放输出资源数组

        av_freep(&fg->graph_desc);
        // 释放滤镜图的描述字符串

        av_freep(&filtergraphs[i]);
        // 释放当前滤镜图的指针
    }
    av_freep(&filtergraphs);
    // 释放 filtergraphs 数组所占用的内存空间

    av_freep(&subtitle_out);
    // 释放 subtitle_out 所占用的内存空间

    /* close files */
    for (i = 0; i < nb_output_files; i++)
    {
        /* 关闭输出文件 */
        OutputFile *of = output_files[i];
        AVFormatContext *s;
        if (!of)
            continue;
        s = of->ctx;
        if (s && s->oformat && !(s->oformat->flags & AVFMT_NOFILE))
            avio_closep(&s->pb);
        // 关闭输出文件的 AVIOContext

        avformat_free_context(s);
        // 释放输出文件的 AVFormatContext

        av_dict_free(&of->opts);
        // 释放输出文件的选项字典

        av_freep(&output_files[i]);
        // 释放 output_files 数组中的每个元素所占用的内存空间
    }

    for (i = 0; i < nb_output_streams; i++)
    {
        /* 释放输出流资源 */
        OutputStream *ost = output_streams[i];

        if (!ost)
            continue;

        for (j = 0; j < ost->nb_bitstream_filters; j++)
            av_bsf_free(&ost->bsf_ctx[j]);
        // 释放输出流的比特流过滤器上下文

        av_freep(&ost->bsf_ctx);
        // 释放输出流的比特流过滤器上下文数组

        av_frame_free(&ost->filtered_frame);
        // 释放输出流的过滤后帧

        av_frame_free(&ost->last_frame);
        // 释放输出流的最后一帧

        av_dict_free(&ost->encoder_opts);
        // 释放输出流的编码器选项字典

        av_freep(&ost->forced_keyframes);
        // 释放输出流的强制关键帧数组

        av_expr_free(ost->forced_keyframes_pexpr);
        // 释放输出流的强制关键帧表达式

        av_freep(&ost->avfilter);
        // 释放输出流的滤镜描述字符串

        av_freep(&ost->logfile_prefix);
        // 释放输出流的日志文件前缀字符串

        av_freep(&ost->audio_channels_map);
        // 释放输出流的音频声道映射数组
        ost->audio_channels_mapped = 0;

        av_dict_free(&ost->sws_dict);
        // 释放输出流的图像转换选项字典

        avcodec_free_context(&ost->enc_ctx);
        // 释放输出流的编码器上下文

        avcodec_parameters_free(&ost->ref_par);
        // 释放输出流的参考参数

        if (ost->muxing_queue)
        {
            /* 释放混流队列资源 */
            while (av_fifo_size(ost->muxing_queue))
            {
                AVPacket pkt;
                av_fifo_generic_read(ost->muxing_queue, &pkt, sizeof(pkt), NULL);
                av_packet_unref(&pkt);
            }
            // 循环释放混流队列中的 AVPacket 资源

            av_fifo_freep(&ost->muxing_queue);
            // 释放混流队列
        }

        av_freep(&output_streams[i]);
        // 释放 output_streams 数组中的每个元素所占用的内存空间
    }

#if HAVE_THREADS
    free_input_threads();
#endif
    for (i = 0; i < nb_input_files; i++)
    {
        /* 关闭输入文件 */
        avformat_close_input(&input_files[i]->ctx);
        // 关闭输入文件的 AVFormatContext

        av_freep(&input_files[i]);
        // 释放 input_files 数组中的每个元素所占用的内存空间
    }

    for (i = 0; i < nb_input_streams; i++)
    {
        /* 释放输入流资源 */
        InputStream *ist = input_streams[i];

        av_frame_free(&ist->decoded_frame);
        // 释放解码后的帧资源

        av_frame_free(&ist->filter_frame);
        // 释放滤波后的帧资源

        av_dict_free(&ist->decoder_opts);
        // 释放解码器选项字典

        avsubtitle_free(&ist->prev_sub.subtitle);
        // 释放前一个字幕

        av_frame_free(&ist->sub2video.frame);
        // 释放转换为视频帧的字幕帧

        av_freep(&ist->filters);
        // 释放滤镜描述字符串

        av_freep(&ist->hwaccel_device);
        // 释放硬件加速设备字符串

        av_freep(&ist->dts_buffer);
        // 释放 DTS 缓冲区

        avcodec_free_context(&ist->dec_ctx);
        // 释放解码器上下文

        av_freep(&input_streams[i]);
        // 释放 input_streams 数组中的每个元素所占用的内存空间
    }

    if (vstats_file)
    {
        /* 关闭 vstats 文件 */
        if (fclose(vstats_file))
            av_log(NULL, AV_LOG_ERROR,
                   "Error closing vstats file, loss of information possible: %s\n",
                   av_err2str(AVERROR(errno)));
    }

    av_freep(&vstats_filename);
    // 释放 vstats_filename 所占用的内存空间

    av_freep(&input_streams);
    // 释放 input_streams 数组所占用的内存空间

    av_freep(&input_files);
    // 释放 input_files 数组所占用的内存空间

    av_freep(&output_streams);
    // 释放 output_streams 数组所占用的内存空间

    av_freep(&output_files);
    // 释放 output_files 数组所占用的内存空间

    uninit_opts();
    // 释放其他选项

    /* 关闭网络模块 */
    avformat_network_deinit();
    // 关闭网络模块

    if (received_sigterm)
    {
        /* 如果收到 SIGTERM 信号,则打印退出信息 */
        av_log(NULL, AV_LOG_INFO, "Exiting normally, received signal %d.\n",
               (int)received_sigterm);
    }
    else if (ret && atomic_load(&transcode_init_done))
    {
        /* 如果转码初始化完成并且返回值非零,则打印转码失败信息 */
        av_log(NULL, AV_LOG_INFO, "Conversion failed!\n");
    }

    /* 退出程序 */
    term_exit();
    ffmpeg_exited = 1;
}

ffmpeg_parse_options

这个函数的主要作用是解析命令行参数,应用全局选项,配置终端和信号处理程序,打开输入文件,创建复杂的过滤器图,打开输出文件,并在出现错误时进行相应的清理工作。

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

    memset(&octx, 0, sizeof(octx));
    // 初始化一个 OptionParseContext 结构体,并将其内存清零

    /* split the commandline into an internal representation */
    ret = split_commandline(&octx, argc, argv, options, groups, FF_ARRAY_ELEMS(groups));
    // 将命令行参数分割成内部表示形式,存储在 octx 中
    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;
    }

    /* configure terminal and setup signal handlers */
    term_init();
    // 配置终端并设置信号处理程序

    /* 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;
    }

    check_filter_outputs();

fail:
    uninit_parse_context(&octx);
    // 清理 OptionParseContext 结构体
    if (ret < 0) {
        av_strerror(ret, error, sizeof(error));
        av_log(NULL, AV_LOG_FATAL, "%s\n", error);
    }
    return ret;
    // 返回处理结果
}

open_files

int open_files(InputFiles *ifiles, const char *type, int (*open_file)(void *opaque, const char *filename, int index))
{
    int ret;
    int i;

    for (i = 0; i < ifiles->nb_files; i++) {
        const char *filename = ifiles->files[i].filename;

        ret = open_file(ifiles->files[i].u.file, filename, i);
        if (ret < 0) {
            av_log(NULL, AV_LOG_FATAL, "Failed to open %s file '%s'\n", type, filename);
            return ret;
        }
    }

    return 0;
}

open_input_file

/**
 * 打开输入文件并设置相关参数
 * @param o OptionsContext 结构体指针,包含了一些选项和配置信息
 * @param filename 要打开的输入文件名
 * @return 成功返回 0,失败返回负数错误码
 */
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; // 是否设置了 scan_all_pmts 标志

    // 处理 -t 和 -to 参数的冲突
    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");
    }

    // 处理 -to 参数
    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;
        }
    }

    // 查找指定格式的输入格式
    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);
        }
    }

    // 处理输入文件名为 "-" 的情况
    if (!strcmp(filename, "-"))
        filename = "pipe:";

    // 设置 stdin_interaction 标志
    stdin_interaction &= strncmp(filename, "pipe:", 5) &&
                         strcmp(filename, "/dev/stdin");

    // 分配 AVFormatContext 结构体
    ic = avformat_alloc_context();
    if (!ic) {
        print_error(filename, AVERROR(ENOMEM));
        exit_program(1);
    }

    // 设置音频采样率参数
    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) {
        // 检查输入格式是否支持 channels 选项
        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) {
        // 设置格式级别的帧率选项
        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);

    // 匹配视频、音频、字幕、数据编解码器选项
    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);

    // 设置视频、音频、字幕、数据编解码器 ID
    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;

    // 设置 AVFMT_FLAG_NONBLOCK 标志
    ic->flags |= AVFMT_FLAG_NONBLOCK;
    // 设置 AVFMT_FLAG_BITEXACT 标志
    if (o->bitexact)
        ic->flags |= AVFMT_FLAG_BITEXACT;

    // 设置中断回调函数
    ic->interrupt_callback = int_cb;

    // 如果未设置 scan_all_pmts 标志,则设置为 1
    if (!scan_all_pmts_set)
        ic->probesize = 5000000;

    // 打开输入文件
    err = avformat_open_input(&ic, filename, file_iformat, &unused_opts);
    if (err < 0) {
        print_error(filename, err);
        if (exit_on_error)
            exit_program(1);
        else
            return err;
    }

    // 移除 scan_all_pmts 标志
    if (!scan_all_pmts_set)
        ic->probesize = 0;

    // 移除未使用的解码器选项
    for (e = av_dict_get(o->g->codec_opts, "", NULL, AV_DICT_IGNORE_SUFFIX);
         e;
         e = av_dict_get(o->g->codec_opts, "", NULL, AV_DICT_IGNORE_SUFFIX)) {
        av_log(ic, AV_LOG_WARNING, "Option %s not found.\n", e->key);
        av_dict_set(&o->g->format_opts, e->key, NULL, 0);
    }

    // 如果需要获取流参数,则获取流信息
    if (o->start_time != AV_NOPTS_VALUE || o->recording_time != 0) {
        err = avformat_find_stream_info(ic, &unused_opts);
        if (err < 0) {
            av_log(NULL, AV_LOG_WARNING,
                   "Could not find stream information for input '%s'\n", filename);
            av_dict_free(&unused_opts);
            return err;
        }
    }

    // 计算起始时间戳
    timestamp = o->start_time == AV_NOPTS_VALUE ? 0 : o->start_time;
    if (timestamp != 0) {
        int64_t pos = av_rescale(timestamp, AV_TIME_BASE, 1000000);
        av_log(NULL, AV_LOG_VERBOSE, "Seeking to position %0.3f sec.\n", pos / 1000000.0);
        ret = avformat_seek_file(ic, -1, INT64_MIN, pos, INT64_MAX, 0);
        if (ret < 0)
            av_log(NULL, AV_LOG_WARNING, "Could not seek to position %0.3f\n", pos / 1000000.0);
    }

    // 打印文件信息
    if (o->print_format)
        av_dump_format(ic, 0, filename, 0);

    // 分配输入文件结构体并设置参数
    f = av_mallocz(sizeof(*f));
    if (!f) {
        av_log(NULL, AV_LOG_FATAL, "Out of memory\n");
        exit_program(1);
    }
    f->ctx = ic;
    f->ist_index = -1;
    f->nb_streams = ic->nb_streams;

    // 检查未使用的解码器选项
    for (i = 0; i < ic->nb_streams; i++) {
        if (ic->streams[i]->codec->codec_id == AV_CODEC_ID_NONE)
            continue;
        if (ic->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
            av_dict_copy(&ic->streams[i]->codec->metadata, ic->metadata, AV_DICT_DONT_OVERWRITE);
    }

    // 处理附件信息
    if (!o->attachments)
        return 0;
    for (i = 0; i < ic->nb_streams; i++) {
        AVStream *st = ic->streams[i];
        AVDictionaryEntry *t = av_dict_get(st->metadata, "filename", NULL, 0);
        if (t) {
            fprintf(stderr, "Attachment: %s\n", t->value);
        }
    }

    // 返回成功
    return 0;
}
相关推荐
一点媛艺11 分钟前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风15 分钟前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生1 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程2 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang