Skip to content

Instantly share code, notes, and snippets.

@burningtnt
Last active May 30, 2026 12:42
Show Gist options
  • Select an option

  • Save burningtnt/7cb96be178c50d121cb14968789a57b4 to your computer and use it in GitHub Desktop.

Select an option

Save burningtnt/7cb96be178c50d121cb14968789a57b4 to your computer and use it in GitHub Desktop.
LiveHelper 设计文档

直播助手 (LiveHelper)

Note

太长不看:ReplayMod, 但是直播。

支持多机位直播。

  • 将渲染结果通过 Spout 库推流至外部程序 (如 OBS);
  • 基于 WebAssembly 的可编程控制。

机位控制流

LiveHelper 使用 技法 (Technique) + 分镜头 (Clip) + 调度器 (Manager) 三级控制系统。

  • 技法 (Technique): 给定一系列输入 (Input), 计算摄像机的位置与视角;
  • 分镜头 (Clip): 用户可配置一系列静态输入 (Input) 和一个技法 (Technique), 向技法 (Technique) 传入输入 (Input), 获得摄像机位置与视角;
  • 调度器 (Manager): 用户可配置一系列静态输入 (Input), 按照当前时间, 激活一个或多个分镜头 (Clip), 输出多个画面或将多个渲染结果合并输出为一个画面。

其中, 技法 (Technique) 和调度器 (Manager) 均为 LiveHelper 内置或用户自定义的 WebAssembly 脚本。

LiveHelper 在每帧按照如下流程完成机位计算:

  1. 向调度器 (Manager) 传入时间等信息, 获得一个或多个被激活的分镜头 (Clip) 及混合方式 (如果有);
  2. 依次让每个分镜头 (Clip) 所对的技法 (Technique) 计算其位置与视角;
  3. 依次渲染每一个所需的画面, 并按照调度器 (Manager) 的命令混合;
  4. 通过 Spout 库推流至外部程序。

WebAssembly API

为最大化可拓展性, LiveHelper 支持用户为技法 (Technique) 和调度器 (Manager) 自定义 WebAssembly 脚本。 虽然您可以使用任何支持 WebAssembly 的语言, 但我们建议您使用 AssemblyScript

句柄 (Handle)

和 Windows 类似,LiveHelper 使用句柄 (Handle) 作为外部资源的标识,其类型为 i32(&handle)i32(handle)

  • i32(&handle): 所有权保留在 WebAssembly 层;
  • i32(handle) (方法返回值): WebAssembly 获得其所有权;
  • i32(handle) (方法参数): WebAssembly 失去其所有权;

WebAssembly 脚本必须妥善管理持有的句柄 (Handle),防止内存泄露。

导入表

句柄 (Handle)

技法 (Technique) 和调度器 (Manager) 均可通过下列 API 管理句柄生命周期:

LH.Handle.Duplicate(i32(&handle)) -> i32(handle)

创建一个新的句柄 (Handle),指向同一个资源。

LH.Handle.Duplicate(
    i32(&handle) handle // 待复制的句柄
) -> i32(owned handle)  // 新句柄

LH.Handle.Release(i32(handle)) -> void

释放一个句柄的所有权。

LH.Handle.Release(
    i32(handle) handle // 待释放的句柄
) -> void

输入 (Input)

技法 (Technique) 和调度器 (Manager) 均可通过下列 API 获取其输入:

LH.Input.GetF32(i32) -> f32

获取一个 f32 类型的输入。

LH.Input.GetF32(
    [IN] i32 pName       // 待查 Input 名称:指向 UTF8 编码的, null-terminated 字符串的指针;
    [IN] i32 pMemoryPage // pName 对应的内存页;
) -> f32                 // 该 Input 的值。若不能以 f32 呈现,方法将出错并立刻中止 WebAssembly 运行。

LH.Input.GetBuffer(i32, i32, i32) -> i32

获取一个二进制字节流类型的输入。

