一、“用 Rust 实现”到底指哪一层?
技术路线:Rust + Axum + Tokio + GPU Chromium + 硬件编码 + WebRTC “Rust 版”至少包含三种不同的路线,不能混为一谈。
| A:Rust 控制层 | Rust 管理会话、容量、浏览器和流送实例;媒体复用 Selkies | Three.js / Cesium 页面基本保留,优先验证 |
| B:Rust 控制层 + 自定义媒体层 | 保留 Chromium,使用 Rust 管理采集、编码与 WebRTC | 需建设输入协议、信令和媒体反馈闭环 |
| C:Rust 原生三维引擎 | 使用 wgpu 等重建渲染核心,再接入远程流送 | 原有场景、材质、GIS 能力与交互需要重新适配 |
本文主推 A,详细讨论 B;C 是新建渲染产品时的另一项工程。
wgpu 是跨平台 Rust 图形 API,可以面向原生和浏览器后端,但它不是能够直接运行 CesiumJS 页面或 Three.js 插件的容器。将已有应用换成 wgpu,属于渲染层迁移,不是替换一个后端框架。15
同样,Rust 写控制服务不等于把服务器上的 JavaScript 自动编译成 Wasm,也不会自动改变显卡的绘制能力。
二、推荐架构:把“管理会话”和“发送画面”分开
建议将平台拆成两个职责明确的部分。
**控制层:**浏览器业务 UI 通过 HTTPS 调用 Axum;Rust 服务鉴权、检查配额、选择 Worker、记录会话和生成短期访问凭证。Worker 再启动对应显示会话、Chromium 和流送进程。
**媒体层:**GPU 生成画面,采集组件交给硬件编码器,编码流通过 WebRTC 到达用户浏览器;操作沿输入通道返回。媒体不应默认绕行中央业务 API 服务。
| 本地业务 UI | Vue / React + TypeScript | 表格、搜索、菜单、表单与权限交互 |
| API 与控制服务 | Axum + Tokio | 会话创建、查询、关闭、状态通知 |
| 数据与状态 | SQLx + PostgreSQL;按需使用 redis-rs | 持久化、短期会话状态与节点心跳 |
| Worker 内部接口 | 小规模可用 HTTPS,规模化评估 tonic | 节点控制,不用于搬运每帧像素 |
| 浏览器自动化 | chromiumoxide 或受控 CDP | 打开页面、检查就绪、管理浏览器 |
| 三维执行 | GPU Chromium + 现有引擎 | 真正执行 Three.js / Cesium |
| 第一阶段媒体 | Selkies + pixelflux + NVENC | 复用采集、编码、输入和流送 |
| 后续媒体选项 | gstreamer-rs + rswebrtc,或 str0m / webrtc | 根据定制程度选一条路线 |
| 穿透与中继 | coturn | 为不能直连的网络提供中继 |
| 诊断 | tracing + 指标采集 | 关联会话、帧、网络路径和资源占用 |
Axum 基于 Tokio / Hyper,并可使用 Tower 体系;SQLx、tonic、tracing 和 redis-rs 分别提供 Rust 数据访问、RPC、结构化诊断与 Redis 客户端能力。上表是职责划分建议,不是必须全部引入的依赖清单。12224
第一版不需要同时接入 Selkies、str0m、webrtc-rs 和 webrtcsink。它们不是越多越好,而是不同层级的替代方案。
三、为什么第一阶段仍然保留 Selkies?
Selkies 的当前架构已经使用 Rust 扩展 pixelflux 处理采集与编码,但外围服务仍包含 Python。因此,准确表述是“Rust 主导的平台”,而不是“整套系统纯 Rust”。7
它默认使用 WebSocket 传输,WebRTC 需要显式启用,例如 –mode=webrtc。生产部署还要固定镜像摘要和配套版本,不能把“默认启动成功”当作“WebRTC 已经工作”。6
选择复用它,是为了避免第一阶段同时建设浏览器输入、剪贴板策略、画面采集、编码适配、浏览器播放和断线恢复。Rust 的首要工作是管好这些实例,而不是为了统一语言把现成能力全部推倒。
3.1 无物理显示器,不等于使用 Headless Chromium
走桌面采集路线时,应用画面必须出现在采集器读取的图形会话里。服务器可以没有显示器,但 Chromium 通常应运行在相应虚拟显示环境中。7
chromiumoxide 提供 Chrome DevTools Protocol 的 Rust API,其有窗口启动方式包含 BrowserConfig::builder().with_head()。这可以替代浏览器自动化层中的 Node.js 代码,但并不自动配置显示服务器、驱动或硬件编码器。5
Headless Chrome 本身也可能使用 GPU,但“GPU 可用”和“画面进入了桌面采集链路”是两回事。需要检查实际图形后端,不能只看启动参数。20
3.2 浏览器控制接口不是业务 API
CDP 只应暴露在受控内部通道。客户端提交的应是允许访问的 scene_id,由服务端映射到已批准的页面,而不是允许用户传入任意 URL、JavaScript 或启动参数。
这些是平台设计要求:否则远程浏览器可能变成内网访问入口,或者被用于执行超出业务权限的操作。
四、Rust 真正值得投入的地方:会话生命周期
远程三维的资源单位不是一次 HTTP 请求,而是一段持续运行的会话。
建议明确以下状态:
| Requested | 请求已鉴权,等待调度 | 尚未启动资源 |
| Allocating | Worker 已预留容量,正在启动 | 容量已经占用 |
| Ready / Active | 可连接或正在交互 | 持续占用 |
| Reconnecting | 暂时断线,保留业务状态 | 宽限期内仍占用 |
| Draining | 停止接收新操作,正在退出 | 不应提前释放 |
| Closed / Failed | 确认退出或完成故障回收 | 经对账后释放 |
这些状态是建议的数据模型,不是 Selkies 自带的 API 名称。
最常见的错误是:收到“停止会话”请求后,数据库立刻标记释放,实际 Chromium 和编码进程却还在运行。下一位用户又被分配进来,于是调度账面没有超卖,GPU 却已经超载。
建议由 Worker 作为本机资源的最终确认方,控制服务负责意图和租约。跨节点操作带上会话 ID、操作 ID 和递增代次,旧代次的停止请求不能关闭新实例。
4.1 RAII 适合管理令牌,但不能替代进程回收
Tokio 的 Semaphore 可以限制并发占用,OwnedSemaphorePermit 在释放时归还名额。3
下面是单进程容量预留示例,依赖 Tokio 1.x 的 sync 功能:
use std::sync::Arc;
use tokio::sync::{
OwnedSemaphorePermit, Semaphore, TryAcquireError,
};
#[derive(Clone)]
pub struct Budget(Arc<Semaphore>);
#[must_use = "令牌必须由会话持有"]
pub struct Reservation {
_permit: OwnedSemaphorePermit,
}
impl Budget {
pub fn new(slots: usize) -> Result<Self, &'static str> {
if slots == 0 || slots > Semaphore::MAX_PERMITS {
return Err("slots 超出允许范围");
}
Ok(Self(Arc::new(Semaphore::new(slots))))
}
pub fn try_reserve(&self)
-> Result<Reservation, TryAcquireError>
{
self.0.clone().try_acquire_owned()
.map(|permit| Reservation { _permit: permit })
}
}
正确的持有范围是会话,而不是创建会话的请求函数。
令牌需要跨过“启动、就绪、运行、关闭”全过程。仅当 Worker 确认对应资源已经退出,才能回收名额。上述代码只展示本地预算,不包含分布式租约,也不保证浏览器进程随令牌析构而结束。
进程崩溃后,内存里的信号量会消失,外部容器却可能继续运行,因此重启时还必须扫描残留实例、核对代次和重建资源账。
4.2 阻塞采集不要占住异步执行器
建议让 Tokio 负责网络、超时、心跳和命令调度,长期阻塞的采集或编码循环使用专门线程,有限 CPU 任务再使用受限的阻塞线程池。
Tokio 官方明确提醒:CPU 密集任务需要限制并行度;已经开始执行的 spawn_blocking 任务不能靠 abort() 强行终止。4
因此,取消会话要传递协作式退出信号,并等待底层停止。不能把“取消了 Rust Future”等同于“GPU 编码器已经退出”。
五、低延迟的关键:不要认真处理每一张过期画面
假设三维应用每秒产生 60 帧,而编码器在某个时段只能处理 40 帧。如果使用无界队列,系统可能一直保持较高吞吐,但用户看到的都是越来越久之前的画面。
对交互式三维而言,最新画面通常比完整保存中间过程重要。
Tokio 的 watch 通道只保留最新值,适合演示“最新原始帧覆盖尚未消费的旧帧”。它不是通用视频丢包策略。2
use std::{sync::Arc, time::Instant};
use tokio::sync::watch;
pub struct RawFrame {
pub sequence: u64,
pub captured_at: Instant,
pub pixels: Arc<[u8]>,
}
pub type FrameTx = watch::Sender<Option<Arc<RawFrame>>>;
pub type FrameRx = watch::Receiver<Option<Arc<RawFrame>>>;
pub fn latest_frame_channel() -> (FrameTx, FrameRx) {
watch::channel(None)
}
pub fn publish_latest(tx: &FrameTx, frame: RawFrame) {
tx.send_replace(Some(Arc::new(frame)));
}
消费端读取后,应先克隆 Arc 并释放 watch 借用,再进行耗时工作;不要持有 borrow_and_update() 返回的借用跨越 .await。2
还需要明确三个边界。
**第一,这里覆盖的是尚未编码的原始帧。**H.264、AV1 等压缩码流存在帧间依赖,不能把编码后的任意一帧都当成可以独立丢弃的数据。
**第二,Arc 不是 GPU 零拷贝。**它只让多个使用者共享同一块 CPU 缓冲区,不能消除之前已经发生的 GPU 回读。
**第三,仍要限制消费端的在途任务。**即使通道只保存一帧,如果每取出一帧就无限启动编码任务,外部队列仍会增长。GPU 表面的复用还要等待硬件处理完成,不能仅根据 Rust 引用数判断。
六、采集与编码:Rust 外壳之外,数据到底搬了几次?
6.1 一次原始帧搬运就可能接近 500 MB/s
以 1080p、60 FPS、4 字节/像素为例:
1920 × 1080 × 4 × 60
= 497,664,000 字节/秒
≈ 497.7 MB/s
4K 对应约 1,990.7 MB/s。这里是完整原始帧的数据率,不是最后的视频码率,也不是测得的 PCIe 吞吐。
如果帧先读回 CPU,再上传给硬件编码器,搬运量还会增加。Rust 能帮助管理内存和生命周期,但无法通过语言名称消除这些复制。
pixelflux 的 GPU 采集路径依赖显示后端和缓冲区互操作;GStreamer 的 nvh264enc 支持包括 CUDA、GL 和系统内存在内的输入形式。应核查最终使用的内存类型,而不是仅根据“编码器叫 NVENC”就断言全链路零拷贝。8
6.2 公开编码数据只能作参考
Epic 官方报告的 H.264 近似编码耗时,1080p 为 8.97 ms,4K 为 24.17 ms;文档列出的对应测试 GPU 为 RTX 3060。17

