ffmpeg播放器17

81 阅读3分钟

1背景

上一篇16中处理到了视频帧的存储,这一篇从视频帧存储开始。

为什么需要处理视频帧的存储

因为处理到着色器渲染才发现没有视频帧可以渲染,所以才从前面start开始找哪里处理视频帧,然后整理了一下调用的顺序。

  1. 线程start中调用了run
  2. 线程run中执行了doTask
  3. 子类hffplayer的dotask中解析并给data赋值
  4. hframe在open的时候指向了data
  5. push_frame(&hframe);
  6. HVideoPlayer中使用HFrameBuf
  7. HFrameBuf使用了HRingBuf
  8. HRingBuf继承了HBuf

2步骤

2.1 HRingBuf

在hv库的base下有个hbuf.h的头文件

class HRingBuf : public HBuf {
public:
    HRingBuf() : HBuf() {_head = _tail = _size = 0;}
    HRingBuf(size_t cap) : HBuf(cap) {_head = _tail = _size = 0;}
    virtual ~HRingBuf() {}

    char* alloc(size_t len) {
        char* ret = NULL;
        if (_head < _tail || _size == 0) {
            // [_tail, this->len) && [0, _head)
            if (this->len - _tail >= len) {
                ret = base + _tail;
                _tail += len;
                if (_tail == this->len) _tail = 0;
            }
            else if (_head >= len) {
                ret = base;
                _tail = len;
            }
        }
        else {
            // [_tail, _head)
            if (_head - _tail >= len) {
                ret = base + _tail;
                _tail += len;
            }
        }
        _size += ret ? len : 0;
        return ret;
    }

    void free(size_t len) {
        _size -= len;
        if (len <= this->len - _head) {
            _head += len;
            if (_head == this->len) _head = 0;
        }
        else {
            _head = len;
        }
    }

    void clear() {_head = _tail = _size = 0;}

    size_t size() {return _size;}

private:
    size_t _head;
    size_t _tail;
    size_t _size;
};
#endif

这个HRingBuf类的实现是一个环形缓冲区,旨在高效管理动态内存的分配和释放,尤其适用于需要连续内存块的场景。 环形缓冲区结构:

 _head :指向待释放数据的起始位置(消费者指针)。  _tail :指向可分配空间的起始位置(生产者指针)。  _size :当前缓冲区中已占用的字节数,用于快速判断剩余空间。

连续内存分配:  alloc 方法优先分配连续的内存块,确保调用者可以方便操作数据,无需处理分段。当剩余空间不连续时,可能无法分配即使总空间足够,这要求调用者处理分配失败的情况。

下面这个只处理了尾部连续或者头部连续。

class RingBuffer {
public:
    RingBuffer(size_t cap) : capacity(cap), size(0), head(0), tail(0) {
        data = new char[cap];
    }
    ~RingBuffer() {
        delete[] data;
    }

    // 分配连续内存(生产者)
    char* alloc(size_t len) {
        if (len == 0 || len > available()) return nullptr;

        size_t contiguous_after_tail = capacity - tail;
        if (contiguous_after_tail >= len) {
            // 尾部有足够连续空间
            char* ret = data + tail;
            tail = (tail + len) % capacity;
            size += len;
            return ret;
        } else {
            // 检查头部是否有足够空间
            if (head >= len) {
                char* ret = data;
                tail = len;
                size += len;
                return ret;
            }
        }
        return nullptr;
    }

    // 释放数据(消费者)
    void free(size_t len) {
        if (len == 0 || len > size) return;

        size_t contiguous_after_head = capacity - head;
        if (len <= contiguous_after_head) {
            head += len;
        } else {
            head = len - contiguous_after_head;
        }
        head %= capacity;
        size -= len;
    }

    // 剩余可用空间
    size_t available() const {
        return capacity - size;
    }

    // 当前数据量
    size_t current_size() const {
        return size;
    }

private:
    char* data;         // 缓冲区指针
    size_t capacity;    // 总容量
    size_t size;        // 已用空间
    size_t head;        // 读取位置
    size_t tail;        // 写入位置
};

正常情况下没必要处理一部分在头部,一部分在尾部的数据,毕竟分段会增加调用复杂度。

  1. 根据数据类型选择策略 分块友好型数据(如文本日志、传感器数据) → 支持分段分配,通过  alloc  返回多块信息。 连续依赖型数据(如TCP封包、音频帧) → 强制连续分配,若空间不足则等待或报错。