音视频同步策略和视频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渲染环境

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的用 GStreamer 实现音视频同步的示例代码: ```python import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject GObject.threads_init() Gst.init(None) player = Gst.ElementFactory.make("playbin", "player") player.set_property("uri", "file:///path/to/media/file") audio_sink = Gst.ElementFactory.make("autoaudiosink", "audio_sink") video_sink = Gst.ElementFactory.make("autovideosink", "video_sink") player.set_property("audio-sink", audio_sink) player.set_property("video-sink", video_sink) bus = player.get_bus() def on_message(bus, message): t = message.type if t == Gst.MessageType.EOS: player.set_state(Gst.State.NULL) loop.quit() elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() print ("Error: %s" % err, debug) player.set_state(Gst.State.NULL) loop.quit() elif t == Gst.MessageType.STATE_CHANGED: if message.src == player: old_state, new_state, pending_state = message.parse_state_changed() if new_state == Gst.State.PLAYING: # Get the current time when playing starts query = Gst.Query.new_seeking(Gst.Format.TIME) if player.query(query): _, start, _ = query.parse_seeking() global start_time start_time = start elif t == Gst.MessageType.QOS: # Get the running time of the pipeline query = Gst.Query.new_position(Gst.Format.TIME) if player.query(query): _, running_time = query.parse_position() running_time += start_time # Get the running time of the last buffer struct = message.get_structure() _, running_time_buffer, _ = struct.get("running-time") running_time_buffer += start_time # Calculate the difference between the two running times diff = running_time - running_time_buffer # If the difference is too big, seek to the correct position if abs(diff) > Gst.SECOND / 10: player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, running_time) bus.add_signal_watch() bus.connect("message", on_message) # Start playing player.set_state(Gst.State.PLAYING) # Start the main loop loop = GObject.MainLoop() loop.run() ``` 此示例中使用了 `autoaudiosink` 和 `autovideosink` 作为音视频的输出,你还可以将其替换为其他的 sink。在收到 QOS 消息时,获取管道的当前时间以及最后一个缓冲区的运行时间,并在两个运行时间之间计算差异,如果差异太大,则使用 `seek_simple()` 函数跳转到正确的位置,以保持音视频同步

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值