FFmpeg 入门学习 03–缓存队列的实现

在之前的文章中实现了打开视频文件并进行解封装的功能,解封装之后即可进行解码操作。
为使解码过程不会受到解封装过程进展的影响,解封装和解码一般并行操作,两者之间通过缓存数据进行交互。

FFmpeg 入门学习 03--缓存队列的实现

背景

线程安全队列

由于 STL 中的容器是非线程安全的,因此要实现解封装和解码的并行操作需要实现一个线程安全的队列。
由于 std::deque 可以方便的在头尾进行数据的读写,此处选用 std::deque 来简单实现一个线程安全的队列。

AVPacket 和 AVFrame

在 FFmpeg 中解码前和解码后的数据分别使用 AVPacket 和 AVFrame 进行存储。
AVPacket 及 AVFrame 是采用引用计数的方式进行内部资源的管理,在对缓存数据进行读写时要特别注意数据的释放问题,以避免内存泄漏。

成员函数模板特例化

由于 AVPacket 和 AVFrame 资源管理的接口不同,为避免特例化类的所有成员,仅把相关方法通过类的成员函数模板特化来实现。

缓存队列的实现

此处实现的缓存队列仅满足对 AVPacket 和 AVFrame 的管理,且读写两个并发操作互相不受影响即可。

接口设计

template <typename T>
class Queue
{
private:
    std::deque<T*>    queue;
    mutable std::mutex   mutex;//访问互斥信号量
    int       maxSize{0};//最大容量
public:
    Queue() = default;
    explicit Queue(int m_size ): maxSize(m_size){}
    ~Queue() = default;
    
    bool    IsEmpty() const;//判断队列是否为空
    int     size() const;//当前元素个数
    bool    IsFull() const;//判断队列是否已满
    void    pushBack( T* t );//尾部添加元素
    void    pushFront( T* t );//头部添加元素
    T *     front();//读取首元素
    T *     back();//读取尾元素
    T *     operator[](int i);//按下标读取元素
    void    popBack();//删除尾元素
    void    popFront();//删除首元素
    void    clear();//清空队列
};

通用方法实现

为实现基本的线程安全保存,所有的成员方法必须加锁,实现如下:

bool    IsEmpty() const
{
      std::unique_lock<std::mutex> lock(mutex);
      return queue.empty();
}//判断队列是否为空
int     size() const
{
      std::unique_lock<std::mutex> lock(mutex);
      return queue.size();
}//当前元素个数
bool    IsFull() const
{
      std::unique_lock<std::mutex> lock(mutex);
      return maxSize <= queue.size();
}//判断队列是否已满
void    pushBack( T* t )
{
      std::unique_lock<std::mutex> lock(mutex);
      queue.push_back(t);
}//尾部添加元素
void    pushFront( T* t )
{
      std::unique_lock<std::mutex> lock(mutex);
      queue.push_front(t);
}//头部添加元素

成员函数模板特例化

涉及到数据读取、删除等资源管理相关操作,要进行成员函数模板特例化实现,以 front() 方法的实现为例:

  • 成员函数实现为模板
T *     front() 
{
    return  frontImpl<T>();
}//读取首元素
private:
    template <typename C>
    void    popFrontImpl();
  • 进行 AVPacket 特例化
template <>
void popFrontImpl<AVPacket>()
{
    std::unique_lock<std::mutex> lock(mutex);
    if (!queue.empty())
    {
          auto packet = queue.front();
          av_packet_unref(packet);
          av_packet_free(&packet);
          queue.pop_front();
    }
}
  • 进行 AVFrame 特例化
template <>
void popFrontImpl<AVFrame>()
{
      std::unique_lock<std::mutex> lock(mutex);
      if (!queue.empty())
      {
          auto frame = queue.front();
          av_frame_unref(frame);
          av_frame_free(&frame);
          queue.pop_front();
      }
}

缓存队列使用示例

完善 FFmpegPlayer 类

在 FFmpegPlayer 类中增加两个成员,用于保存音视频解封装后的数据包:

class FFmpegPlayer {
public:
    explicit FFmpegPlayer(const char* m_url);
    ~FFmpegPlayer();
public:
    bool        openFile();//打开文件
    void        startDecode();//开启循环解码
    void        abortDecode();//中断解码
private:
    int         readOnePacket();//读一个包
    int         readPacket();//循环读包
private:
    std::string       url;//文件路径
    AVFormatContext*  formatContext = nullptr;//封装格式上下文
    AVPacket          packet{}; //用于读包

    std::thread *     read_thread = nullptr;//数据读取线程
    bool              abort_request{false};//强制结束
    bool              readEof{false};//读包结束

    int               video_index{-1};//视频流索引
    int               audio_index{-1};//音频流索引
    Queue<AVPacket>*  video_packet_Queue{ nullptr };//视频 packet 队列
    Queue<AVPacket>*  audio_packet_Queue{ nullptr };//视频 packet 队列

};

初始化队列大小

在 FFmpegPlayer 类构造函数中初始化两个缓存队列,代码如下:

FFmpegPlayer::FFmpegPlayer(const char *m_url):
        url(m_url) ,
        video_packet_Queue(new Queue<AVPacket>(20)),
        audio_packet_Queue(new Queue<AVPacket>(20))
{
    
}

完善 readPacket() 方法

使用 av_packet_move_ref 方法转移对资源的引用、以保证资源的正确释放,代码如下:

int FFmpegPlayer::readPacket() 
{
    while(true)
    {
        if( abort_request ) break;//用户退出
        std::shared_ptr<AVPacket> pktClear(&packet, [](AVPacket *p)
        {
            av_packet_unref(p);
        });//用来确保每次 av_read_frame 后 pkt 缓存被清理了

        int ret = readOnePacket();
        if( ret == 0 )
        {
            if (packet.stream_index == video_index && !video_packet_Queue->IsFull() )
            {
                AVPacket* m_packet =  av_packet_alloc();
                av_packet_move_ref(m_packet,&packet);
                video_packet_Queue->pushBack(m_packet);

                std::cout << "video_packet_Queue size " << video_packet_Queue->size() << std::endl;
            }
            else  if (packet.stream_index == audio_index && !audio_packet_Queue->IsFull())
            {
                AVPacket* m_packet =  av_packet_alloc();
                av_packet_move_ref(m_packet,&packet);
                audio_packet_Queue->pushBack(m_packet);

                std::cout << "audio_packet_Queue size " << audio_packet_Queue->size() << std::endl;
            }
        }

    }
    return 0;
}

代码运行示例

文件打开成功后,调用解复用接口,代码如下:

#include "FFmpegPlayer.h"
int main() {

    const char  * url = "C:\\Lzc\\WorkCode\\test.mkv";
    FFmpegPlayer * player = new FFmpegPlayer(url);
    if( player->openFile())
    {
        std::cout << "文件打开成功!"<<std::endl;

        player->startDecode();
    }
    
    system("pause");
    return 0;
}

运行结果如下:

FFmpeg 入门学习 03--缓存队列的实现

作者:litanyuan | 来源:公众号——编程猿来如此

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论