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

1、前言

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

2、背景

2.1 线程安全队列

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

2.2 AVPacket 和 AVFrame

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

2.3 成员函数模板特例化

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

3、缓存队列的实现

3.1 概述

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

3.2 接口设计

template 
class Queue
{
private:
    std::deque    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();//清空队列
};

3.2 通用方法实现

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

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

3.3 成员函数模板特例化

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

T *     front() 
{
    return  frontImpl();
}//读取首元素
private:
    template 
    void    popFrontImpl();
template <>
void popFrontImpl()
{
    std::unique_lock lock(mutex);
    if (!queue.empty())
    {
          auto packet = queue.front();
          av_packet_unref(packet);
          av_packet_free(&packet);
          queue.pop_front();
    }
}
template <>
void popFrontImpl()
{
      std::unique_lock lock(mutex);
      if (!queue.empty())
      {
          auto frame = queue.front();
          av_frame_unref(frame);
          av_frame_free(&frame);
          queue.pop_front();
      }
}

4、缓存队列使用示例

C++音视频学习资料免费获取方法:关注音视频开发T哥,点击「链接」即可免费获取2023年最新C++音视频开发进阶独家免费学习大礼包!

4.1 完善 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*  video_packet_Queue{ nullptr };//视频 packet 队列
    Queue*  audio_packet_Queue{ nullptr };//视频 packet 队列

};

4.2 初始化队列大小

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

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

4.2 完善 readPacket() 方法

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

int FFmpegPlayer::readPacket() 
{
    while(true)
    {
        if( abort_request ) break;//用户退出
        std::shared_ptr 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;
}

4.3 代码运行示例

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

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

    const char  * url = "C:LzcWorkCodetest.mkv";
    FFmpegPlayer * player = new FFmpegPlayer(url);
    if( player->openFile())
    {
        std::cout << "文件打开成功!"<startDecode();
    }
    
    system("pause");
    return 0;
}

运行结果如下:


原文链接:FFmpeg 入门学习 03--缓存队列的实现

展开阅读全文

页面更新:2024-05-15

标签:队列   缓存   解封   特例   线程   入门   元素   成员   操作   方法   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top