OBS Studio libobs 双端队列 deque 详解:API 全解与环形缓冲实现原理
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
本文以 OBS Studio 官方 API 文档docs/sphinx/reference-libobs-util-deque.rst为主体,完整讲解 libobs 工具库中双端队列(deque)的struct deque结构与全部 14 个内联函数,并深入libobs/util/deque.h源码剖析其环形缓冲布局、倍增扩容与数据搬移策略。读完后,你将能够直接在插件开发中正确使用 deque 缓存音频帧、编码器包、输出延迟数据等字节流,并理解其底层行为边界与适用场景。
一、deque 是什么:自动扩容的字节级双端队列
官方文档对 deque 的定位很精炼:
A double-ended queue (deque) that will automatically increase in size as necessary as data is pushed to the front or back.
即一个自动扩容的双端队列:数据无论推到前端还是尾端,容量都不够时都会自动增长。使用方式如下:
#include <util/deque.h>文档标注该 API 自30.1 版本引入(versionadded:: 30.1)。
从源码结构看,deque 有几个鲜明的工程特征(见 libobs/util/deque.h):
- 纯头文件实现(header-only):所有函数均为
static inline,没有任何配套的.c文件,编译进每个使用它的翻译单元; - 字节级而非元素级 API:接口以
size_t size(字节数)为操作单位,不依赖 C++ 模板,任何二进制数据(PCM 音频帧、struct encoder_packet、时间戳等)都可以直接存放; - 依赖 libobs 内存工具:底层分配使用 libobs/util/bmem.h 中的
brealloc/bfree,与 libobs 全局分配器保持一致; - 头文件通过 libobs/util/c99defs.h 保证 C99 兼容,并以
extern "C"包裹,可被 C++ 代码直接调用。
该页面是 libobs/util API 参考总目录 中 toctree 的一个子页面,与darray(动态数组)、bmem等工具并列。
二、结构体struct deque:五个成员定义环形缓冲
libobs/util/deque.h 中的结构体定义与官方文档完全一致:
struct deque { void *data; /* 底层缓冲区 */ size_t size; /* 当前有效数据大小(字节) */ size_t start_pos; /* 逻辑头部的物理偏移 */ size_t end_pos; /* 逻辑尾部的物理偏移 */ size_t capacity; /* 已分配的总容量(字节) */ };| 成员 | 类型 | 含义 |
|---|---|---|
data | void * | 实际分配的环形缓冲区,容量为capacity字节 |
size | size_t | 当前“激活”的数据量(字节),0 <= size <= capacity |
start_pos | size_t | 逻辑起点在data中的物理下标,逻辑第 0 字节位于此处 |
end_pos | size_t | 逻辑终点(不含),即下一个 push_back 写入的位置 |
capacity | size_t | 已分配的物理容量(字节) |
这五个成员共同描述了一个环形缓冲(circular buffer):逻辑区间[start_pos, end_pos)按capacity取模环绕,恰好覆盖size字节数据。空闲区间是[end_pos, start_pos)(环绕计算),其大小为capacity - size。
物理缓冲区 (capacity 字节, 下标取模 capacity): [start_pos ─── 数据 ───> ┌──────────────┐ │ │ 空闲 [end_pos ───────> ▼ start_pos │ └──────────────────── 0 ... capacity-1┘ end_pos 指向下一个 push_back 的落点三、内联函数总览
官方文档共列出 14 个公开内联函数,全部在 libobs/util/deque.h 中实现:
| 函数 | 作用 | 关键参数 |
|---|---|---|
deque_init(dq) | 初始化(整体清零) | dq |
deque_free(dq) | 释放并清零结构体 | dq |
deque_reserve(dq, capacity) | 预留容量(字节) | capacity: 新容量,字节 |
deque_upsize(dq, size) | 设置当前激活大小,新增部分清零 | size: 新大小,字节 |
deque_place(dq, pos, data, size) | 在相对位置处覆写数据 | position: 相对起点的位置 |
deque_push_back(dq, data, size) | 尾端写入 | data、size |
deque_push_front(dq, data, size) | 头端写入 | data、size |
deque_push_back_zero(dq, size) | 尾端填充零数据 | size |
deque_push_front_zero(dq, size) | 头端填充零数据 | size |
deque_peek_front(dq, data, size) | 窥视头端数据 | data: 输出缓冲,可为NULL |
deque_peek_back(dq, data, size) | 窥视尾端数据 | data: 输出缓冲,可为NULL |
deque_pop_front(dq, data, size) | 从头部弹出 | data: 输出缓冲,可为NULL |
deque_pop_back(dq, data, size) | 从尾部弹出 | data: 输出缓冲,可为NULL |
deque_data(dq, idx) | 取相对字节的直接指针 | idx: 相对起点字节下标 |
四、逐函数详解
4.1 初始化与释放:deque_init/deque_free
static inline void deque_init(struct deque *dq) { memset(dq, 0, sizeof(struct deque)); } static inline void deque_free(struct deque *dq) { bfree(dq->data); memset(dq, 0, sizeof(struct deque)); }文档描述与实现一一对应:deque_init是“just zeroes out the entire structure”,即对结构体做memset清零,data指向NULL,size/capacity为 0;deque_free先bfree底层缓冲区再整体清零,因此同一个结构体变量在deque_free后可直接再次deque_init复用(libobs/util/deque.h)。
4.2 容量管理:deque_reserve/deque_upsize
deque_reserve用于一次性预留足够的物理容量,文档原文是 “Reserves a specific amount of buffer space to ensure minimum upsizing”,其中 capacity 单位为字节:
static inline void deque_reserve(struct deque *dq, size_t capacity) { if (capacity <= dq->capacity) return; dq->data = brealloc(dq->data, capacity); deque_reorder_data(dq, capacity); dq->capacity = capacity; }从源码看有三点行为边界:
- 只增不减:
capacity <= dq->capacity时直接返回,调用它试图“缩小”容量是无效操作; - 只做分配,
size不变——预留出来的空间仍是空闲区间; - 扩容后调用
deque_reorder_data维持环形布局不变量(见第五节)。
deque_upsize用于设置“当前激活大小”(不是预留),文档特别强调 “Any new data is zeroed”——扩展出来的新区域全部清零:
static inline void deque_upsize(struct deque *dq, size_t size) { size_t add_size = size - dq->size; size_t new_end_pos = dq->end_pos + add_size; if (size <= dq->size) return; ... }实现细节(libobs/util/deque.h):size <= dq->size时提前返回(即不能用来缩容);扩容后先deque_ensure_capacity保证物理容量,再把新增区间清零——如果新增区间跨越了缓冲区末尾(new_end_pos > capacity),则分两段memset:先清尾部[end_pos, capacity),再绕回清头部[0, loop_size),最后更新end_pos。
4.3 写入:deque_push_back/deque_push_front及零填充变体
deque_push_back是最典型的写入路径,完整处理了环绕与自动扩容:
static inline void deque_push_back(struct deque *dq, const void *data, size_t size) { size_t new_end_pos = dq->end_pos + size; dq->size += size; deque_ensure_capacity(dq); if (new_end_pos > dq->capacity) { /* 跨越缓冲区末尾, 分两段拷贝 */ size_t back_size = dq->capacity - dq->end_pos; size_t loop_size = size - back_size; if (back_size) memcpy((uint8_t *)dq->data + dq->end_pos, data, back_size); memcpy(dq->data, (uint8_t *)data + back_size, loop_size); new_end_pos -= dq->capacity; } else { memcpy((uint8_t *)dq->data + dq->end_pos, data, size); } dq->end_pos = new_end_pos; }流程是:先把size累加到dq->size,交给deque_ensure_capacity按需倍增扩容;随后判断逻辑尾端end_pos + size是否越过物理容量,若越过则将数据分成“尾部一段 + 绕回首地址一段”两次memcpy,并把end_pos取模回卷(libobs/util/deque.h)。
deque_push_front的分支更多,因为它要处理“头端空间不足,需要向低地址扩张并可能越过 0”的情形(libobs/util/deque.h):
if (dq->size == size) { /* ① 队列原本是空的: 直接从 0 开始 */ dq->start_pos = 0; dq->end_pos = size; memcpy((uint8_t *)dq->data, data, size); } else if (dq->start_pos < size) { /* ② 头端空间不足: 数据跨越缓冲区首端 */ size_t back_size = size - dq->start_pos; if (dq->start_pos) memcpy(dq->data, (uint8_t *)data + back_size, dq->start_pos); dq->start_pos = dq->capacity - back_size; memcpy((uint8_t *)dq->data + dq->start_pos, data, back_size); } else { /* ③ 头端空间充足: 直接前移 start_pos */ dq->start_pos -= size; memcpy((uint8_t *)dq->data + dq->start_pos, data, size); }注意分支②中start_pos = dq->capacity - back_size:新写入的数据一半落在物理缓冲区末尾[start_pos, capacity),另一半“绕回”到物理开头[0, start_pos_old),这正是双端写入必须处理环绕的原因。
deque_push_back_zero/deque_push_front_zero与上文两个函数逻辑完全同构,只是把memcpy换成memset(…, 0, …)——即“push zeroed data”,用于预分配占位、填充未到达的延迟窗口等场景。
deque_place是唯一定点覆写接口,文档描述为 “Places data at a specific positional index (relative to the starting point) within the deque”,参数position是相对逻辑起点的字节下标:
static inline void deque_place(struct deque *dq, size_t position, const void *data, size_t size) { size_t end_point = position + size; size_t data_end_pos; if (end_point > dq->size) /* 越界写入 => 先 upsize(新增部分清零) */ deque_upsize(dq, end_point); position += dq->start_pos; /* 相对位置 => 物理位置(取模) */ if (position >= dq->capacity) position -= dq->capacity; ... }两个值得注意的行为:其一,若position + size超过当前size,会先触发deque_upsize自动扩容;其二,该函数是覆写(overwrite)语义,不会改变size(除非触发 upsize),典型用途是回改缓冲中已写入的头部信息(如流头字段)。
4.4 窥视:deque_peek_front/deque_peek_back
peek 系列只读不弹,文档说明data是 “Buffer to store data in”,但源码允许传入NULL(此时仅做断言检查,不执行拷贝):
static inline void deque_peek_front(struct deque *dq, void *data, size_t size) { assert(size <= dq->size); if (data) { size_t start_size = dq->capacity - dq->start_pos; if (start_size < size) { /* 头端数据跨越首端: 两段拷贝 */ memcpy(data, (uint8_t *)dq->data + dq->start_pos, start_size); memcpy((uint8_t *)data + start_size, dq->data, size - start_size); } else { memcpy(data, (uint8_t *)dq->data + dq->start_pos, size); } } }deque_peek_back同理,反向处理尾部跨越首端的分段拷贝(libobs/util/deque.h)。两个函数都有assert(size <= dq->size)——窥视量不能超过当前数据量,越界在断言开启时即崩溃,这是使用上最常见的错误来源。
4.5 弹出:deque_pop_front/deque_pop_back
pop = peek + 收缩,文档特意注明data参数可以是*NULL*,即“丢弃式弹出”:
static inline void deque_pop_front(struct deque *dq, void *data, size_t size) { deque_peek_front(dq, data, size); dq->size -= size; if (!dq->size) { dq->start_pos = dq->end_pos = 0; /* 清空后指针归位 */ return; } dq->start_pos += size; if (dq->start_pos >= dq->capacity) dq->start_pos -= dq->capacity; }实现要点(libobs/util/deque.h):先 peek 拷贝输出;size减到 0 时start_pos/end_pos同时归零(保留已分配容量,下次写入从 0 开始,避免长期弹出后布局碎片);非空时start_pos前移并做一次性取模回卷。deque_pop_back的对称实现中有一个细节分支if (dq->end_pos <= size) dq->end_pos = dq->capacity - (size - dq->end_pos),专门处理end_pos回卷越过 0 的边界。
4.6 直接寻址:deque_data
deque_data返回逻辑字节下标处的直接指针,是 O(1) 随机访问的关键:
static inline void *deque_data(struct deque *dq, size_t idx) { uint8_t *ptr = (uint8_t *)dq->data; size_t offset = dq->start_pos + idx; if (idx >= dq->size) return NULL; /* 越界返回 NULL, 而非崩溃 */ if (offset >= dq->capacity) offset -= dq->capacity; /* 环绕修正 */ return ptr + offset; }文档描述为 “Gets a direct pointer to data at a specific positional index within the deque, relative to the starting point”。注意它越界时返回NULL(而不是断言),调用者需要判空;同时由于环形布局,只有当idx < dq->size时才保证指向的位置可安全读写到capacity - offset字节(若逻辑数据恰好跨越物理首端,可读长度以capacity - offset为上限)。
五、底层原理:倍增扩容与deque_reorder_data的搬移时机
自动扩容由两个文档未列出的内部辅助函数完成,这是理解“容量如何增长、何时发生搬移”的关键。
deque_ensure_capacity:倍增策略
static inline void deque_ensure_capacity(struct deque *dq) { size_t new_capacity; if (dq->size <= dq->capacity) return; new_capacity = dq->capacity * 2; /* 容量翻倍 */ if (dq->size > new_capacity) new_capacity = dq->size; /* 一次写入超过 2x 时直接取 size */ dq->data = brealloc(dq->data, new_capacity); deque_reorder_data(dq, new_capacity); dq->capacity = new_capacity; }只有当size > capacity(即本次 push 直接超出已分配容量)才扩容;策略是capacity × 2,若单次写入量使 size 超过 2 倍容量,则一步到位取size。这是经典的几何级数扩容,使均摊写入成本保持 O(1)(libobs/util/deque.h)。
deque_reorder_data:仅在数据“已环绕”时搬移
static inline void deque_reorder_data(struct deque *dq, size_t new_capacity) { size_t difference; uint8_t *data; if (!dq->size || !dq->start_pos || dq->end_pos > dq->start_pos) return; difference = new_capacity - dq->capacity; data = (uint8_t *)dq->data + dq->start_pos; memmove(data + difference, data, dq->capacity - dq->start_pos); dq->start_pos += difference; }从源码结构看,它的触发条件非常克制:size == 0、start_pos == 0(数据没有环绕,从物理 0 开始),或end_pos > start_pos(同样表示未环绕)时直接返回,不搬移任何字节;只有当数据确实环绕(逻辑头在物理尾端附近)时,才把头端区段[start_pos, capacity)整体memmove到新容量的同位偏移处,并同步前移start_pos。其目的是维持环形布局的核心不变量——“逻辑区间[start_pos, end_pos)恰好覆盖size字节、空闲区间是单一环绕区间[end_pos, start_pos)”:容量增长使物理尾部多出difference字节空闲空间,若数据处于环绕态,头端必须随之前移,否则后续的 push/pop 偏移计算将与实际布局不一致。可以推断,这一设计把扩容代价压到最小:未环绕的常见路径上扩容是“零搬移”的,只有环绕态才付出一次 O(size) 的memmove(libobs/util/deque.h)。
六、deque 在 OBS 仓库中的实际应用
deque 在 libobs 核心与多个插件中都有真实用例,以下路径均可在仓库中直接查证:
| 位置 | 用途 |
|---|---|
| libobs/obs-audio.c | 音频源缓冲:audio_input_buf[ch]每声道一个 deque;buffered_timestamps时间戳队列 |
| libobs/obs-output-delay.c | 输出延迟:delay_data缓存已写入但未到期的视频/音频数据块 |
| libobs/obs-encoder.c | 编码器音频缓冲audio_input_buffer[i]的 push/pop |
| libobs/util/task.c | 任务队列tq->tasks的入队/出队 |
| plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c | ffm->io.data缓存待写文件的数据块 |
| plugins/obs-ffmpeg/obs-ffmpeg-mux.c、obs-ffmpeg-hls-mux.c | stream->packets:以sizeof(struct encoder_packet)为元素单位的包队列 |
| plugins/obs-outputs/mp4-mux.c | MP4 复用器的包队列与随机访问(见下文) |
| libobs/audio-monitoring/ 下 win32/pulse/osx 各实现 | 音频监控new_data/delay_buffer/empty_buffers队列 |
两个代表性用法值得细看:
1. 任务队列的“取出-再入队”模式(libobs/util/task.c):当任务执行失败需要重试时,代码先把任务 pop 出来再 push 回去,保证 FIFO 顺序不被破坏:
if (status != OBS_TASK_SUCCESS) { deque_pop_front(&tq->tasks, &ti, sizeof(ti)); if (ti.flags & OBS_TASK_FLAG_RESCHEDULE) { deque_push_back(&tq->tasks, &ti, sizeof(ti)); deque_pop_front(&tq->tasks, &ti, sizeof(ti)); } ... }2. MP4 复用器的随机寻址(plugins/obs-outputs/mp4-mux.c):muxer 需要按序号随机读取已入队编码包(如估算 mdat 长度、回写 moov),deque_data提供了直接指针:
static inline struct encoder_packet *get_pkt_at(struct deque *dq, size_t idx) { return deque_data(dq, idx * sizeof(struct encoder_packet)); }这里以“元素下标 × 元素大小”换算成deque_data要求的相对字节偏移,是 deque 字节级 API 封装“类型化元素队列”的惯用写法——push/pop 时统一传sizeof(struct encoder_packet)(参见 plugins/obs-ffmpeg/obs-ffmpeg-hls-mux.c 中stream->packets的deque_push_back/deque_pop_front调用)。
另外,音频管线中的丢弃式弹出也大量使用data == NULL的能力,例如 libobs/obs-audio.c 在需要丢弃陈旧音频时:
deque_pop_front(&source->audio_input_buf[ch], NULL, drop * sizeof(float));七、使用注意事项与行为边界
综合官方文档描述与 libobs/util/deque.h 源码实现,使用 deque 时应把握以下边界:
- 所有尺寸单位是字节。文档中
deque_reserve/deque_upsize/push/pop/peek 的 size 参数均标注 “in bytes”,存放结构体时统一换算N * sizeof(T); - 只增不减:
deque_reserve对更小的 capacity、deque_upsize对更小的 size 都是 no-op;deque 不会自动缩容,峰值容量会被保留直到deque_free; - 新增数据语义:只有
deque_upsize与两个push_*_zero会把新增区间清零;push_back/front是纯覆写拷贝,deque_place也是覆写且可能连带 upsize; - 越界行为不一致:peek/pop 系列用
assert(size <= dq->size)(开启断言时越界即中止),而deque_data越界返回NULL,调用者必须判空; - peek 的
data可为NULL,pop 的data同样可为NULL(丢弃语义),这是官方文档明确写出的 “orNULL” 用法; - 线程安全:
struct deque本身不含任何锁或原子原语(从源码结构看),OBS 仓库中的现有用例(音频源、输出延迟、任务队列、muxer)均在各自所属的线程上下文内访问,跨线程共享时由调用方自行加锁; - 生命周期:
deque_init后data为NULL,首次 push 才触发brealloc;deque_free之后结构体已清零,可直接复用。
参考路径
- 官方 API 文档:docs/sphinx/reference-libobs-util-deque.rst
- 文档父目录(toctree):docs/sphinx/reference-libobs-util.rst
- 实现源码(全部 14 个函数与内部辅助):libobs/util/deque.h
- 内存分配依赖:
brealloc/bfree:libobs/util/bmem.h - 典型应用:libobs/obs-audio.c、libobs/obs-output-delay.c、libobs/util/task.c、plugins/obs-outputs/mp4-mux.c、plugins/obs-ffmpeg/ffmpeg-mux/ffmpeg-mux.c
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考