FFmpeg视频解析实战:从容器到编码,解构video_parser核心原理
简介:这是一款使用TypeScript语言开发的视频解析工具库,主要面向需要在应用中读取视频文件信息、提取媒体元数据的开发者。资源包采用zip压缩,共包含13个文件,总体积只有61KB;其中7个源码文件负责核心解析逻辑,3个JSON配置与依赖文件用于工程化构建与运行,另附忽略规则、启动配置和依赖锁定文件,麻雀虽小但结构完整。目前已有203人浏览学习,可作为轻量级视频解析实现的有效参考。借助这份代码,可以了解模块化的源码组织方式,包括服务层、控制器、实体与公共接口的划分,并能学习到容器格式识别、编码参数提取、音视频时间轴同步等关键知识。同时,TypeScript工程配置与脚本命令也为读者提供了一个规范、可复用的库开发样板,适合前端工程师或Node.js开发者研究借鉴。
1. video_parser 不是读取视频,而是把视频“拆”成可消费的数据
拿到一个视频文件,能不能播、时长多少、真实分辨率多大、帧率多少、有没有音轨,这些不能靠双击播放器肉眼判断。video_parser 就是做这件事的解析器:先拆容器,再读编码参数,最后输出统一结构的数据。它适合做视频审核、封面抽取、转码管道、AI 抽帧这类场景的前置步骤。很多人以为解析视频就是把文件读进内存,但视频是容器和编码流的复合体,不解析容器就不知道有几条流,不解析编码层连真实分辨率都拿不准。下面从解析模型、最小实现、参数调优到常见坑一次讲透,新手能照着落代码,熟手也能拿边界情况做对照。
2. video_parser 的两层解析模型:容器层与编码层先搞清,后面才不翻车
一个 MP4 文件,用普通文本编辑器打开能看到 ftyp、moov、mdat 这些字母,但看不到画面。视频文件不是一个纯数据块,而是容器里装了多条编码流。video_parser 必须做两层解析:第一层处理容器,回答“文件是什么格式、有几条流、每条流的时间基和压缩参数在哪个位置”;第二层处理编码层,回答“流内部到底是什么规格、真实分辨率是多少、帧是不是恒定”。这两层没分开,后面时长、分辨率、帧率全都会错。
2.1 容器层:从 MP4 的 box 写到 AVI 的 riff,第一刀切在哪里
容器层的核心职责不是解码画面,而是找到流、拿到流索引和时间轴。MP4 的 moov box 里藏着 trak,trak 里的 stbl 告诉你每个 sample 的时间戳和字节偏移;AVI 的 hdrl 里写主音频流参数,movi 里按 chunk 存数据;MKV 用 EBML 元素一层层包出 Segment、Tracks、Cluster。不同格式差异很大,但解析路径一致:先读头部识别格式,再找流索引,最后把每条流的编码参数读出来。
在 FFmpeg 里,第一步是 avformat_open_input。它只负责打开文件并识别容器格式,此时流信息可能还不完整。很多人直接从这里拿 duration 和分辨率,结果拿到一堆 0。第二步必须调用 avformat_find_stream_info,让解析器读取足够多的数据去探测每条流的真实参数。先跑一个 ffprobe 看容器结构:
ffprobe -v error -show_format -show_streams input.mp4 -of json > container_info.json这条命令把容器摘要和所有流信息输出成 JSON。看 container_info.json 时,重点关注 format.duration、format.bit_rate、streams 里的 codec_name、width、height、avg_frame_rate。ffprobe 显示的是 FFmpeg 探测后的结果,和你在 C API 里用 avformat_find_stream_info 拿到的字段一一对应。如果这个文件能正常出时长和分辨率,再进到代码实现阶段;如果 ffprobe 都取不到,说明文件本身结构有问题,后面不用继续写解析器了。
参数说明:-of json 让输出变成结构化数据,方便后续用 jq 或者 Python 做断言;-show_format 和 -show_streams 必须同时开,因为容器层的时长和流层的时间基可能对不上,两边都拿到才能交叉验证。
2.2 编码层:SPS、PPS、VPS 拿到了,才算真正认识一条视频流
容器层告诉你“有一条 H.264 视频流”,但 H.264 流里真正决定画面规格的是 SPS(序列参数集)和 PPS(图像参数集)。HEVC 则还有 VPS。SPS 里的 pic_width_in_mbs 和 frame_mbs_only 决定编码分辨率,crop 信息决定实际显示分辨率。很多 MP4 文件容器里写的宽高是 1920x1088,但实际画面是 1920x1080,因为编码器按 16x16 宏块对齐,多出来的 8 行是 padding。不解析编码层,你拿到的分辨率永远是错的。
这些参数一般挂在 AVCodecParameters 的 extradata 上。H.264 的 extradata 是 AVCDecoderConfigurationRecord,里面包含 SPS/PPS 的 NALU;FLV 也是同样的结构。video_parser 不需要自己解析 NALU 位流,调用 FFmpeg 的 h264 解码器做完 avcodec_open2 之后,decoder 会从 extradata 里解出裁剪后的宽高。调试时打开 FFmpeg 日志,能看到解码器打印的 SPS 信息:
av_log_set_level(AV_LOG_DEBUG);在日志里搜 “h264” 和 “Reinit context”。看到类似 “Reinit context to 1920x1088” 时,说明编码层给出的尺寸和容器层不一致,后续业务逻辑要以解码后的实际尺寸为准。如果日志里搜不到 SPS,基本可以断定 extradata 没有被正确传递,原因我放在第五章第 2 条讲。
2.3 选型:为什么我建议用 FFmpeg 做解析引擎,而不是自己读字节
自己写 MP4 解析器看起来不难,但一旦要覆盖 HLS、RTSP、FLV、AVI、MKV,再叠加 H.264、HEVC、VP9、AAC 的编码参数解析,工程量会失控。专有格式的边界情况尤其多:MP4 的 edit list、MKV 的默认 duration、FLV 的延迟帧,每一条都够踩几天。用 FFmpeg 作为底层引擎,不是偷懒,而是把最不稳定的格式解析交给被大量生产环境验证过的代码。video_parser 真正值钱的部分在业务层:把 FFmpeg 吐出来的底层字段映射成业务需要的数据模型。
常见做法是先写一条 ffprobe 命令做原型验证,确认文件可以被解析,再落到 C API 或 PyAV 绑定。这里有个快速确认视频流信息的命令:
ffprobe -v error -select_streams v:0 \ -show_entries stream=codec_name,width,height,avg_frame_rate,time_base \ -of csv=p=0 input.mp4参数说明:-select_streams v:0 只挑第一条视频流,减少无用输出;-show_entries 指定字段,避免 JSON 里刷屏;-of csv=p=0 去掉行头,方便写脚本做断言。这条命令适合在 CI 里做冒烟测试,确认一个文件能不能被 video_parser 读取,但不适合做高频在线解析,因为每次都要拉起进程,开销太大。生产环境我一般直接把 FFmpeg 的库嵌进服务进程,用 C API 或 PyAV 保持长连接上下文。
3. 用 video_parser 跑通最小解析任务:从打开文件到输出 JSON
理论讲完,现在做能用的东西。这一章用 FFmpeg C API 搭一个最小可用的 video_parser。它不负责转码,只负责打开文件、探测流、抽关键字段、输出结构化数据。代码以片段形式给出,每段后面解释参数含义和调用顺序。
3.1 初始化并探测流信息:avformat_open_input 之后要立刻读什么
先写一个打开文件的函数。avformat_open_input 的第四个参数是 AVDictionary 选项,普通本地文件传 NULL,网络流和高精度探测时接第 4.4 小节的参数。
#include <stdio.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> int video_parser_open(const char *path) { AVFormatContext *fmt_ctx = NULL; if (avformat_open_input(&fmt_ctx, path, NULL, NULL) < 0) { fprintf(stderr, "open failed: %s\n", path); return -1; } if (avformat_find_stream_info(fmt_ctx, NULL) < 0) { fprintf(stderr, "find_stream_info failed\n"); avformat_close_input(&fmt_ctx); return -1; } printf("format=%s nb_streams=%d\n", fmt_ctx->iformat->name, fmt_ctx->nb_streams); // 这里先不关,后续遍历和抽帧还需要 fmt_ctx return 0; }逻辑说明:avformat_open_input 成功只代表容器头部解析通过。avformat_find_stream_info 会读取一部分媒体数据,把每条流的 AVCodecParameters 和 AVStream 的时长、帧率填充完整。这两个函数都失败时,分别打日志,不要合并成一句失败,否则排错时不知道卡在哪一层。
参数说明:AVFormatContext 在打开成功后由 FFmpeg 内部分配,后续必须用 avformat_close_input 释放,不能直接 free。第三个参数传入 NULL 时由 FFmpeg 自动探测格式,实际项目里可以显式传入“mp4”、“mov”等,但会让 video_parser 失去对未知格式的兼容性,一般不建议。
3.2 遍历流并抽取关键信息:codecpar、time_base 与 metadata
探测完成后,fmt_ctx->streams 里已经有每条流的完整信息。遍历流的代码:
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) { AVStream *st = fmt_ctx->streams[i]; AVCodecParameters *par = st->codecpar; const char *type = av_get_media_type_string(par->codec_type); printf("stream[%u] type=%s codec=%s\n", i, type ? type : "unknown", avcodec_get_name(par->codec_id)); if (par->codec_type == AVMEDIA_TYPE_VIDEO) { printf(" coded_size=%dx%d\n", par->width, par->height); printf(" avg_frame_rate=%d/%d\n", st->avg_frame_rate.num, st->avg_frame_rate.den); printf(" time_base=%d/%d\n", st->time_base.num, st->time_base.den); printf(" duration=%ld timebase_units\n", st->duration); } else if (par->codec_type == AVMEDIA_TYPE_AUDIO) { printf(" sample_rate=%d channels=%d\n", par->sample_rate, par->channels); } }逻辑说明:type 通过 av_get_media_type_string 转成可读字符串,没有匹配时返回 NULL,所以要兜底写 “unknown”。codec 名字用 avcodec_get_name,它接受 codec_id,返回如 “h264”、“hevc”、“aac” 的字符串。打印 avg_frame_rate 时,用 AVRational 的 num 和 den 分开打印,避免转浮点后丢失精确比例。time_base 是这一条流内部时间戳的单位,例如 1/90000 表示时间戳每增加 1 等于 1/90000 秒。st->duration 的单位就是 time_base,不是秒。
参数说明:par->width 和 par->height 是容器层记录的编码尺寸,可能包含 padding。AV_NOPTS_VALUE 表示没有可用值,后面算时长和帧率时要判断它,否则会得到负数。
3.3 把解析结果输出为结构化 JSON:字段设计、单位换算和空值策略
输出 JSON 时不要直接拼字符串,推荐用 cJSON 或手写一个简单的构建器。下面是一个输出 schema 示例:
{ "container": { "format": "mov,mp4,m4a,3gp,3g2,mj2", "duration_seconds": 123.456, "bitrate": 1200000 }, "streams": [ { "index": 0, "type": "video", "codec": "h264", "coded_width": 1920, "coded_height": 1088, "width": 1920, "height": 1080, "avg_frame_rate": 25.0, "time_base": "1/90000" }, { "index": 1, "type": "audio", "codec": "aac", "sample_rate": 44100, "channels": 2 } ] }字段设计上,container.duration_seconds 由 fmt_ctx->duration 除以 AV_TIME_BASE 得到,单位统一成秒。streams 列表里保留 coded_width/coded_height 和 width/height 两套字段,前者是容器层的编码尺寸,后者是经过解码器裁剪后的实际尺寸,分别对应 avcodec_parameters_to_context 前后的值。avg_frame_rate 这里直接输出 double,但如果你要做帧定位,建议同时保留 AVRational 的 num/den,浮点会损失精度。
参数说明:AV_TIME_BASE 是 FFmpeg 的全局时间基,固定为 1,000,000,表示把 fmt_ctx->duration 除以 AV_TIME_BASE 得到秒。它和 st->time_base 不是一回事,二者换算必须用 av_rescale_q 或 av_q2d,不能想当然地直接除。bitrate 为 0 时表示未知,JSON 里输出 0 并且调用方要能容忍这个值。
double duration_seconds = 0.0; if (fmt_ctx->duration != AV_NOPTS_VALUE) { duration_seconds = (double)fmt_ctx->duration / AV_TIME_BASE; }这段代码放在输出前,作为所有 JSON 构建的基础。注意先判 AV_NOPTS_VALUE,因为有些流在打开后还没有有效时长,直接除会得到负值,污染整条记录。
4. 精确解析时长、分辨率与帧率:video_parser 的三个必调参数和一套校验方法
从 demo 到线上,会发现同一个文件在不同环节给出的时长、分辨率、帧率可能都不一样。容器层给一个数,编码层给另一个数,流的时间戳算出来还有第三个。这一章讲清楚三个必调参数,以及如何用 ffprobe 做交叉校验。
4.1 时长计算:用容器 duration 还是流 duration,取决于你要回答什么问题
容器时长和流时长是两种不同的数据。fmt_ctx->duration 是容器封装层记录的整个播放时长,单位是 AV_TIME_BASE;AVStream.duration 是该条流在流时间基下的总时长,单位是 st->time_base。对 MP4,moov 里的 mvhd 写的时长就是容器 duration;对 MKV,时间基更杂,流时长往往更可信。
计算时用 av_q2d 把 AVRational 转成 double:
double stream_duration_seconds = 0.0; if (st->duration != AV_NOPTS_VALUE) { stream_duration_seconds = av_q2d(st->time_base) * st->duration; }逻辑说明:av_q2d(st->time_base) 返回 time_base.num 除以 time_base.den 的浮点值。比如 time_base 是 1/1000,一帧的时间戳单位是 1 毫秒,st->duration 是 123456,那这条流时长是 123.456 秒。不要把 st->duration 除以 AV_TIME_BASE,那是把流时间戳当成微秒理解,结果会差几百倍。
实际项目里,我会把容器时长和流时长都算出来,然后看差异。差异大于 0.5 秒时,打 warning 并参考 ffprobe 的结果来判定哪个更可信。对 MP4,优先容器 duration;对 MKV,优先流 duration。这种判定没有绝对标准,video_parser 应该在输出里同时保留两个字段,让业务层自己做决策。
4.2 分辨率与旋转:crop 信息、display matrix 和 codec 宽高
编码分辨率不是显示分辨率,最大变量是 crop 和 rotation。H.264 的 SPS 里包含 frame_crop_left_offset 等字段,解码器会把这些值作用到最终输出尺寸上。拿到真实分辨率的代码如下:
AVCodec *dec = avcodec_find_decoder(par->codec_id); if (!dec) { fprintf(stderr, "decoder not found\n"); return; } AVCodecContext *dec_ctx = avcodec_alloc_context3(dec); if (avcodec_parameters_to_context(dec_ctx, par) < 0) { fprintf(stderr, "parameters_to_context failed\n"); avcodec_free_context(&dec_ctx); return; } if (avcodec_open2(dec_ctx, dec, NULL) < 0) { fprintf(stderr, "codec open failed\n"); avcodec_free_context(&dec_ctx); return; } int real_width = dec_ctx->width; int real_height = dec_ctx->height;逻辑说明:avcodec_parameters_to_context 把 codecpar 里的 extradata 等参数拷贝进 decoder context,这一步是必须的。avcodec_open2 打开解码器后,h264/hevc 解码器会根据 SPS/PPS 里的裁剪信息修正 dec_ctx->width 和 height。这里只做解析不抽帧的话,开销仍然可控,因为解码器初始化时不分配大的帧缓冲。
旋转问题更隐蔽。手机拍摄的视频,存储分辨率可能是 1920x1080,但带了一个 90 度旋转的 display matrix,实际展示时应该按 1080x1920 显示。读取方式:
uint8_t *side_data = av_stream_get_side_data( st, AV_PKT_DATA_DISPLAYMATRIX, NULL); if (side_data) { int32_t rotation = av_display_rotation_get((int32_t *)side_data); if (rotation == 90 || rotation == 270) { int tmp = real_width; real_width = real_height; real_height = tmp; } }参数说明:av_display_rotation_get 返回逆时针旋转角度。有些封装格式会把旋转角度写在 metadata 的 rotate 字段里,这种也要兼容。实际处理时 rotation 还可能带 -90 之类的值,建议统一归约到 0、90、180、270。交换宽高后,输出 JSON 里的 width/height 要使用旋转变换后的值,coded_width/coded_height 保留原始值。
4.3 帧率计算:avg_frame_rate 与 r_frame_rate 的取舍
FFmpeg 在 AVStream 里给了两个帧率字段:avg_frame_rate 是整体平均帧率,r_frame_rate 是容器里语义上的基本帧率。前者对 VFR(可变帧率)视频也能给一个统计值,后者可能与实际某些片段不符。推荐优先级:
double fps = 0.0; if (st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0) { fps = av_q2d(st->avg_frame_rate); } else if (st->r_frame_rate.num > 0 && st->r_frame_rate.den > 0) { fps = av_q2d(st->r_frame_rate); } else if (st->time_base.den > 0 && st->time_base.num > 0) { fps = 1.0 / av_q2d(st->time_base); }优先级理由:avg_frame_rate 是根据容器里的 pts 统计出来的,对 VFR 视频它代表平均出帧速度;r_frame_rate 更像编码器的名义帧率,对某些剪辑软件导出的文件不可信。只有两个都无效时,才用 time_base 倒数兜底。注意 time_base 不一定等于帧间隔,比如视频流 time_base 是 1/90000,但实际帧率是 25,直接取倒数会得到 90000 fps。所以最后一个分支要谨慎,最好只做兜底并打日志。
校验方法:把 video_parser 的输出和 ffprobe 对比。写一个脚本,遍历测试样本集,用 ffprobe 的 CSV 输出做断言:
ffprobe -v error -select_streams v:0 \ -show_entries stream=duration,width,height,avg_frame_rate \ -of csv=p=0 input.mp4我平时会准备一个包含 MP4/H.264、MKV/HEVC、AVI/MPEG4、FLV/H.264 和手机竖屏视频的测试集,每次改 video_parser 都跑一遍对比。对比失败时先看是不是 crop 和 rotation 引起的,这两个最容易在 parser 内部被忽略。
4.4 三个必调参数:probesize、analyzeduration 和网络超时
线上解析网络流时,avformat_open_input 很容易长时间阻塞或者探测不完整。三个参数直接影响解析行为:
AVDictionary *opts = NULL; av_dict_set(&opts, "probesize", "10000000", 0); av_dict_set(&opts, "analyzeduration", "5000000", 0); av_dict_set(&opts, "stimeout", "3000000", 0); avformat_open_input(&fmt_ctx, url, NULL, &opts); av_dict_free(&opts);probesize 单位是字节,默认约 5MB,调大后能读到更多编码参数,代价是打开变慢。analyzeduration 单位微秒,控制 avformat_find_stream_info 分析时长的上限,对异常文件尤其有用。stimeout 针对 RTSP,单位微秒,设置成 3000000 表示 3 秒超时。普通 HTTP 流可以用 timeout 选项,RTSP 用 stimeout,两者不要混用,否则超时配置不生效。
参数说明:这些 option 在 avformat_open_input 时传入,是 AVDictionary 结构。打开成功后要 av_dict_free 释放。如果没有设置 stimeout,RTSP 源断流时 avformat_open_input 可能卡在 TCP 层很久,视频审核服务会因此挂掉一堆 worker。本地文件不需要设置超时,但要小心 probesize 过大导致打开变慢。
5. 排查 video_parser 的七个常见坑:现象、原因、解决
写完第一版 video_parser 后,真正消耗时间的是各种异常样本。这里记下我踩过的七个坑,每条都按现象、原因、解决三个维度写,方便对号入座。
5.1 时长翻倍显示:st->duration 被当成 AV_TIME_BASE 微秒
现象:同一个 MKV 文件,用 video_parser 解析出的时长是 ffprobe 的两倍多,播放器播放却正常。原因:解析器把 st->duration 直接当成微秒除以 AV_TIME_BASE,但这个 duration 的单位是 st->time_base,不等于微秒。对某些 MKV,time_base 是 1/1000,st->duration 是 256000,正确结果是 256 秒,错误代码却算成 0.256 秒,反过来也常见。解决:统一用 av_q2d(st->time_base) 乘以 st->duration,不要直接除 AV_TIME_BASE。同时保留容器层 duration 和流层 duration 两个字段,出现偏差时按容器格式选择优先级。
5.2 某些 MP4 解析出 0 帧:extradata 没有在解码前传给解码器
现象:一个网上下载的 MP4,容器探测正常,codec 名字也是 h264,但 avcodec_open2 之后解码器拿不到任何帧。原因:代码只调用了 avcodec_alloc_context3,却没有调用 avcodec_parameters_to_context,导致 codec context 里的 extradata 为空。H.264 的 SPS/PPS 作为 extradata 的一部分,缺失时解码器不知道视频规格,自然出不了帧。解决:在 avcodec_open2 之前调用 avcodec_parameters_to_context,并检查返回值和 dec_ctx->extradata_size。如果 extradata_size 为 0,说明容器层本身没携带 SPS/PPS,这种情况需要从流里抓第一个 keyframe 解析出 SPS/PPS 再喂回去。
if (dec_ctx->extradata_size == 0) { fprintf(stderr, "extradata missing, skip\n"); return; }5.3 横屏视频识别成竖屏:display matrix 被忽略
现象:手机竖拍视频,解析结果是 1920x1080,业务方拿去生成竖版封面,画面方向全错。原因:容器里存的分辨率是传感器原始方向,展示用的旋转角度在 display matrix 里。video_parser 只读了 codecpar.width/height,没有读 side_data。解决:按 4.2 小节的逻辑读取 av_stream_get_side_data,对 90/270 度旋转交换宽高。同时把 rotation 字段写进 JSON,让下游业务知道视频需要旋转。调试时打开 FFmpeg 日志,能看到 display matrix 的旋转值,但不要依赖日志做线上判断。
5.4 解析网络流卡死:没有设置超时和协议白名单
现象:服务里一个 RTSP 源掉线,video_parser 的 avformat_open_input 阻塞了十分钟,后续任务全部堆积。原因:RTSP 默认没有超时控制,TCP 连接断开时 TCP 栈可能要很久才能感知。解决:打开前设置 stimeout 和 rw_timeout,并考虑协议白名单:
av_dict_set(&opts, "stimeout", "3000000", 0); av_dict_set(&opts, "rw_timeout", "30000000", 0); av_dict_set(&opts, "protocol_whitelist", "file,http,rtsp,rtp,tcp,udp", 0);protocol_whitelist 能防止 ffmpeg 被恶意文件引导到本地文件或特殊协议,做视频解析服务时是安全底线。不要为了省事全部放开,业务用不到 ftp、ssh 协议就不该出现在白名单里。
5.5 解析高码率 4K 文件时内存暴涨:packet 没有被及时释放
现象:批量解析一批 4K H.264 视频,内存占用持续上升,直至 OOM。原因:在 av_read_frame 循环里拿到 AVPacket,处理完没有调用 av_packet_unref。AVPacket 内部引用的是缓冲区,不 unref 的话每个包都会泄漏一部分内存。解决:每次循环结束前调用 av_packet_unref,并且配合 av_init_packet? 现在已经不用手动 init,重点是把 packet 生命周期控制在一次循环内。
AVPacket *pkt = av_packet_alloc(); while (av_read_frame(fmt_ctx, pkt) >= 0) { // 处理 pkt av_packet_unref(pkt); } av_packet_free(&pkt);逻辑说明:av_packet_unref 释放本包持有的 buffer 引用,让 pkt 可以被下一次 av_read_frame 重新填充。av_packet_alloc 只需要调用一次。这里不能用 av_packet_free 替代 unref,否则循环里第二次读包就是 use-after-free。
5.6 音频采样率解析为 0:只写了 video 分支
现象:解析一个带 AAC 音轨的 MP4,视频信息正常,但音频采样率和声道数都是 0。原因:遍历流时代码里只处理了 AVMEDIA_TYPE_VIDEO,音频流的 codecpar 虽然存在,但没有被读取。解决:在遍历分支里补全 audio 分支,读取 par->sample_rate 和 par->channels。注意 codecpar 的 channels 可能是 0,如果容器没写声道布局,需要通过 channel_layout 计算,但大多数 MP4 都有 channels 值。同时把音频流的 duration 和 time_base 也输出,后续做静音检测或音频指纹时要用。
5.7 ffprobe 正常但 video_parser 解析失败:AOT 或 event 顺序不同
现象:同一个文件,ffprobe 能输出完整信息,video_parser 却在 avformat_find_stream_info 阶段失败。原因:部分封装格式的编码参数解析依赖解码器的 AVOption,比如 MPEG-4 的 profile 需要正确设置严格模式,或 H.264 需要显式解码。也有可能是代码调用了 avformat_find_stream_info 后没有等待线程结束就直接读字段。解决:检查解码器是否找到,avcodec_find_decoder 返回空时不要继续。设置日志级别 AV_LOG_DEBUG 后对比 ffprobe 输出,定位是哪条流导致失败。如果问题集中在特定编码器,可以在 avformat_find_stream_info 后多调用一遍 avcodec_parameters_to_context 确认参数被完整读取。
6. 从解析到应用:用 video_parser 批量抽帧做视频指纹的小技巧
解析不是终点。视频审核和检索场景里,最常做的是按固定间隔抽帧,抽出的帧再交给图像模型。抽帧的关键是别用浮点秒直接换算时间戳,而要用 av_rescale_q。假设要从第 n 帧抽,每 3 秒抽一帧:
AVRational interval = {3, 1}; int64_t target_ts = av_rescale_q(n * interval.num, interval, st->time_base); av_seek_frame(fmt_ctx, st->index, target_ts, AVSEEK_FLAG_BACKWARD);av_rescale_q 把“业务时间 3 秒”换算成视频流的时间戳单位,保证不丢精度。AVSEEK_FLAG_BACKWARD 表示向后定位到关键帧,之后循环 av_read_frame,跳过非关键帧,直到当前帧 pts 大于等于目标时间戳,再进入解码器取出帧。抽帧前先跑一遍第 4 章的校验,否则你把 duration 算错了,后面所有抽帧点都会偏移。我的习惯是把 video_parser 的解析结果存储一份带版本号的 JSON,抽帧任务从这份 JSON 读 time_base 和 duration,而不是每次重新解析原始文件。这样才能保证同源视频在不同批次抽帧时结果可复现。这个方向踩坑不少,但把容器层、编码层、时间基三件事拎清楚,video_parser 就能成为视频业务里最稳的一块地基。希望帮到你。
本文还有配套的精品资源,点击获取