OBS Studio libobs Canvas API(obs_canvas_t)完整参考:多画布渲染体系的设计与使用
本文围绕 OBS Studio 的 Canvas API 参考文档 reference-canvases.rst 展开,系统讲解 libobs 中引用计数画布对象 obs_canvas_t 的完整 API:信号、标志位、创建/销毁、保存/加载、引用计数、通道(Channel)、场景归属与视频输出等全部接口。结合 libobs/obs-canvas.c、libobs/obs.h 等源码实现,读者可以掌握如何在插件或前端中正确使用多画布能力(如独立的预览/设备输出画布),并理解每个参数在底层实际影响的行为。需要特别注意:文档原文以醒目的警告框声明——Canvas 仍处于早期实现阶段,API 视为不稳定(unstable),可能不经警告就发生变化,插件开发者须格外谨慎。
1. 什么是 Canvas:概念与类型定义
Canvas(画布)是 libobs 中新引入的一等对象,官方文档给出的定义为:
Canvases are reference-counted objects that contain scenes and define how those are rendered. They provide a video object which can be used with encoders or raw outputs. (画布是引用计数的对象,包含场景并定义场景的渲染方式;它提供一个可用于编码器或原始输出的视频对象。)
从源码结构看(libobs/obs-internal.h),画布的内部结构印证了这一设计:
struct obs_canvas {
struct obs_context_data context; /* 通用上下文:名称、UUID、私有标志、信号处理器 */
uint32_t flags; /* obs_canvas_flags 组合 */
struct obs_video_info ovi; /* 该画布的视频信息,FPS 字段被忽略 */
struct obs_source *sources; /* 哈希表:归属于该画布的场景(和组)源 */
pthread_mutex_t sources_mutex;
struct obs_view view; /* 视图上下文:MAX_CHANNELS 个通道的源 */
struct obs_core_video_mix *mix; /* 视频混合器:画布实际的"视频输出" */
};
注释明确写道:"canvas objects mainly act as a proxy for the existing view and video mix objects"(画布对象目前主要是既有 view 与 video mix 对象的代理)。也就是说,一个画布 = 一组归属于它的场景源(sources)+ 一个多通道视图(view)+ 一个独立的视频混合器(mix)。mix 即文档所说的"可交给编码器或原始输出的 video 对象"。
核心类型
#include <obs.h>
// 引用计数的画布(强引用)
obs_canvas_t *canvas;
// 画布的弱引用(不会阻止对象释放)
obs_weak_canvas_t *weak;
主画布(Main Canvas)
libobs 内部始终维护一个主画布,用于默认的视频输出。从源码看(libobs/obs.c),obs_init 时通过 obs_create_main_canvas() 创建它;其实现(libobs/obs-canvas.c)固定了两点:
static const char *MAIN_CANVAS_NAME = "Main";
static const char *MAIN_CANVAS_UUID = "6c69626f-6273-4c00-9d88-c5136d61696e";
obs_canvas_t *obs_create_main_canvas(void)
{
const uint32_t main_flags = MAIN | PROGRAM;
return obs_canvas_create_internal(MAIN_CANVAS_NAME, MAIN_CANVAS_UUID, NULL, main_flags, false);
}
即主画布名称恒为 "Main",UUID 固定为 6c69626f-6273-4c00-9d88-c5136d61696e,且标志为 MAIN | PROGRAM。创建时不传视频信息(ovi 为 NULL),说明画布可以没有自己的 mix——主画布的视频由全局 obs_reset_video() 驱动。
2. Canvas 信号(Signals)
文档定义了画布可用的 7 个信号。对照 libobs/obs-canvas.c 中的 canvas_signals 数组,每个画布对象拥有自己的 signal_handler_t,可注册以下信号:
| 信号 | 参数 | 触发时机 |
|---|---|---|
remove |
ptr canvas |
对画布调用 obs_canvas_remove() 时 |
destroy |
ptr canvas |
画布即将被销毁时 |
video_reset |
ptr canvas |
调用 obs_reset_video() 或 obs_canvas_reset_video() 之后,画布的视频混合被重建时 |
source_add |
ptr canvas, ptr source |
源被加入画布时 |
source_remove |
ptr canvas, ptr source |
源从画布移除时 |
rename |
ptr canvas, string new_name, string prev_name |
画布被重命名时 |
channel_change |
ptr canvas, int channel, in out ptr source, ptr prev_source |
通道上的源被更换时 |
两个值得注意的源码级细节:
in out修饰符的含义。channel_change中的source参数是in out,源码注释解释(libobs/obs-canvas.c):为了兼容原始实现,允许回调在channel_change中直接改写source指针来覆盖最终绑定的源("This isn't used anywhere in OBS itself"——OBS 自身未使用此特性)。- 主画布信号会同时广播到全局。
canvas_dosignal()(libobs/obs-canvas.c)在signal_obs参数非空且画布非私有时,会同时向obs->signals(全局信号处理器)和画布自身的信号处理器发信号。此外obs_canvas_set_channel中,只有当画布带MAIN标志时,channel_change才会额外广播到全局(libobs/obs-canvas.c)——这与旧的OBS_FRONTEND_PRIVATE_OUTPUT_CHANGED语义衔接。
获取信号处理器本身可用 obs_canvas_get_signal_handler(canvas)(libobs/obs.h)。
3. Canvas 标志位(Flags)
画布的行为通过创建时传入的 flags 参数控制。取值可以是 0 或以下值的按位或(OR)组合,定义见 libobs/obs.h:
enum obs_canvas_flags {
MAIN = 1 << 0, // 主画布:由 libobs 创建,不可重命名、不可重置,用户不能设置
ACTIVATE = 1 << 1, // 画布中的源在可见时会变为激活状态
MIX_AUDIO = 1 << 2, // 该画布通道中的音频会混入音频输出
SCENE_REF = 1 << 3, // 画布持有场景源的(强)引用
EPHEMERAL = 1 << 4, // 表示该画布不应被保存
/* Presets */
PROGRAM = ACTIVATE | MIX_AUDIO | SCENE_REF,
PREVIEW = EPHEMERAL,
DEVICE = ACTIVATE | EPHEMERAL,
};
| 标志 | 含义 | 源码行为印证 |
|---|---|---|
MAIN |
主画布,不可重命名、不可重置 | obs_canvas_set_name 遇到 MAIN 直接返回(obs-canvas.c L499-L500);obs_canvas_reset_video 对 MAIN 画布返回 false |
ACTIVATE |
源可见即激活 | 创建时决定画布视图类型:flags & ACTIVATE ? MAIN_VIEW : AUX_VIEW(obs-canvas.c L159),MAIN_VIEW 下源可见会真正激活采集/播放 |
MIX_AUDIO |
通道音频混入输出 | 决定 mix 的 mix_audio 字段:mix->mix_audio = (flags & MIX_AUDIO) != 0(obs-canvas.c L168) |
SCENE_REF |
持有场景源引用 | 插入源时若是场景则 obs_source_get_ref,销毁画布时统一释放(obs-canvas.c L324-L334、L212-L219) |
EPHEMERAL |
不参与保存 | obs_save_canvas 对 EPHEMERAL 画布返回 NULL(obs-canvas.c L237-L240) |
三个预置组合:
PROGRAM = ACTIVATE | MIX_AUDIO | SCENE_REF——典型的"节目输出"画布,也是主画布实际使用的组合(MAIN | PROGRAM);PREVIEW = EPHEMERAL——临时预览画布;DEVICE = ACTIVATE | EPHEMERAL——虚拟设备类画布:源可见即激活,但不保存。
一个容易踩坑的事实:obs_canvas_create / obs_canvas_create_private 都会先执行 flags &= ~MAIN(obs-canvas.c L194-L204),即用户无法创建出带 MAIN 标志的画布,主画布只能由 libobs 自己产生。
4. 通用画布函数
4.1 获取主画布
/** Get a strong reference to the main OBS canvas. */
obs_canvas_t *obs_get_main_canvas(void);
获取主画布的强引用。实现非常简洁(libobs/obs.c):
obs_canvas_t *obs_get_main_canvas(void)
{
return obs_canvas_get_ref(obs->data.main_canvas);
}
注意它每次调用都会增加一次引用计数,用完必须 obs_canvas_release。
4.2 创建画布
/** Creates a new canvas. */
obs_canvas_t *obs_canvas_create(const char *name, struct obs_video_info *ovi, uint32_t flags);
/** Creates a new private canvas. */
obs_canvas_t *obs_canvas_create_private(const char *name, struct obs_video_info *ovi, uint32_t flags);
| 参数 | 说明 |
|---|---|
name |
名称。普通画布会去重(重名时自动改名);私有画布不去重 |
ovi |
该画布视频输出使用的视频配置(struct obs_video_info)。可以为 NULL——源码注释"A canvas can be created without a mix"(画布可以在没有 mix 的情况下创建) |
flags |
第 3 节所述的画布标志(MAIN 位会被强制清除) |
| 返回值 | 画布对象(强引用,创建成功非 NULL) |
两者差异的本质在内部实现(obs-canvas.c L138-L186):私有画布(private=true)只注册进按 UUID 查找的哈希表 canvases,不注册进按名称查找的 named_canvases,也不发出全局 canvas_create 信号;普通画布两者皆做。因此私有画布不会被 obs_get_canvas_by_name 找到,适合插件内部使用。
创建时的关键步骤依次为:
bzalloc分配并保存flags;obs_context_data_init初始化名称/UUID/私有标志/信号处理器;signal_handler_add_array挂上第 2 节的画布信号;obs_view_init按ACTIVATE标志选择MAIN_VIEW/AUX_VIEW;- 若传入了
ovi,调用obs_create_video_mix(ovi)创建 mix,绑定画布的 view、按MIX_AUDIO设置混音开关,并加锁推入全局obs->video.mixes数组(libobs 的核心视频渲染循环会渲染所有 mix); - 按 UUID(及名称)注册到全局查找表。
4.3 标记移除与查询移除状态
/** Signal that references to canvas should be released and mark the canvas as removed. */
void obs_canvas_remove(obs_canvas_t *canvas);
/** Returns if a canvas is marked as removed (i.e., should no longer be used). */
bool obs_canvas_removed(obs_canvas_t *canvas);
obs_canvas_remove 不立即销毁画布,而是打上一个内部标志并触发 remove 信号(obs-canvas.c L565-L582):
/* Internal flag to mark a canvas as removed */
static const uint32_t REMOVED = 1u << 31; // 内部标志,不属于公开 API
void obs_canvas_remove(obs_canvas_t *canvas)
{
/* Do not allow removing the main canvas, or canvases already marked as removed. */
if (canvas->flags & (REMOVED | MAIN))
return;
...
}
要点:主画布不可被 remove;重复 remove 无效;obs_canvas_removed 即检测这个第 31 位内部标志。这是一种"软删除"语义——画布仍可通过强引用访问,但应视为不再使用(渲染循环等也会跳过已移除对象)。
4.4 名称、UUID 与标志的读写
void obs_canvas_set_name(obs_canvas_t *canvas, const char *name);
const char *obs_canvas_get_name(const obs_canvas_t *canvas);
const char *obs_canvas_get_uuid(const obs_canvas_t *canvas);
uint32_t obs_canvas_get_flags(const obs_canvas_t *canvas);
set_name:空字符串无效;MAIN画布拒绝改名;名称相同则跳过。非私有画布改名时(普通画布)额外广播全局canvas_rename信号,画布自身始终触发rename信号(obs-canvas.c L495-L523)。get_name/get_uuid:直接返回内部context.name/context.uuid指针(生命周期与画布一致,勿自行释放)。get_flags:返回创建时(加载时)写入的flags原始值。
5. 保存 / 加载函数
/** Saves a canvas to settings data */
obs_data_t *obs_save_canvas(obs_canvas_t *canvas);
/** Loads a canvas from settings data */
obs_canvas_t *obs_load_canvas(obs_data_t *data);
序列化格式可以直接从 obs_save_canvas 读出,只有 4 个键:
| 键 | 类型 | 内容 |
|---|---|---|
name |
string | 画布名称 |
uuid |
string | 画布 UUID |
private |
bool | 是否私有画布 |
flags |
int | 标志位组合 |
两条规则:
EPHEMERAL或已被REMOVED标记的画布,obs_save_canvas返回NULL——临时画布根本不进入存档;obs_load_canvas读回这 4 个字段后调用内部创建函数,并且同样会先清掉MAIN位(obs-canvas.c L252-L261),所以存档永远无法伪造一个主画布;加载时不恢复视频信息(ovi传 NULL),视频配置需要后续obs_reset_video()/obs_canvas_reset_video()重建。
6. 引用计数函数
画布是引用计数对象,API 与 libobs 其他对象(source 等)同构,共 6 个函数:
obs_canvas_t *obs_canvas_get_ref(obs_canvas_t *canvas); // 增加强引用
void obs_canvas_release(obs_canvas_t *canvas); // 释放强引用
void obs_weak_canvas_addref(obs_weak_canvas_t *weak); // 增加弱引用
void obs_weak_canvas_release(obs_weak_canvas_t *weak); // 释放弱引用
obs_weak_canvas_t *obs_canvas_get_weak_canvas(obs_canvas_t *canvas); // 强引用 → 弱引用
obs_canvas_t *obs_weak_canvas_get_canvas(obs_weak_canvas_t *weak); // 弱引用 → 强引用(可能失败)
弱引用的底层结构见 libobs/obs-internal.h:
struct obs_weak_ref { long refs; }; // refs < 0 表示对象已过期
struct obs_weak_canvas {
struct obs_weak_ref ref;
struct obs_canvas *canvas;
};
源码印证了两个关键行为:
- 强引用归零即销毁:
obs_canvas_release中当obs_ref_release返回真(引用归零)时调用obs_canvas_destroy并释放 weak 控制块(obs-canvas.c L73-L88)。 - 弱引用换强引用可能失败:
obs_weak_canvas_get_canvas用原子 CAS 提升引用;若对象已过期(owners < 0)则返回NULL(obs-canvas.c L125-L134)。这就是信号回调里保存ptr canvas参数的标准做法——先拿弱引用防自引用泄漏,回调内再obs_weak_canvas_get_canvas换回强引用。 - 还有一个防御性细节:核心已关闭(
obs == NULL)时调用obs_canvas_release只会打印告警而不会崩溃(obs-canvas.c L75-L78),提示插件在OBS_SHUTDOWN阶段的引用管理要自行把关。
7. 画布通道(Channel)函数
/** Sets the source to be used for a canvas channel. */
void obs_canvas_set_channel(obs_canvas_t *canvas, uint32_t channel, obs_source_t *source);
/** Gets the source currently in use for a canvas channel. */
obs_source_t *obs_canvas_get_channel(obs_canvas_t *canvas, uint32_t channel);
通道即 obs_view 中的 channels[MAX_CHANNELS] 数组,MAX_CHANNELS 固定为 64(libobs/obs-defs.h)。obs_canvas_set_channel 的完整语义(obs-canvas.c L431-L482):
channel >= MAX_CHANNELS时直接忽略;- 对
source先obs_source_get_ref(通道持有强引用);若与当前相同,直接释放并返回; - 组装 calldata(
canvas、channel、prev_source、source)后触发画布级channel_change信号;MAIN画布还会把channel_change广播到全局信号处理器; - 回读可能被回调改写的
source(in out特性),写入view->channels[channel]; - 对新源
obs_source_activate(source, view->type),对旧源obs_source_deactivate+obs_source_release。
obs_canvas_get_channel 走 obs_view_get_source(obs-view.c L74-L89),返回的源引用计数已递增,用完需 obs_source_release;越界或无源时返回 NULL。
激活语义与 ACTIVATE 标志联动:因为创建时 view 类型由 ACTIVATE 决定(MAIN_VIEW vs AUX_VIEW),是否真正激活采集设备取决于画布标志——这正是 PREVIEW/DEVICE 预置组合的行为差异所在。
8. 画布源(场景)管理函数
/** Create scene attached to a canvas. */
obs_scene_t *obs_canvas_scene_create(obs_canvas_t *canvas, const char *name);
/** Remove a scene from a canvas. */
void obs_canvas_scene_remove(obs_scene_t *scene);
/** Move scene to another canvas, detaching it from the previous one and deduplicating the name if needed. */
void obs_canvas_move_scene(obs_scene_t *scene, obs_canvas_t *dst);
/** Enumerates scenes belonging to a canvas. */
void obs_canvas_enum_scenes(obs_canvas_t *canvas, bool (*enum_proc)(void *, obs_source_t *), void *param);
/** Gets a canvas source by its name. */
obs_source_t *obs_canvas_get_source_by_name(obs_canvas_t *canvas, const char *name);
/** Gets a canvas scene by its name. */
obs_scene_t *obs_canvas_get_scene_by_name(obs_canvas_t *canvas, const char *name);
obs_canvas_scene_create:内部是obs_source_create_canvas(canvas, "scene", name, NULL, NULL)(obs-canvas.c L484-L488)——场景本质上就是创建时直接绑定到指定画布的 source。新场景会自动进入source_add信号流程。obs_canvas_scene_remove:即obs_canvas_remove_source,从画布的sources哈希表摘除、触发source_remove、按SCENE_REF释放场景的强引用(obs-canvas.c L356-L374);若被移除的是组(group),还会遍历该画布其他场景把组从各场景中清出。obs_canvas_move_scene:先obs_canvas_remove_source再obs_canvas_insert_source(dst, ...),名称冲突时自动去重;还会把场景内的组一并迁移到新画布(obs-canvas.c L540-L563)。这是把场景从主画布搬到独立输出画布(或反向)的关键接口。obs_canvas_enum_scenes:回调返回true继续枚举、false结束。obs_canvas_get_source_by_name/obs_canvas_get_scene_by_name:按名在画布的sources表中查找。返回值引用计数已递增,前者用obs_source_release()释放,后者用obs_scene_release()释放。
关于 SCENE_REF 标志值得强调:只有场景源(scene)会被画布额外持有强引用(obs_canvas_insert_source 中 flags & SCENE_REF && obs_source_is_scene(source) 才 obs_source_get_ref,obs-canvas.c L324-L327)。这意味着 PROGRAM 画布天然保证场景不消失,而 DEVICE/PREVIEW 画布则把场景的生命周期交还给使用者自己管理。
9. 画布视频函数
/** Reset a canvas's video configuration. */
bool obs_canvas_reset_video(obs_canvas_t *canvas, struct obs_video_info *ovi);
/** Returns true if the canvas video is configured. */
bool obs_canvas_has_video(obs_canvas_t *canvas);
/** Get canvas video output */
video_t *obs_canvas_get_video(const obs_canvas_t *canvas);
/** Get canvas video info (if any) */
bool obs_canvas_get_video_info(const obs_canvas_t *canvas, struct obs_video_info *ovi);
/** Render the canvas's view. Must be called on the graphics thread. */
void obs_canvas_render(obs_canvas_t *canvas);
9.1 重置视频配置
obs_canvas_reset_video 内部先做校验(obs-canvas.c L404-L410):
bool obs_canvas_reset_video(obs_canvas_t *canvas, struct obs_video_info *ovi)
{
if (!ovi || canvas->flags & MAIN || obs_video_active())
return false;
return obs_canvas_reset_video_internal(canvas, ovi);
}
即三种情况直接失败:未传 ovi、画布是 MAIN 画布(主画布视频由全局 obs_reset_video() 管理)、全局视频已经处于活动状态。成功后内部流程(obs_canvas_reset_video_internal,obs-canvas.c L304-L322):清除旧 mix → 保存新 ovi → obs_create_video_mix 建新 mix 并推入全局 mix 列表 → 触发 video_reset 信号。文档特别注明:ovi 中的帧率字段被忽略,实际使用全局渲染帧率(内部结构体注释亦写明 "FPS ignored",obs-internal.h L728-L729)。
9.2 查询与渲染
obs_canvas_has_video:canvas->mix != NULL(obs-canvas.c L584-L587);obs_canvas_get_video:返回canvas->mix->video,即可直接video_open/ 交给编码器或原始输出(obs_output)的video_t(obs-canvas.c L412-L415);obs_canvas_get_video_info:要求全局图形线程已初始化且画布有 mix,否则返回false,成功则拷贝出 mix 的ovi(obs-canvas.c L417-L424);obs_canvas_render:转发到obs_view_render(obs-view.c L118-L141),按序遍历全部MAX_CHANNELS个通道,对未移除的源逐一obs_source_video_render,对已移除的源就地释放并清空槽位。必须在图形线程调用。
10. 典型使用模式与注意事项
综合以上 API,一个"创建独立视频输出画布"的插件侧流程大致如下(所有调用均来自文档所列 API):
struct obs_video_info ovi;
// 填好 base_width / output_width / fps 等后:
obs_canvas_t *canvas = obs_canvas_create("My Output", &ovi, DEVICE);
// 在画布上建场景、挂源,并把当前场景源绑到通道 0
obs_scene_t *scene = obs_canvas_scene_create(canvas, "Scene 1");
obs_canvas_set_channel(canvas, 0, (obs_source_t *)scene);
video_t *video = obs_canvas_get_video(canvas); // 交给编码器 / 原始输出
// 图形线程中每帧:obs_canvas_render(canvas);
// 不再需要时:
obs_canvas_remove(canvas);
obs_canvas_release(canvas);
obs_scene_release(scene);
使用前的检查清单(对照本文各节):
- API 不稳定:Canvas 仍在早期阶段,文档明确提示插件开发者"proceed with great caution";
- 引用纪律:
obs_get_main_canvas、obs_canvas_get_ref、obs_canvas_get_channel、obs_canvas_get_source_by_name、obs_canvas_get_scene_by_name都会递增引用,务必配对释放; MAIN画布不可重命名、reset、remove;用户创建画布时MAIN位会被强制清除;EPHEMERAL画布不会被保存,obs_save_canvas对其返回NULL;- 通道上限 64(
MAX_CHANNELS),越界调用被静默忽略; obs_canvas_reset_video在全局视频活动期或对主画布调用会返回false;obs_canvas_render只能跑在图形线程;- 私有画布不入名称表、不发全局
canvas_create,适合插件内部使用,但无法通过obs_get_canvas_by_name找回。
11. 参考文件
| 文件 | 内容 |
|---|---|
| docs/sphinx/reference-canvases.rst | 本文对应的官方 Canvas API 参考文档 |
| libobs/obs-canvas.c | Canvas 全部公开 API 的实现 |
| libobs/obs.h | Canvas API 声明与 enum obs_canvas_flags 定义 |
| libobs/obs-internal.h | struct obs_canvas、struct obs_weak_canvas 内部结构 |
| libobs/obs-view.c | 画布背后的 view/通道实现(激活、去激活、渲染) |
| libobs/obs-defs.h | MAX_CHANNELS 64 通道数上限 |
| libobs/obs.c | 主画布在 obs_init 中的创建位置 |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00