Bootstrap

Android FFmpeg系列——5 音视频同步播放

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。

StreamIPBB
PTS1423
DTS1234

这下你该懂了吧!!

音视频同步方式

要实现音视频同步,通常需要选择一个参考时钟,参考时钟上的时间是线性递增的,编码音视频流时依据参考时钟上的时间给每帧数据打上时间戳。在播放时,读取数据帧上的时间戳,同时参考当前参考时钟上的时间来安排播放。这里的说的时间戳就是我们前面说的 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 音视频同步

;