基于 FFmpeg 的视频编辑器开发—踩坑记
折腾了两周,视频编辑器已初具规模:
- 解封装
- 解码
- 快速跳转和精准跳转
- 格式转换
- 缩放
- 重采样
- 添加文字
- 添加 srt 字幕
- 编码
- 封装
基本满足了当初想做一个 gif 生成器的需求,也是时候回顾下过去两周踩过的坑了。
1. av_seek_frame()
后解码,第一帧的 pts 仍为 seek 前的 pts
av_seek_frame()
是 avformat 模块接口,seek 后信息没有同步到 avcodec 模块。 seek 后马上调用 avcodec_flush_buffers()
即可。
2. 解码后视频帧 pts 为 AV_NOPTS_VALUE
AV_NOPTS_VALUE
是一个极大的负数值的宏定义。pts 字段值是它时,说明此视频格式不支持或此视频未设置 pts。应把 pts 值设为同 pkt_dts 值:
1 | // frame is a decoded AVFrame |
3. av_read_frame()
后 调用 av_packet_unref()
释放 AVPacket 对象
av_read_frame()
会将传入的 AVPacket 对象变为引用计数形态,或在传入 AVPacket 对象已经是引用计数形态时将计数 + 1,应用层负责在合适时机调用 av_packet_unref
将计数 - 1。
否则 AVPacket 对象内缓存区将永远不会释放,导致内存泄漏。
4. 将滤镜图输出到文本文件进行调试分析
这其实是个官方提供的命令行工具来的,叫 graph2dot 。
只需将其中 print_digraph()
函数定义复制到自己的工程内,即可打印出如下图所示的滤镜图内的关系链:
结合 print_digraph()
源码和生成的文本描述,对 filter graph 的内部逻辑也能略窥一二。
5. 使音频滤镜吐出固定尺寸(采样数)
许多音频编码器如 AAC,要求传递给 avcodec_send_frame()
的 AVFrame 对象包含固定尺寸(采样数)的音频数据,否则返回值将会是 AVERROR(EINVAL)
。
没必要自己缓存音频数据至指定尺寸再发送给编码器,可以直接调用 av_buffersink_set_frame_size()
接口,指示 sink 滤镜总是吐出指定尺寸的帧数据。
1 | // 在 avfilter_graph_config() 后调用一次即可 |
6. 封装时应重新计算 pts
编码时,应以写入的实际帧率/采样率设置编码器的 time_base 字段,同时将输入帧的 pts 从零计算:
1 | // for video encoder |
最后,在编码后写入前,应将 AVPacket 时间戳转换为相应流的 time_base 单位:
1 | av_packet_rescale_ts(&packet, codecContext->time_base, stream->time_base); |
7. 解封装到文件尾(EOF)时,末尾若干帧丢失
av_read_frame()
返回 AVERROR_EOF 时,不应该直接结束后续操作。编解码器、滤镜均不是一进一出,所以很有可能在 av_read_frame()
返回 AVERROR_EOF 时,编解码器、滤镜中仍存在待处理的帧数据。
对解码器,以一个空 AVPacket 指针传入 avcodec_send_packet()
可以起到 flush 对应解码器的作用;
同理,对编码器应以一个空 AVFrame 指针为参数调用 avcodec_send_frame()
;
滤镜的 flush 有两种方式:
- 以空 AVFrame 指针调用
av_buffersrc_add_frame_flags()
- 不需要调用
av_buffersrc_add_frame()
或av_buffersrc_add_frame_flags()
,直接以 AV_BUFFERSINK_FLAG_NO_REQUEST 参数调用av_buffersink_get_frame_flags()