需求分析

最近在做项目时,对解码后的yuv数据需要做缓存,界面线程按照可配置的帧率,设定定时器去从缓存中获取YUV数据然后渲染播放。注意的是,因为界面是多画面监控网格,最多需要16画面,而视频分辨率都是1080p,甚至4k,在低性能的机器上根本无法带动。所以需要可配置FPS去播放,比如25帧的YUV,实际只渲染15帧或者10帧,只在某个全屏时才按照实际FPS去渲染播放。那么问题来了,25帧去播放哪15帧呢,如果只播放前15帧,丢掉后10帧,监控画面出现时间段跳跃肯定是不行的,为了解决这个问题,只好自己写了一个环形buf,采用后来的帧自动覆盖还未播放的帧,实现任意配置FPS的目的。

源代码

#ifndef HRINGBUFFER_H
#define HRINGBUFFER_H

#include <malloc.h>
#include <stdio.h>
#include <string.h>

#define CAN_WRITE   0
#define CAN_READ    1

#define DISCARD_WHEN_NO_CAN_WRITE   0

/**
 * @note: use in multi thread, please lock for read and write.
 *
 *
**/

class HRingBuffer
{
public:
    HRingBuffer(int size, int num = 10){
        _size = size;
        _num = num;

        long long total = (1+size)*num; // 1 for flag : CAN_WRITE or CAN_READ
        _ptr = (char*)malloc(total);
        memset(_ptr, 0, total); // init CAN_WRITE

        read_index = 0;
        write_index = 0;
    }

    ~HRingBuffer(){
        if (_ptr){
            free(_ptr);
            _ptr = NULL;
        }
    }

    char* read(){
        char* ret = get(read_index);

        if (*ret == CAN_READ){
            read_index = (read_index + 1)%_num;
            *ret = CAN_WRITE;
            return ret+1;
        }

        return NULL;
    }

    char* write(){
        char* ret = get(write_index);
        if (*ret == CAN_READ){
            if (DISCARD_WHEN_NO_CAN_WRITE){
                return NULL;
            }
            // edge out read_index
            read_index = (read_index+1)%_num;
            // qDebug("edge out read_index");
        }

        write_index = (write_index+1)%_num;
        *ret = CAN_READ;

        return ret+1;
    }

    char* get(int index){
        if (index < 0 || index >= _num)
            return NULL;
        return _ptr + index*(1+_size);
    }

private:
    int _size;
    int _num;

    char* _ptr;

    int read_index;
    int write_index;
};

#endif // HRINGBUFFER_H

讲解

这个buf虽然使用类HRingBuffer进行封装,内部实际是c的实现,非常的高效,完全是指针偏移操作。

_size用来指定每段缓存的大小,比如1080p的yuv数据,每帧的缓存大小为1920*1080*3/2
_num指定缓存的数目,比如缓存10帧
_ptr指向开辟缓存的首地址
read_index代表当前读索引
write_index代表当前写索引

在构造函数中,根据给定的size和num,我们使用malloc开辟缓存区,使用_ptr保存首地址

注意的是,我使用了每帧缓存的第一个字节代表标识,用来记录当前帧缓存是可读还是可写

所以总的缓存大小是(1+size)*num

初始化将所有字节置0,memset(_ptr, 0, total)
因为我定义的0代表可写

#define CAN_WRITE   0
#define CAN_READ    1

开始时read_index和write_index自然都是0

在get方法中,通过索引获取到每段内存的首地址

return _ptr + index*(1+_size);

对外提供的接口是read和write,返回代表可读和可写的内存首地址

char* read(){
    char* ret = get(read_index);

    if (*ret == CAN_READ){
        read_index = (read_index + 1)%_num;
        *ret = CAN_WRITE;
        return ret+1;
    }

    return NULL;
}

通过get方法获取到当前读索引代表的首地址,判断第一个字节是否是可读,是的话就返回ret+1并将read_index 读索引+1,采用取余%即可实现环形buf的自动从尾索引跳到头索引,标志置为可写,不是的话说明缓存中没有可读的帧数据,返回NULL,所以使用时需要对返回值进行非NULL判断

char* write(){
    char* ret = get(write_index);
    if (*ret == CAN_READ){
        if (DISCARD_WHEN_NO_CAN_WRITE){
            return NULL;
        }
        // edge out read_index
        read_index = (read_index+1)%_num;
        qDebug("edge out read_index");
    }

    write_index = (write_index+1)%_num;
    *ret = CAN_READ;

    return ret+1;
}

write方法和get类似,不同的是我设置了一个策略,定义了一个宏DISCARD_WHEN_NO_CAN_WRITE,表示当没有可写(缓存里全部塞满了)时是否丢弃,如果是直接返回NULL,代表没有可写内存,如果不是我们需要将可读的内存给覆盖掉,所以将read_index +1,write_index +1,标志位置为可读。

说明

可以看到是读写返回的都是内存指针,都没有上锁,如果是用在单线程中完全不会冲突,如果用来多线程中,肯定是要自己上锁的。调用形式如下:

// 读线程
mutex.lock();
char* p = pBuf->read();
if (p){
    // use p    
}
mutex.unlock();

// 写线程
mutex.lock();
char* p = pBuf->write();
if (p){
    // use p    
}
mutex.unlock();

在use p的地方只做内存的拷入和拷出操作,不用担心锁的太久。

采用这个环形buf后,就实现了均匀丢帧渲染播放的效果。