虚拟背景与超分
面向 WebRTC 实时通信场景的虚拟背景(人像分割 / 抠图 / 背景替换 / 模糊)与超分(发送端降采样 + 接收端超分恢复)工程实践。覆盖 MediaPipe Selfie Segmentation、ONNX Runtime Web、TensorFlow.js、WebGPU、WGSL compute shader、Insertable Streams 链路与性能预算。
1. 全链路架构
1.1 链路总览
虚拟背景与超分都是在 getUserMedia 拿到原始帧之后、RTCPeerConnection 编码上行之前(或下行解码之后),插入一段「读帧 → AI 推理 → 合成 → 写回 MediaStreamTrack」的处理管线。WebRTC 提供的标准入口是 MediaStreamTrack Insertable Streams(MediaStreamTrackProcessor + MediaStreamTrackGenerator),把 MediaStreamTrack 转成 ReadableStream<VideoFrame> 与 WritableStream<VideoFrame>,这样就可以在 Worker 或主线程对 VideoFrame 做任意处理。
1.2 为什么要走 Insertable Streams 而不是 Canvas captureStream
早期实现是 <video> → <canvas> drawImage → canvas.captureStream() → addTrack,这条链路有三个硬伤:
- 时间戳丢失:
captureStream不保留原始VideoFrame.timestamp,丢给编码器后 RTP 时间戳抖动严重,B 帧或 SVC 编码的参考关系错乱。 - 同步靠 rAF:
requestAnimationFrame与摄像头帧率不同步,主线程被布局阻塞时直接掉帧或叠帧。 - 零拷贝缺失:
drawImage强制走主线程 GPU upload / readback,720p 30 FPS 在中端机型已经吃 30%+ 的主线程。
Insertable Streams 直接以 VideoFrame 为基本单位,保留 timestamp 与 duration,可在 Worker 中以 GPU 纹理零拷贝交给 WebGPU / WebGL2,是当前 Chrome / Edge 上唯一工程可控的方案。Safari 17.4+ 已支持,Firefox 仍在 nightly。
1.3 什么时候不适用
- 目标机型是 iOS 16 及以下、Firefox stable:Insertable Streams 不可用,只能退回
captureStream或放弃虚拟背景,不要硬上。 - 端到端加密(E2EE)已经占用
RTCRtpScriptTransform:注意 Insertable Streams 与 Encoded Transform 是两套不同 API,不冲突,但 CPU 预算要叠加估算。 - 仅做静态背景替换且无需精度:直接在编码层用 SVC + 简单虚化也能达到接近效果,省 50%+ CPU。
2. MediaStreamTrack Insertable Streams 实战
2.1 主链路骨架(TypeScript)
// main.ts
interface VirtualBgOptions {
mode: 'blur' | 'image' | 'transparent';
bgImage?: ImageBitmap;
blurRadius?: number; // px,默认 12
}
async function setupVirtualBackground(
inputTrack: MediaStreamVideoTrack,
options: VirtualBgOptions,
): Promise<MediaStreamVideoTrack> {
if (!('MediaStreamTrackProcessor' in window)) {
throw new Error('Insertable Streams 不可用,需降级到 canvas.captureStream');
}
const processor = new MediaStreamTrackProcessor({ track: inputTrack });
const generator = new MediaStreamTrackGenerator({ kind: 'video' });
const worker = new Worker(new URL('./bg-worker.ts', import.meta.url), {
type: 'module',
});
// 把 readable / writable 直接 transfer 给 Worker,零拷贝
worker.postMessage(
{
type: 'init',
readable: processor.readable,
writable: generator.writable,
options,
},
[processor.readable, generator.writable, options.bgImage].filter(Boolean) as Transferable[],
);
return generator;
}
为什么要 transfer:ReadableStream<VideoFrame> 与 WritableStream<VideoFrame> 是 transferable,转移到 Worker 后主线程不再持有引用,所有帧处理都在 Worker 线程,主线程只负责 UI。否则 pipeTo 在主线程会和 React/Vue 渲染抢时间片。
2.2 Worker 内部处理(含 frame drop 策略)
// bg-worker.ts
let busy = false;
let dropped = 0;
self.onmessage = async (e: MessageEvent) => {
if (e.data.type !== 'init') return;
const { readable, writable, options } = e.data as {
readable: ReadableStream<VideoFrame>;
writable: WritableStream<VideoFrame>;
options: VirtualBgOptions;
};
const segmenter = await loadSegmenter(); // MediaPipe / ONNX 见 §3
const compositor = await createCompositor(options); // WebGPU 合成器见 §4
const reader = readable.getReader();
const writer = writable.getWriter();
while (true) {
const { value: frame, done } = await reader.read();
if (done) break;
if (!frame) continue;
// frame drop:上一帧未处理完则丢当前帧,避免延迟累积
if (busy) {
frame.close();
dropped++;
continue;
}
busy = true;
try {
const mask = await segmenter.segment(frame); // 返回 GPUTexture 或 ImageBitmap
const out = await compositor.composite(frame, mask, frame.timestamp);
frame.close();
await writer.write(out);
} catch (err) {
frame.close();
console.warn('[bg-worker] frame error', err);
} finally {
busy = false;
}
}
};
关键点:
- 必须
frame.close():VideoFrame持有 GPU 资源,不释放会在 5~10 秒内 OOM 崩溃。 - frame drop 而非 frame queue:实时通信优先低延迟,丢帧比累积延迟好。累积 200 ms+ 后听感和口型对不上,体验崩塌。
- 不要
await阻塞 reader:示例里用busy标志而不是串行await,是因为串行会让 reader 内部 buffer 涨到 OOM。
2.3 与编码器 keyframe 对齐
WebRTC 编码器在带宽抖动或丢包时会请求 keyframe(PLI / FIR)。如果你在虚拟背景链路里替换了 VideoFrame,编码器看到的是合成帧,keyframe 节奏由编码器自行决定,不需要你手动插帧。但要注意:
- 若 frame drop 率超过 30%,编码器会把丢帧当成场景突变,频繁触发 keyframe,码率瞬间打满。对策:drop 时也写一个「上一帧重复」的
VideoFrame(同 timestamp 递增),让编码器看到连续帧。 - 用
RTCRtpSender.getParameters().encodings[i].maxBitrate给上行限速,避免 keyframe burst。
2.4 失败时的样子
- 黑屏 / 卡画面:通常是没
frame.close()或 Worker 抛异常未 catch,链路 stall。看chrome://webrtc-internals的framesEncoded是否还在涨。 - 音画不同步:frame drop 后 timestamp 错位。务必把
out的 timestamp 设成原始frame.timestamp,而不是performance.now()。 - 首帧 2 秒+:模型加载阻塞了第一帧。对策:模型加载期间直接透传原始
frame(不做处理),加载完成再切换。
3. 模型选型与运行时对比
3.1 三大运行时横评
| 运行时 | 模型格式 | 后端 | 包大小 | 1080p 延迟 (M2) | 1080p 延迟 (中端 Android) | 工程成熟度 |
|---|---|---|---|---|---|---|
| MediaPipe Tasks (Web) | .tflite | WASM SIMD / WebGL / WebGPU | 1.2 MB(runtime)+ 250 KB(selfie_segmenter landscape) | 6 ms | 22 ms | 高,Google 官方维护,文档全 |
| ONNX Runtime Web | .onnx | WASM SIMD / WebGL / WebGPU (1.17+) | 9 MB(all backends)/ 2 MB(仅 wasm-simd-threaded) | 8~12 ms | 35~55 ms | 中,模型生态最广,调优空间大 |
| TensorFlow.js | .json + .bin | WASM / WebGL / WebGPU | 1.5 MB(core)+ 后端 | 10~18 ms | 50~80 ms | 中,BodyPix/SelfieSeg 老牌,新模型迭代慢 |
测试条件:1080p 输入、Selfie Segmentation landscape 模型、单帧推理(不含合成)。
结论:
- MediaPipe Tasks 是当前最稳的默认选项:包体小、首帧快、Google 自家模型质量高,落地阻力最小。
- ONNX Runtime Web 适合自定义模型:要跑 RVM / MODNet / Real-ESRGAN 等社区模型,ONNX 是唯一路径。WebGPU EP 在 ORT 1.17+ 才稳定。
- TensorFlow.js 不再推荐做新项目:维护节奏慢,WebGPU backend 仍是 alpha,包体大。仅用于已有 tfjs 模型的存量项目。
3.2 分割 / 抠图模型对比
| 模型 | 任务 | 大小 | 输入分辨率 | M2 推理 | 中端 Android 推理 | 头发 / 边缘质量 | 适用场景 |
|---|---|---|---|---|---|---|---|
| MediaPipe Selfie Segmenter (general) | 二值分割 | 250 KB (tflite) | 256×256 | 4 ms | 18 ms | 中,发丝糊 | 实时会议、低端机 |
| MediaPipe Selfie Segmenter (landscape) | 二值分割 | 250 KB | 144×256 | 3 ms | 12 ms | 中下 | 16:9 横屏会议 |
| MediaPipe Image Segmenter (Multiclass) | 多类(人/背景/天空) | 1.5 MB | 257×257 | 8 ms | 32 ms | 中 | 需要类别区分 |
| BiSeNet v2 (人像) | 语义分割 | 12 MB (onnx) | 512×512 | 18 ms | 不推荐 | 中上 | PC 端高质量 |
| MODNet | matting(带 alpha) | 25 MB (onnx fp32) / 7 MB (fp16) | 512×512 | 25 ms | 不推荐 | 高 | 录播、虚拟主播 |
| Robust Video Matting (RVM) | 视频 matting(recurrent) | 14 MB (mobilenetv3) / 50 MB (resnet50) | 任意,建议 480×270 内推 | 12 ms (mbv3) | 45 ms (mbv3) | 高,时序稳定 | 虚拟主播、绿幕替代 |
判断标准:
- 实时通信 30 FPS 1080p 上行,预算 8~10 ms / 帧给分割。MediaPipe Selfie Segmenter 几乎是唯一全机型可用的选项。
- 录播或单人主播场景,画质优先,RVM mobilenetv3 是性价比最高的,时序一致性远好于逐帧 MODNet(MODNet 在头发边缘逐帧抖动)。
- BiSeNet 不推荐做实时,更适合服务端预处理。
3.3 超分模型对比
| 模型 | 倍数 | 大小 | 输入 → 输出 | M2 GPU 推理 | 视觉质量 | 适用 |
|---|---|---|---|---|---|---|
| ESPCN | 2x / 3x / 4x | 60 KB (onnx) | 540p → 1080p | 3 ms | 中(锐化为主) | 弱机型保底 |
| FSRCNN | 2x / 3x | 90 KB | 540p → 1080p | 4 ms | 中 | 替代 ESPCN |
| Real-ESRGAN-general-x4-v3 | 4x | 4.7 MB | 270p → 1080p | 35 ms | 高(GAN 质感) | 离线 / 录播 |
| Real-ESRGAN-anime / tiny | 4x | 1.5 MB | 360p → 1440p | 12 ms | 中上 | 卡通 / 录屏 |
| RealSR | 2x | 4 MB | 540p → 1080p | 15 ms | 高 | 摄像头实拍 |
| 自研轻量(MobileSR) | 2x | <500 KB | 540p → 1080p | 4~6 ms | 中 | 移动端实时 |
结论:
- 实时 WebRTC 接收端超分,预算极紧(要在 16 ms 内完成解码 + 超分 + 渲染),ESPCN / FSRCNN / 自研 MobileSR 是唯一可选项,目标是「锐化 + 抗压缩块效应」,不是真正补细节。
- Real-ESRGAN 系只能用在录播或非实时场景,或者用作云端推理。在客户端跑 Real-ESRGAN-tiny 也要 12 ms,叠加分割就超预算。
4. WebGPU 落地:WGSL Compute Shader
4.1 为什么要 WebGPU 而不是 WebGL2
| 维度 | WebGL2 | WebGPU |
|---|---|---|
| Compute Shader | 不支持,靠 fragment shader 模拟 | 原生 compute pipeline |
| 线程组 / barrier | 无 | 有,可写共享内存做 separable blur |
| 与 ML 框架互操作 | 不便,需 readback | ORT/MediaPipe 的 GPUTexture 直通 |
| 浏览器支持 | 全平台 | Chrome 113+、Edge、Safari 17.4+,Android Chrome 121+ |
| 调试工具 | 成熟 | 仍在完善 |
判断:2026 年起,做新项目直接走 WebGPU + WebGL2 兜底。WebGL2 已不适合做 separable Gaussian blur 这类计算密集型操作。
4.2 高斯模糊 compute shader(WGSL,分离卷积)
// gaussian_blur_h.wgsl,水平方向 1D 高斯卷积
@group(0) @binding(0) var srcTex: texture_2d<f32>;
@group(0) @binding(1) var dstTex: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(2) var<uniform> params: BlurParams;
struct BlurParams {
radius: u32,
sigma: f32,
width: u32,
height: u32,
};
const MAX_RADIUS: u32 = 32u;
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
if (gid.x >= params.width || gid.y >= params.height) {
return;
}
let r = i32(params.radius);
let sigma = params.sigma;
let twoSigmaSq = 2.0 * sigma * sigma;
var sum: vec4<f32> = vec4<f32>(0.0);
var weightSum: f32 = 0.0;
for (var i: i32 = -r; i <= r; i = i + 1) {
let x = clamp(i32(gid.x) + i, 0, i32(params.width) - 1);
let w = exp(-f32(i * i) / twoSigmaSq);
let c = textureLoad(srcTex, vec2<i32>(x, i32(gid.y)), 0);
sum = sum + c * w;
weightSum = weightSum + w;
}
textureStore(dstTex, vec2<i32>(gid.xy), sum / weightSum);
}
垂直方向写一份 gaussian_blur_v.wgsl,把 srcTex 作为水平 pass 的输出,分离卷积把复杂度从 O(r²) 降到 O(r)。半径 12 的 1080p 高斯模糊在 M2 上 1.2 ms,中端 Android (Adreno 730) 上 4 ms。
4.3 合成 shader:mask + 原图 + 背景
@group(0) @binding(0) var srcTex: texture_2d<f32>; // 原始相机帧
@group(0) @binding(1) var bgTex: texture_2d<f32>; // 模糊后背景或自定义图
@group(0) @binding(2) var maskTex: texture_2d<f32>; // 分割 mask,单通道
@group(0) @binding(3) var dstTex: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(4) var samp: sampler;
@group(0) @binding(5) var<uniform> p: CompositeParams;
struct CompositeParams {
width: u32,
height: u32,
featherPx: f32, // 边缘羽化像素,建议 1.5~3
};
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
if (gid.x >= p.width || gid.y >= p.height) { return; }
let uv = vec2<f32>(f32(gid.x) / f32(p.width), f32(gid.y) / f32(p.height));
let src = textureSampleLevel(srcTex, samp, uv, 0.0);
let bg = textureSampleLevel(bgTex, samp, uv, 0.0);
var m = textureSampleLevel(maskTex, samp, uv, 0.0).r;
// 羽化:对 mask 做 smoothstep,缓解锯齿
m = smoothstep(0.5 - p.featherPx * 0.01, 0.5 + p.featherPx * 0.01, m);
let outColor = mix(bg, src, m);
textureStore(dstTex, vec2<i32>(gid.xy), outColor);
}
4.4 WGSL compute vs WASM SIMD 实测
1080p Gaussian blur(半径 12)+ mask 合成:
| 平台 | WASM SIMD(线程化) | WebGPU compute | 提升 |
|---|---|---|---|
| M2 Mac | 8.5 ms | 1.6 ms | 5.3x |
| 中端 Android (Adreno 730) | 28 ms | 5.2 ms | 5.4x |
| 低端 PC (Intel UHD 620) | 35 ms | 12 ms | 2.9x |
| iPhone 12 (Safari 17.4) | 14 ms | 3.8 ms | 3.7x |
结论:WebGPU 在所有现代平台上都有 3~5 倍提升,且把 GPU 占用从 fragment shader 的「全屏 quad pingpong」降到 compute 的精确线程组,发热和功耗显著低于 WebGL2 模拟方案。
4.5 失败时的样子
- WebGPU adapter 拿不到:在企业内网受 GPU 沙箱策略影响时常见。必须有 WebGL2 / WASM SIMD 兜底分支,不能让虚拟背景整体崩。
- GPU 进程 crash:长时间运行后 Chrome 偶发 GPU process restart,所有 GPUTexture 失效。需要监听
device.lost事件,重建 device 与 pipeline。 - iOS 17 早期版本 WGSL 编译报错:iOS 17.0~17.3 的 WebGPU WGSL 编译器对
textureLoad的 mip level 参数很敏感,建议显式传0。
5. 超分链路:上行降采样 + 下行恢复
5.1 为什么要做
会议场景多人入会时,发送端上行带宽是稀缺资源。把摄像头从 1080p 降到 540p 上行,码率从 2.5 Mbps 降到 0.9 Mbps,节省 60%+ 上行带宽;接收端用 ESPCN 2x 把 540p 还原到 1080p,主观质量介于「原生 720p」和「原生 1080p」之间,远好于直接观看 540p 拉伸。
5.2 链路设计
关键决策:
- 发送端降采样必须在编码前,让编码器拿到的就是 540p。如果在编码后做,码率根本省不下来。
- 接收端超分必须在解码后。在解码前对编码流做 SR 是错的:H.264/VP9 比特流不能在客户端 SR。
- 超分倍数选 2x 而不是 4x:4x 模型大、延迟高,且 540p → 2160p 大多数场景显示不出来(用户屏幕没那么大)。
5.3 发送端降采样代码
async function downscaleFrame(
frame: VideoFrame,
targetW: number,
targetH: number,
): Promise<VideoFrame> {
// 优先用 VideoFrame.copyTo + 自定义 layout,零拷贝最快
// 退化方案:drawImage 到 OffscreenCanvas
const canvas = new OffscreenCanvas(targetW, targetH);
const ctx = canvas.getContext('bitmaprenderer') ??
canvas.getContext('2d')!;
if ('transferFromImageBitmap' in ctx) {
const bmp = await createImageBitmap(frame, {
resizeWidth: targetW,
resizeHeight: targetH,
resizeQuality: 'high',
});
(ctx as ImageBitmapRenderingContext).transferFromImageBitmap(bmp);
} else {
(ctx as OffscreenCanvasRenderingContext2D).drawImage(frame, 0, 0, targetW, targetH);
}
return new VideoFrame(canvas, {
timestamp: frame.timestamp,
duration: frame.duration ?? undefined,
});
}
注意:createImageBitmap 的 resizeQuality: 'high' 在 Chrome 上走 GPU,但在 Safari 仍是 CPU。Safari 上建议直接走 WebGPU compute 做 bilinear / Lanczos 缩放。
5.4 接收端超分(ONNX Runtime Web + WebGPU)
import * as ort from 'onnxruntime-web/webgpu';
let sr: ort.InferenceSession | null = null;
async function loadESPCN() {
ort.env.wasm.numThreads = 4;
sr = await ort.InferenceSession.create('/models/espcn_x2.onnx', {
executionProviders: ['webgpu', 'wasm'], // webgpu 失败自动 fallback
graphOptimizationLevel: 'all',
});
}
async function superResolve(frame: VideoFrame): Promise<VideoFrame> {
if (!sr) return frame;
const w = frame.codedWidth;
const h = frame.codedHeight;
// 用 GPUBuffer 直通(ORT 1.17+ 支持 ml-tensor + GPUTexture 互操作)
const tensor = await ort.Tensor.fromImage(frame, {
format: 'YCbCr', // ESPCN 仅在 Y 通道做超分
tensorFormat: 'NCHW',
dataType: 'float32',
});
const output = await sr.run({ input: tensor });
const outTensor = output.output as ort.Tensor;
// outTensor 已是 2x 尺寸,转回 VideoFrame
const bitmap = await tensorToImageBitmap(outTensor, w * 2, h * 2);
return new VideoFrame(bitmap, { timestamp: frame.timestamp });
}
ESPCN 只对 Y 通道超分,UV 双线性放大:这是论文原始做法,也是带宽 / 质量平衡最优解。色度信息在 H.264/VP9 已经是 4:2:0,超分对色度收益极低。
5.5 性能与失败
- 首帧延迟:ESPCN 60 KB 模型加载 < 100 ms,编译 + warmup ~150 ms,首帧总延迟 ~250 ms 内可控。
- 帧率不足时:监测
outFrame实际 FPS < 24 时自动关闭超分,直接渲染原始 540p 拉伸。 - 失败信号:超分输出出现棋盘格 artifact(model checkerboard),多半是 deconv 模型选错;ESPCN 用 sub-pixel conv,正确实现不会有棋盘格。
6. 性能预算
6.1 30 FPS 1080p 各方案占用
每帧预算 33.3 ms,扣掉浏览器合成与编码 8 ms,留给虚拟背景 + 超分约 25 ms。
| 设备 | 方案 | 分割 | Blur | 合成 | 超分 | 编码 | 总计 / 33.3 ms | CPU% | GPU% | 是否可行 |
|---|---|---|---|---|---|---|---|---|---|---|
| M2 Mac | MediaPipe + WebGPU blur | 4 ms | 1.2 ms | 0.8 ms | - | 4 ms | 10 ms | 18% | 22% | 是 |
| M2 Mac | MediaPipe + WebGPU + ESPCN 2x | 4 | 1.2 | 0.8 | 3 | 4 | 13 ms | 22% | 35% | 是 |
| M2 Mac | RVM mbv3 + WebGPU + ESPCN | 12 | 1.2 | 0.8 | 3 | 4 | 21 ms | 30% | 55% | 是 |
| 中端 Android (SD 8 Gen1) | MediaPipe + WebGPU blur | 18 | 4 | 2 | - | 6 | 30 ms | 35% | 60% | 勉强 |
| 中端 Android | MediaPipe + WebGPU + ESPCN 2x | 18 | 4 | 2 | 8 | 6 | 38 ms | 50% | 80% | 不可行,需降到 720p |
| 中端 Android | MediaPipe + WebGPU blur,720p | 9 | 2 | 1 | - | 4 | 16 ms | 22% | 40% | 是 |
| 低端 PC (Intel UHD 620) | MediaPipe + WASM SIMD blur | 22 | 35 | 8 | - | 8 | 73 ms | 95% | 30% | 不可行 |
| 低端 PC | MediaPipe + WebGL2 blur | 22 | 14 | 5 | - | 8 | 49 ms | 60% | 70% | 不可行,需降到 540p |
| 低端 PC | MediaPipe + WebGL2,540p | 8 | 5 | 2 | - | 4 | 19 ms | 35% | 55% | 是 |
6.2 降级策略
按优先级从高到低自动降级:
- 目标 1080p 30 FPS + 完整虚拟背景 + 超分。
- 帧间隔超过 40 ms 持续 2 秒 → 关闭超分。
- 帧间隔仍超 → 分辨率降到 720p。
- 仍超 → WebGPU blur 半径从 12 降到 6。
- 仍超 → 降到 540p,关闭 mask 羽化。
- 仍超 → 关闭虚拟背景,仅保留原始视频。
实测电池供电的 MacBook 在 powerSave 模式下 GPU 频率被限制到 50%,必须监听 navigator.getBattery() 与 MediaStreamTrack.getStats() 的 framesPerSecond,主动降级。
6.3 内存与 GC
VideoFrame 持有 GPU 资源,未 close 会让 V8 在 30 秒内 OOM。压测:1080p 30 FPS 持续 1 小时,正确实现下 JS Heap 稳定在 80120 MB,GPU 内存 200300 MB。任何泄漏(漏 close 一个 frame)都会以 ~100 MB / 分钟速度涨。
7. 反模式
| 反模式 | 现象 | 正确做法 |
|---|---|---|
| 主线程跑模型 | UI 卡顿、滚动掉帧、React commit 阻塞 | Worker + Insertable Streams transfer |
| 不做 frame drop | 延迟从 30 ms 涨到 500 ms+,音画不同步 | busy 标志立即丢帧,不入队 |
用 canvas.captureStream 做新项目 | 时间戳错乱、码率不稳、CPU 高 | Insertable Streams(MediaStreamTrackProcessor / Generator) |
漏 frame.close() | 5~10 秒内 GPU 内存 OOM 崩溃 | 所有分支必走 close,含 try/catch |
| 超分模型 > 5 MB 直接上 | 首帧 2 秒+,弱网用户白屏 | 选 ESPCN / FSRCNN,模型 < 500 KB |
| 用 fragment shader 模拟 compute | 1080p blur 14 ms,GPU 占用 70%+ | WebGPU compute pipeline + workgroup barrier |
| 不做掉电降级 | MacBook 拔电后 GPU 降频,链路立刻爆延迟 | 监听 battery + framesPerSecond,自动关超分 |
| 头发边缘不羽化 | 锯齿严重,肉眼可见马赛克 | mask 出来后做 smoothstep + 半径 1~2 的小高斯 |
| 在 RTCRtpScriptTransform 里跑模型 | 阻塞编码线程,码率震荡 | encoded transform 仅用于 E2EE,AI 走 raw insertable |
| 让用户上传任意背景图 | XSS / 违规图、内存爆炸 | 先 canvas decode 限尺寸 4K 内,再压成 ImageBitmap |
| 把超分用在编码前 | 上行码率不降反升,毫无意义 | 超分只在接收端解码后做 |
| 模型 warmup 用真实第一帧 | 用户看到首帧延迟 2 秒+ | 启动时用 dummy tensor warmup,期间透传原始帧 |
8. 落地清单
实施顺序建议:
- 先用 MediaPipe Selfie Segmenter + WebGL2 blur 跑通最小链路,确认 Insertable Streams 在目标浏览器矩阵全部可用。
- 加 Worker 隔离与 frame drop,跑 1 小时压测,确认 JS Heap 与 GPU memory 稳定。
- 用 WebGPU compute shader 替换 blur,做 A/B 对比,确认中低端 Android 与 Intel 集显机型的提升。
- 接入降级策略,覆盖电池、热降频、网络抖动三类触发条件。
- 选定一个目标场景(多人会议 / 主播 / 教育)再决定是否上超分。超分不是默认必备,是带宽紧张时的额外手段。
- 灰度发布前必做:弱光、强逆光、深肤色、长发、戴眼镜五个固定测试 case,覆盖头发抠图穿帮与边缘锯齿。
9. 权威资料
- W3C MediaStreamTrack Insertable Streams of Media — https://w3c.github.io/mediacapture-transform/
- W3C WebCodecs (
VideoFrame) — https://w3c.github.io/webcodecs/ - W3C WebGPU — https://www.w3.org/TR/webgpu/
- W3C WGSL — https://www.w3.org/TR/WGSL/
- MediaPipe Solutions Tasks (Web) — https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter/web_js
- MediaPipe Selfie Segmentation 模型卡 — https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter#selfie_model
- ONNX Runtime Web — https://onnxruntime.ai/docs/tutorials/web/
- ONNX Runtime WebGPU EP — https://onnxruntime.ai/docs/execution-providers/WebGPU-ExecutionProvider.html
- TensorFlow.js — https://www.tensorflow.org/js
- Robust Video Matting (RVM) — Lin et al., WACV 2022, https://peterl1n.github.io/RobustVideoMatting/
- MODNet — Ke et al., AAAI 2022, https://github.com/ZHKKKe/MODNet
- BiSeNet v2 — Yu et al., IJCV 2021, https://arxiv.org/abs/2004.02147
- ESPCN — Shi et al., CVPR 2016, https://arxiv.org/abs/1609.05158
- Real-ESRGAN — Wang et al., ICCV Workshops 2021, https://github.com/xinntao/Real-ESRGAN
- Chrome Origin Trial / Status: Insertable Streams for MediaStreamTrack — https://chromestatus.com/feature/5499415634640896
- WebRTC Samples: Insertable Streams — https://webrtc.github.io/samples/src/content/insertable-streams/
核对日期:2026-06-22