概述
此文章讲的seek测率有一定缺陷,即用户非常快速点击seek操作,主线程会卡,下一篇ffmpeg音视频同步,seek策略总结是对这篇的改进,若想直接了解较好的策略,直接移步到我的下一篇中。
音视频同步一般有三种方式,
1、设置共同时间标志。这种多用于多视频播放中。
2、音频同步视频,(由于人耳对音频比视频敏感,比较少用)
3、视频同步音频
下面就介绍一种视频同步音频的方法。
视频同步音频
1、音频的解码和重采样相对于视频解码来说,只占很小的比率,因此这里只需要在解析音频的时候开启一个线程专门解码视频。
2、现在手机配置越来越高,因此视频要同步音频,只需要在解码视频后,看当前frame的帧率的pts和 当前播放的 音频的 currentaudioPts 比较,如果 Vpts 大于currentaudioPts,就停止显示 1ms,然后继续判断。这样音视频的相差就会在2ms内。人是感觉不出来的。
详细看下面的流程图
一共有三个线程:
- 蓝色的为解封装和音频重采样线程,并且在解封装过程中,将视频的packet存入video packet list当中
- 红色为音频播放线程,在回调函数中,不停取出audio data(重采样数据)、pts list 来播放音频,并且当前压入播放队列的data 对应pts就为 currentAudiopts。
- 绿色为视频显示线程,不停的从video packet list 取出packet解码,然后currentAudiopts小于 当前解码完的视频的pts时候,就显示视频,否则就等等显示。
node:
1、从流程图可以看出,同步的关键在:pts的精装计算。还有就是音频的pts和视频额pts的basetime不一样,因此在packet的时候就计算出音视频的分别的basetime,然后将音视频的pts 计算到同时间单位上。
2、对list操作时候,注意枷锁,一个音频锁、一个视频锁。
3、无法用来针对电视频道如:
http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8
进行同步,因为发现音频pts 总视为0.
seek策略
基于上面的流程,在seek的时候很容易导致(特别从后往前退时候),因为pts问题,导致视频卡住,等待音频pts更新,而视频的pts一直是seek前的比较大的pts,无论怎么更新,都是等待。因此要保证一下几条测率:
1、在seek的时候,设置seek锁,锁住的时候清理掉 audio data list ,audio pts list、 videoPacket list,并且清理掉解封装缓存、音频解码器缓存,视频解码器缓存,将seek结束时候,currentPts 置为duration,防止线程堵塞中存在一帧,导致堵塞视频显示:
void DataManager::clearData()
{
videoLock.lock();
while(videoPackets.size()>0)
{
AVPacket *avPacket = videoPackets.front();
videoPackets.pop_front();
av_packet_unref(avPacket);
}
avcodec_flush_buffers(videoCodecContext);
videoLock.unlock();
audioLock.lock();
while(audioData.size()>0)
{
uint8_t * data = audioData.front();
audioData.pop_front();
if(data!=0)
free(data);
}
avcodec_flush_buffers(audioCodecContext);
while(audioPts.size() >0)
{
audioPts.pop_front();
}
currentAudioPts =duration;
audioLock.unlock();
}
avformat_flush(dataManager->avFormatContext);
bool FFMDemux::seekTo(float pos)
{
if(pos < 0 || pos >1)
{
return false;
}
bool re = false;
if(dataManager->avFormatContext == 0)
{
return false;
}
LOGE("FFMDemux::seekTo 0 ");
// 清理缓存
avformat_flush(dataManager->avFormatContext);
long long pos2 = dataManager->avFormatContext->streams[dataManager->videoStreamIndex]->duration* pos;
LOGE("FFMDemux::seekTo 1 pos2 = %lld",pos2 );
re = av_seek_frame(dataManager->avFormatContext, dataManager->videoStreamIndex, pos2, AVSEEK_FLAG_FRAME|AVSEEK_FLAG_BACKWARD);
LOGE("FFMDemux::seekTo 2 ");
return re;
}
2、seek的同步锁加在av_read_frame
dataManager->seekLock.lock();
re = av_read_frame(dataManager->avFormatContext,avPacket);
dataManager->seekLock.unlock();
3、解码中,sendPacket的时候必须加 seek锁,因为:清理解码器中的缓存数据 和 解码共同操作了解码器队列。注意 得把 音视频同步的一起加上,这样保证,最后seek完 ,最多只有一帧没有释放。
dataManager->seekLock.lock();
re = avcodec_send_packet(context,avPacket );
if(re!=0){
av_packet_free(&avPacket);
LOGE("send packet failed");
return nullptr;
}
AVFrame* avFrame = av_frame_alloc();// dataManager->vFrame;
re = avcodec_receive_frame(context, avFrame);
// dataManager->seekLock.unlock();
av_packet_free(&avPacket);
if (re == 0)
{
LOGE("receive frame video success ......");
int vPts = avFrame->pts *dataManager->vBasetime;
while(!dataManager->isExit)
{
if(dataManager->currentAudioPts < vPts)//&& dataManager->seekPos == dataManager->seekOver
{
dataManager->seekLock.unlock();
LSleep(1);
continue;
}
dataManager->seekLock.unlock();
return avFrame;
}
}
dataManager->seekLock.unlock();
LOGE("receive video frame failed");
return nullptr;
}
node:测试receive 和 send 不是同步的,因此这里耗时并不多,索引可以都锁着,也可以只锁avcodec_send_packet,receive失败并不会影响下一次接受。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
4.17更新,新的同步策略
根据上面的线程,我们可以让 音频线程demux 和 视频线程 video 在 刚好显示 和发声后,暂停,然后在seek的时候先pause,然后清空所有数据,然后 再开始,这样就不会在卡在中途 有一些帧率在等待中,此测率实用于任何播放源,也是这几天研究的结晶。
优点:
1、变量来判定,少了大量的同步锁,让音视频更好的多线程处理。
2、逻辑更加清楚,可以确定seek的时候清理完毕所有 缓存,不会卡死。
1、视频线程
先将videoPauseReady 如果是在 isPausing 状态,下,执行到结尾就表示要进入暂停,因此设置videoPauseReady=true
void LammyOpenglVideoPlayer::videoThreadMain()
{
if(!dataManager->isExit)
{
if( dataManager->videoPauseReady){
// LOGI("pause videoPauseReady .....");
LSleep(2);
return;
}
AVFrame * avFrame = ffMdecode->decode(0);
if(avFrame != 0 && avFrame != nullptr){
openglVideoShow->show(avFrame);
}
else if(avFrame == 0){
LSleep(2);
}
if(dataManager->isPausing)
{
LOGI("pause videoPauseReady .open....");
dataManager->videoPauseReady = true;
}
}else{
dataManager->isVideoRunning = false;
}
}
2、音频线程
因为音视频同步,是视频小于音频,因此,如果音频先停止则可能导致 视频卡在了等音频的位置,因此要视频videoPauseReady=true后,再停止音频,如下:
void LammyOpenglVideoPlayer::demuxThreadMain()
{
while(!dataManager->isExit)
{
while(dataManager->audioPauseReady)
{
LSleep(2);
// LOGI("pause audioPauseReady .....");
continue;
}
int mode = ffmDemux->demux();
if(mode == 1)
{
AVFrame * avFrame = ffMdecode->decode(mode);
if(avFrame != 0 && avFrame != nullptr)
{
ffmResample->resample(avFrame);
}
}
if(dataManager->isPausing && dataManager->videoPauseReady)
{ LOGI("pause audioPauseReady .open....");
dataManager->audioPauseReady = true;
}
}
dataManager->isDemuxRunning = false;
}
3、暂停和继续功能
只有当音频和视频都停止了,才会暂停。isPause = true
void LammyOpenglVideoPlayer::pause()
{
if(!dataManager->isPause)
{
dataManager->isPausing = true;
while(true)
{
if( dataManager->videoPauseReady &&dataManager->audioPauseReady )
{
dataManager->isPause =true;
dataManager->isPausing =false;
dataManager->videoPauseReady =false;
dataManager->audioPauseReady =false;
LOGE("pause success");
return;
}else{
LSleep(20);
continue;
}
}
}
else
{
dataManager->isPause =false;
dataManager->isPausing =false;
dataManager->videoPauseReady =false;
dataManager->audioPauseReady =false;
LOGE("un pause success");
return;
}
}
seek功能
void LammyOpenglVideoPlayer::seekTo(float progress)
{
if(!dataManager-> isPause)
pause();
dataManager->clearData();
// 清空队列后,等待放空 codec里面的数据
av_frame_free(&dataManager->vFrame);
dataManager->vFrame = nullptr;
// 将 解码缓冲队列放空。
ffmDemux->seekTo(progress);
pause();
}
node:
1、这里视频的播放,暂停于播放功能,主要是上层控制的,因为用到了opengl,因此再render里面 绘制,然后调用这个videoThreadMain函数,这样导致播放器的管理部分在底层,部分在上层,在设置 setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
不方便,底层绘制完毕后手动刷新(在c++类方法中无法通过env对象 来调用java层代码来更新,且效率也不高),,将播放器的所有代码放在底层来做,在底层初始化egl环境,请参考:android glSurfaceview 底层创建EGL渲染环境。
最后
以上就是拉长胡萝卜为你收集整理的音视频同步策略和视频seek策略的全部内容,希望文章能够帮你解决音视频同步策略和视频seek策略所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复