第十三章:AI 视频场景专题(终章)

这是本知识库的最终章。前十二章系统覆盖了 Web 视频开发从基础到工程化的完整技术栈,而这一章将视角推向前沿——AI 与视频的深度融合。随着 WebCodecs、WebGPU、ONNX Runtime Web 等浏览器能力的成熟,曾经只能在服务端运行的 AI 视频增强算法,正在逐步下沉到前端,成为可以实时运行在用户浏览器中的能力。


第一部分:WebCodecs——帧级视频处理的基础设施

为什么需要 WebCodecs

在 WebCodecs 出现之前,前端处理视频帧的方式极为低效:将 <video> 绘制到 <canvas>,通过 getImageData() 逐帧读取像素,再进行处理。这条路径经过了多次 CPU 拷贝,帧率通常只能达到 15~20fps,完全无法满足实时 AI 推理的需求。

WebCodecs 提供了对浏览器内置编解码器的直接底层访问能力,绕过了高层 Media 抽象,让开发者可以直接操作 VideoFrame 对象——这是一个可以直接传入 WebGL/WebGPU 纹理的原始帧数据,无需经过 CPU 拷贝。developer.chrome.com zhuanlan.zhihu.com

传统方案(低效):
video → canvas.drawImage → getImageData → Uint8Array → CPU处理 → putImageData

WebCodecs 方案(高效):
EncodedVideoChunk → VideoDecoder → VideoFrame → WebGL/WebGPU 纹理 → GPU处理

核心 API:VideoDecoder / VideoEncoder / VideoFrame

WebCodecs 的三个核心类构成了帧级处理的基础:

// 检测支持
if (!('VideoDecoder' in window)) {
  console.warn('当前浏览器不支持 WebCodecs');
}
 
// ① VideoDecoder:将编码数据解码为 VideoFrame
const decoder = new VideoDecoder({
  // 解码成功回调,output 是 VideoFrame
  output(frame) {
    // frame 包含原始像素数据,可直接用于 WebGL/WebGPU
    processFrame(frame);
 
    // 重要:用完必须手动释放,否则内存泄漏
    frame.close();
  },
  error(e) {
    console.error('解码错误:', e);
  },
});
 
// 配置解码器(必须在 decode 之前调用)
await decoder.configure({
  codec:       'avc1.42E01E', // H.264 Baseline Profile
  codedWidth:  1920,
  codedHeight: 1080,
  // 可选:硬件加速偏好
  hardwareAcceleration: 'prefer-hardware',
});
 
// 送入编码数据
const chunk = new EncodedVideoChunk({
  type:      'key',            // 'key' | 'delta'
  timestamp:  0,               // 微秒
  duration:   33333,           // 微秒(约 30fps)
  data:       encodedData,     // ArrayBuffer
});
decoder.decode(chunk);
 
// ② VideoFrame:从 Canvas / ImageBitmap / 视频元素创建
const video  = document.querySelector('video');
const frame  = new VideoFrame(video, { timestamp: video.currentTime * 1e6 });
 
// 从 Canvas 创建
const canvas = document.createElement('canvas');
const frame2 = new VideoFrame(canvas, { timestamp: 0 });
 
// 读取原始像素数据(RGBA 格式)
const buffer = new Uint8Array(frame.allocationSize({ format: 'RGBA' }));
await frame.copyTo(buffer, { format: 'RGBA' });
// 此时 buffer 包含完整的帧像素数据,可传入 AI 模型
 
// ③ VideoEncoder:将 VideoFrame 编码为压缩数据
const encoder = new VideoEncoder({
  output(chunk, metadata) {
    // chunk 是编码后的数据,可以写入文件或推流
    muxer.addVideoChunk(chunk, metadata);
  },
  error(e) { console.error(e); },
});
 
encoder.configure({
  codec:        'avc1.4d0034', // H.264 High Profile
  width:        1920,
  height:       1080,
  bitrate:      5_000_000,     // 5Mbps
  framerate:    30,
  hardwareAcceleration: 'prefer-hardware',
});
 
// 编码一帧
encoder.encode(frame, { keyFrame: false });
await encoder.flush(); // 等待所有帧编码完成

高性能处理流水线:Worker + OffscreenCanvas

AI 推理是计算密集型任务,必须放在 Worker 中执行,避免阻塞主线程。结合 OffscreenCanvas 可以在 Worker 内完成完整的解码→推理→渲染流水线:cloud.baidu.com

