【Android FFMPEG 开发】FFMPEG 音视频同步 ( 音视频同步方案 | 视频帧 FPS 控制 | H.264 编码 I / P / B 帧 | PTS | 音视频同步 )

2023-03-27 18:58:35 浏览数 (1)

文章目录
  • 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 ) 播放 , 音频与视频同步 ;

③ 以一个外部时钟为基准 : 定义一个外部的开始时间

t

, 音频 和 视频 都基于该时间进行同步 ; 即 音频 / 视频 与

t

的相对时间差尽量保持一致 ;

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 计算出一个视频帧间隔 , 计算公式是

frac{1}{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 )
                }
            }
        }
    }

0 人点赞