OBS Studio libobs 双端队列 deque 详解:API 全解与环形缓冲实现原理
2026/9/7 7:23:43 网站建设 项目流程

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; /* 已分配的总容量(字节) */ };
成员类型含义
datavoid *实际分配的环形缓冲区,容量为capacity字节
sizesize_t当前“激活”的数据量(字节),0 <= size <= capacity
start_possize_t逻辑起点在data中的物理下标,逻辑第 0 字节位于此处
end_possize_t逻辑终点(不含),即下一个 push_back 写入的位置
capacitysize_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)尾端写入datasize
deque_push_front(dq, data, size)头端写入datasize
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_freebfree底层缓冲区再整体清零,因此同一个结构体变量在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; }

从源码看有三点行为边界:

  1. 只增不减:capacity <= dq->capacity时直接返回,调用它试图“缩小”容量是无效操作;
  2. 只做分配,size不变——预留出来的空间仍是空闲区间;
  3. 扩容后调用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 == 0start_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.cffm->io.data缓存待写文件的数据块
plugins/obs-ffmpeg/obs-ffmpeg-mux.c、obs-ffmpeg-hls-mux.cstream->packets:以sizeof(struct encoder_packet)为元素单位的包队列
plugins/obs-outputs/mp4-mux.cMP4 复用器的包队列与随机访问(见下文)
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->packetsdeque_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 时应把握以下边界:

  1. 所有尺寸单位是字节。文档中deque_reserve/deque_upsize/push/pop/peek 的 size 参数均标注 “in bytes”,存放结构体时统一换算N * sizeof(T);
  2. 只增不减:deque_reserve对更小的 capacity、deque_upsize对更小的 size 都是 no-op;deque 不会自动缩容,峰值容量会被保留直到deque_free;
  3. 新增数据语义:只有deque_upsize与两个push_*_zero会把新增区间清零;push_back/front是纯覆写拷贝,deque_place也是覆写且可能连带 upsize;
  4. 越界行为不一致:peek/pop 系列用assert(size <= dq->size)(开启断言时越界即中止),而deque_data越界返回NULL,调用者必须判空;
  5. peek 的data可为NULL,pop 的data同样可为NULL(丢弃语义),这是官方文档明确写出的 “orNULL” 用法;
  6. 线程安全:struct deque本身不含任何锁或原子原语(从源码结构看),OBS 仓库中的现有用例(音频源、输出延迟、任务队列、muxer)均在各自所属的线程上下文内访问,跨线程共享时由调用方自行加锁;
  7. 生命周期:deque_initdataNULL,首次 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询