欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

用circular_buffer实现的播放缓存队列

程序员文章站 2022-07-01 15:37:55
...

背景

在我们的一个项目中,开音视频会议时,音频比视频慢了将近一秒,由于历史问题,会议服务器没法进行改动,所以要求在解码端做这样一个兼容处理,主动缓存视频达到将视频延时播放的目的,从而实现音视频的同步。延时的大小可配。

解码播放的基本流程

整个视频解码端的流程如下:

用circular_buffer实现的播放缓存队列
在这个流程中,rtp模块与JitterBuffer模块是串行的。JitterBuffer模块与解码模块是异步关系,解码模块与渲染模块是串行关系。加入缓存队列后的流程图如下

用circular_buffer实现的播放缓存队列
加入缓存队列后,解码模块与渲染模块即是异步关系了。

基于circular_buffer的实现

在我前面的文章介绍过circular_buffer的特性,其相比stl中的deque,vector实现的缓存队列效率要高,实现简洁。代码如下:

template <class T>
class DisplayQeque :public boost::noncopyable
{
public:
    typedef boost::circular_buffer<T> BufferType;
    typedef typename BufferType::size_type size_type;
    typedef typename BufferType::value_type value_type;

    //构造函数指定队列的大小
    explicit DisplayQeque(size_type size) :m_Qeque(size), m_iUnread(0), m_iSize(size)
    {}


    void Put(const value_type &data)
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        m_not_full.wait(lock, std::bind(&DisplayQeque<value_type>::is_can_put, this));
        m_Qeque.push_front(data);
        ++m_iUnread;

        if (m_Qeque.full())
        {
            lock.unlock();
            m_not_empty.notify_one();
        }
    }

    void Get(value_type &data)
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        m_not_empty.wait(lock, std::bind(&DisplayQeque<value_type>::is_can_get, this));

        data = m_Qeque[--m_iUnread];

        if (m_iUnread < m_iSize)
        {
            lock.unlock();
            m_not_full.notify_one();
        }

    }

    void Clear()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_Qeque.clear();
    }

    value_type Back()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_Qeque.front();
    }

    int Size()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_Qeque.size();
    }

private:
    bool is_can_get() const { return m_iUnread == m_iSize; }
    bool is_can_put() const { return m_iUnread < m_iSize; }
private:
    BufferType m_Qeque;
    std::mutex m_mutex;
    std::condition_variable m_not_empty;
    std::condition_variable m_not_full;
    size_type m_iUnread;
    size_type m_iSize;
};

基本流程如下:
用circular_buffer实现的播放缓存队列
1. 在解码线程中调用Put函数将解码后的一帧数据放入队列,在渲染线程中调用Get函数取出一帧数据后进行渲染。该队列的机制是渲染线程在队列满时才开始取数据,队列满了以后会在解码线程每放入一帧数据后,渲染线程才取数据,队列大小始终维持指定的大小,从而达到延迟渲染的目的。
2. 队列的size是指定缓存的帧数,在该队列的实现机制下,渲染的帧率是等同于解码的帧率,那么在配置缓存的数据,可以根据解码的帧率来估算缓存的秒数,比如解码帧率为25fps,那么将size指定为100时(缓存100帧数据),即表示为缓存了4秒的数据。

实现剖析

在队列的实现中有一个下标成员变量m_iUnread,取出数据是通过m_iUnread去取,这样做有个好处是不用显式的调用pop_back函数去主动删除已取元素。在circular_buffer的内部实现中,当队列满时会直接以覆盖内存的方式去实现删除,从而提高效率,这样不使用pop_back也免去了一次对象的析构。

voip中的音视频同步

在voip系统中,作为解码端是应该加入音视频同步机制的,在解码端用rtcp中的ntp时间来判断音视频流的绝对时间,从而进行同步,但是有个最基本的条件是,在发送端发送音视频流时,必须保证音视频是同一个会话中的并且保证打包时的时间戳基准线是相同的。如果发送端无法保证这两点,那么在解码端通过时间戳是无法进行同步的。也只能通过如上的估算机制缓存数据进行同步了。

以上。