文章目录- I . FFMPEG 音视频同步流程总结
- II . FFMPEG 音视频同步方案选择
- III . FFMPEG 以音频播放时间线为基准进行音视频同步
- IV . FFMPEG 有理数 AVRational 结构体
- V . 获取 AVRational 结构体代表的有理数值
- VI . PTS 数据帧播放理论相对时间
- VII . 通过 PTS 计算音频播放时间
- VIII . FFMPEG 中的时间单位 AVRational time_base
- IX . FFMPEG 中 H.264 视频帧编码
- X . FFMPEG 视频帧绘制帧率 FPS
- XI . 视频帧绘制的 FPS 帧间隔
- XII . 视频帧绘制的额外延迟间隔
- XIII . 视频帧绘制的间隔
- XIV . 获取视频当前播放时间
- XV . 视频帧绘制的间隔控制
- XVI . 视频帧丢弃方案
- XVII . 音视频同步代码示例
I . FFMPEG 音视频同步流程总结
以音频播放的时间为基准 , 调整视频的播放速度 , 让视频与音频进行同步 ;
先计算出音频的播放时间 ; 再计算视频的播放时间 ;
根据视频与音频之间的比较 , 如果视频比音频快 , 那么增大视频帧之间的间隔 , 降低视频帧绘制速度 ; 如果视频比音频慢 , 那么需要丢弃部分视频帧 , 以追赶上音频的速度 ;
II . FFMPEG 音视频同步方案选择
1 . 视频播放 : 视频文件 或 视频流中 , 分别封装了 音频数据 和 视频数据 , 两种数据被封装在了数据包中 , 按照时间线存放 ; 播放的时候 , 音频 和 视频 同时播放 , 这里就需要进行同步 , 让音频的时间 与 画面播放的时间 尽量保持一致 ;
2 . 音视频不能完全同步 : 音频播放时间线 和 视频播放时间线 不可能做到完全同步 , 音频播放 与 视频播放始终都处于一个相对对其播放进度的过程中 , 二者始终 处于你追我赶的过程中 ;
3 . 在音视频同步 , 有以下三种常用的方案 :
① 以音频为基准进行同步 ( 推荐方式 ) : 这种方案是最常用的 , 因为音频有采样率 , 时间 , 指定的采样个数在指定的时间内播放时间是固定的 , 天然是一种计时方式 ;
② 以以视频为基准进行同步 : 控制视频帧按照指定的帧率 ( FPS ) 播放 , 音频与视频同步 ;
③ 以一个外部时钟为基准 : 定义一个外部的开始时间
, 音频 和 视频 都基于该时间进行同步 ; 即 音频 / 视频 与
的相对时间差尽量保持一致 ;
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 表示有理数分母 ;
代码语言:javascript复制/**
* 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 结果返回 ;
代码语言:javascript复制/**
* 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 值 ;
代码语言:javascript复制/**
* 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 时间单位 , 才能获取一个单位为秒的时间 ;
代码语言:javascript复制//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 值是多少秒 ;
代码语言:javascript复制/**
* 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 获取该时间单位 ;
代码语言:javascript复制//获取音视频 同步校准的 PTS 的 time_base 单位
AVRational time_base = stream->time_base;
5 . PTS 转换为秒 代码示例 :
代码语言:javascript复制//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 帧率 ;
代码语言:javascript复制/**
* 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 ) 值 ;
代码语言:javascript复制int fps = frame_rate.num / frame_rate.den;
//int fps = av_q2d(frame_rate);
6 . 帧率 FPS 获取代码示例 :
代码语言:javascript复制//获取视频的 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);
XI . 视频帧绘制的 FPS 帧间隔
1 . 根据帧率 ( fps ) 计算两次图像绘制之间的间隔 : 视频绘制时 , 先参考帧率 FPS 计算出一个视频帧间隔 , 计算公式是
, 即如果 FPS 为 100Hz , 那么1 秒钟绘制 100 张画面 , 每隔 10ms 绘制一张图像 ;
2 . 帧率间隔计算方式 : 上面计算出了 fps 值 , 这里直接使用 1 / fps 值 , 可以获取帧之间的间隔时间 , 单位是秒 ;
代码语言:javascript复制AVRational frame_rate = stream->avg_frame_rate;
int fps = frame_rate.num / frame_rate.den;
//根据帧率 ( fps ) 计算两次图像绘制之间的间隔
// 注意单位换算 : 实际使用的是微秒单位 , 使用 av_usleep ( ) 方法时 , 需要传入微秒单位 , 后面需要乘以 10 万
double frame_delay = 1.0 / fps;
注意单位换算 : 实际使用的是微秒单位 , 使用 av_usleep ( ) 方法时 , 需要传入微秒单位 , 后面需要乘以 10 万
XII . 视频帧绘制的额外延迟间隔
1 . 解码额外延迟 : 视频帧解码时 , 还需要添加一个额外的延迟间隔 extra_delay , 该值表示需要在视频帧之间添加一个额外延迟 , 这是系统规定的 ;
2 . 额外延迟 extra_delay 的计算方式 : extra_delay = repeat_pict / (2*fps) , 需要获取 repeat_pict 值 ;
3 . repeat_pict 原型 : 该值封装在了 AVFrame 视频帧中 , 原型如下 :
代码语言:javascript复制/**
* When decoding, this signals how much the picture must be delayed.
* extra_delay = repeat_pict / (2*fps)
*/
int repeat_pict;
4 . 额外延迟计算代码示例 :
代码语言:javascript复制//解码时 , 该值表示画面需要延迟多长时间在显示
// extra_delay = repeat_pict / (2*fps)
// 需要使用该值 , 计算一个额外的延迟时间
// 这里按照文档中的注释 , 计算一个额外延迟时间
double extra_delay = avFrame->repeat_pict / ( fps * 2 );
XIII . 视频帧绘制的间隔
1 . 视频帧间隔 : 视频帧绘制之间的间隔是 FPS 帧间隔 ( frame_delay ) 额外延迟 ( extra_delay ) 的总和 ;
2 . 代码示例如下 : 上面已经根据 FPS 值计算出了理论帧间隔 , 和 根据 AVFrame 中封装的 repeat_pict 计算出了 额外延迟 extra_delay , 二者相加 , 就是总的延迟 , 单位是秒 , 如果需要做延迟操作 , 需要传递给休眠函数 av_usleep ( ) 微妙值 , 在秒的基础上乘以 10 万 ;
代码语言:javascript复制//计算总的帧间隔时间 , 这是真实的间隔时间
double total_frame_delay = frame_delay extra_delay;
XIV . 获取视频当前播放时间
1 . 视频的 PTS 时间 : 视频帧也可以像音频一样直接获取 PTS 时间 , 并计算其相对的播放时间 ;
2 . 视频的推荐时间获取方式 : 但是视频中建议使用另外一个值 best_effort_timestamp , 该值也是视频的播放时间 , 但是比 pts 更加精确 , best_effort_timestamp 参考了其它的许多因素 , 如编码 , 解码等参数 ;
该 best_effort_timestamp 值 , 在大部分时候等于 pts 值 ;
3 . best_effort_timestamp 原型 : 在 AVFrame 结构体中定义 ;
代码语言:javascript复制/**
* frame timestamp estimated using various heuristics, in stream time base
* - encoding: unused
* - decoding: set by libavcodec, read by user.
*/
int64_t best_effort_timestamp;
4 . 计算视频的播放时间 : 从 AVFrame 中获取了 best_effort_timestamp 值后 , 还需要乘以 time_base 时间单位值 , 转换成秒 , 代码示例如下 :
代码语言:javascript复制//获取当前画面的相对播放时间 , 相对 : 即从播放开始到现在的时间
// 该值大多数情况下 , 与 pts 值是相同的
// 该值比 pts 更加精准 , 参考了更多的信息
// 转换成秒 : 这里要注意 pts 需要转成 秒 , 需要乘以 time_base 时间单位
// 其中 av_q2d 是将 AVRational 转为 double 类型
double vedio_best_effort_timestamp_second = avFrame->best_effort_timestamp * av_q2d(time_base);
XV . 视频帧绘制的间隔控制
1 . 延迟控制策略 :
① 延迟控制 ( 降低速度 ) : 通过调用 int av_usleep(unsigned usec) 函数 , 调整视频帧之间的间隔 , 来控制视频的播放速度 , 增加帧间隔 , 就会降低视频的播放速度 , 反之会增加视频的播放速度 ;
② 丢包控制 ( 增加速度 ) : 如果视频慢了 , 说明积压的视频帧过多 , 可以通过丢包 , 增加视频播放速度 ;
2 . 视频本身的帧率 : 视频本身有一个 FPS 绘制帧率 , 默认状态下 , 每个帧之间的间隔为 1/fps 秒 , 所有的控制都是相当于该间隔进行调整 , 如增加间隔 , 是在该 1/fps 秒的基础上增加的 ;
3 . 计算视频与音频的间隔 : 将从视频帧中获取的播放时间 与 音频帧中获取的播放时间进行对比 , 计算出一个差值 ;
4 . 降低视频速度的实现 : 如果视频比音频快 , 那么在帧率间隔基础上 , 增加该差值 , 多等待一会 ;
5 . 提高视频速度实现 : 如果视频速度慢 , 那么需要丢弃一部分视频帧 , 以赶上音频播放的进度 ;
XVI . 视频帧丢弃方案
1 . 编码帧 AVPacket 丢弃 : 如果丢弃的视频帧是 AVPacket 编码帧 , 那么需要考虑 H.264 视频帧编码类型 ;
① 保留关键帧 : I 帧不能丢 , 只能丢弃 B 帧 和 P 帧 ;
② 丢弃关键帧方案 : 如果丢弃 I 帧 , 就需要将 I 帧后面的 B / P 帧 都要丢掉 , 直到下一个 I 帧 ;
③ 推荐方案 : 一般情况下是将两个 I 帧之间的 B / P 帧丢弃 ; 因为丢掉一帧 B 帧或 P 帧 , 意味着后面的 B / P 帧也无法解析了 , 后面的 B / P 帧也一并丢弃 , 直到遇到 I 帧 ;
2 . 解码帧 AVFrame 丢弃 : 每个 AVFrame 都代表了一个完整的图像数据包 , 可以丢弃任何一帧数据 , 因此这里建议丢包时选择 AVFrame 丢弃 ;
XVII . 音视频同步代码示例
音视频同步代码示例 :
代码语言:javascript复制//根据帧率 ( fps ) 计算两次图像绘制之间的间隔
// 注意单位换算 : 实际使用的是微秒单位 , 使用 av_usleep ( ) 方法时 , 需要传入微秒单位 , 后面需要乘以 10 万
double frame_delay = 1.0 / fps;
while (isPlaying){
//从线程安全队列中获取 AVFrame * 图像
...
//获取当前画面的相对播放时间 , 相对 : 即从播放开始到现在的时间
// 该值大多数情况下 , 与 pts 值是相同的
// 该值比 pts 更加精准 , 参考了更多的信息
// 转换成秒 : 这里要注意 pts 需要转成 秒 , 需要乘以 time_base 时间单位
// 其中 av_q2d 是将 AVRational 转为 double 类型
double vedio_best_effort_timestamp_second = avFrame->best_effort_timestamp * av_q2d(time_base);
//解码时 , 该值表示画面需要延迟多长时间在显示
// extra_delay = repeat_pict / (2*fps)
// 需要使用该值 , 计算一个额外的延迟时间
// 这里按照文档中的注释 , 计算一个额外延迟时间
double extra_delay = avFrame->repeat_pict / ( fps * 2 );
//计算总的帧间隔时间 , 这是真实的间隔时间
double total_frame_delay = frame_delay extra_delay;
//将 total_frame_delay ( 单位 : 秒 ) , 转换成 微秒值 , 乘以 10 万
unsigned microseconds_total_frame_delay = total_frame_delay * 1000 * 1000;
if(vedio_best_effort_timestamp_second == 0 ){
//如果播放的是第一帧 , 或者当前音频没有播放 , 就要正常播放
//休眠 , 单位微秒 , 控制 FPS 帧率
av_usleep(microseconds_total_frame_delay);
}else{
//如果不是第一帧 , 要开始考虑音视频同步问题了
//获取音频的相对时间
if(audioChannel != NULL) {
//音频的相对播放时间 , 这个是相对于播放开始的相对播放时间
double audio_pts_second = audioChannel->audio_pts_second;
//使用视频相对时间 - 音频相对时间
double second_delta = vedio_best_effort_timestamp_second - audio_pts_second;
//将相对时间转为 微秒单位
unsigned microseconds_delta = second_delta * 1000 * 1000;
//如果 second_delta 大于 0 , 说明视频播放时间比较长 , 视频比音频快
//如果 second_delta 小于 0 , 说明视频播放时间比较短 , 视频比音频慢
if(second_delta > 0){
//视频快处理方案 : 增加休眠时间
//休眠 , 单位微秒 , 控制 FPS 帧率
av_usleep(microseconds_total_frame_delay microseconds_delta);
}else if(second_delta < 0){
//视频慢处理方案 :
// ① 方案 1 : 减小休眠时间 , 甚至不休眠
// ② 方案 2 : 视频帧积压太多了 , 这里需要将视频帧丢弃 ( 比方案 1 极端 )
if(fabs(second_delta) >= 0.05){
//丢弃解码后的视频帧
...
//终止本次循环 , 继续下一次视频帧绘制
continue;
if
}else{
//如果音视频之间差距低于 0.05 秒 , 不操作 ( 50ms )
}
}
}
}