// main.js:主线程
const worker  = new Worker('video-worker.js', { type: 'module' });
const canvas  = document.getElementById('output-canvas');
const offscreen = canvas.transferControlToOffscreen(); // 将 canvas 控制权转移给 Worker
 
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
 
// 将视频流发送给 Worker 处理
async function streamToWorker(videoUrl) {
  const response = await fetch(videoUrl);
  const reader   = response.body.getReader();
 
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    worker.postMessage({ type: 'chunk', data: value }, [value.buffer]);
  }
}
// video-worker.js:Worker 线程(完整处理流水线)
import * as ort from 'onnxruntime-web';
 
let canvas, ctx, session;
 
// 生产者-消费者模式:解码队列
const frameQueue  = [];
let   isProcessing = false;
 
self.onmessage = async ({ data }) => {
  if (data.type === 'init') {
    canvas = data.canvas;
    ctx    = canvas.getContext('2d');
 
    // 初始化 ONNX Runtime(WebGPU 后端)
    session = await ort.InferenceSession.create('/models/sr_model.onnx', {
      executionProviders: ['webgpu', 'wasm'],
    });
  }
 
  if (data.type === 'chunk') {
    // 解码视频块并加入队列
    decoder.decode(new EncodedVideoChunk({
      type:      'delta',
      timestamp:  frameCount++ * 33333,
      data:       data.data,
    }));
  }
};
 
// VideoDecoder 在 Worker 中初始化
const decoder = new VideoDecoder({
  output(frame) {
    frameQueue.push(frame);
    if (!isProcessing) processNext();
  },
  error: console.error,
});
 
// 串行处理帧(防止 AI 推理积压)
async function processNext() {
  if (frameQueue.length === 0) {
    isProcessing = false;
    return;
  }
 
  isProcessing = true;
  const frame  = frameQueue.shift();
 
  try {
    // 将 VideoFrame 绘制到 OffscreenCanvas 提取像素
    ctx.drawImage(frame, 0, 0);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
 
    // 送入 AI 模型推理
    const enhanced = await runAIInference(imageData);
 
    // 将增强后的帧写回 canvas(用户可见)
    ctx.putImageData(enhanced, 0, 0);
  } finally {
    frame.close(); // 必须释放
    processNext(); // 处理下一帧
  }
}

第二部分:AI 推理引擎接入

onnxruntime-web:工业级推理引擎

ONNX Runtime Web 是目前前端 AI 推理的首选方案,支持 WebGPU、WebGL、WASM 三种后端,可以加载标准 .onnx 格式模型,兼容 PyTorch、TensorFlow 导出的模型。juejin.cn

import * as ort from 'onnxruntime-web';
 
// 配置 WASM 文件路径(从 CDN 加载)
ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/';
 
// 后端选择策略:WebGPU > WebGL > WASM(CPU)
async function createSession(modelPath) {
  const backends = ['webgpu', 'webgl', 'wasm'];
 
  for (const backend of backends) {
    try {
      const session = await ort.InferenceSession.create(modelPath, {
        executionProviders: [backend],
        // 模型优化选项
        graphOptimizationLevel: 'all',
        // WebGPU 专属配置
        ...(backend === 'webgpu' && {
          preferredOutputLocation: 'gpu-buffer', // 输出保留在 GPU,避免回读
        }),
      });
      console.log(`使用后端: `);
      return session;
    } catch (e) {
      console.warn(` 后端不可用,尝试下一个`);
    }
  }
  throw new Error('所有推理后端均不可用');
}
 
// 推理调用
async function runInference(session, inputTensor) {
  const feeds = { input: inputTensor };
  const results = await session.run(feeds);
  return results.output;
}

三种后端的性能差异显著,选择时需要权衡兼容性与速度:

后端加速方式适用场景兼容性
WebGPUGPU 计算着色器高性能推理,现代浏览器Chrome 113+,Edge 113+
WebGLGPU 着色器(OpenGL ES)中等性能,广泛兼容主流浏览器均支持
WASMCPU SIMD 指令集兜底方案,最广兼容所有支持 WASM 的浏览器

模型格式转换:从训练到部署

AI 模型从训练框架到前端部署,需要经过标准化转换流程:

# PyTorch 模型导出为 ONNX
python -c "
import torch
import torch.onnx
 
model = load_your_model()
model.eval()
 
