需求分析
最近在做项目时,对解码后的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后,就实现了均匀丢帧渲染播放的效果。