1. 核心概念:Frame vs Packet
在 PyAV 中,Frame 是解码后的原始像素数据载体,而 Packet 是压缩码流。理解两者的时间戳映射是视频同步的关键。
💡 黄金法则:视频渲染/同步永远使用
frame.pts或frame.time。frame.dts仅在分析解码延迟或调试时参考。
2. 时间戳系统详解
PTS (Presentation Timestamp)
frame.pts # int: time_base 单位下的显示时间戳
frame.time # float: 以秒为单位的显示时间(= pts * time_base)
frame.time_base # Fraction: 时间基准(如 1/30, 1/90000)
用途:音视频同步、字幕对齐、帧率计算、Seek 目标定位
注意:PTS 可能不连续(丢帧、可变帧率 VFR),不要假设
pts[n+1] = pts[n] + 1
DTS (Decoding Timestamp)
frame.dts # int: time_base 单位下的解码时间戳
来源:从触发该帧输出的 Packet.dts 复制而来
特殊行为:当未启用帧级多线程时,DTS 也作为仅基于 Packet.dts 计算的显示时间(无 PTS 时的降级方案)
用途:调试解码顺序、检测 B 帧重排异常
⚠️ 时间戳陷阱
3. 数据安全:make_writable()
为什么需要它?
FFmpeg 内部广泛使用引用计数缓冲区。多个 Frame 可能共享同一块内存(如解码器输出帧被滤镜引用)。直接修改共享数据会导致:
其他引用者看到意外修改
内存损坏 / Segfault
编码器/滤镜内部状态污染
正确用法
frame = next(container.decode(video=0))
# ❌ 危险:直接修改可能共享的缓冲区
frame.planes[0].update(new_data)
# ✅ 安全:确保独占可写副本
frame.make_writable()
frame.planes[0].update(new_data)
性能提示
make_writable()仅在数据不可写时才复制,已独占时为零开销如果你确定帧是新创建的(如
VideoFrame(width, height, fmt)),无需调用来自
decode()/filter.vpull()的帧始终应调用后再修改底层封装自
av_frame_make_writable()
4. 帧完整性校验
is_corrupt
if frame.is_corrupt:
logger.warning(f"Corrupt frame at PTS={frame.pts}, skipping")
continue
含义:FFmpeg 解码器标记该帧数据不完整或损坏
触发条件:码流截断、传输错误、编码器 bug
处理策略:
实时播放:跳过该帧,用上一帧替代或黑屏过渡
转码/分析:记录位置,决定是否终止或容错继续
配合标志:设置
CodecContext.flags |= 'OUTPUT_CORRUPT'才能收到损坏帧(否则被静默丢弃)
side_data
for sd in frame.side_data:
print(sd.type, sd.data)
携带额外元数据:HDR 信息、ROI 区域、运动向量、SEI 消息等
视频 HDR 处理中常用于提取
MASTERING_DISPLAY_METADATA和CONTENT_LIGHT_LEVEL
5. 视频帧处理最佳实践清单
6. 开发排查备忘
💡 核心记忆点
显示看 PTS,调试看 DTS,业务用 time,精度靠 Fraction
改数据前必 make_writable(),零开销保安全
is_corrupt 不是异常而是信号,优雅降级胜过崩溃
side_data 是 HDR/元数据的唯一可靠来源