视频水印与逐帧叠加
视频进 canvas 是 drawImage(HTMLVideoElement | VideoFrame)。时间轴必须跟 视频帧 走,不能跟 rAF 瞎猜。跨源无 CORS 的视频会把表面弄脏,预览正常、导出/录制失败。
1. 时间:requestVideoFrameCallback
export function attachOverlayRenderer(
video: HTMLVideoElement,
canvas: HTMLCanvasElement,
drawOverlay: (ctx: CanvasRenderingContext2D, timeSec: number) => void,
): () => void {
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2d unavailable");
let cancelled = false;
let handle = 0;
const onFrame: VideoFrameRequestCallback = (_now, meta) => {
if (cancelled) return;
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth || 1;
canvas.height = video.videoHeight || 1;
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(video, 0, 0);
drawOverlay(ctx, meta.mediaTime);
handle = video.requestVideoFrameCallback(onFrame);
};
handle = video.requestVideoFrameCallback(onFrame);
return () => {
cancelled = true;
video.cancelVideoFrameCallback(handle);
};
}
mediaTime 是媒体秒。水印、字幕、进度条都用它,不要用 performance.now()。暂停时 rvfc 可能不再回调:需要在 pause 上补一帧,否则最后一帧 overlay 停在旧时间。
没有 rvfc 的引擎降级:video.requestAnimationFrame 不存在,用 rAF + video.currentTime,知道会有一帧相位差。
显示尺寸和 videoWidth 不同。若 canvas CSS 是 object-fit 式 letterbox,overlay 几何必须按 视频像素 算,再让 CSS 拉伸整张 canvas,不要在 CSS 像素里对视频内容。
2. 水印几何
export function paintWatermark(
ctx: CanvasRenderingContext2D,
text: string,
timeSec: number,
): void {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.save();
ctx.font = `${Math.max(16, w / 40)}px system-ui`;
ctx.fillStyle = "rgba(255,255,255,0.45)";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
ctx.fillText(text, w - 24, h - 24);
ctx.fillStyle = "rgba(0,0,0,0.45)";
ctx.fillText(formatTc(timeSec), 24 + ctx.measureText(formatTc(timeSec)).width, 40);
ctx.restore();
}
function formatTc(sec: number): string {
const s = Math.max(0, Math.floor(sec));
const m = Math.floor(s / 60);
const r = s % 60;
return `${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}`;
}
烧录导出:离线 seek video.currentTime = n / fps,等 seeked,再 draw。不要用 live captureStream 当「成片」,帧间隔不稳。见 录像。
3. CORS 与安全
video.crossOrigin = "anonymous";
video.src = url;
必须在设 src 前设 crossOrigin。CDN 要 Access-Control-Allow-Origin。否则 drawImage(video) 污染 canvas,captureStream / toBlob / WebRTC 再取轨都会失败。
VideoFrame + WebCodecs 路径同样:脏帧不能进需要读回的 2D 表面。
4. 性能
全分辨率 1080p 每帧 drawImage + 文字,主线程通常扛得住。4k + shadowBlur 水印不行。动态文字先画到小离屏再 blit。
desynchronized: true 对跟视频时钟对齐 不友好,水印预览不要开。
5. 失败形态
| 症状 | 原因 |
|---|---|
| overlay 和画面一快一慢 | 用 rAF 而视频变速播放 |
| 暂停后水印时间停错 | 没在 pause 补帧 |
| 导出 SecurityError | 视频无 CORS |
| letterbox 里字幕偏 | 按 canvas CSS 尺寸而不是 videoWidth |
| iOS 黑帧 | 未播放前 videoWidth=0 就 resize 成 1×1 |
权威资料
核对日期:2026-08-26