Android FFmpeg系列——0 编译.so库
Android FFmpeg系列——1 播放视频
Android FFmpeg系列——2 播放音频
Android FFmpeg系列——3 C多线程使用
Android FFmpeg系列——4 子线程播放音视频
Android FFmpeg系列——5 音视频同步播放
Android FFmpeg系列——6 Java 获取播放进度
Android FFmpeg系列——7 实现快进/快退功能
音视频同步播放是做播放器的难点之一,在博主用到的播放器中,有一款播放器我真的无法忍受,那就是百度云播放器!这里我真的得吐槽一下,卡顿之后,然后视频可以正常播放,但是没有声音,声音竟然没了!!你这么一个大厂,连这点都搞不定?!而且我还提过2次建议(因为实在忍不了,但是又必须得用),也不是出现的概率很低,真搞不明白!!好啦,吐槽完了,回归正题,我们来学习怎么实现音视频用不播放!
首先,我们得先了解几个概念,只有了解了这几个东西,我们才容易理解实现音视频同步流程。
概念
时间戳
在 FFmpeg 中,有2个时间戳需要我们注意的:DTS(Decoding Time Stamp,解码时间戳)和 PTS(Presentation Time Stamp,显示时间戳)。
DTS 表示压缩数据应该什么时候被解码;PTS 表示解码数据应该什么时候被显示;对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。
但是为什么需要两个时间戳呢?来,我们再了解一下视频帧相关知识。
I,P,B 帧
视频压缩中,每帧代表一幅静止的图像。而在实际压缩时,会采取各种算法减少数据的容量,其中IPB就是最常见的。简单地说,I帧是关键帧,属于帧内压缩,解码时单独的该帧便可完成解码;P帧为向前预测编码帧,即P帧解码时需要参考前面相关帧的信息才能解码;B帧为双向预测编码帧,解码时既需要参考前面已有的帧又需要参考后面待解码的帧;他们都是基于I帧来压缩数据。
I帧表示关键帧,又称intra picture,I帧画面完整保留,解码时只需要本帧数据就可以完成(因为包含完整画面)。
P帧前向预测编码帧 又称predictive-frame,表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
B帧双向预测内插编码帧 又称bi-directional interpolated prediction frame,是双向差别帧,也就是B帧记录的是本帧与前后帧的差别,换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。
因此,I帧和P帧的解码算法比较简单,资源占用也比较少,I帧只要自己完成就行了,至于P帧,也只需要解码器把前一个画面缓存一下,遇到P帧时就使用之前缓存的画面就行。如果视频流只有I和P,解码器可以不管后面的数据,边读边解码,线性前进。如果视频流还有B帧,则需要缓存前面和当前的视频帧,待后面视频帧获得后,再解码。
再回到我们之前的问题,为什么需要2个时间戳,举个例子:
对于一个电影,帧是这样来显示的:I B B P。由于显示B帧之前知道P帧中的信息,因此,帧可能会按照这样的方式来存储:I P B B。
Stream | I | P | B | B |
PTS | 1 | 4 | 2 | 3 |
DTS | 1 | 2 | 3 | 4 |
这下你该懂了吧!!
音视频同步方式
要实现音视频同步,通常需要选择一个参考时钟,参考时钟上的时间是线性递增的,编码音视频流时依据参考时钟上的时间给每帧数据打上时间戳。在播放时,读取数据帧上的时间戳,同时参考当前参考时钟上的时间来安排播放。这里的说的时间戳就是我们前面说的 PTS。
一般来说有三种音视频同步方式 :
- 音频同步到视频
- 视频同步到音频
- 音视频同步到外部时间
在音频的播放中,也有 DTS、PTS 的概念,但是音频没有类似视频中 B 帧,不需要双向预测,所以音频帧的 DTS、PTS 顺序是一致的。
所以在一般情况下,我们可以把视频同步到音频上,实现音视频的同步。
在实现代码之前,我们有几个函数需要了解一下,不然会看得一脸懵逼。
时间基
虽然知道了 PTS 是显示时间戳,AVPacket 里面有 pts 这个属性,你知道 pts 这个值的单位是什么吗?要想知道 pts 值的单位是什么,我们得知道时间基的概念,也就是 time_base,它也是用来度量时间的。
// time_base 表示方法
typedef struct AVRational{
int num; //numerator 分子
int den; //denominator 分母
} AVRational;
如果把1秒分为25等份,你可以理解就是一把尺,那么每一格表示的就是1/25秒,此时的time_base={1,25} 。
如果把1秒分成90000等份,每一个刻度就是1/90000秒,此时的time_base={1,90000}。
所谓时间基表示的就是每个刻度是多少秒,pts的值就是占多少个时间刻度(占多少个格子)。它的单位不是秒,而是时间刻度。只有pts加上time_base两者同时在一起,才能表达出时间是多少。
好比我只告诉你,某物体的长度占某一把尺上的20个刻度。但是我不告诉你,这把尺总共是多少厘米的,你就没办法计算每个刻度是多少厘米,你也就无法知道物体的长度。
pts=20个刻度
time_base={1,10} 每一个刻度是1/10厘米
所以物体的长度=ptstime_base=201/10 厘米
在 FFmpeg 中,函数av_q2d(time_base)=每个刻度是多少秒
static inline double av_q2d(AVRational a){
/**
* Convert rational to double.
* @param a rational to convert
**/
return a.num / (double) a.den;
}
所以**pts*av_q2d(time_base)**才是帧的显示时间戳(PTS)。
实现音视频同步
我是基于上一节代码继续加了,看不懂的可以看我上一节的博文。
思路已经很清晰了,就是音频正常播放,然后让视频播放速度做调整!
C 层结构播放器结构
// C 层播放器结构体
typedef struct _Player {
...
// 音频相关
...
double audio_clock;
...
} Player;
音频添加一个属性,音频时钟,用于记录!
音频时钟
因为我们参考是音频,音频的 PTS 顺序是递增的,所以可以直接获取:
// 消费函数
// 获取 packet 解码之后
player->audio_clock = packet->pts * av_q2d(stream->time_base);
// 播放音频
直接使用 packet 的 pts,乘以时间基。
视频同步
// 消费函数
// 获取 packet 解码之后
double audio_clock = player->audio_clock;
double timestamp;
if(packet->pts == AV_NOPTS_VALUE) {
timestamp = 0;
} else {
timestamp = av_frame_get_best_effort_timestamp(frame) * av_q2d(stream->time_base);
}
double frame_rate = av_q2d(stream->avg_frame_rate);
frame_rate += frame->repeat_pict * (frame_rate * 0.5);
if (timestamp == 0.0) {
usleep((unsigned long)(frame_rate * 1000));
}else {
if (fabs(timestamp - audio_clock) > AV_SYNC_THRESHOLD_MIN &&
fabs(timestamp - audio_clock) < AV_NOSYNC_THRESHOLD) {
if (timestamp > audio_clock) {
usleep((unsigned long)((timestamp - audio_clock)*1000000));
}
}
}
// 播放视频
代码很简单,av_frame_get_best_effort_timestamp 函数获取解码后 Frame 的显示时间戳,可能获取不到!
然后比较视频 PTS 和音频时钟,如果在可控范围之内,视频做延时后,才播放视频。
这段代码也是参考别人的,只是我有点不明白的是,为什么当timestamp = 0 时要 usleep frame_rate * 1000 时间?目前使用这段代码是暂时没有什么问题,但是肯定不是最优的。我接下来想参考开源的 ijkplayer 源码,详细理解之后再做优化。
对了,ijkplayer 是 B 站开源的播放器代码,目前市场上有很多播放器都是基于其上面开发的,值得学习!
代码地址:https://github.com/JohanMan/Player
参考资料
关于FFMPEG 中I帧、B帧、P帧、PTS、DTS
ffmpeg中的时间
Android NDK开发之旅36–FFmpeg音视频同步播放用C实现
深入理解pts,dts,time_base
FFmpeg 音视频同步