资讯动态

FFmpeg API 实战:读取视频信息与避坑指南

发布时间:2026/10/16 11:06:17 来源:尧图企业网站定制
FFmpeg 这个库做音视频开发的没人绕得开。但很多人第一次翻开它的头文件看到AVFormatContext、AVCodecParameters、AVStream这一堆结构体第一反应是懵的——文档少、示例老、API 还老变。我当初也是这样为了拿一个视频的宽高和时长硬是折腾了大半天。这篇就聊聊怎么用 FFmpeg 的 API 把视频信息读出来包括容器层面的元数据、每条流的编码参数以及那些文档里不写、但实际会踩的坑。适合刚接触 FFmpeg、准备做播放器、转码工具或者媒体管理系统的朋友也适合已经用过命令行但想转到 API 层面的人。1. 先搞清楚 FFmpeg 到底能读出哪些信息很多人以为获取视频信息就是拿个时长和分辨率其实远不止。FFmpeg 把信息分成两个层次容器层和流层。容器层描述的是这个文件本身比如格式名称、时长、比特率、元数据标签标题、作者、创建时间这些流层描述的是文件里每一条独立的轨道比如视频流、音频流、字幕流每条流有自己的编码格式、分辨率、采样率、帧率等。1.1 容器层信息文件的身份证容器层对应的是AVFormatContext这个结构体。当你调用avformat_open_input打开一个文件后这个结构体里就填满了容器相关的信息。几个关键字段iformat输入格式比如 mov,mp4,m4a,3gp,3g2,mj2 这种它告诉你 FFmpeg 是用哪个 demuxer 解析的这个文件。duration时长单位是 AV_TIME_BASE也就是微秒。注意这个值不一定准尤其是某些流式格式或者文件头没写好的情况。bit_rate整体比特率单位是 bit/s。nb_streams流的数量。metadata一个AVDictionary里面是键值对形式的元数据。这里有个容易忽略的点duration在打开文件后可能还是 0 或者一个很小的值因为有些格式的时长信息在文件末尾。这时候你需要调用avformat_find_stream_info之后再看或者干脆自己根据帧数和帧率估算。1.2 流层信息每条轨道的体检报告每条流对应一个AVStream通过formatContext-streams[i]访问。AVStream里最重要的两个东西是codecpar编码参数和time_base时间基。codecpar是AVCodecParameters类型它替代了老版本里的AVCodecContext在打开解码器之前的那部分信息。里面包含codec_type流类型AVMEDIA_TYPE_VIDEO、AVMEDIA_TYPE_AUDIO、AVMEDIA_TYPE_SUBTITLE等。codec_id编码器 ID比如AV_CODEC_ID_H264、AV_CODEC_ID_AAC。视频相关width、height、format像素格式、bit_rate。音频相关sample_rate、channels、channel_layout、frame_size。time_base是个AVRational分子分母的形式。视频流通常是 1/90000 或者 1/1000 这种音频流可能是 1/44100。这个值决定了流里时间戳的单位后面做时间换算全靠它。提示codecpar里的bit_rate经常是 0尤其是视频流。别指望它真要算比特率得自己根据文件大小和时长估。2. 打开文件与读取流信息的完整流程光看结构体没用得知道按什么顺序调用 API。FFmpeg 的 API 设计是先打开再探测最后读顺序错了要么拿不到数据要么直接崩。2.1 第一步avformat_open_input 打开输入AVFormatContext *fmt_ctx NULL; int ret avformat_open_input(fmt_ctx, filename, NULL, NULL); if (ret 0) { // 打开失败用 av_strerror 拿错误信息 char errbuf[128]; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, Could not open file: %s\n, errbuf); return ret; }这里第一个参数是二级指针传NULL进去FFmpeg 会自己分配一个AVFormatContext。第三个参数是强制指定输入格式一般传NULL让它自动探测。第四个参数是AVInputFormat的选项字典也传NULL。打开成功后fmt_ctx-iformat、fmt_ctx-nb_streams这些就已经有值了。但注意这时候流里的编码参数可能还不完整尤其是网络流或者某些特殊容器。2.2 第二步avformat_find_stream_info 补全信息ret avformat_find_stream_info(fmt_ctx, NULL); if (ret 0) { fprintf(stderr, Failed to find stream info\n); avformat_close_input(fmt_ctx); return ret; }这个函数会实际去读一部分数据通常是几帧来探测每条流的真实编码参数。对于本地文件它很快对于网络流可能会阻塞一会儿。它的作用是把codecpar里的width、height、sample_rate这些填完整。我见过有人跳过这一步直接读codecpar-width结果拿到 0然后到处找 bug。这一步不能省除非你非常确定文件头里已经写全了所有信息。2.3 第三步遍历流提取需要的信息for (unsigned int i 0; i fmt_ctx-nb_streams; i) { AVStream *stream fmt_ctx-streams[i]; AVCodecParameters *par stream-codecpar; if (par-codec_type AVMEDIA_TYPE_VIDEO) { printf(Video stream #%u\n, i); printf( Codec: %s\n, avcodec_get_name(par-codec_id)); printf( Resolution: %dx%d\n, par-width, par-height); printf( Pixel format: %s\n, av_get_pix_fmt_name(par-format)); printf( Time base: %d/%d\n, stream-time_base.num, stream-time_base.den); } else if (par-codec_type AVMEDIA_TYPE_AUDIO) { printf(Audio stream #%u\n, i); printf( Codec: %s\n, avcodec_get_name(par-codec_id)); printf( Sample rate: %d\n, par-sample_rate); printf( Channels: %d\n, par-channels); } }avcodec_get_name能把AV_CODEC_ID_H264转成 h264 这种可读字符串。av_get_pix_fmt_name同理把AV_PIX_FMT_YUV420P转成 yuv420p。2.4 第四步别忘了 avformat_close_inputavformat_close_input(fmt_ctx);这个函数会释放AVFormatContext及其内部所有流、元数据占用的内存并把指针置为NULL。不调用就是内存泄漏。我见过一个项目跑久了内存一直涨最后查出来就是打开文件后没关。3. 时长、帧率、比特率三个最容易算错的值这三个值看起来简单但 FFmpeg 给出来的原始数据往往不能直接用得换算。3.1 时长微秒、时间基、还是秒fmt_ctx-duration的单位是微秒AV_TIME_BASE 1000000。所以double duration_sec fmt_ctx-duration / (double)AV_TIME_BASE;但前面说了这个值可能是 0。更可靠的做法是用流的时长AVStream *video_stream ...; double stream_duration video_stream-duration * av_q2d(video_stream-time_base);av_q2d把AVRational转成double。stream-duration的单位是stream-time_base乘上时间基的倒数就是秒。如果两个都是 0 或者AV_NOPTS_VALUE那就只能靠读帧来数了。这种情况在直播流或者某些损坏文件里很常见。3.2 帧率r_frame_rate 和 avg_frame_rate 的区别AVStream里有两个帧率字段r_frame_rate基础帧率通常是实际帧率的近似值比如 30000/1001。avg_frame_rate平均帧率根据实际帧的时间戳算出来的。对于恒定帧率CFR的视频两者基本一致。对于可变帧率VFR的视频avg_frame_rate更接近真实观感但r_frame_rate才是解码器实际用的。AVRational fr stream-avg_frame_rate; if (fr.num ! 0 fr.den ! 0) { double fps av_q2d(fr); printf(FPS: %.2f\n, fps); }注意分母为 0 的情况直接除会崩。必须先判断。3.3 比特率文件大小除以时长最靠谱fmt_ctx-bit_rate有时候是 0codecpar-bit_rate也经常是 0。最稳的办法是自己算// 假设你已经知道文件大小 file_size字节 double bitrate (file_size * 8) / duration_sec;单位是 bit/s。如果要更精确可以分别算视频流和音频流的比特率但那就得解析 packet 了属于进阶操作。4. 元数据读取AVDictionary 的用法与坑元数据就是那些标题作者专辑之类的标签。FFmpeg 用AVDictionary存键值都是字符串。4.1 遍历元数据AVDictionaryEntry *tag NULL; while ((tag av_dict_get(fmt_ctx-metadata, , tag, AV_DICT_IGNORE_SUFFIX))) { printf(%s %s\n, tag-key, tag-value); }av_dict_get的第二个参数是键名传空字符串表示匹配所有键。第三个参数是上一次返回的 entry用来迭代。第四个参数AV_DICT_IGNORE_SUFFIX表示忽略键的后缀匹配。4.2 读取特定键AVDictionaryEntry *title av_dict_get(fmt_ctx-metadata, title, NULL, 0); if (title) { printf(Title: %s\n, title-value); }注意键名是大小写敏感的而且不同容器用的键名不一样。MP4 里可能是 titleMKV 里可能是 TITLE。实际项目里通常得准备一个映射表。4.3 流级别的元数据每条流也有自己的metadata比如语言标签AVDictionaryEntry *lang av_dict_get(stream-metadata, language, NULL, 0);这个在有多音轨的视频里很有用能告诉用户哪条是中文、哪条是英文。注意元数据里的字符串编码不一定是 UTF-8有些老文件是 GBK 或者 Latin-1。直接 printf 可能乱码。稳妥的做法是先检测编码再转换或者至少做个容错。5. 那些文档不写但一定会踩的坑FFmpeg 的 API 有几个地方特别容易出问题我一个个说。5.1 返回值检查不能偷懒FFmpeg 几乎所有函数都返回 int小于 0 表示出错。很多人图省事不检查结果后面用空指针或者非法值程序直接段错误。每个可能失败的调用都要检查尤其是avformat_open_input、avformat_find_stream_info、avcodec_parameters_to_context这几个。5.2 AV_NOPTS_VALUE 的判断时间戳字段可能是AV_NOPTS_VALUE这是一个宏值是INT64_MIN。直接拿来做运算会溢出。判断方法if (stream-duration ! AV_NOPTS_VALUE) { // 安全使用 }5.3 多线程与线程安全FFmpeg 的AVFormatContext不是线程安全的。一个 context 同时只能被一个线程操作。如果你要在多线程环境里读多个文件每个线程用自己的 context别共享。5.4 内存管理的三条铁律avformat_open_input成功后必须avformat_close_input。avcodec_alloc_context3分配的 context 必须avcodec_free_context。av_packet_alloc分配的 packet 必须av_packet_free。这三条记住了能避免 90% 的内存泄漏。5.5 版本兼容性FFmpeg 的 API 在不同大版本之间会变。比如AVStream-codec在 4.0 之后被废弃改用codecpar。如果你看的教程是 2016 年写的很可能用的是老 API。查文档时注意看版本号或者直接看头文件里的注释。6. 一个完整的可编译示例把上面的东西串起来写一个能直接编译运行的小程序。它接收一个文件路径打印出容器信息和所有流的详细信息。#include stdio.h #include libavformat/avformat.h #include libavcodec/avcodec.h int main(int argc, char *argv[]) { if (argc 2) { fprintf(stderr, Usage: %s input file\n, argv[0]); return 1; } AVFormatContext *fmt_ctx NULL; int ret avformat_open_input(fmt_ctx, argv[1], NULL, NULL); if (ret 0) { char errbuf[128]; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, Open failed: %s\n, errbuf); return 1; } ret avformat_find_stream_info(fmt_ctx, NULL); if (ret 0) { fprintf(stderr, Find stream info failed\n); avformat_close_input(fmt_ctx); return 1; } printf(Format: %s\n, fmt_ctx-iformat-name); printf(Duration: %.2f s\n, fmt_ctx-duration / (double)AV_TIME_BASE); printf(Bitrate: %lld bit/s\n, (long long)fmt_ctx-bit_rate); printf(Streams: %u\n, fmt_ctx-nb_streams); AVDictionaryEntry *tag NULL; while ((tag av_dict_get(fmt_ctx-metadata, , tag, AV_DICT_IGNORE_SUFFIX))) { printf( Metadata: %s %s\n, tag-key, tag-value); } for (unsigned int i 0; i fmt_ctx-nb_streams; i) { AVStream *st fmt_ctx-streams[i]; AVCodecParameters *par st-codecpar; printf(\nStream #%u [%s]\n, i, av_get_media_type_string(par-codec_type)); printf( Codec: %s\n, avcodec_get_name(par-codec_id)); printf( Time base: %d/%d\n, st-time_base.num, st-time_base.den); if (par-codec_type AVMEDIA_TYPE_VIDEO) { printf( Resolution: %dx%d\n, par-width, par-height); printf( Pixel format: %s\n, av_get_pix_fmt_name(par-format)); AVRational fr st-avg_frame_rate; if (fr.num fr.den) printf( FPS: %.2f\n, av_q2d(fr)); } else if (par-codec_type AVMEDIA_TYPE_AUDIO) { printf( Sample rate: %d\n, par-sample_rate); printf( Channels: %d\n, par-channels); printf( Sample format: %s\n, av_get_sample_fmt_name(par-format)); } } avformat_close_input(fmt_ctx); return 0; }编译命令假设 FFmpeg 装在标准路径gcc -o probe probe.c $(pkg-config --cflags --libs libavformat libavcodec)如果pkg-config找不到就手动指定gcc -o probe probe.c -lavformat -lavcodec -lavutil跑一下./probe test.mp4输出大概是这样Format: mov,mp4,m4a,3gp,3g2,mj2 Duration: 12.34 s Bitrate: 1500000 bit/s Streams: 2 Metadata: major_brand isom Metadata: encoder Lavf58.29.100 Stream #0 [video] Codec: h264 Time base: 1/90000 Resolution: 1920x1080 Pixel format: yuv420p FPS: 30.00 Stream #1 [audio] Codec: aac Time base: 1/44100 Sample rate: 44100 Channels: 2 Sample format: fltp7. 从能读到读得准几个进阶思路基础功能跑通之后实际项目里还会遇到一些更细的需求。7.1 精确时长读最后一帧的时间戳如果duration不可靠可以手动 seek 到文件末尾附近读最后一帧的 PTS再换算成秒。这个方法对大多数本地文件有效但网络流不行。7.2 旋转角度display matrix手机拍的视频经常带旋转信息存在流的 side data 里。codecpar-width和height是编码时的尺寸实际显示可能要旋转 90 度。读取方法for (int i 0; i st-codecpar-nb_coded_side_data; i) { AVPacketSideData *sd st-codecpar-coded_side_data[i]; if (sd-type AV_PKT_DATA_DISPLAYMATRIX) { // sd-data 里是 3x3 矩阵可以算出旋转角度 } }这个在播放器里必须处理否则竖屏视频会横着显示。7.3 HDR 信息color transfer 和 primariesHDR 视频的codecpar里有color_trc、color_primaries、color_space这些字段。判断是不是 HDR看color_trc是不是AVCOL_TRC_SMPTE2084PQ或者AVCOL_TRC_ARIB_STD_B67HLG。7.4 多音轨的语言识别前面提过流的metadata里有 language 键。但有些文件不写或者写的是 ISO 639-2 的三字母代码如 chi、eng。实际项目里通常得准备一个映射表把代码转成用户能看懂的语言名。8. 实际项目里的经验教训最后分享几个我在真实项目里踩过的坑都是文档里不会写的。第一个坑网络流的超时。avformat_open_input对网络流默认没有超时如果服务器不响应程序会一直卡住。解决办法是传一个AVDictionary进去设置timeout选项AVDictionary *opts NULL; av_dict_set(opts, timeout, 5000000, 0); // 5 秒单位微秒 avformat_open_input(fmt_ctx, url, NULL, opts); av_dict_free(opts);第二个坑某些 MP4 的 moov box 在文件末尾。这种文件打开时 FFmpeg 需要 seek 到末尾读 moov如果输入是不可 seek 的流比如管道就会失败。解决办法是先用qt-faststart之类的工具把 moov 移到前面或者用AVIOContext自定义读取逻辑。第三个坑avformat_find_stream_info对某些文件很慢。它默认会读最多probesize字节的数据默认值是 5MB。对于大文件或者慢速存储这个探测过程可能要好几百毫秒。如果只是想知道基本信息可以调小probesizefmt_ctx-probesize 1024 * 1024; // 1MB但调太小可能导致探测不出编码格式得根据实际情况权衡。第四个坑字符编码。前面提过元数据可能是非 UTF-8。我遇到过一个日文文件标题是 Shift-JIS 编码直接打印全是乱码。后来用iconv转了一下才正常。如果项目面向国际用户这个必须处理。第五个坑codecpar-format对音频流是采样格式对视频流是像素格式。同一个字段名含义不同别搞混了。判断类型之后再用对应的转换函数。这些经验看起来零碎但每一个都是实际调试出来的。FFmpeg 的 API 就是这样基础流程不难难的是处理各种边界情况和异常输入。把基础流程跑通只是第一步真正让代码健壮的是对这些细节的处理。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