I . FFMPEG 音视频同步流程总结
以音频播放的时间为基准 , 调整视频的播放速度 , 让视频与音频进行同步 ;
先计算出音频的播放时间 ; 再计算视频的播放时间 ;
根据视频与音频之间的比较 , 如果视频比音频快 , 那么增大视频帧之间的间隔 , 降低视频帧绘制速度 ;
如果视频比音频慢 , 那么需要丢弃部分视频帧 , 以追赶上音频的速度 ;
II . FFMPEG 音视频同步方案选择
1 . 视频播放 : 视频文件 或 视频流中 , 分别封装了 音频数据 和 视频数据 , 两种数据被封装在了数据包中 , 按照时间线存放 ; 播放的时候 , 音频 和 视频 同时播放 , 这里就需要进行同步 , 让音频的时间 与 画面播放的时间 尽量保持一致 ;
2 . 音视频不能完全同步 : 音频播放时间线 和 视频播放时间线 不可能做到完全同步 , 音频播放 与 视频播放始终都处于一个相对对其播放进度的过程中 , 二者始终 处于你追我赶的过程中 ;
3 . 在音视频同步 , 有以下三种常用的方案 :
① 以音频为基准进行同步 ( 推荐方式 ) : 这种方案是最常用的 , 因为音频有采样率 , 时间 , 指定的采样个数在指定的时间内播放时间是固定的 , 天然是一种计时方式 ;
② 以以视频为基准进行同步 : 控制视频帧按照指定的帧率 ( FPS ) 播放 , 音频与视频同步 ;
③ 以一个外部时钟为基准 : 定义一个外部的开始时间 t tt , 音频 和 视频 都基于该时间进行同步 ; 即 音频 / 视频 与 t tt 的相对时间差尽量保持一致 ;
III . FFMPEG 以音频播放时间线为基准进行音视频同步
1 . 视频 与 音频时间线 :
① 视频播放时间线控制 : 视频解码后是一帧帧的图像 , 其绘制时间都需要开发者进行手动控制 , 通过控制视频帧之间的绘制间隔 , 来达到视频播放时间线的控制 ;
② 音频播放时间线控制 : 音频解码后的数据 , 自带采样率 , 采样个数等信息 , 设置好 OpenSLES 播放器的采样率 , 采样位数 , 通道数等信息 , 将解码后的音频帧丢到缓冲队列 , 就可以自动进行播放 , 这个时间线是随着播放而自动生成的 ;
2 . 以音频为基准进行同步 : 视频时间线需要手动控制 , 音频的时间线是随着音频播放自动生成 , 因此以音频为基准进行同步 , 比较容易 ;
3 . 以音频时间线为基准的同步方案 :
① 视频比音频快 : 如果视频比音频播放的快 , 那么就加降低视频的播放速度 ;
② 视频比音频慢 : 如果视频比音频播放的慢 , 那么就加增加视频的播放速度 ;
IV . FFMPEG 有理数 AVRational 结构体
1 . 有理数 : 有理数是整数和分数的集合 ; 有理数可以用两个整数相除 ( 分数 ) 来表示 ;
2 . FFMPEG 中的有理数变量保存 :
① 数值损失 : 使用 float 或 double 表示有理数 , 会产生数值损失 , 如 无限循环小数 ;
② AVRational 结构体 : 有理数中有无限循环小数 , 为了更精确的表示无限循环小数 , FFMPEG 中定义了 AVRational 结构体更精确的表示有理数 ;
3 . AVRational 结构体原型 : 为了更精确的表示 FFMPEG 中的有理数 , FFMPEG 中定义了 AVRational 结构体 , 其中 int num 表示有理数分子 , int den 表示有理数分母 ;
/** * Rational number (pair of numerator and denominator). */ typedef struct AVRational{ int num; ///< Numerator 分子 int den; ///< Denominator 分母 } AVRational;
V . 获取 AVRational 结构体代表的有理数值
1 . 有理数 -> Double 浮点值 : AVRational 表示一个有理数 , 计算时需要将其转为浮点数 , 调用 av_q2d ( ) 方法 , 可以将其转为 double 双精度浮点类型进行计算 ;
2 . av_q2d ( ) 函数原型 : 该函数直接将 分子 除以 分母 的 double 结果返回 ;
/** * Convert an AVRational to a `double`. * @param a AVRational to convert * @return `a` in floating-point form * @see av_d2q() */ static inline double av_q2d(AVRational a){ return a.num / (double) a.den; }
VI . PTS 数据帧播放理论相对时间
1 . PTS ( Presentation TimeStamp ) : 该值表示视频 / 音频解码后的数据帧应该播放的相对时间 , 这个相对时间是相对于播放开始的时间 , 即 视频 / 音频 开始播放的时间是 0 , PTS 是从该开始时间开始计数 , 到某数据帧播放的时间 ;
2 . PTS 值获取 : PTS 数据被封装在了 AVFrame 结构体中 , 音频解码后的 PCM 数据帧 , 和视频解码后的图片数据帧 , 都可以获取 PTS 值 ;
/** * Presentation timestamp in time_base units * (time when frame should be shown to user). */ int64_t pts;
VII . 通过 PTS 计算音频播放时间
通过 PTS 获取 音频 播放的时间 : 直接获取 音频帧 AVFrame 结构体的 pts 值 , 这里注意获取的 PTS 值的单位不是秒 , 而是一个特殊单位 , 需要乘以一个 AVRational time_base 时间单位 , 才能获取一个单位为秒的时间 ;
//1 . 获取音视频 同步校准的 PTS 的 time_base 单位 AVRational time_base = stream->time_base; //2 . 计算该音频播放的 相对时间 , 相对 : 即从播放开始到现在的时间 // 转换成秒 : 这里要注意 pts 需要转成 秒 , 需要乘以 time_base 时间单位 // 其中 av_q2d 是将 AVRational 转为 double 类型 audio_pts_second = avFrame->pts * av_q2d(time_base);
PTS 的单位是 time_base , 从 AVStream 可以获取该 time_base 单位 ;
VIII . FFMPEG 中的时间单位 AVRational time_base
1 . FFMPEG 时间值 : FFMPEG 中很多地方涉及到时间值 , 如获取视频帧的理论播放时间 PTS ;
2 . 时间值的单位 : 这些值获取后并不是实际意义上的秒 , 毫秒等时间 , 其单位是 time_base , 是一个有理数 , 代表每单位的 PTS 值是多少秒 ;
/** * This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. * * decoding: set by libavformat * encoding: May be set by the caller before avformat_write_header() to * provide a hint to the muxer about the desired timebase. In * avformat_write_header(), the muxer will overwrite this field * with the timebase that will actually be used for the timestamps * written into the file (which may or may not be related to the * user-provided one, depending on the format). */ AVRational time_base;
3 . 单位转换 : 将 PTS 值转为单位为秒的值 , 使用 PTS 乘以 time_base 代表的有理数 , 即可获取 PTS 代表的秒数 ;
4 . 时间单位获取 : AVStream 结构体中的 time_base 是 FFMPEG 的时间单位 , 可以直接通过 AVStream 获取该时间单位 ;
//获取音视频 同步校准的 PTS 的 time_base 单位 AVRational time_base = stream->time_base;
5 . PTS 转换为秒 代码示例 :
//1 . 获取音视频 同步校准的 PTS 的 time_base 单位 AVRational time_base = stream->time_base; //2 . 计算该音频播放的 相对时间 , 相对 : 即从播放开始到现在的时间 // 转换成秒 : 这里要注意 pts 需要转成 秒 , 需要乘以 time_base 时间单位 // 其中 av_q2d 是将 AVRational 转为 double 类型 audio_pts_second = avFrame->pts * av_q2d(time_base);
IX . FFMPEG 中 H.264 视频帧编码
1 . H.264 视频编码帧类型 : H.264 编码的帧有三种类型 , I 帧 , P 帧 , B 帧 三种 ;
① I 帧 ( I Frame ) : 帧内编码帧 , 可以单独解码并显示 ; 解压后是一张完整图片 ;
② P 帧 ( P Frame ) : 前向预测编码帧 , 如果要解码 P 帧 , 需要参考 P 帧前面的编码帧 ; 需要参考前面的 I 帧或 B 帧编码成一张完整图片 ;
③ B 帧 ( B Frame ) : 双向预测帧 , 解码 B 帧 , 需要参考前面的编码帧 和 后面的编码帧 ; 需要参考前面的 I 帧 或 P 帧 , 和 后面的 P 帧编码成一张完整图片 ;
2 . 视频帧图片完整性分析 :
① I 帧 ( I Frame ) : 解压后是一张完整图片 ;
② P 帧 ( P Frame ) : 需要参考前面的 I 帧或 B 帧编码成一张完整图片 ;
③ B 帧 ( B Frame ) : 需要参考前面的 I 帧 或 P 帧 , 和 后面的 P 帧编码成一张完整图片 ;
3 . I / P 帧 举例 : 在一个房间内 , 人在动 , 房间背景不懂 , I 帧是完整的画面 , 其后面的 P 帧只包含了相对于 I 帧改变的画面内容 , 大部分房间背景都需要从 I 帧提取 ;
4 . 编解码的时间与空间考量 :
① 编码 :
B 帧 和 P 帧 的使用 , 能大幅度减小视频的空间 ;
② 解码 :
I 帧 解码时间最短 , 最占用空间 ;
P 帧解码时间稍长 , 需要参考前面的帧进行解码 , 能小幅度节省空间 ;
B 帧解码时间最长 , 需要参考前后两帧进行解码 , 能大幅度节省空间
X . FFMPEG 视频帧绘制帧率 FPS
1 . 帧率 ( FPS ) : 单位时间内 ( 1 秒 ) , 需要显示的图像个数 , 单位是 Hz ;
① 帧率不固定 : 这里要特别注意 , FFMPEG 在播放视频过程中 , 视频的帧率不是固定的 , 中途可能改变 ;
② 视频卡顿问题 : 如果视频播放过程中出现了卡顿 , 是因为没有控制好播放的帧率 ;
3 . 视频帧率获取 : 视频帧率信息封装在音视频流 AVStream 结构体中 , 通过访问 stream->avg_frame_rate 结构体元素 , 即可获取帧率 , 每秒播放的帧数 ;
4 . 帧率数据原型 : 定义在 AVStream 中的 AVRational avg_frame_rate 帧率 ;
/** * Average framerate * * - demuxing: May be set by libavformat when creating the stream or in * avformat_find_stream_info(). * - muxing: May be set by the caller before avformat_write_header(). */ AVRational avg_frame_rate;
5 . 帧率 FPS 计算 : 调用 av_q2d(frame_rate) 方法 , 或者直接将 AVRational 结构体中的分子分母相除 , 两种方式都可以获得帧率 ( FPS ) 值 ;
int fps = frame_rate.num / frame_rate.den; //int fps = av_q2d(frame_rate);
6 . 帧率 FPS 获取代码示例 :
//获取视频的 FPS 帧率 ( 1秒中播放的帧数 ) /* 该结构体由一个分子和分母组成 , 分子 / 分母就是 fps typedef struct AVRational{ int num; ///< Numerator int den; ///< Denominator } AVRational; */ AVRational frame_rate = stream->avg_frame_rate; // AVRational 结构体由一个分子和分母组成 , 分子 / 分母就是 fps // 也可以使用 av_q2d() 方法传入 AVRational 结构体进行计算 // 上面两种方法都可以获取 帧率 ( FPS ) // FPS 的值不是固定的 , 随着视频播放 , 其帧率也会随之改变 int fps = frame_rate.num / frame_rate.den; //int fps = av_q2d(frame_rate);