NDK学习笔记:FFmpeg音视频同步2.5(内存池的优化)
继续上一篇 AV_PACKET_BUFFER内存池应用。FFmpeg音视频同步1~2的代码,demo运行的效果应该是怪怪的。 怎么怪?前几秒视频丢失 \ 混乱花屏 \ 视频慢、音频快。这一章我们一一分析这些问题的根本原因,以及解决方法。
所以出现这些问题的原因是什么?为啥在之前的教学例子没有出现?显然这跟我们新添加的AV_PACKET_BUFFER有着密切的关系。我们在AVPacket分发线程 和 解码线程分别加上调试日志,就很容易的发现,AVPacket分发线程的运行速度 比 音视频解码线程快多了。第一次BUFFER_SIZE填满了,循环第二次继续填充数据的时候,此时解码线程还没读到BUFFER_SIZE的前十个单元。这就是导致前几秒音视频丢失,视频花屏,音频杂音等问题出现的根本原因。
既然原因已经分析出来了,那么怎么解决这个问题?无非就是每次写入新数据的时候,判断一下当前位置是否已经被读过了。如果还没被读取,那就挂起等待一下呗。既然要挂起等待,那就需要互斥锁和条件变量了。 我们修改AVPacket_buffer的代码:
typedef struct _AVPacket_buffer{
//长度
int size;
//AVPacket指针的数组,总共有size个
AVPacket * * avpacket_ptr_array;
//push或者pop元素时需要按照先后顺序,依次进行
int write_current_position;
int read_current_position;
// 互斥锁(为了解决 前数据比后数据覆盖的问题)
pthread_mutex_t mutex;
// 条件变量(为了解决 前数据比后数据覆盖的问题)
pthread_cond_t cond;
//保留字段
void * reserve;
} AVPacket_buffer, AV_PACKET_BUFFER;
我们在AV_PACKET_BUFFER结构体上维护一组互斥锁条件变量。这样就方便区分video / audio的avpacket_buffer。然后在创建AVPacket缓冲区方法alloc_avpacket_buffer初始化互斥锁和条件变量,在回收AVPacket缓冲区方法free_avpacket_buffer释放销毁。这些代码我就不放上来了。我们把注意点放到 获取写入AVPacket指针的方法get_write_packet 和 读取AVPacket指针的方法get_read_packet。
// 获取一个写入AVPacket的单元
AVPacket* get_write_packet(AV_PACKET_BUFFER * pAVPacketBuffer)
{
int current = pAVPacketBuffer->write_current_position;
int next_to_write;
while(1) {
//下一个要读的位置等于下一个要写的,等我写完,在读
//不等于,就继续
next_to_write = get_next(pAVPacketBuffer,current);
if(next_to_write != pAVPacketBuffer->read_current_position){
break;
}
//阻塞 等待 get_read_packet 的 broadcast
//LOGD("wait AVPacketBuffer next_to_write:%d\n",next_to_write);
pthread_cond_wait(&(pAVPacketBuffer->cond), &(pAVPacketBuffer->mutex));
}
pAVPacketBuffer->write_current_position = next_to_write;
// 通知可读
pthread_cond_broadcast(&(pAVPacketBuffer->cond));
return pAVPacketBuffer->avpacket_ptr_array[current];
}
// 获取一个读取AVPacket的单元
AVPacket* get_read_packet(AV_PACKET_BUFFER * pAVPacketBuffer)
{
int current = pAVPacketBuffer->read_current_position;
int next_to_read;
while (1) {
next_to_read = get_next(pAVPacketBuffer,current);
if(next_to_read != pAVPacketBuffer->write_current_position){
break;
}
//阻塞 等待 get_write_packet 的 broadcast
//LOGD("wait AVPacketBuffer next_to_read:%d\n",next_to_read);
pthread_cond_wait(&(pAVPacketBuffer->cond), &(pAVPacketBuffer->mutex));
}
pAVPacketBuffer->read_current_position = next_to_read;
// 通知可写
pthread_cond_broadcast(&(pAVPacketBuffer->cond));
return pAVPacketBuffer->avpacket_ptr_array[current];
}
来看看经过改造后的get_write_packet / get_read_packet ,两者的改造思路都是一样的。以在get_write_packet为例,每次get_next的时候,不急着赋值到 write_current_position 返回,而是判断get_next获取的下一个写位置 是否和当前的读位置相等,如果下一个写位置是当前在读的,那就要挂起等待了。这个等待要在 get_read_packet 的 pthread_cond_broadcast 解除。如此读写两个方法相互限制,以确保每一个AVPacket 都得到有效的处理,不被后来的AVPacket覆盖。
要注意一点,pthread_cond_wait 是要在pthread_mutex_lock 和 pthread_mutex_unlock之间工作。所以我们还要在get_write_packet / get_read_packet 的使用位置修改一下下。
//sync_player.c / avpacket_distributor
if (pkt->stream_index == player->video_stream_index)
{
AV_PACKET_BUFFER *video_buffer = player->video_avpacket_buffer;
pthread_mutex_lock(&video_buffer->mutex);
AVPacket *video_avpacket_buffer_data = get_write_packet(video_buffer);
*video_avpacket_buffer_data = packet;
pthread_mutex_unlock(&video_buffer->mutex);
}
if (pkt->stream_index == player->audio_stream_index)
{
AV_PACKET_BUFFER *audio_buffer = player->audio_avpacket_buffer;
pthread_mutex_lock(&audio_buffer->mutex);
AVPacket *audio_avpacket_buffer_data = get_write_packet(audio_buffer);
*audio_avpacket_buffer_data = packet;
pthread_mutex_unlock(&audio_buffer->mutex);
}
//sync_player.c / audio_avframe_decoder
pthread_mutex_lock(&audioAVPacketButter->mutex);
AVPacket* packet = get_read_packet(audioAVPacketButter);
pthread_mutex_unlock(&audioAVPacketButter->mutex);
//sync_olayer.c / video_avframe_decoder
pthread_mutex_lock(&videoAVPacketButter->mutex);
AVPacket* packet = get_read_packet(videoAVPacketButter);
pthread_mutex_unlock(&videoAVPacketButter->mutex);
由于篇幅关系,我就不把所有代码都copy上来了,需要详细代码的可以到 https://github.com/MrZhaozhirong / BlogApp 下载。
好了,这样改造之后就ojbk了 ... ... 吗?demo跑起来之后,之前说的问题基本上都没了。然后开心的按返回退出,卡住了?10s后报ANR了,又是咋回事啊?而且这个问题只有在退出的时候才会发生?退出是要走nativeRelease的,加上调试日志顺藤摸瓜,我们可以发现thread_video_decoder / thread_audio_decoder的pthread_join卡住了,两个解码线程没能退出? 显然AV_PACKET_BUFFER新增的挂起逻辑有毛病啊。还是加上调试日志,即上方的LOGD("wait ... ...) ,思考发现,读写最后一个AVPacket数据包位置,更好是相等的,所以会进入挂起的操作。这样就一直不能正常退出解码线程了。。。
怎么解?显然只要突破这个相等的判断就可以了。我们在AVPacket分发线程结束av_read_frame之后,写入一个特殊的位置的不就大功告成了?
// avpacket_distributor:负责不断的读取视频文件中AVPacket,分别放入对应的解码器
void* avpacket_distributor(void* arg)
{
SyncPlayer *player = (SyncPlayer *) arg;
AVFormatContext *pFormatContext = player->input_format_ctx;
//AVPacket* packet = av_packet_alloc();
// 不用堆内存空间,因为线程创建的堆内存通过memcopy复制自定义的AVPacket_buffer当中,不高效。
AVPacket packet; // 栈内存空间
AVPacket *pkt = &packet; // 指向栈内存空间的指针
int video_frame_count = 0;
int audio_frame_count = 0;
while (av_read_frame(pFormatContext, pkt) >= 0)
{
if(player->stop_thread_avpacket_distributor != 0)
break;
if (pkt->stream_index == player->video_stream_index)
{
AV_PACKET_BUFFER *video_buffer = player->video_avpacket_buffer;
pthread_mutex_lock(&video_buffer->mutex);
AVPacket *video_avpacket_buffer_data = get_write_packet(video_buffer);
//buffer内部堆空间 = 当前栈空间数据,间接赋值。
*video_avpacket_buffer_data = packet;
pthread_mutex_unlock(&video_buffer->mutex);
video_frame_count++;
}
if (pkt->stream_index == player->audio_stream_index)
{
AV_PACKET_BUFFER *audio_buffer = player->audio_avpacket_buffer;
pthread_mutex_lock(&audio_buffer->mutex);
AVPacket *audio_avpacket_buffer_data = get_write_packet(audio_buffer);
*audio_avpacket_buffer_data = packet;
pthread_mutex_unlock(&audio_buffer->mutex);
audio_frame_count++;
}
}
//av_packet_unref(packet);
// 不需要在此解引用,应当在解码线程使用之后。
LOGI("video_frame_count:%d", video_frame_count);
LOGI("audio_frame_count:%d", audio_frame_count);
// 分发线程结束,也就是说AVPacket写操作也结束了。
// 我们把AVPacketBuffer->write_current_position+1取反 表示写入已经结束。
// 解码线程读取操作进行判断,以便能正确退出线程。
player->video_avpacket_buffer->write_current_position = -(player->video_avpacket_buffer->write_current_position+1);
player->audio_avpacket_buffer->write_current_position = -(player->audio_avpacket_buffer->write_current_position+1);
LOGI("thread_avpacket_distributor exit ...\n");
return 0;
}
完整的avpacket_distributor代码如上所示,正如注释的理解,分发线程结束,也就是说AVPacket写操作也结束了。我们把AVPacketBuffer->write_current_position+1再取反(或者直接=-1也是可以的),用负数位置表示写入已经结束。解码线程读取操作进行判断,(宗旨就是进入break,跳出挂起逻辑)就能很方便的正确退出线程了。
音视同步
现在我们先暂停代码,开展音视同步的基础理论学习。]我们现在的代码视频和音频是各自独立播放的,并不同步。那么有什么好的策略来实现音视频的同步?从理论上来说分以下三种可用策略:
- 将视频同步到音频上,就是以音频的播放速度为基准来同步视频。视频比音频播放慢了,加速播放;快了,则延迟播放。
- 将音频同步到视频上,就是以视频的播放速度为基准来同步音频。
- 将视频和音频同步外部的时钟上,选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。
在视频流和音频流中已包含了其以怎样的速度播放的相关数据,视频的帧率(Frame Rate)指示视频一秒显示的帧数(图像数);音频的采样率(Sample Rate)表示音频一秒播放的样本(Sample)的个数。可以使用以上数据通过简单的计算得到其在某一Frame(Sample)的播放时间,以这样的速度音频和视频各自播放互不影响,在理想条件下,其应该是同步的,不会出现偏差。But,理想条件是什么大家都懂得。如果用上面那种简单的计算方式,慢慢的就会出现音视频不同步的情况。要不是视频播放快了,要么是音频播放快了,很难准确的同步。这就需要一种随着时间会线性增长的量,视频和音频的播放速度都以该量为标准,播放快了就减慢播放速度;播放快了就加快播放的速度。所以呢,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以选择的播放速度量为标准,快的等待慢的,慢的则加快速度,是一个你等我赶的过程。
参考链接:https://www.cnblogs.com/laughingQing/p/5901740.html
DTS/PTS && 视频IPB帧
上面提到,视频和音频的同步过程是一个你等我赶的过程,快了则等待,慢了就加快速度。这就需要一个量来判断(和选择基准比较),到底是播放的快了还是慢了,或者正以同步的速度播放。在音视频流中的包中都含有DTS和PTS,就是这两个变量用以判断播放速度(准确来说是PTS)。DTS,Decoding Time Stamp,解码时间戳,告诉解码器packet的解码顺序;PTS,Presentation Time Stamp,显示时间戳,指示从packet中解码出来的数据的显示顺序。
视音频都是顺序播放的,其解码的顺序不应该就是其播放的顺序么,为啥还要有DTS和PTS之分呢。对于音频来说,DTS和PTS是相同的,也就是其解码的顺序和解码的顺序是相同的,但对于视频来说情况就有些不同了。视频的编码要比音频复杂一些,特别的是预测编码是视频编码的基本工具,这就会造成视频的DTS和PTS的不同。这样视频编码后会有三种不同类型的帧:
- I帧 关键帧,包含了一帧的完整数据,解码时只需要本帧的数据,不需要参考其他帧。
- P帧 前向预测编码帧,该帧的数据不完全的,解码时需要参考其前一帧的数据。
- B帧 双向预测内插编码帧,解码这种类型的帧是最复杂,不但需要参考其一帧的数据,还需要其后一帧的数据。
I帧的解码是最简单的,只需要本帧的数据;P帧也不是很复杂,值需要缓存上一帧的数据即可,总体来说都是线性,其解码顺序和显示顺序是一致的。B帧就比较复杂了,需要前后两帧的顺序,并且不是线性的,也是造成了DTS和PTS的不同的“元凶”,也是在解码后有可能得不到完整一帧的原因。
譬如举个栗子:假如一个视频序列,要这样的帧排列顺序 I B P B I。这样其解码顺序和显示的顺序就不同了(通常来说只有在流中含有B帧的时候,PTS和DTS才会不同),DTS指示解码顺序,PTS指示显示顺序。所以流中可以是这样的:
Stream : I B P B I
DTS 1 3 2 5 4
PTS 1 2 3 4 5
有了以上初步的理论知识,我们下一章开始着手解决音视频同步的问题。