上一篇音视频同步策略和视频seek策略讲过一些方法,但是总视存在一些小问题,这里花费了近三天的时间对整个 音视频同步,以及seek测率进行较大的调整,使得整个程序更健壮,用户在界面胡乱操作,seek和pause都不会引起程序卡顿和崩溃了。
音视频seek策略最简单的方法,就是一个大锁,将音频解码 和 视频解码播放 各用同一个锁锁住,然后,将seek部分用同一个锁锁住,这样seek的时候清空数据就不会导致缓冲区有数据,或者死锁问题,但是这样效率很低,且看似音视频各 一个线程,其实同时只有一个线程能跑。这里将自己的心血总结一些。大致是对上一篇的优化。
结构图
如图:有四个线程,橙色为条件判断和赋值。
- demux 解封装出来,分别为videoPacket 和 audioPacket,分别存入一个 list里面。
- audioThread 不停的从audioPacket list 取audioPacket 进行decode 和resample,并且将frame的pts和 重采样数据data存储在 audio data list 和 pts list中
- videoThread 不停从 videoPacket list 取出 videoPacket 进行decode,然后于当前播放音频 pts比较,小于就进行显示
- 音频播放线程,openSELS进行播放,在回调函数中取 audio data 和 pts 进行播放,并且将当前pts(curAudioPts) 设置为播放的pts
node: 为了方便播放器资源的管理,图中其实还有个dataManager类没有画出,这个类是所有对象的成员变量,并且一个播放器只能有一个,所有的数据都在dataManager 对象。一些 list 数据和播放器状态都在 这个类对象当中,包括ffmpeg的一些解码器 和 上下文 都存储在里面,当对播放器操作,seek 和 pause 和close的时候对数据的清理和通信,都是通过公用的dataManager来进行的。
音视频同步策略
同步测率没有什么变化,同上一篇一样:因为视频解码后很大,不建议缓存,只能缓存packet,然后与当前音频比较,如果小于音频的pts就显示和播放。没有多大的变化。
Pause策略
通过将音频 线程 和 demux线程分开,现在音频 、 视频 、demux这三个线程都是完全独立的,除了 同步那里会阻塞其他地方都不会堵塞了。并且,在decode 和 resample的时候 不要用while(!isExit)没取到数据就睡2ms然后继续取数据。因为在过程中尽量不要堵塞,方便再后面暂停播放器。
pause:当我们每个线程的一个周期执行完毕后,再进行暂停,因为每个线程都是一秒至少30次,因此人是感觉不到这个暂停的延迟的,即在线程开通进行沉睡(2ms),然后看播放器状态,选择是否继续睡眠。
node:
- 如果用glsurfaceView的时候,可能绘制视频的那个线程是主线程,可能不能堵塞哦。
- demux的packet 必须存入后才能暂停,因此,需要用到while(!isExit),可能暂停会堵塞在这里,因此 需要在堵塞的时候判断是否isPauing (正在进行暂停操作),若是,则直接返回不等待,在暂停和seek的时候,丢一帧是感觉不到的。
- openSELS有数据就播放,没数据就没声音,因此主要是在获取音频data的时候去控制播放于否。
下面给出 三个线程的:
demux线程:
void LammyOpenglVideoPlayer::demuxThreadMain()
{
while(!dataManager->isExit)
{
while(dataManager->demuxPauseReady)
{
LSleep(2);
continue;
}
/********************* 解封装部分****************************/
int mode = ffmDemux->demux();
/********************* 解封装部分结束****************************/
if(dataManager->isPausing)
{
LOGI("pause demuxPauseReady .open....");
dataManager->demuxPauseReady = true;
}
}
}
因为demux中要等待存入到packet list当中,才能进行下一次循环,因此暂停的时候,会卡在这里,因此如果在暂停的时候就不存,直接返回:
if(dataManager->isPausing)
{
dataManager->videoLock.unlock();
return 0;
}
视频线程
视频的播放在主线程,因此开头没有用while而是if:
void LammyOpenglVideoPlayer::videoThreadMain()
{
if(!dataManager->isExit)
{
if(dataManager->videoPauseReady){
LSleep(2);
LOGI("pause videoPauseReady .....");
return;
}
AVFrame * avFrame = ffMdecode->decode(0);
if(avFrame != 0 && avFrame != nullptr){
LSleep(2); LOGI("avFrame video show .....");
openglVideoShow->show(avFrame);
}
else if(avFrame == 0){
LSleep(2);
}
if(dataManager->isPausing)//&&dataManager->demuxPauseReady
{
LOGI("pause videoPauseReady .open....");
dataManager->videoPauseReady = true;
}
}else{
dataManager->isVideoRunning = false;
}
}
因为decode的时候取不到数据也不会堵塞,show的时候也不会堵塞,整个过程很快。 videoPauseReady 很快就会true,然后在开头的地方为了不显示并且不堵塞主线程,堵塞2ms后只能直接返回。
音频线程
void LammyOpenglVideoPlayer::audioThreadMain()
{
while(!dataManager->isExit)
{
while(dataManager->audioPauseReady)
{
LSleep(2);
//continue;
}
/********************* 解码重采样部分****************************/
AVFrame * avFrame = ffMdecode->decode(1);
if(avFrame != 0 && avFrame != nullptr)
{
// LOGI("pause resample ..........");
ffmResample->resample(avFrame);
}else{
LSleep(2);
}
/********************* 解码重采样部分结束****************************/
if(dataManager->isPausing )
{
LOGI("pause audioPauseReady .open....");
dataManager->audioPauseReady = true;
}
}
音频不在主线程,因此后台不停的解码 和重采样,存入到缓冲区
openSELS获得数据
void OpenSLESAudioPlayer::getAudioData()
{
// 当暂停后,就等待
while ((dataManager->isPause)&&!dataManager->isExit){
LOGE("音频暂停中。。。。。。。。");
LSleep(10);
continue;
}
char *data = nullptr;
while (!dataManager->isExit) {
dataManager->audioLock.lock();
if (dataManager->audioData.size() > 0 && dataManager->audioPts.size()>0) {
data = (char *) (dataManager->audioData.front());
dataManager->currentAudioPts = dataManager->audioPts.front();
dataManager->audioPts.pop_front();
dataManager->audioData.pop_front();
memcpy(buf,data,dataManager->audioDateSize);
free(data);
dataManager->audioLock.unlock();
return ;
}
LOGE("没有数据了,等等");
dataManager->audioLock.unlock();
LSleep(2);
continue;
}
}
pause函数:
void LammyOpenglVideoPlayer::pauseOrContinue()
{
if(!dataManager->isPause)
{
dataManager->isPausing = true;
while(true)
{
if( dataManager->videoPauseReady &&dataManager->audioPauseReady&&dataManager->demuxPauseReady )
{
dataManager->isPause =true;
dataManager->isPausing =false;
// 只有 取消暂停的时候才能 将下面置为true
// dataManager->videoPauseReady =false;
// dataManager->audioPauseReady =false;
// dataManager->demuxPauseReady=false;
LOGE("pause success");
return;
}else{
LSleep(20);
continue;
}
}
}
else
{
dataManager->isPause =false;
dataManager->isPausing =false;
dataManager->videoPauseReady =false;
dataManager->audioPauseReady =false;
dataManager->demuxPauseReady=false;
LOGE("un pause success");
return;
}
}
只有当三个线程都准备完毕后,isPausing 完毕置为false,isPause为true。
这样pause的策略就完成了,这个策略这样设计主要是方便后面的seek操作。
seek策略
上面pause策略可以看出,暂停后,线程都会停留在线程的开头,不会对解码器或者重采样等ffmpeg的数据进行操作,这样可以省去不进行pause 和 seek的时候 大量的锁操作,大大减少了开销,并且 音频 和 视频的解码完全独立开来,不会解码音频的时候视频就无法进行解码。
seek策略:seek操作是在主线程,上一篇中讲到无法快速点击seek,这会seek操作延迟会很严重,因此这里进行了改进:
- 将seek操作放入子线程进行操作,防止堵塞主线程。
- 为了减小开销和延迟,当用户进行seek操作时,如果清理数据等一切操作完毕,而没有进行 ffmpeg的seek操作时候,我们只需要将seek的seekPos修改为最新的用户点击的seekPos,前面的seekPos就不执行了。
- 增加的seekLock只在 ffmpeg的seek的时候锁住 和 点击seek键的时候判断是否正在seeking当中这2步同步,这2个操作都很短,并且保证了进程中只有一个seek线程。用户点击seek键存在2种情况 :1、 一旦 进入了seekTo函数,下面的ffmpeg线程就无法seek操作,等修改好了seePos,直接seek到新点击的pos点,不创建线程。2、无法进入seekTo函数,等待 seek完毕,再创建线程进行seek。
先给出seek的函数:
float progress = 0;
void LammyOpenglVideoPlayer::seekTo(float seekPos)
{
LOGE("seekPos = %f", seekPos);
dataManager->seekLock.lock();
if (dataManager->isSeeking){
progress = seekPos;
LOGE(" progress = seekPos = %f", seekPos);
dataManager->seekLock.unlock();
return;
}else{
progress = seekPos;
std::thread seek_th(&LammyOpenglVideoPlayer::seekThreadMain,this);
seek_th.detach();
}
dataManager->seekLock.unlock();
}
void LammyOpenglVideoPlayer::seekThreadMain()
{
dataManager->isSeeking = true;
if(!dataManager-> isPause)
{
pauseOrContinue();
}
dataManager->clearData();
dataManager->seekLock.lock();
long long pos2 = dataManager->avFormatContext->streams[dataManager->videoStreamIndex]->duration* progress;
av_seek_frame(dataManager->avFormatContext, dataManager->videoStreamIndex,
pos2, AVSEEK_FLAG_FRAME|AVSEEK_FLAG_BACKWARD);
ffmDemux->seekTo(progress);
dataManager->currentAudioPts =LLONG_MAX;
pauseOrContinue();
dataManager->isSeeking = false;
dataManager->seekLock.unlock();
}
新的seek操作是异步的,并且只会执行最新的seek操作,不会感觉到延迟,还减少了开销和主线程卡死的情况。