图 1:根据 Epic 官方数据绘制。不是 Rust、Selkies 或本文环境的实测;不能由单帧编码耗时直接推导并发用户数。
采购时要核对具体型号的编码能力、编码引擎和会话限制。NVIDIA 的官方矩阵把这些项目分别列出,显存大、AI 算力高,并不等于适合目标远程图形负载。16
对于 PoC,可以从 H.264 开始验证;再根据服务端硬件、浏览器解码能力和网络条件评估其他编码。目标应是可接受的清晰度与延迟,而不是只追求更高压缩率。
七、自己做 Rust 媒体层,应该选哪条路线?
7.1 优先组合现成媒体能力:gstreamer-rs + webrtcsink
gstreamer-rs 是 GStreamer 的 Rust 绑定,不是用 Rust 重写全部 GStreamer 和驱动。它适合用 Rust 管理管线、状态和错误,同时利用现成媒体插件。9
webrtcsink 可以把媒体通过 WebRTC 发送给多个接收端,配套项目提供信令与浏览器客户端接口。10
比较适合的分工是:Rust 管控制与生命周期,GStreamer 管格式转换、编码协商、传输和反馈。需要更精细控制时,再向底层 webrtcbin 或自定义组件下沉。
**但有一个并发陷阱:官方文档明确说明,当前接收端之间不共享编码。**多个观众连接同一个 webrtcsink,不能直接按照“一次编码服务所有人”估算资源。11
多观众复用应单独验证已有编码流输入、共享编码拓扑或 SFU 路线,不能看到“支持多个消费者”就认定只消耗一路 NVENC。
初步检查安装环境可以使用:
gst-inspect-1.0 webrtcsink
gst-inspect-1.0 nvh264enc
这只是确认插件与参数,不是 GPU 零拷贝、浏览器互通或完整流送已经通过验证。
7.2 自定义 WebRTC 引擎:str0m 或 webrtc
| str0m | 需要掌握事件循环、网络调度和 RTP 处理 | 驱动 I/O、时间推进、候选地址收集与业务集成 |
| webrtc crate | 希望使用异步 PeerConnection 风格 API | 信令、采集编码、输入协议及容量控制 |
| webrtcsink | 希望尽快组装媒体发送管线 | 特定业务控制、资源治理及自定义信令适配 |
str0m 是 Sans I/O 实现,协议对象自身不进行网络收发,也没有内部线程或异步任务。使用者必须驱动它的输入、输出与超时,并遵守相应版本的事件循环契约。13
当前 webrtc 文档描述的是异步接口与协议核心分离的实现。历史教程中的导入路径和构造方式可能不再匹配;示例、依赖和文档应固定到同一版本,而不是拼接不同年代的代码。14
无论选哪一个,都还需要获得正确的编码帧,并把网络反馈送回编码控制逻辑。
**“发出了 RTP 包”只是起点。生产链路还要处理关键帧请求、码率自适应、时间戳、丢包恢复、发送节奏和重连。**本文不把几十行 API 调用包装成完整的 WebRTC 平台。
八、网络反馈必须能影响编码,而不只是写进日志
自定义媒体层时,建议建立一条明确的闭环:
接收端反馈 → 带宽与拥塞判断 → 目标码率 / 帧率 / 分辨率 → 编码器配置 → 新的发送状态。
webrtcsink 已有相应的拥塞控制机制;自行使用协议库时,需要明确哪些能力由库负责,哪些回调必须接入应用。11
关键帧请求也需要合并和限流,防止弱网或多个接收端频繁触发昂贵的编码工作。
输入通道则建议分成两类:持续相机状态可以用“新状态覆盖旧状态”的语义;点击、确认、键盘按下/抬起和业务修改需要可靠处理及幂等确认。
特别注意,相对鼠标位移不能直接按“丢了也没关系”处理。消息丢失会改变累计位移,应发送带序号的绝对状态或累计量,或者设计明确的补偿策略。
点击拾取也需要把坐标映射到实际视频内容区域,扣除黑边。需要精确命中的场景,还应关联画面对应的相机状态,而不是始终使用服务器的最新相机。
这些输入和拾取规则是业务协议设计,不应通过任意执行客户端 JavaScript 来实现。
九、换成 Rust,收益到底有多大?
9.1 控制层加速,不等于渲染速度翻倍
假设一个模块占原总串行耗时的比例为 p,重写后执行速度变成原来的 3 倍,而其他部分不变:
新耗时 / 原耗时 = (1 – p) + p / 3
总耗时降幅 = p × (1 – 1 / 3)

