4、查看媒体信息:使用 ffprobe 分析文件,理解流、包、帧的概念

说实话,我刚开始接触多媒体开发时,也被一堆术语搞晕过。

什么流啊、包啊、帧啊,听起来都差不多。

直到我真正用 ffprobe 去扒开一个视频文件,才恍然大悟——原来它们各自扮演着完全不同的角色。

这一节,我们就用 ffprobe 这个利器,把媒体文件拆开来看个清楚。

4.1 流(Stream)—— 文件里的独立轨道

你可以把流想象成一条独立的「数据轨道」。

一个 MP4 文件里,通常至少有一条视频流和一条音频流。有的还有字幕流、数据流。

它们彼此独立,但又通过时间戳同步在一起。

核心理解:流是封装容器里的逻辑轨道。每条流都有自己的编码格式、参数、时长。

用 ffprobe 看一眼就明白了。打开终端,执行:

ffprobe -v quiet -show_streams input.mp4

输出会很长,我挑关键字段解释一下:

字段 含义 常见值举例
index 流在文件中的序号 0, 1, 2
codec_name 编码器名称 h264, aac, hevc
codec_type 流类型 video, audio, subtitle
width/height 视频分辨率 1920, 1080
sample_rate 音频采样率 44100, 48000
bit_rate 码率(bps) 2000000

我个人习惯先看 codec_type,快速确认文件里到底有几路视频、几路音频。有一次我拿到一个监控录像,ffprobe 一查发现居然有 4 条视频流——原来摄像头同时录了主码流和子码流。

4.2 包(Packet)—— 编码后的数据块

流是宏观的轨道,而包是这条轨道上最小的「传输单元」。

编码器把原始帧压缩后,打包成一个一个的包。包里面装的是已经编码好的数据,比如 H.264 的 NAL 单元。

我的经验:包是 FFmpeg 内部传递数据的基本单位。解封装时,容器格式(如 MP4)被拆成包;封装时,包又被组装成容器。你写代码时打交道最多的就是 AVPacket。

用 ffprobe 看包的细节:

ffprobe -v quiet -show_packets input.mp4 | head -30

输出示例:

[PACKET]
codec_type=video
stream_index=0
pts=0
pts_time=0.000000
dts=0
dts_time=0.000000
duration=2002
duration_time=0.033367
size=45678
pos=48
flags=K_
[/PACKET]

这里有几个关键字段:

  • pts/dts:显示时间戳和解码时间戳。简单说,pts 决定什么时候显示,dts 决定什么时候解码。
  • duration:这个包在时间轴上的长度。
  • size:包的大小(字节)。
  • flags:标记位。K_ 表示关键帧,_ 表示非关键帧。

嗯,这里要注意:包不等于帧。一个包可能包含一整个帧,也可能只包含帧的一部分(比如 H.264 的切片)。反过来,一个帧也可能被拆成多个包。

4.3 帧(Frame)—— 你真正看到听到的东西

帧是解码后的原始数据。

视频帧就是一张一张的图片,音频帧就是一段一段的 PCM 采样。

包经过解码器处理后,才变成帧。

避坑指南:我曾经在做一个实时推流项目时,发现画面卡顿。排查了半天,最后用 ffprobe 看帧信息才发现——视频帧的 pts 间隔不均匀,有的帧间隔 40ms,有的突然跳到 80ms。这就是编码器参数没调好导致的。

查看帧信息:

ffprobe -v quiet -show_frames input.mp4 | head -40

输出示例:

[FRAME]
media_type=video
stream_index=0
key_frame=1
pkt_pts=0
pkt_pts_time=0.000000
pkt_dts=0
pkt_dts_time=0.000000
best_effort_timestamp=0
best_effort_timestamp_time=0.000000
pkt_duration=2002
pkt_duration_time=0.033367
pkt_pos=48
pkt_size=45678
width=1920
height=1080
pix_fmt=yuv420p
pict_type=I
[/FRAME]

帧信息比包信息多了很多解码后的参数:

  • pict_type:帧类型。I 帧(关键帧)、P 帧(前向预测帧)、B 帧(双向预测帧)。
  • key_frame:是否为关键帧。1 表示是,0 表示否。
  • width/height/pix_fmt:解码后的实际分辨率、像素格式。

4.4 三者的关系 —— 一张图说清楚

你想想看,从文件到画面,经历了什么?

我画了一张图,帮你理清流、包、帧的关系:

流、包、帧的关系图 封装容器(MP4 / MKV / TS) 流(Stream) 视频流(h264) | 音频流(aac) | 字幕流 包(Packet) 包(Packet) 包(Packet) pts=0, size=45678 pts=2002, size=32000 pts=4004, size=51000 帧(Frame) I帧 1920x1080 帧(Frame) P帧 1920x1080 帧(Frame) B帧 1920x1080 容器 → 拆成流 → 流被切成包 → 包解码后变成帧 → 帧才是最终渲染的画面

说白了,整个流程就是:

  1. 容器(MP4 文件)包含多条流。
  2. 每条流被分割成一个个包(编码后的数据块)。
  3. 包经过解码器解码,变成帧(原始图像/音频数据)。
  4. 帧按照 pts 时间戳顺序渲染,最终呈现给你。

4.5 实战:用一条命令看全貌

我平时最常用的调试命令是这条:

ffprobe -v quiet -print_format json -show_format -show_streams input.mp4

输出 JSON 格式,一目了然。你可以看到:

  • format 部分:文件总时长、总码率、封装格式。
  • streams 数组:每条流的详细参数。

小技巧:如果你只想看关键信息,加个 -select_streams v:0 只选第一条视频流。我经常用这个来快速确认视频编码参数,省得被音频流干扰。

4.6 避坑总结

最后,分享几个我踩过的坑:

  • 包和帧不是一一对应的。别以为一个包就是一个帧。H.264 的 B 帧可能跨包,音频包可能包含多个采样帧。
  • pts 和 dts 不一样。B 帧的存在导致解码顺序和显示顺序不同。你用 -show_packets 看到的 dts 是递增的,但 pts 可能跳来跳去。
  • 关键帧很重要。我曾经在切片时没注意关键帧间隔,导致用户拖动进度条后黑屏了好几秒。后来强制设了 -g 30 才解决。