# 创建示例输入(决定 ONNX 模型的输入形状)
dummy_input = torch.randn(1, 3, 256, 256)  # batch=1, C=3, H=256, W=256
 
torch.onnx.export(
    model,
    dummy_input,
    'model.onnx',
    opset_version=17,
    input_names=['input'],
    output_names=['output'],
    # 动态轴(允许输入尺寸变化)
    dynamic_axes={
        'input':  {0: 'batch', 2: 'height', 3: 'width'},
        'output': {0: 'batch', 2: 'height', 3: 'width'},
    }
)
"
 
# 使用 onnxsim 优化模型(减少算子,提升推理速度)
pip install onnxsim
onnxsim model.onnx model_optimized.onnx
 
# 量化为 INT8(模型体积减少 75%,速度提升 2-4x,精度略有损失)
python -c "
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic('model_optimized.onnx', 'model_int8.onnx', weight_type=QuantType.QInt8)
"

第三部分:AI 超分辨率——让低清视频变高清

原理:SRCNN 到 Real-ESRGAN

视频超分辨率(Video Super-Resolution,VSR)的目标是从低分辨率帧重建高分辨率帧。从早期的 SRCNN(2014,三层卷积网络)到 Real-ESRGAN(2021,腾讯 ARC 实验室),模型能力大幅提升。Real-ESRGAN 使用高阶退化过程模拟真实世界的复杂退化(模糊+噪声+JPEG 压缩+下采样叠加),训练出的模型对老旧视频、压缩视频的修复效果远超传统双三次插值。devpress.csdn.net

前端部署超分模型面临的核心挑战是模型体积与推理速度的平衡。完整的 Real-ESRGAN x4 模型约 64MB,在 CPU 上单帧推理需要数秒,远不能满足实时需求。实际工程中通常采用以下策略:

  • 使用轻量化变体(如 RealESRGAN-x2plus,参数量减少 60%)
  • 对视频帧进行分块推理(Tile Inference),避免显存溢出
  • 降低推理频率(每隔 N 帧推理一次,中间帧用双线性插值)
  • 使用 INT8 量化模型

分块推理实现

超分模型对大分辨率帧直接推理会导致显存溢出,分块(Tile)策略是标准解法:

// 超分辨率推理器(支持分块处理)
class SuperResolutionProcessor {
  constructor(session, scale = 2) {
    this.session   = session;
    this.scale     = scale;
    this.tileSize  = 128;   // 每块 128x128 像素
    this.tileOverlap = 16;  // 块间重叠像素(避免边缘伪影)
  }
 
  async process(videoFrame) {
    const { codedWidth: w, codedHeight: h } = videoFrame;
 
    // 提取帧像素数据
    const inputBuffer = new Uint8Array(w * h * 4); // RGBA
    await videoFrame.copyTo(inputBuffer, { format: 'RGBA' });
 
    // 转换为 Float32 并归一化到 [0, 1]
    const inputFloat = new Float32Array(3 * h * w); // CHW 格式
    for (let i = 0; i < h * w; i++) {
      inputFloat[i]           = inputBuffer[i * 4]     / 255; // R
      inputFloat[h * w + i]   = inputBuffer[i * 4 + 1] / 255; // G
      inputFloat[2 * h * w + i] = inputBuffer[i * 4 + 2] / 255; // B
    }
 
    // 分块推理
    const outputFloat = await this._tileInference(inputFloat, w, h);
 
    // 转换回 Uint8 RGBA
    const outputW = w * this.scale;
    const outputH = h * this.scale;
    const outputBuffer = new Uint8ClampedArray(outputW * outputH * 4);
 
    for (let i = 0; i < outputH * outputW; i++) {
      outputBuffer[i * 4]     = Math.round(outputFloat[i] * 255);
      outputBuffer[i * 4 + 1] = Math.round(outputFloat[outputH * outputW + i] * 255);
      outputBuffer[i * 4 + 2] = Math.round(outputFloat[2 * outputH * outputW + i] * 255);
      outputBuffer[i * 4 + 3] = 255; // Alpha
    }
 
    return new ImageData(outputBuffer, outputW, outputH);
  }
 