图 2:假设模型,不是 Rust 与 Go 的实测。未加入新增通信、复制或调度开销,也没有把 CPU/GPU 并行流水线当作串行过程。
目标模块只占 5% 时,即使加速 3 倍,总耗时也只下降约 3.3%;如果占 50%,理论降幅才达到约 33.3%。
因此,原来只是创建会话时执行的 API,改成 Rust 通常不应被宣传成“画面 FPS 大幅提升”。反过来,若真正重的是逐帧解析、无谓复制或媒体调度,优化这些路径才可能影响交互。
Rust 的工程价值还可能表现为更明确的所有权、资源生命周期和边界控制,但是否降低总 CPU 或内存,仍要通过同场景对照确认。Chromium、驱动及第三方媒体组件的行为不会因为控制服务换语言而自动改变。
9.2 网络距离比框架差异常常更关键
假设非网络处理合计 44 ms,上下行传播合计近似 RTT:

图 3:44 ms 固定处理预算 + RTT 的简化模型,不是实际观测或 P95 延迟。
RTT 为 30 ms 时,预算约为 74 ms;RTT 为 100 ms 时,预算约为 144 ms。哪怕把某个 1.5 ms 的控制步骤降到 0.5 ms,也只能省 1 ms。
这说明:选择靠近用户的渲染节点、减少排队,往往比纠结某个 HTTP 框架更有意义。
60 FPS 下积压 3 帧约多出 50 ms。更短且可控的队列,也许比“再换一个更快的库”更直接。
十、容量和成本:用 Rust 也绕不开这两笔账
10.1 独立交互与共享观看要分开核算
100 人观看同一相机画面,和 100 人分别旋转、缩放、切图层,是不同的资源负载。SFU 可以帮助转发媒体,但不能替代 100 个独立视图的计算。
建议用以下约束组织压测:
独立会话容量 ≤ min(
显存容量,
满足帧时间目标的渲染容量,
硬件编码吞吐和会话限制,
CPU / 内存容量,
出口带宽容量
)
举一个与具体 GPU 型号无关的假设:显存允许 14 路,渲染允许 8 路,编码允许 12 路,网络允许 21 路,则上限受渲染侧的 8 路约束。再预留 20% 运行空间,向下取整可按 6 路作为初步调度值。
这些不是硬件测试结果。真实数据加载、相机飞行和关键帧同时发生时,需要重新检查尖峰。
10.2 一百名持续在线用户,月视频数据可能超过 95 TB
假设 100 人、每路平均 12 Mbps、每天观看 8 小时、每月 22 天:
持续视频出口 = 100 × 12 Mbps = 1.2 Gbps
每用户每小时 = 12,000,000 ÷ 8 × 3600
= 5.4 GB
每月纯视频 = 100 × 5.4 × 8 × 22
= 95.04 TB
GB / TB 使用十进制。额外预留 15% 传输空间后,规划出口约为 1.38 Gbps,月数据量约为 109.30 TB。

