WebCodecs浏览器录屏实战:无插件H.264编码与MP4封装
先说结论WebCodecs 这套 API 能让浏览器直接调用系统底层的视频与音频编解码器把屏幕画面编码成 H.264、封装成 MP4 下载整个流程不需要任何插件、不需要后端转码更不会像某些在线录屏服务那样给你盖一个平台水印。这篇内容不是官方文档的翻译而是我最近把一个自用的浏览器录屏工具从 MediaRecorder 迁移到 WebCodecs 时的完整记录包含参数选择、踩坑过程、可直接跑的代码骨架以及那些“文档里查不到但实战一定会遇到”的细节。这篇文章适合两种人一种是做在线教学、远程协作、产品演示、录屏软件替代品真正需要把高清屏幕内容保存成 MP4 的前端开发者另一种是已经被 MediaRecorder 的封装格式、码率失控、浏览器兼容性问题搞到头大想看看 WebCodecs 到底能不能解决问题的技术人。看完你应该能自己写出一套无插件、无后端的录屏方案。1. 为什么选 WebCodecs先理解这套 API 解决的是什么1.1 MediaRecorder 的四个憋屈点以前浏览器录屏几乎所有前端方案都绕不开 MediaRecorder。这个 API 用起来确实简单navigator.mediaDevices.getDisplayMedia()拿到屏幕流丢给 MediaRecorderondataavailable里收 chunk最后合成 Blob 就能下载。但真要做到“高清录屏”MediaRecorder 会让你非常憋屈。第一个憋屈点是编码参数不可控。MediaRecorder 允许通过videoBitsPerSecond大概指定码率但浏览器底层具体怎么分配、是否用硬件编码、GOP 多大、profile 和 level 什么级别你完全说了不算。同一段屏幕流在 Chrome 和 Edge 上录出来的画质、体积、关键帧间隔可能完全不同遇到复杂的动态画面时码率控制抽风画面直接糊成一团。第二个憋屈点是封装格式和音视频同步被浏览器写死。MediaRecorder 最稳的输出格式是 webm但很多人要的是 MP4因为视频剪辑软件、教学平台、微信/企微文件传输对 MP4 的兼容性远好于 webm。想让 MediaRecorder 直接出 MP4只能在支持该容器的时间段碰运气或者录制完成后用 ffmpeg 转封装这就引入了额外工具链。第三个憋屈点是没有逐帧处理能力。你想在录屏时叠一个水印、画一个高亮框、在某一时刻打码敏感信息MediaRecorder 本身做不到只能在 getDisplayMedia 出来的视频轨道和 MediaRecorder 之间强行插一层 Canvas先重绘再录操作很绕而且重绘后的 Canvas 流再进 MediaRecorder画质损耗是双重的。第四个憋屈点是暂停和切片逻辑有时候不可靠。MediaRecorder 的pause()只暂停录制音频轨道状态在不同浏览器上表现不一致暂停回来以后音画不同步的案例不在少数。1.2 WebCodecs 带来的新链路WebCodecs 是 W3C 的一套底层媒体编解码 API它把浏览器内部已经存在的编码器、解码器能力开放给了 JavaScript。简单说以前你只能用高层 API 让浏览器“帮我录个视频”现在你可以自己拿原始帧、自己设置编码参数、自己决定每一帧是不是关键帧、自己把编码后的二进制数据接走。在录屏这个场景里核心链路变成这样getDisplayMedia()获取屏幕画面流把视频帧绘制到 Canvas 上做统一处理缩放、叠加标注、水印用VideoFrame包装 Canvas 内容喂给VideoEncoder编码成 H.264 数据块按 MP4 的封装规则把 H.264 数据块写进 Box 结构最后生成 Blob 触发下载第二步到第四步是 MediaRecorder 完全做不到的。你既能控制码率精确到多少 Mbps、分辨率、帧率也能控制关键帧间隔解决拖动进度条的问题还能在编码前对每一帧做任意绘制本质上是把浏览器变成了一个可编程的录屏机。1.3 能力边界与适用场景WebCodecs 不是银弹先说清楚边界。它依赖浏览器对底层编解码器的支持桌面端 Chrome、Edge 目前对 H.264 编码的支持比较成熟Safari 部分版本支持但兼容性细节更多Firefox 默认支持情况一般。因此生产环境通常需要能力检测必要时降级到 MediaRecorder。另外WebCodecs 只负责编解码和裸流输出MP4 封装要自己处理所以我会在方案里引入一个轻量级封装库把底层细节包住。适合用 WebCodecs 的场景很明确需要高画质录屏、需要精确码率、需要逐帧叠加内容、需要输出 MP4、且能够接受“用现代浏览器打开”。如果你要做的只是在页面里快速录一段 webm 分享MediaRecorder 仍然够用不必上这套重方案。2. 整体方案设计屏幕流、Canvas 中转与编码封装2.1 一条完整的数据链路我最终采用的架构可以拆成相对独立的四个模块采集模块、帧处理模块、编码模块、封装模块。采集模块通过getDisplayMedia或getUserMedia拿到原始MediaStream。视频轨道不直接进编码器而是先接到一个隐藏的video元素上播放这个 video 元素不作为 UI 展示只作为“帧来源”。帧处理模块使用requestAnimationFrame或requestVideoFrameCallback按节奏从 video 元素取帧绘制到 Canvas 上再把 Canvas 转为VideoFrame。编码模块创建VideoEncoder把VideoFrame编码成 H.264 二进制 chunk。封装模块使用mp4-muxer库把每个编码后的 chunk 按时间顺序写入 MP4 容器结束后拿到 ArrayBuffer/Blob触发文件下载。音频链路同理麦克风或系统音频轨道进入AudioContext经过AudioWorklet切片成AudioData交给AudioEncoder编码成 Opus再送入同一个封装器。我画不了流程图但你可以记住一条最核心的原则不要在浏览器线程里直接拿MediaStreamTrack去构造VideoFrame先用 Canvas 作为统一中转后面所有问题都会好解决很多。这个选择的原因我在下一节展开。2.2 为什么必须经过 Canvas 中转我第一次做这套方案时想省掉 Canvas直接用getDisplayMedia返回的video track构造VideoFrame去编码。结果有三个问题接连出现。第一个问题是分辨率不可控。不同用户的显示器可能是 1280x720、1920x1080、2560x1440如果录制工具要固定在 1920x1080 并自动缩放就必须处理分辨率归一化。用 Canvas 中转画布大小自己定义drawImage会自动缩放输出帧自然就是目标分辨率。第二个问题是系统权限和法律约束。录屏时不可避免地可能录到通知弹窗、浏览器地址栏、手机验证码等敏感信息。生产级录屏工具一定会提供“遮挡/模糊区域”功能。这个功能只能靠 Canvas 重绘时覆盖色块或者画高斯模糊区域跳过 Canvas 就不可能实现。第三个问题是编码器对VideoFrame的格式要求。直接由 track 构造的 VideoFrame 可能是零拷贝的底层纹理格式在部分浏览器里送入 VideoEncoder 会直接抛错或导致黑帧。Canvas 转出的VideoFrame更接近 RGBA 格式兼容性更好也方便做后续的 OpenGL/WebGL 叠加。此外Canvas 中转还可以让你轻松实现“录制鼠标高亮”“键盘按键显示”“局部放大镜”等录屏软件常见功能。这些功能本质上都是每帧绘制逻辑写在一个drawFrame()函数里就行。2.3 MP4 封装选型自己拼 Box还是用 mp4-muxerMP4 文件本质上是 ISO BMFF 结构的容器内部有ftyp、moov、mdat等 Box。理论上你可以自己实现一个“最小的 MP4 Muxer”我也确实见过有人为了减少依赖手写六七百行代码拼 Box。但是这里面坑非常多stts、stss、ctts、stsz、stco这些 Box 的偏移计算只要错一个字节播放器就会花屏或无法拖拽。除非你有一个星期时间专门研究 MP4 规范否则不建议自己写。我的选择是使用 mp4-muxer 这个库它专门为 WebCodecs 设计体积很小支持 H.264/AVC、HEVC、VP9、AV1 视频以及 AAC、Opus 音频。它不做编码、不做解码只负责把 WebCodecs 输出封装成 MP4职责单一比较值得信赖。你不需要关心 Box 细节只要把VideoChunk/AudioChunk喂进去即可。如果你的项目对依赖极度敏感也可以用 Web Worker 里跑一个专门做封装的最小实现但作为第一版我不建议重复造轮子。3. 核心实操从零实现一个浏览器录屏导出 MP4 的工具3.1 获取屏幕画面和音频流先说视频采集。屏幕流用navigator.mediaDevices.getDisplayMedia()获取这是浏览器原生支持的 API调用时会弹出系统级的选择窗口用户选择要共享的屏幕、窗口或浏览器标签页。const screenStream await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30 }, audio: { systemAudio: include } });frameRate: 30是建议值屏幕录制通常 30fps 足够如果录制高动态游戏画面或代码演示时有大量滚动可以提升到 60fps但注意编码压力和文件体积会显著增加。systemAudio用于告诉浏览器“我希望能包含系统内放声音”不过要注意这个参数在不同浏览器和不同操作系统上支持程度不一样Chrome 对标签页内音频的捕获支持较好但对系统全局内放的支持可能受限所以我通常会同时考虑麦克风轨道。如果你还需要同时录制摄像头可以用getUserMedia再拿一个 camera 流后面放到 Canvas 画中画区域。接着把屏幕流丢给一个隐藏的video元素这是后续取帧的基础const video document.createElement(video); video.srcObject screenStream; video.autoplay true; video.playsInline true; video.muted true; // 避免出现回声和啸叫 await video.play();这里有个容易被忽略的点video.muted true并不能阻止你录制系统声音因为真正的音频轨道在screenStream或额外的麦克风流里不是从 video 元素输出。设置 muted 只是防止用户环境突然外放屏幕声音导致啸叫。想要录制音频需要单独采集轨道。3.2 用 VideoEncoder 编 H.264 的关键参数编码器是整个方案的心脏参数设置直接决定画质和文件体积。创建VideoEncoder时需要提供一个output回调WebCodecs 编码完成后会把数据块通过这个回调交给你你在这里把它转发给 MP4 Muxer。下面是我在一台 Windows 11 的 Chrome 上实测可用的配置const encoder new VideoEncoder({ output: (chunk, metadata) { // 转发给 mp4-muxer muxer.addVideoChunk(chunk, metadata); }, error: (e) { console.error(VideoEncoder error:, e); } }); encoder.configure({ codec: avc1.42001f, width: canvas.width, height: canvas.height, bitrate: 10_000_000, // 10 Mbps可根据场景调整 framerate: 30, latencyMode: realtime, // 录屏必须用实时模式quality 模式延迟太高 avc: { format: avc // 让编码器输出 avcC 格式方便封装 MP4 } });codec: avc1.42001f是 H.264 Baseline Profile Level 3.1 的编码字符串兼容性最好但压缩比偏低。如果你要追求更高画质和更小的体积可以尝试avc1.4d002aMain Profile或avc1.640028High Profile。不过要注意不是所有设备上的硬编都支持这些 profile设置前最好用VideoEncoder.isConfigSupported()检测一遍。关于latencyMode这里必须强调录屏和视频通话一样属于低延迟场景必须设置realtime。如果设置成quality编码器会为了压缩率大排长队录制产生的延迟会越来越高最后你看到的录制窗口和实际画面差出好几秒用户早就把页面切走了。还有一个性能相关参数hardwareAccelerationconst support await VideoEncoder.isConfigSupported({ codec: avc1.42001f, width: 1920, height: 1080, hardwareAcceleration: prefer-hardware }); if (support.supported) { encoder.configure({ ...support.config, hardwareAcceleration: prefer-hardware }); }硬件编码速度极快但画质和码率控制的细节没有软编好而且在驱动/显卡型号复杂的电脑上可能触发isConfigSupported返回不支持。我建议优先prefer-hardware如果编码器初始化失败再降级到no-preference。3.3 帧循环与关键帧策略编码器配置好了接下来是每一帧的处理。我在循环里做三件事从 video 元素绘制到 Canvas、创建VideoFrame、交给编码器。let stopFlag false; let lastKeyFrameTime 0; const KEYFRAME_INTERVAL_MS 2000; // 每 2 秒一个关键帧 function tick(now) { if (stopFlag) { return; } // 1. 绘制到 Canvas这里可以做缩放、水印、标注 ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 2. 构造 VideoFrame时间戳单位是微秒 const frame new VideoFrame(canvas, { timestamp: now * 1000, duration: 1_000_000 / 30 // 假设 30fps }); // 3. 判断是否需要插入关键帧 const needKeyFrame now - lastKeyFrameTime KEYFRAME_INTERVAL_MS; if (needKeyFrame) { lastKeyFrameTime now; } encoder.encode(frame, { keyFrame: needKeyFrame }); frame.close(); requestAnimationFrame(tick); } requestAnimationFrame(tick);关键帧是 MP4 里“能独立解码的完整画面帧”没有关键帧的视频在播放器里拖动进度条时会卡住或显示黑屏。设置合理的关键帧间隔很重要我一般用2s短视频足够大视频也不会让文件膨胀太多。如果你要剪辑出来逐帧抠画面可以缩短到 1s但编码压力也会变大。requestAnimationFrame在页面处于后台标签页时会自动降速甚至暂停如果录制过程中用户切走了浏览器标签页视频就会产生“时间断层”。更严谨的做法是用video.requestVideoFrameCallback来驱动循环它只有在 video 元素真正出现新帧时才回调和采集节奏天然对齐。不过 Canvas 中转后requestVideoFrameCallback的帧率和编码目标的帧率不一定完全一致实际操作中用 rAF 也足够。3.4 音频采集与 Opus 编码可选的音轨大多数录屏工具都需要声音。WebCodecs 的AudioEncoder可以用来编码音频但浏览器对音频编码格式的支持比较乱Chrome 对 Opus 支持很好对 MP3 部分版本支持对 AAC 编码则基本不可用。我最终采用的音频方案是编码成 Opus 后放进 MP4因为 mp4-muxer 支持 Opus 音轨且audio/mp4里带 Opus 在技术上是合法的。音频采集链路比视频稍微绕一点。需要把MediaStream里的音频轨道接到AudioContext再通过AudioWorklet把音频数据切割成AudioData交给编码器。核心代码如下const audioCtx new AudioContext(); const sourceNode audioCtx.createMediaStreamSource(audioStream); await audioCtx.audioWorklet.addModule(audio-processor.js); const workletNode new AudioWorkletNode(audioCtx, capture-processor); sourceNode.connect(workletNode);在audio-processor.js里每次processor.process()回调会把输入声道数据包装成AudioData然后发送到主线程的AudioEncoder进行编码。AudioEncoder 的配置类似const audioEncoder new AudioEncoder({ output: (chunk, metadata) { muxer.addAudioChunk(chunk, metadata); }, error: (e) { console.error(AudioEncoder error:, e); } }); audioEncoder.configure({ codec: opus, sampleRate: 48000, numberOfChannels: 2, bitrate: 128_000 });如果时间有限第一版可以先只做视频不录声音后面再把音频链路接进来。毕竟画质和核心链路足以验证 WebCodecs 的价值。想合并系统声和麦克风的时候用两个MediaStreamAudioSourceNode在 AudioContext 里混音即可AudioContext 本身就是干这个的。3.5 MP4 封装与文件下载封装我用 mp4-muxer。下面是一段以 mp4-muxer 2.x 版本为例的初始化代码import { Muxer, ArrayBufferTarget } from mp4-muxer; const muxer new Muxer({ target: new ArrayBufferTarget(), video: { codec: avc, // 与 VideoEncoder 的 avc1 对应 width: canvas.width, height: canvas.height }, audio: { codec: opus, // 如果没有音轨就省略 audio 字段 sampleRate: 48000, numberOfChannels: 2 }, fastStart: in-memory, // 把 moov Box 放在文件头部方便在线播放 firstTimestampBehavior: offset // 让第一个 chunk 的时间戳从 0 开始 });fastStart: in-memory会把moovBox 先缓存到内存中最后放在 MP4 文件头部。这样生成的视频放在网上可以直接渐进式播放用户不需要等文件全部下载完才能拖动进度条。如果文件特别大比如超过 4GB可能需要调整策略但一般录屏不会到这么大。录制结束时一定要先停止采集然后等待编码器把缓冲排空最后再让 muxer 生成文件async function stopRecording() { stopFlag true; // 停止采集轨道避免后续还有帧进入 screenStream.getTracks().forEach(track track.stop()); if (microphoneStream) { microphoneStream.getTracks().forEach(track track.stop()); } // 等待编码器排空已提交的帧 await encoder.flush(); if (audioEncoder) { await audioEncoder.flush(); } muxer.finalize(); const { buffer } muxer.target; const blob new Blob([buffer], { type: video/mp4 }); const url URL.createObjectURL(blob); const link document.createElement(a); link.href url; link.download recording.mp4; link.click(); URL.revokeObjectURL(url); }encoder.flush()是必须的它会让编码器输出队列里尚未回调的数据全部调完否则最后几帧会丢失或者 muxer 拿不到完整的 chunk 表。这个顺序错了录出来的文件经常是“时长不对”或者“结尾花屏”。3.6 可运行的完整代码骨架把上面几段拼起来就是一个可以运行的最小录屏工具。下面是一段骨架级代码省略了 UI 和细节组件但核心逻辑完整你可以基于它修改。import { Muxer, ArrayBufferTarget } from mp4-muxer; class ScreenRecorder { constructor() { this.video document.createElement(video); this.video.autoplay true; this.video.playsInline true; this.video.muted true; this.canvas document.createElement(canvas); this.ctx this.canvas.getContext(2d); this.encoder null; this.muxer null; this.stopFlag false; this.lastKeyFrameTime 0; } async start({ width 1920, height 1080, fps 30, bitrate 10_000_000 } {}) { const screenStream await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: fps }, audio: { systemAudio: include } }); this.video.srcObject screenStream; await this.video.play(); this.canvas.width width; this.canvas.height height; this.muxer new Muxer({ target: new ArrayBufferTarget(), video: { codec: avc, width, height }, fastStart: in-memory, firstTimestampBehavior: offset }); this.encoder new VideoEncoder({ output: (chunk, meta) this.muxer.addVideoChunk(chunk, meta), error: console.error }); this.encoder.configure({ codec: avc1.42001f, width, height, bitrate, framerate: fps, latencyMode: realtime, avc: { format: avc } }); this.stopFlag false; this.fps fps; requestAnimationFrame((now) this.tick(now)); // 保留引用停止时使用 this.screenStream screenStream; } tick(now) { if (this.stopFlag) return; this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height); const frame new VideoFrame(this.canvas, { timestamp: now * 1000, duration: 1_000_000 / this.fps }); const needKeyFrame now - this.lastKeyFrameTime 2000; if (needKeyFrame) { this.lastKeyFrameTime now; } this.encoder.encode(frame, { keyFrame: needKeyFrame }); frame.close(); requestAnimationFrame((t) this.tick(t)); } async stop() { this.stopFlag true; this.screenStream.getTracks().forEach((t) t.stop()); await this.encoder.flush(); this.muxer.finalize(); const { buffer } this.muxer.target; const blob new Blob([buffer], { type: video/mp4 }); const url URL.createObjectURL(blob); const link document.createElement(a); link.href url; link.download recording.mp4; link.click(); URL.revokeObjectURL(url); } }在你自己的项目里启动时用recorder.start({ width: 1920, height: 1080, bitrate: 8_000_000 })结束录制时调用recorder.stop()即可。4. 踩坑记录与常见问题速查4.1 录出来黑屏或画面静止这是 WebCodecs 录屏最常见的坑。初版代码里我直接把getDisplayMedia得到的video track交给 Canvas 绘制有时候视频元素还没loadedmetadata就执行drawImagecanvas 画面自然是黑的。解决思路确保video.play()这个 Promise 已经 resolve 之后再启动循环启动后的第一帧不要立刻编码可以先用一个空帧跳过或者人为延时 200ms。对 rAF 的now时间戳要做防抖避免短时间内重复绘制同一帧给编码器造成无意义压力。还有一个隐蔽点如果用户选择共享窗口后把窗口最小化getDisplayMedia的视频轨道在某些系统上不会产生新帧。此时画面会定格在最后一帧录出来看起来像“卡住”。这时候要么提示用户保持窗口可见要么在 UI 上展示实时预览窗口让用户知道当前录制状态。4.2 codec 不支持或 configure 报错我在另一台显卡驱动比较旧的 Windows 机器上遇到过VideoEncoder.configure()直接抛NotSupportedError。原因是指定的avc1.4d002a在该设备的硬编能力之外而我没有做能力检测。解决办法非常直接初始化前用VideoEncoder.isConfigSupported()检测你想用的所有 codec 字符串按优先级排列avc1.42001fBaselineavc1.4d002aMainavc1.640028High哪个通过就用哪个。如果全部不通过就说明浏览器当前环境不支持 H.264 编码需要降级方案。configure还有一个坑编码器已经处于工作状态时不能连续重复configure否则会中断现有流。如果你需要在录制中动态调整码率我建议只在关键帧写入之后做并且使用encodeControl或干脆停止后重新配置稳妥优先。4.3 视频卡顿、变糊、文件过大视频卡顿通常不是编码器的问题而是 Canvas 绘制开销和 rAF 的资源竞争。如果 Canvas 上还叠加了复杂的标注或滤镜每帧绘制时间超过 33ms帧率就会掉到 30fps 以下。你可以开一个统计面板观察drawFrame的实际耗时如果超过 16ms就要考虑用 offscreen canvas 或 WebGL 来加速绘制。视频变糊多半是码率不够。屏幕录制和摄像头录制不一样静态桌面和动态页面在相同码率下观感差异巨大。常见设置建议参考下表分辨率帧率静态演示码率动态页面/视频码率1280x720304 Mbps8 Mbps1920x1080308 Mbps12 Mbps1920x10806012 Mbps16 Mbps2560x14403012 Mbps20 Mbps文件过大的情况正好相反说明你给码率太高或者画面内容本身变化大。录屏不是剪辑母带我一般建议默认 8~10 Mbps 即可做产品演示、教程视频在 1080p 下这个码率已经非常清晰。4.4 结束导出时 Blob 损坏或缺少 moov如果你下载的 MP4 无法在播放器里打开大概率是flush()没有完成就执行了finalize()。encoder.flush()不是一个同步操作它会等所有已提交帧编码完成但它返回的 Promise 可能不代表所有 output 回调已经执行完毕。在部分浏览器里flush 的 Promise resolve 后仍可能存在微任务队列里的输出回调尚未被 muxer 消费。稳妥的做法是多等一个微任务或者直接等待 output 队列长度归零await encoder.flush(); await new Promise((r) setTimeout(r, 0)); muxer.finalize();第二个常见的 MP4 问题是对比播放器不能拖拽进度条。这通常是因为没有设置fastStart: in-memory或关键帧间隔太长。没有关键帧的 MP4播放器不知道该从哪里开始解码拖动进度条就会黑屏建议关键帧间隔不要超过 5 秒。4.5 浏览器兼容性降级方案即使 WebCodecs 很强大项目真正上线前还是要准备好降级路径。我的建议是做一个统一接口下层同时实现 WebCodecs 和 MediaRecorder 两种录制器运行时通过能力检测决定启用哪个。if (VideoEncoder in window) { recorder new WebCodecsRecorder(); } else { recorder new MediaRecorderFallback(); }降级到 MediaRecorder 的时候输出格式大概率是 webm。如果业务方坚持要 MP4可以把 webm 文件上传到服务端转封装或者引导用户用 Chrome/Edge 浏览。安全底线是任何情况下录制不可用都比白屏闪退好这一点在产品里比技术参数更重要。5. 进阶玩法与体验优化5.1 动态码率控制与文件体积控制MediaRecorder 想动态调码率基本不可能WebCodecs 给了你机会。你可以在输出回调里统计编码后 chunk 的字节数结合已经经过的时长估算当前实际码率。然后每 5 秒和目标码率比较如果实际码率长时间高于目标说明画面复杂度高可以适当提升码率如果长时间低于目标则降下来避免浪费文件体积。注意一点调码率不能做得太频繁频繁configure会导致编码上下文重置。我建议只在画面内容发生重大变化时调整比如用户从“纯文本页面”切到“全屏视频播放”时检测到帧间差异突然变大才触发一次升高码率。每次调整后把下一帧设置为关键帧保证解码端能正常衔接。5.2 画面叠加水印、标注、画中画前面说 Canvas 中转最大的好处就是可以任意绘制这里展开说几个实用功能。水印是最简单的每帧drawFrame时在 Canvas 上画一段半透明文字即可。注意文字大小要根据 canvas 缩放比例适配不然录出来小屏显示不清楚。标注功能可以监听鼠标事件把鼠标轨迹通过ctx.lineTo和stroke画在 Canvas 上形成“电子教鞭”效果。画中画则是把摄像头流的 video 元素再drawImage到 Canvas 的某个矩形区域同时给区域加圆角边框。这些绘制逻辑不会显著影响编码性能但如果画布很大且绘制复杂建议用离屏 Canvas 做缓存比如背景画面先绘制一次再每帧对叠加层做增量更新能在不损失流畅度的前提下减轻主线程负担。5.3 多音轨合并系统声 麦克风很多录屏软件都支持“系统声音 麦克风讲解”双轨录制。用 WebCodecs 做这件事时可以有两个策略。策略一把两路音频在 AudioContext 里混音成一路编码进一个音轨。优点是实现简单缺点是后期无法分别调节两份声音的音量。策略二把两路音频分别用两个AudioEncoder编码在 MP4 里封装成两条音轨。mp4-muxer 支持添加多个 audio 轨道播放器可以让用户自由切换或混合音量。缺点是部分播放器对多音轨 MP4 支持一般浏览器内置的video元素默认只播放第一条音轨。如果你做的是教学视频我倾向于策略一因为老师的声音和系统声音混在一起本来就是用户预期的效果如果是多语种配音录制策略二才有意义。5.4 结合 WebCodecs 做二次转码与压缩WebCodecs 的能力不止从零编码还可以做“解码再编码”的二次转码。你录制完得到一个巨大的 MP4 后还可以在浏览器里把它丢进VideoDecoder解码再重新喂给码率更低的VideoEncoder编码完成一次纯前端压缩。这个链路对不想上传文件的用户非常友好所有操作都在本地完成。二次转码时要注意解码出的VideoFrame不需要经过 Canvas直接重新encode到新编码器即可。但要重新计算时间戳和关键帧策略因为解码器的输出帧率可能和源文件不完全一致。这个方案对新手来说复杂度较高如果没有明确需求我建议先不碰等核心录制链路稳定了再扩展。最后再分享一个我个人的经验WebCodecs 相关 API 的浏览器更新迭代速度非常快网上很多教程里的 codec 字符串、mp4-muxer 的构造函数写法都会随版本变化。如果你照着今天这篇文章的代码运行时发现某些 API 被废弃了不要慌打开官方说明和 WebCodecs 的规范文档对照isConfigSupported的返回值去排查基本都能解决。录屏这个需求看起来只是一个小功能但从 MediaRecorder 走到 WebCodecs你能获得的“对素材资源的精确控制力”是以前完全不敢想象的值得花一个下午把链路亲手跑通。