  async _tileInference(input, w, h) {
    const step    = this.tileSize - this.tileOverlap;
    const outputW = w * this.scale;
    const outputH = h * this.scale;
    const output  = new Float32Array(3 * outputH * outputW);
 
    for (let ty = 0; ty < h; ty += step) {
      for (let tx = 0; tx < w; tx += step) {
        // 裁剪 tile(含重叠区域)
        const tileW = Math.min(this.tileSize, w - tx);
        const tileH = Math.min(this.tileSize, h - ty);
 
        const tileTensor = this._cropTile(input, w, h, tx, ty, tileW, tileH);
 
        // 推理
        const inputTensor = new ort.Tensor('float32', tileTensor, [1, 3, tileH, tileW]);
        const result      = await this.session.run({ input: inputTensor });
        const outputTile  = result.output.data;
 
        // 将推理结果写回输出(去掉重叠边缘)
        const validX = tx === 0 ? 0 : this.tileOverlap / 2;
        const validY = ty === 0 ? 0 : this.tileOverlap / 2;
        this._mergeTile(output, outputTile, outputW, outputH,
          tx * this.scale, ty * this.scale,
          tileW * this.scale, tileH * this.scale,
          validX * this.scale, validY * this.scale);
      }
    }
 
    return output;
  }
 
  _cropTile(input, w, h, tx, ty, tileW, tileH) {
    const tile = new Float32Array(3 * tileH * tileW);
    for (let c = 0; c < 3; c++) {
      for (let y = 0; y < tileH; y++) {
        for (let x = 0; x < tileW; x++) {
          tile[c * tileH * tileW + y * tileW + x] =
            input[c * h * w + (ty + y) * w + (tx + x)];
        }
      }
    }
    return tile;
  }
 
  _mergeTile(output, tile, outW, outH, ox, oy, tileW, tileH, skipX, skipY) {
    for (let c = 0; c < 3; c++) {
      for (let y = skipY; y < tileH; y++) {
        for (let x = skipX; x < tileW; x++) {
          const gy = oy + y;
          const gx = ox + x;
          if (gy < outH && gx < outW) {
            output[c * outH * outW + gy * outW + gx] =
              tile[c * tileH * tileW + y * tileW + x];
          }
        }
      }
    }
  }
}

第四部分:AI 智能抠图——实时背景替换

原理:语义分割 + Alpha Matting

视频抠图(Video Matting)的目标是逐帧预测前景 Alpha 蒙版,将人物从背景中分离。第十章介绍了基于 WebGL 着色器的绿幕色键抠图,但绿幕需要特定拍摄环境。AI 抠图(如 RobustVideoMatting、MediaPipe Selfie Segmentation)可以在任意背景下实时分割人像,无需绿幕。

前端实现的核心是将视频帧送入语义分割模型,获得 Alpha 蒙版后,通过 WebGL 着色器合成新背景:

// AI 实时抠图处理器
class AIMatting {
  constructor(videoEl, canvasEl) {
    this.video   = videoEl;
    this.canvas  = canvasEl;
    this.gl      = canvasEl.getContext('webgl2');
    this.session = null;
    this.rafId   = null;
 
    // 上一帧的隐藏状态(RVM 模型是循环网络,需要传递状态)
    this.recurrentState = null;
  }
 
  async init(modelPath) {
    this.session = await ort.InferenceSession.create(modelPath, {
      executionProviders: ['webgpu', 'webgl', 'wasm'],
    });
    this._initWebGL();
  }
 
  _initWebGL() {
    const gl = this.gl;
    // 顶点着色器:全屏四边形
    const vsSource = `#version 300 es
      in vec2 a_position;
      out vec2 v_texCoord;
      void main() {
        gl_Position = vec4(a_position, 0, 1);
        v_texCoord  = a_position * 0.5 + 0.5;
      }`;
 
    // 片元着色器:前景 × Alpha + 背景 × (1 - Alpha)
    const fsSource = `#version 300 es
      precision highp float;
      uniform sampler2D u_foreground;  // 原始视频帧
      uniform sampler2D u_alpha;       // AI 预测的 Alpha 蒙版
      uniform sampler2D u_background;  // 替换背景图
      in  vec2 v_texCoord;
      out vec4 fragColor;
      void main() {
        vec2  uv   = vec2(v_texCoord.x, 1.0 - v_texCoord.y);
        vec4  fg   = texture(u_foreground, uv);
        float a    = texture(u_alpha, uv).r;
        vec4  bg   = texture(u_background, uv);
        // Alpha 合成
        fragColor  = vec4(fg.rgb * a + bg.rgb * (1.0 - a), 1.0);
      }`;
 
    // 编译并链接着色器(略去 boilerplate)
    this.program = createProgram(gl, vsSource, fsSource);
  }
 