LH.Input.GetBuffer(
    [IN] i32 pName,      // 待查 Input 名称:指向 UTF8 编码的, null-terminated 字符串的指针;
    [IN] i32 pMemoryPage // pName 对应的内存页;
    [OUT] i32 pBuffer,   // 该 Input 的二进制序列:指向至少有 lBuffer 字节长缓冲区的指针 pBuffer;
    [OUT] i32 lBuffer    // pBuffer 所指向缓冲区的字节长度;
) -> i32                 // 返回输入的二进制数据字节长度:若输入的二进制数据少于或等于 lBuffer 字节, 
                         // 数据会被写入到 pBuffer 中;否则, pBuffer 指向的数据未定义,调用方应按照
                         // 返回的所需空间,申请足量内存后重试。

LH.Input.GetPos(i32, i32) -> (f32, f32, f32, f32, f32, f32, f32)

获取一个 Pos 类型的输入。

LH.Technique.GetPos(
    [IN] i32 pName,      // 待查 Input 名称:指向 UTF8 编码的, null-terminated 字符串的指针;
    [IN] i32 pMemoryPage // pName 对应的内存页;
) -> (
    i32, i32, i32,       // Input 位置,以 Minecraft 风格的 X, Y, Z 空间坐标系呈现。
    i32, i32, i32, i32   // Input 角度,以四元数呈现。
)

技法 (Technique)

技法 (Technique) 可计算摄像机的位置与视角。

必须导出函数 LH.Technique.EntryPoint() -> i32(handle, clip) 作为入口点:

LH.Technique.EntryPoint(
) -> i32(handle, clip) // 摄像机配置。 

拥有以下特别的 API:

预定义输入 (Input) lh.progress / GetF32

当前程序的进度: [0, 1) 间浮点数

LH.Technique.MakeClip(f32, f32, f32, f32, f32) -> i32(handle, clip)

创建一个摄像机配置供 LiveHelper 渲染。

LH.Technique.MakeClip(
    [IN] f32 pX,        // 机位位置,以 Minecraft 风格的 X, Y, Z 空间坐标系呈现。
    [IN] f32 pY,
    [IN] f32 pZ,
    [IN] f32 rX,        // 机位朝向,以四元数呈现。
    [IN] f32 rY,
    [IN] f32 rZ,
    [IN] f32 rW,
) -> i32(handle, clip)  // 摄像机配置,请在 LH.Technique.EntryPoint 中返回。

调度器 (Manager)

调度器 (Manager) 可激活一个或多个分镜头 (Clip), 并设置其混合方式 (如果需要)。

必须导出函数 LH.Manager.EntryPoint() -> i32(handle, clip) 作为入口点:

LH.Manager.EntryPoint(
) -> i32(handle, clip) // 摄像机配置。 

拥有以下特别的 API:

预定义输入 (Input) lh.camera_counts / GetF32

可支配的分镜头 (Clip) 个数:正整数

预定义输入 (Input) lh.camera.$id.duration_ms / GetF32

可支配的第 N 个 (0-based) 分镜头 (Clip) 所需的时间毫秒数:正整数

预定义输入 (Input) lh.camera.$id.name / GetBuffer

可支配的第 N 个 (0-based) 分镜头 (Clip) 名称:UTF-8 编码的 null-terminated 字符串

LH.Manager.Render.Single(i32(unsigned)) -> i32(handle, render_request)

使 LiveHelper 激活一个特定的分镜头 (Clip)。

LH.Manager.Render.ActivateSingle(
   i32(unsigned) cameraIndex      // 摄像机下标 (0-based)
) -> i32(handle, render_request)  // 帧渲染配置,LiveHelper 渲染该分镜头 (Clip)

LH.Manager.Render.Mix(i32(handle, render_request), i32(handle, render_request), f32) -> i32(handle, render_request)

使 LiveHelper 同时激活两个分镜头 (Clip) 并线性插值合并。

LH.Manager.Render.Mix(
   i32(handle, render_request) frame1, // 帧渲染配置 1
   i32(handle, render_request) frame2, // 帧渲染配置 2
   f32 progress                        // [0, 1] 间浮点数。若为 0,则只渲染 frame1;若为 1,则只渲染 frame2;否则,线性插值。
) -> i32(handle, render_request)
@8MiYile

8MiYile commented May 25, 2026

Copy link
Copy Markdown

该方案是否支持Linux (指 Spout库)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment