基于 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
2
3
4
// frame is a decoded AVFrame
if (frame->pts == AV_NOPTS_VALUE) {
frame->pts = frame->pkt_dts;
}

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
2
3
4
// 在 avfilter_graph_config() 后调用一次即可
if (!(audioCodecContext->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)) {
av_buffersink_set_frame_size(sinkContext, audioCodecContext->frame_size);
}

6. 封装时应重新计算 pts

编码时,应以写入的实际帧率/采样率设置编码器的 time_base 字段,同时将输入帧的 pts 从零计算:

1
2
3
4
5
6
7
8
9
10
11
12
// for video encoder
videoCodecContext->time_base = { 1, fps };
// ...
// int vPts = 0;
videoFrame->pts = vPts++;

// for audio encoder
audioCodecContext->time_base = { 1, sampleRate };
// ...
// int aPts = 0;
audioFrame->pts = aPts;
aPts += audioFrame->nb_samples;

最后,在编码后写入前,应将 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()

评论