  async processFrame() {
    const { videoWidth: w, videoHeight: h } = this.video;
 
    // 从 video 元素创建 VideoFrame
    const frame = new VideoFrame(this.video, {
      timestamp: this.video.currentTime * 1e6,
    });
 
    // 提取像素并转换为模型输入格式(NCHW Float32,归一化)
    const pixelBuffer = new Uint8Array(w * h * 4);
    await frame.copyTo(pixelBuffer, { format: 'RGBA' });
    frame.close();
 
    const inputData = new Float32Array(3 * h * w);
    for (let i = 0; i < h * w; i++) {
      inputData[i]         = pixelBuffer[i * 4]     / 255;
      inputData[h * w + i] = pixelBuffer[i * 4 + 1] / 255;
      inputData[2 * h * w + i] = pixelBuffer[i * 4 + 2] / 255;
    }
 
    // 构建推理输入(RVM 需要传入上一帧的循环状态)
    const feeds = {
      src:           new ort.Tensor('float32', inputData, [1, 3, h, w]),
      downsample_ratio: new ort.Tensor('float32', [0.25], [1]),
      ...(this.recurrentState || {}), // 首帧为空,后续帧传入上一帧状态
    };
 
    const results = await this.session.run(feeds);
 
    // 保存循环状态供下一帧使用
    this.recurrentState = {
      r1i: results.r1o,
      r2i: results.r2o,
      r3i: results.r3o,
      r4i: results.r4o,
    };
 
    // 获取 Alpha 蒙版(单通道,[1, 1, H, W])
    const alphaData = results.pha.data;
 
    // 更新 WebGL 纹理并渲染合成结果
    this._renderComposite(pixelBuffer, alphaData, w, h);
  }
 
  _renderComposite(fgData, alphaData, w, h) {
    const gl = this.gl;
 
    // 更新前景纹理
    updateTexture(gl, this.fgTexture, w, h, fgData);
 
    // 将 alpha Float32 转换为 Uint8 并更新纹理
    const alphaUint8 = new Uint8Array(alphaData.length);
    for (let i = 0; i < alphaData.length; i++) {
      alphaUint8[i] = Math.round(alphaData[i] * 255);
    }
    updateTexture(gl, this.alphaTexture, w, h, alphaUint8, gl.RED);
 
    // 绘制
    gl.useProgram(this.program);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  }
 
  start(backgroundImage) {
    this._updateBackground(backgroundImage);
 
    const loop = async () => {
      if (!this.video.paused && !this.video.ended) {
        await this.processFrame();
      }
      this.rafId = requestAnimationFrame(loop);
    };
    loop();
  }
 
  stop() {
    cancelAnimationFrame(this.rafId);
    this.recurrentState = null;
  }
}
 
// 使用示例
const matting = new AIMatting(
  document.getElementById('video'),
  document.getElementById('output')
);
await matting.init('/models/rvm_mobilenetv3.onnx');
matting.start('/images/virtual-bg.jpg');

第五部分:AI 视频补帧——让 24fps 变 60fps

原理:光流估计与中间帧合成

视频补帧(Frame Interpolation)通过预测相邻帧之间的运动轨迹,生成中间帧,从而将低帧率视频提升到更高帧率。RIFE(Real-Time Intermediate Flow Estimation)是目前效果最好的实时补帧算法之一,其核心是直接估计双向光流(而非传统的单向光流),再通过 warping 合成中间帧。

在前端实现补帧,需要同时持有相邻两帧(frame_t 和 frame_t+1),将它们拼接后送入模型:

// 视频补帧处理器(基于 RIFE-lite 轻量模型)
class FrameInterpolator {
  constructor(session) {
    this.session    = session;
    this.prevFrame  = null; // 上一帧数据
    this.outputFps  = 60;
    this.inputFps   = 24;
    // 需要在每两帧之间插入的帧数
    this.insertCount = Math.round(this.outputFps / this.inputFps) - 1; // 1
  }
 
