我是靠谱客的博主 拉长胡萝卜,最近开发中收集的这篇文章主要介绍音视频同步策略和视频seek策略,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

此文章讲的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策略所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部