图 4:给定观看时长与平均码率下的测算。15% 是规划余量,不是 WebRTC 固定开销;不含音频、额外中继链路和具体网络计费。
TURN 中继会占用实际媒体吞吐,不只是处理握手。是否增加某一段的出口账单,要根据部署位置和网络路径核实,不能统一说费用翻倍。19
因此,不建议把本地已经流畅的所有用户都强制迁到远程。终态更适合保留本地模式,把远程 GPU 作为弱终端、复杂视图和特定业务的按需能力。
十一、部署与安全:先解决正确性,再谈单卡极限
建议先验证单机、单会话、1080p、真实业务场景,再逐步增加并发。
| GPU 渲染 | Chromium 确实使用目标 GPU,而非软件回退 |
| 显示与采集 | 页面进入正确会话;尺寸、缩放与鼠标映射一致 |
| 编码路径 | 实际硬件编码器、输入内存类型、格式转换与复制 |
| 网络路径 | 直连或中继,最终候选对与协议 |
| 会话退出 | 浏览器、媒体、显存、临时文件和名额同步回收 |
| 异常恢复 | 控制进程崩溃、Worker 重启、断线和重复请求 |
建议把同一 GPU 主机上的驱动、Chromium、Selkies 或 GStreamer、Rust 可执行文件组成经过验证的版本组合。Rust 依赖提交 Cargo.lock,镜像固定摘要,记录 GPU 型号和驱动。不要把本文的库名列表理解为任意版本都能随意组合。
不同租户的浏览器资料目录、显示会话和凭证应隔离。多个标签页不是充分的租户安全边界;高隔离要求还要评估虚拟机等方案。
控制面与 Worker 通信应鉴权;外部请求设置大小、频率和操作权限限制。Rust 的类型安全不能防止逻辑越权、无限请求或资源耗尽。
TURN 可使用临时凭证,生成凭证的共享密钥留在可信服务端;不要把固定主密钥下发给浏览器。coturn 提供相应的认证配置能力。19
最后,不向客户端发送完整模型,并不能防止截图或录屏。远程渲染不是独立完整的数据防泄露方案。
十二、怎样证明 Rust 版值得保留?
12.1 对照必须一次只改变一个主要因素
建议对比:优化后的本地版本、原控制层 + 相同媒体链路、Rust 控制层 + 相同媒体链路,以及在必要时增加的 Rust 自定义媒体层。
固定 GPU、驱动、Chromium、场景、相机路线、分辨率、画质和网络条件。只要同时更改编码器或模型精度,就不能把全部收益归给 Rust。
容量测试用独立视图递增会话数,冷热缓存分别记录;单用户 60 FPS 不能证明多用户也能达到目标。
12.2 不只记录平均 FPS
| 会话 | 首帧时间、连接成功率、恢复时间、回收时延 |
| 三维 | 应用实际更新次数,CPU / GPU 帧时间分布 |
| 媒体 | 捕获、编码耗时,队列年龄,发送与解码帧率 |
| 网络 | RTT、丢包、选中路径、码率与冻结 |
| 资源 | 每会话 CPU、内存、显存、编码器与出口 |
| 交互 | 操作到实际显示的完整延迟 |
浏览器 WebRTC 统计提供 RTP、候选对和解码相关指标。对累计字段需要按窗口取差,并处理计数器重置、缺失字段和分母为零的情况。18
平均解码耗时(ms)
= 1000 × ΔtotalDecodeTime / ΔframesDecoded
平均抖动缓冲(ms)
= 1000 × ΔjitterBufferDelay / ΔjitterBufferEmittedCount
这些指标不是完整的操作到画面延迟。收到 DataChannel 确认也不代表对应视频已经显示;需要可校准的端到端观测,必要时使用高速摄影。
此外,各环节的 P95 不能直接相加当作总链路 P95。统计应基于同一条完整交互记录。
12.3 本文代码与数据的验证范围
配套 Rust 示例包含单进程容量预留、最新原始帧通道和 6 项测试源码;当前制作环境没有 Rust/Cargo 工具链,未执行编译与 Rust 测试。示例也没有连接浏览器、编码器或真实 GPU。
具备工具链后,可进入 rust-examples 目录执行:
cargo test
cargo fmt — –check
cargo clippy –all-targets — -D warnings
本地单元测试通过,只能确认示例对应的行为,不能替代完整媒体与 GPU 验收。
文章中编码数据来自官方参考,其余收益、延迟和带宽图表为明确假设下的计算。没有虚构“Rust 比 Go 快几倍”的项目实测。
十三、最终选型:用 Rust 管住复杂性,而不是扩大改造范围
对于已有 Three.js / Cesium 系统,建议采用以下推进顺序。
**先交付可用能力。**Rust + Axum + Tokio 管理会话和 Worker,chromiumoxide 管理现有浏览器应用,Selkies / pixelflux 负责媒体链路。没有明确瓶颈时,不重复开发编码和传输。
**再替换确实受限的模块。**需要精细管线控制时,优先评估 gstreamer-rs + webrtcsink;需要自行控制协议与网络调度时,再选择 str0m 或 webrtc,并补齐输入、编码反馈和容量治理。
**最后才讨论原生重构。**只有产品本身需要新的渲染核心、跨平台复用或长期差异化能力时,才把 wgpu 原生路线列为独立项目,而不是把它当成优化现有地图的快捷按钮。
Rust 最值得发挥的地方,是可控的资源生命周期、明确的任务边界和可观测的实时链路。真正决定体验的,仍然是画面有没有及时生成、有没有少搬数据、有没有少排队,以及服务器离用户有多远。
参考资料与图表说明
外部资料均来自项目官方文档、标准组织或相关厂商。库文档的 latest 页面可能继续变化,实施时应固定到实际依赖版本。图 1 为官方数据重绘;图 2—4 为本文假设模型,不是 GPU 实测。
网硕互联帮助中心






评论前必须登录!
注册