  async interpolate(currentFrameData, w, h) {
    if (!this.prevFrame) {
      this.prevFrame = currentFrameData;
      return [currentFrameData]; // 第一帧直接返回
    }
 
    const interpolatedFrames = [];
 
    // 生成中间帧(timestep 从 0 到 1 均匀分布)
    for (let i = 1; i <= this.insertCount; i++) {
      const timestep = i / (this.insertCount + 1); // e.g., 0.5 for 1 insert
 
      const midFrame = await this._runRIFE(
        this.prevFrame,
        currentFrameData,
        w, h,
        timestep
      );
      interpolatedFrames.push(midFrame);
    }
 
    interpolatedFrames.push(currentFrameData);
    this.prevFrame = currentFrameData;
 
    return interpolatedFrames;
  }
 
  async _runRIFE(frame0, frame1, w, h, timestep) {
    // 将两帧拼接为 6 通道输入(RIFE 的输入格式)
    const input = new Float32Array(6 * h * w);
 
    for (let i = 0; i < h * w; i++) {
      // frame0: 前 3 通道(RGB)
      input[i]             = frame0[i * 4]     / 255;
      input[h * w + i]     = frame0[i * 4 + 1] / 255;
      input[2 * h * w + i] = frame0[i * 4 + 2] / 255;
      // frame1: 后 3 通道(RGB)
      input[4 * h * w + i] = frame1[i * 4 + 1] / 255;
      input[5 * h * w + i] = frame1[i * 4 + 2] / 255;
    }
 
    const feeds = {
      input:    new ort.Tensor('float32', input, [1, 6, h, w]),
      timestep: new ort.Tensor('float32', [timestep], [1]),
    };
 
    const result = await this.session.run(feeds);
    const outputData = result.output.data; // Float32Array, CHW
 
    // 转换回 RGBA Uint8
    const rgba = new Uint8Array(h * w * 4);
    for (let i = 0; i < h * w; i++) {
      rgba[i * 4]     = Math.round(outputData[i] * 255);
      rgba[i * 4 + 1] = Math.round(outputData[h * w + i] * 255);
      rgba[i * 4 + 2] = Math.round(outputData[2 * h * w + i] * 255);
      rgba[i * 4 + 3] = 255;
    }
 
    return rgba;
  }
}

补帧的工程化挑战

补帧在实际落地时有几个关键问题需要处理:

场景切换检测:在镜头切换处插入补帧会产生严重的鬼影(ghosting)。需要在补帧前计算相邻帧的差异(如帧间像素均值差),超过阈值时跳过补帧,直接复制原帧:

function detectSceneChange(frame0, frame1, threshold = 30) {
  let totalDiff = 0;
  const pixelCount = frame0.length / 4;
 
  for (let i = 0; i < frame0.length; i += 4) {
    totalDiff += Math.abs(frame0[i] - frame1[i])
               + Math.abs(frame0[i + 1] - frame1[i + 1])
               + Math.abs(frame0[i + 2] - frame1[i + 2]);
  }
 
  const avgDiff = totalDiff / (pixelCount * 3);
  return avgDiff > threshold; // 超过阈值认为是场景切换
}

推理速度与帧率匹配:补帧的推理必须快于原始帧率,否则会造成播放卡顿。对于 24fps 视频,每帧处理时间预算约为 41ms(1000ms / 24fps)。在 WebGPU 后端下,RIFE-lite 在现代 GPU 上通常可在 20~35ms 内完成 720P 帧的推理,基本满足实时要求。


第六部分:完整 AI 视频增强流水线

端到端架构设计

将超分、抠图、补帧整合为一条可配置的处理流水线,是工程化的最终目标。下面展示一个基于 Worker 的模块化架构:

// AIVideoPipeline:可配置的 AI 视频增强流水线
class AIVideoPipeline {
  constructor(config = {}) {
    this.config = {
      superResolution: false,  // 是否启用超分
      matting:         false,  // 是否启用抠图
      interpolation:   false,  // 是否启用补帧
      background:      null,   // 背景替换图片(抠图模式)
      srScale:         2,      // 超分倍率
      targetFps:       60,     // 目标帧率(补帧模式)
      ...config,
    };
 
    // 每个 AI 模块运行在独立 Worker 中,互不阻塞
    this._workers = {};
    this._canvas  = null;
  }
 
