主题
架构设计
1. 设计目标
挂件作为一种画布元素存在于主播酱,与文本、图片、视频等内置元素并列。客户端提供:
- 运行容器:内嵌 HTTP 静态托管 + iframe 渲染;
- 实时数据:复用客户端已有的直播事件 WebSocket 广播,挂件在 iframe 内直连消费;
- 配置面板:依据
manifest.json的字段 schema 自动生成。
挂件作者只需交付一个符合 manifest 规范 的页面包,不需要修改任何客户端代码。
2. 总体架构
┌─ 主播酱客户端(Electron)─────────────────────────────────────┐
│ │
│ 画布渲染层 │
│ └─ 挂件容器组件 │
│ ├─ <iframe │
│ │ src="http://127.0.0.1:9855/widgets/<id>/ │
│ │ index.html?zbjHost=…#zbjInit=<JSON>" │
│ │ style="pointer-events:none"> │
│ ├─ 出站 postMessage:config/theme/resize/test-event/ │
│ │ rpc-result │
│ └─ 入站 message:rpc/state/log/request-resize │
│ │
│ 配置面板(按 manifest.config 自动生成 Naive UI 表单) │
│ 挂件库(已安装管理 / 开发者注册 / 本地导入) │
│ │
└────────────────────────────────────────────────────────────────┘
│
▼ iframe 内:作者静态页 + Widget SDK
┌─ Widget SDK (zbj-widget-sdk.js) ──────────────────────────────┐
│ init: 同步读 location.hash 的 zbjInit JSON,立即 ready │
│ ZBJ.on('gift'|'chat'|'like'|…) ← 直连本机 /ws WebSocket │
│ ZBJ.config / onConfigChange ← parent.postMessage │
│ ZBJ.invoke('getGifts') ← postMessage RPC(宿主中转) │
└────────────────────────────────────────────────────────────────┘
┌─ 主播酱内嵌服务 ──────────────────────────────────────────────┐
│ HTTP │
│ ├─ /widgets/<id>/* → 已安装挂件目录 │
│ ├─ /dev-widgets/<id>/* → 开发者注册的本地 manifest 目录 │
│ └─ /widget-sdk/* → Widget SDK 运行时 │
│ WebSocket │
│ └─ /ws → 直播平台事件流(gift/like/chat/...) │
└────────────────────────────────────────────────────────────────┘3. 渲染管线
挂件元素在以下三类场景下表现一致:
| 场景 | 窗口 | 模式 | 说明 |
|---|---|---|---|
| 编辑 | 主窗口编辑区 | edit | 元素实时运行,可拖拽 / 缩放 |
| 预览 | 浏览器侧预览路由 | preview | 只读展示,storage.set 不落盘 |
| 推流 | 虚拟摄像头离屏窗 | virtualcam | 同上;由 OSR 帧采集进入虚拟摄像头输出 |
三类场景共用同一份挂件代码,渲染同一个 iframe。iframe 子文档由 Chromium 正常合成与采集,因此挂件自动进入虚拟摄像头输出,无需做任何额外配置。
<iframe> 在所有场景下恒设置 pointer-events: none:
- 编辑模式下不阻碍画布编辑工具的选中 / 拖拽 / 缩放;
- 预览与推流模式本无交互需求。
需要用户操作的玩法应通过直播事件驱动(如礼物触发动画);当前规范不支持挂件接收鼠标 / 键盘事件。
4. 数据通道分工
挂件运行涉及三条通道:
| 数据 | 通道 | 方向 |
|---|---|---|
| 初始 init(mode / platform / config / theme / widgetState / size / wsUrl 等) | iframe URL #zbjInit=<JSON> hash,SDK 启动时同步读出 | 宿主 → iframe |
| 配置 / 主题 / 尺寸 / 测试事件 / RPC 结果 | window.postMessage | 宿主 → iframe |
| RPC / 状态持久化 / 日志 / 尺寸请求 | window.parent.postMessage | iframe → 宿主 |
| 实时直播事件(礼物 / 弹幕 / 点赞 / 房间状态等) | WebSocket,SDK 直连本机 /ws 订阅 event:"platform-message" | 服务端 → 挂件 |
完整协议见 通信协议。
iframe 的对外网络行为只有两类:① 直连本机
/ws(WebSocket,不涉及 CORS);② 与父窗口postMessage(同进程,不涉及 CORS)。开发者模式下挂件运行在自有 dev server 时,也无需调整服务端 CORS 配置。
5. 文件存储
| 用途 | 位置 |
|---|---|
| 已安装挂件 | userData/widgets/<id>/(Windows: %APPDATA%\主播酱\widgets\<id>\) |
| Widget SDK 运行时 | 主播酱安装目录的 resources/widget-sdk/,由内嵌服务在 /widget-sdk/ 下提供 |
实例数据(ZBJ.storage) | 随场景一起持久化到客户端配置文件,与挂件元素绑定 |
6. 安全模型
| 维度 | 策略 |
|---|---|
| 信任模型 | 第一阶段为审核制 / 团队自研;挂件来源经审核,不面向公开市场创作者 |
| 隔离 | 挂件运行在独立 iframe 文档中,与主播酱本体内存隔离 |
| 网络范围 | iframe 默认只能访问本机 /ws 与父窗口 postMessage;外部网络访问由 CSP 控制 |
| CSP | 挂件资源响应携带宽松 CSP:放行 'self'、data:、blob:、https:;connect-src 放行 ws://127.0.0.1:* 与 http://127.0.0.1:* |
| 消息隔离 | 宿主用 ev.source === iframe.contentWindow 把 message 精确路由到对应实例;不同挂件之间的 postMessage 不会串数据 |
| 安装期校验 | manifest schema 校验、id 唯一性、sdkVersion 兼容、解压路径穿越防护、包体积上限(默认 50 MB)、入口存在性等 |
| 暴露面 | /widgets/* 与 /dev-widgets/* 通过 127.0.0.1 暴露,仅本机可达 |