Prhub

#27249 [diffusion] Fix realtime webui recording timeline

原始 PR 作者 mickqian 合并时间 2026-06-04 19:58 文件变更 2 提交数 1 评论 1 代码增减 +40 / -17

执行摘要

修复实时录制时间线,使用解码帧序列而非预览帧

实时 WebUI 的录制功能之前依赖于从画布捕获帧,但画布帧受预览播放器的帧率控制和丢帧策略影响,导致录制的 MP4 视频中出现卡顿和跳帧。需要让录制直接从解码后的帧序列中采样,避免预览缓冲的干扰。

建议精读 app.js 中的重构逻辑,特别是 recordDecodedFramecreateRecordingFrame 的实现,这是将录制从预览循环解耦的关键设计。关注编码错误级联问题,可作为后续改进方向。

讨论亮点

review 评论中,gemini-code-assist[bot] 指出当编码过程中出现错误时,.catch 块将 recordingActive 置为 false,但不会阻止后续帧的编码尝试,导致错误级联。建议在 recordingEncodeChain.then 块开头检查录制是否已中止,若已中止则跳过编码并关闭帧。此问题未在 PR 中解决,属于已识别的改进点。

实现拆解

  1. 新增录制专用画布和独立的时间计算:在 app.js 中新增 recordingCanvasrecordingCtx,用于处理 ImageData 类型的帧数据;用 recordingFps 替代原有的 recordingStartedAtrecordingFirstFrameAt,时间戳改为基于帧索引和固定 FPS 计算,不再依赖 performance.now()
  2. 重构帧捕获函数:将 captureRecordingFrame(item, now) 替换为 recordDecodedFrame(image),直接从解码后的帧(item.image)创建 VideoFrame,使用 createRecordingFrameImageData 绘制到专用画布上。新增 recordDecodedFrameBatch(decodedFrames) 处理批量帧录入。
  3. 更新记录 UI 逻辑updateRecordButton 中录制时长计算改为 recordingFrameIndex / recordingFps,更精确反映已记录的帧数。startRecording 中初始化 recordingFps 为当前预览目标 FPS。
  4. HTML 缓存版本更新:在 index.html 中将 app.js 的缓存版本从 v72 更新为 v73
文件 模块 状态 重要度
python/sglang/multimodal_gen/apps/realtime_webui/app.js 实时录制 modified 8.13
python/sglang/multimodal_gen/apps/realtime_webui/index.html 实时录制 modified 2.14

关键符号

recordDecodedFrameBatch recordDecodedFrame createRecordingFrame updateRecordButton startRecording

关键源码片段

python/sglang/multimodal_gen/apps/realtime_webui/app.js core-logic

核心变更文件,重构了录制帧捕获逻辑,新增画布和 FPS 固定时间线,是功能修复的主要实现。

// 新增专用录制画布,用于从 ImageData 源创建 VideoFrame
const recordingCanvas = document.createElement("canvas");
const recordingCtx = recordingCanvas.getContext("2d", { alpha: false });// 录制帧索引和固定 FPS ( 在 startRecording 时从预览目标 FPS 锁定 )
let recordingFrameIndex = 0;
let recordingFps = DEFAULT_TARGET_FPS;// 从解码帧批量录入
function recordDecodedFrameBatch(decodedFrames) {
  if (!recordingActive || recordingSaving) return;
  for (const item of decodedFrames) {
    if (!recordingActive) break; // 录制中途停止则退出
    recordDecodedFrame(item.image); // 录入每帧解码后的图像
  }
  updateRecordButton();
}// 录入单个解码帧
function recordDecodedFrame(image) {
  if (!recordingActive || recordingSaving) return;
  const frameIndex = recordingFrameIndex;
  // 基于固定 FPS 计算每帧时长 ( 微秒 )
  const duration = Math.round(1_000_000 / Math.max(1, recordingFps));
  const timestamp = frameIndex * duration; // 帧索引 × 固定帧时长
  let frame;
  try {
    // 使用 createRecordingFrame 将 ImageData 绘制到专用画布并创建 VideoFrame
    frame = createRecordingFrame(image, timestamp, duration);
  } catch (error) {
    recordingActive = false;
    addHistory(error.message || "recording frame capture failed");
    updateRecordButton();
    return;
  }
  recordingFrameIndex += 1;
  recordingEncodeChain = recordingEncodeChain
    .then(async () => {
      await ensureRecordingEncoder(frame.displayWidth, frame.displayHeight);
      recordingEncoder.encode(frame, { keyFrame: frameIndex === 0 || frameIndex % 120 === 0 });
      frame.close();
    })
    .catch((error) => {
      // 注意:error 发生时会停止录制,但不会阻止后续帧执行 .then,可能导致级联错误
      recordingActive = false;
      addHistory(error.message || "recording encode failed");
      updateRecordButton();
    });
}

评论区精华

编码错误级联问题 正确性

reviewer 指出当编码出错时,仅设置 recordingActive = false,但后续帧仍会执行 .then,导致每个剩余帧都触发错误,日志淹没。

结论:未解决;建议在 .then 块开头检查 !recordingActive && !recordingSaving 以跳过编码。 · unresolved

风险与影响

主要风险在于编码错误处理不完善:若编码中途失败,当前实现可能引发错误级联,影响用户体验。此外,recordingFps 在录制开始时固定为预览目标 FPS,若用户在录制过程中动态调整 FPS,可能导致时间轴与实际帧率不匹配。由于缺少自动化测试,回归风险无法通过 CI 完全覆盖。

影响范围仅限于 realtime WebUI 的录制功能,不涉及其他模块。用户录制的 MP4 文件将更加流畅,不再包含预览丢帧导致的抖动;时长显示更精确。对系统性能影响轻微,新增了专用画布和 ImageData 处理。

缺少测试覆盖 错误处理不完善 编码错误级联

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论