  async init(outputCanvas) {
    this._canvas = outputCanvas;
 
    const workerInit = [];
 
    if (this.config.superResolution) {
      this._workers.sr = new Worker('/workers/sr-worker.js', { type: 'module' });
      workerInit.push(this._initWorker('sr', {
        modelPath: `/models/realesrgan_x${this.config.srScale}.onnx`,
        scale:     this.config.srScale,
      }));
    }
 
    if (this.config.matting) {
      this._workers.matting = new Worker('/workers/matting-worker.js', { type: 'module' });
      workerInit.push(this._initWorker('matting', {
        modelPath:  '/models/rvm_mobilenetv3.onnx',
        background: this.config.background,
      }));
    }
 
    if (this.config.interpolation) {
      this._workers.interp = new Worker('/workers/interp-worker.js', { type: 'module' });
      workerInit.push(this._initWorker('interp', {
        modelPath:  '/models/rife_lite.onnx',
        targetFps:  this.config.targetFps,
      }));
    }
 
    await Promise.all(workerInit);
    console.log('AI 流水线初始化完成');
  }
 
  _initWorker(name, config) {
    return new Promise((resolve, reject) => {
      const worker = this._workers[name];
      worker.postMessage({ type: 'init', config });
      worker.onmessage = ({ data }) => {
        if (data.type === 'ready') resolve();
        if (data.type === 'error') reject(new Error(data.message));
      };
    });
  }
 
  // 处理单帧(各模块串行执行)
  async processFrame(videoFrame) {
    let currentFrame = videoFrame;
 
    // 1. 补帧(需要在超分之前,处理原始分辨率更快)
    let frames = [currentFrame];
    if (this.config.interpolation && this._workers.interp) {
      frames = await this._sendToWorker('interp', currentFrame);
    }
 
    // 2. 对每一帧执行超分 + 抠图
    const processedFrames = [];
    for (const frame of frames) {
      let f = frame;
 
      if (this.config.superResolution && this._workers.sr) {
        f = await this._sendToWorker('sr', f);
      }
 
      if (this.config.matting && this._workers.matting) {
        f = await this._sendToWorker('matting', f);
      }
 
      processedFrames.push(f);
    }
 
    return processedFrames;
  }
 
  _sendToWorker(name, frameData) {
    return new Promise((resolve) => {
      const worker = this._workers[name];
      // Transferable 对象零拷贝传输
      worker.postMessage({ type: 'frame', data: frameData }, [frameData.buffer]);
      worker.onmessage = ({ data }) => {
        if (data.type === 'result') resolve(data.frames);
      };
    });
  }
 
  destroy() {
    Object.values(this._workers).forEach(w => w.terminate());
  }
}
 
// 使用示例
const pipeline = new AIVideoPipeline({
  superResolution: true,
  srScale:         2,
  matting:         true,
  background:      '/images/office-bg.jpg',
  interpolation:   false, // 超分 + 抠图已经很耗时,不叠加补帧
});
 
await pipeline.init(document.getElementById('output-canvas'));
 
// 接入视频帧循环
const video = document.getElementById('source-video');
video.addEventListener('play', () => {
  const loop = async () => {
    if (video.paused || video.ended) return;
 
    const frame = new VideoFrame(video, { timestamp: video.currentTime * 1e6 });
    const processedFrames = await pipeline.processFrame(frame);
    frame.close();
 
    // 将处理后的帧渲染到输出 canvas
    const ctx = outputCanvas.getContext('2d');
    for (const pf of processedFrames) {
      ctx.putImageData(pf, 0, 0);
    }
 
    requestAnimationFrame(loop);
  };
  loop();
});

第七部分:性能调优与工程实践

推理性能基准

在实际项目中,需要根据用户设备能力动态选择处理策略。以下是各场景的典型性能数据(720P,现代中端设备):

任务模型WebGPU 延迟WebGL 延迟WASM 延迟实时可行性
超分 ×2RealESRGAN-x2plus~80ms~200ms~2000msWebGPU 勉强可行
人像抠图RVM-MobileNetV3~15ms~40ms~300msWebGPU/WebGL 可行
补帧RIFE-lite~25ms~70ms~800msWebGPU 可行
目标检测YOLOv8n~10ms~25ms~150ms全平台可行

设备能力检测与降级策略

// 设备能力检测器
async function detectDeviceCapability() {
  const caps = {
    webgpu:    false,
    webgl2:    false,
    wasm:      false,
    hardwareDecoding: false,
    estimatedGpuTier: 'low', // low | mid | high
  };
 
  // 检测 WebGPU
  if ('gpu' in navigator) {
    const adapter = await navigator.gpu.requestAdapter();
    if (adapter) {
      caps.webgpu = true;
      const info = await adapter.requestAdapterInfo();
      // 根据 GPU 型号粗略判断性能等级
      const gpuName = info.device?.toLowerCase() || '';
      if (gpuName.includes('rtx') || gpuName.includes('rx 6') || gpuName.includes('m2')) {
        caps.estimatedGpuTier = 'high';
      } else if (gpuName.includes('gtx') || gpuName.includes('iris') || gpuName.includes('m1')) {
        caps.estimatedGpuTier = 'mid';
      }
    }
  }
 
  // 检测 WebGL2
  const testCanvas = document.createElement('canvas');
  caps.webgl2 = !!testCanvas.getContext('webgl2');
 
  // 检测硬件解码(WebCodecs)
  if ('VideoDecoder' in window) {
    const support = await VideoDecoder.isConfigSupported({
      codec: 'avc1.42E01E',
      hardwareAcceleration: 'prefer-hardware',
    });
    caps.hardwareDecoding = support.supported;
  }
 
  caps.wasm = 'WebAssembly' in window;
  return caps;
}
 
// 根据设备能力选择处理策略
async function selectStrategy(caps) {
  if (caps.webgpu && caps.estimatedGpuTier === 'high') {
    return {
      backend:         'webgpu',
      superResolution: true,
      srScale:         2,
      matting:         true,
      interpolation:   true,
    };
  } else if (caps.webgpu || caps.webgl2) {
    return {
      backend:         caps.webgpu ? 'webgpu' : 'webgl',
      superResolution: false,    // 超分太耗时,跳过
      matting:         true,     // 抠图轻量,保留
      interpolation:   false,
    };
  } else {
    // 低端设备:仅保留最轻量的处理
    return {
      backend:         'wasm',
      superResolution: false,
      matting:         false,
      interpolation:   false,
    };
  }
}

内存管理:VideoFrame 泄漏是最常见的问题

VideoFrame 持有 GPU/系统内存,必须在使用后显式调用 close(),否则会导致严重的内存泄漏:

// 错误示范:忘记 close()
decoder.output = (frame) => {
  ctx.drawImage(frame, 0, 0);
  // ❌ 没有 frame.close(),每帧 ~8MB 内存永久泄漏
};
 
// 正确做法:try/finally 保证释放
decoder.output = async (frame) => {
  try {
    await processFrame(frame);
  } finally {
    frame.close(); // ✅ 无论是否出错都会释放
  }
};
 
// 如果需要在异步操作中传递 frame,使用 clone()
decoder.output = async (frame) => {
  const cloned = frame.clone(); // clone 会增加引用计数
  frame.close();                // 原始 frame 立即释放
 
  // 异步处理 cloned(处理完后也要 close)
  await someAsyncWork(cloned);
  cloned.close();
};

第八部分:技术展望

WebNN:下一代原生 AI 加速

WebNN(Web Neural Network API)是 W3C 正在标准化的原生神经网络推理接口,可以直接调用操作系统的 AI 加速层(CoreML on macOS/iOS、DirectML on Windows、NNAPI on Android),绕过 WebGPU/WebGL 的通用 GPU 计算层,在特定硬件上获得更低的延迟和功耗。目前 Chrome 已在 Windows 和 macOS 上实现了初步支持,未来有望成为浏览器端 AI 推理的标准路径。

// WebNN 基础用法(目前处于实验阶段)
const context = await navigator.ml.createContext({ deviceType: 'gpu' });
const builder = new MLGraphBuilder(context);
 
// 构建简单的卷积层
const input  = builder.input('input', { type: 'float32', dimensions: [1, 3, 224, 224] });
const filter = builder.constant({ type: 'float32', dimensions: [32, 3, 3, 3] }, filterData);
const conv   = builder.conv2d(input, filter);
const output = builder.relu(conv);
 
const graph = await builder.build({ output });
const result = await context.compute(graph, { input: inputTensor }, { output: outputBuffer });

流媒体 AI 增强的未来

结合本知识库覆盖的完整技术栈,可以展望一条完整的"AI 增强视频播放"链路:CDN 下发低码率流(节省带宽成本)→ 客户端 WebCodecs 解码 → WebGPU 超分还原画质 → WebGL 合成水印/字幕 → &lt;canvas&gt; 展示。这条链路在技术上已经完全可行,工程化落地的主要障碍是推理延迟设备兼容性,而这两点都在随着 WebGPU 普及和模型轻量化研究的推进快速改善。