尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

纯前端基于WebCodecs实现高清录屏并导出MP4的完整方案

纯前端基于WebCodecs实现高清录屏并导出MP4的完整方案 最近接了个内部培训系统的需求要求在浏览器里给学员录制一段操作演示录完直接下载 MP4不给装任何插件也不接受在线录屏工具那种强制水印。最开始想到的方案是 MediaRecorder毕竟它 API 简单几行代码就能把屏幕流落盘。但测了三天以后我发现 MediaRecorder 在真实业务场景里有点不够用输出格式、码率、关键帧间隔全都不可控而且指望它直接输出兼容性好的 MP4 基本是碰运气。于是我把目光转向了 WebCodecs——这是一组浏览器原生提供的音视频编解码接口配合 getDisplayMedia 采集屏幕画面、mp4-muxer 做 MP4 封装可以在浏览器本地完成“高清录屏 → H.264 编码 → MP4 导出”的完整链路整个过程不走服务器也不依赖任何第三方客户端。这篇文章是我把这套方案真正落地到项目里的完整记录包括技术选型的思考、每一步的代码和参数调优、以及调试过程中踩过的坑。适合谁看想在纯前端实现录屏上传的 Web 开发者做在线教育、面试评估、远程协作工具的同学还有被第三方录屏 SDK 授权费折磨的产品团队。你不需要提前懂视频编解码原理我会尽量把每个关键环节讲透。1. 整体设计WebCodecs 做录屏的技术路线1.1 为什么不用 MediaRecorderMediaRecorder 的 API 确实友好但它的“友好”建立在牺牲控制力的基础上。录屏这个场景里你通常需要指定输出分辨率、码率、关键帧间隔甚至希望每一帧都能拿到编码前的数据去做水印、裁剪、合成等处理。MediaRecorder 把这些都藏在了浏览器内部你只能通过 MIME type 和 bitrate 参数“建议”浏览器怎么录最终产物往往变成 webm因为这是浏览器最稳的默认容器。另一个麻烦是浏览器之间的行为差异很大。Chrome 里可以指定video/mp4但 Safari、Firefox 的表现不一定一致同一个浏览器不同版本也可能悄悄改变转封装策略。而 WebCodecs 的思路完全不同它把编码器直接暴露给开发者你给一帧视频进去它把编码后的数据和元数据吐出来给你至于怎么封装成 MP4、什么时间戳、什么关键帧策略全由你控制。还有一个我可以直接说理由的问题MediaRecorder 录出来的视频是“黑盒”很多在线录屏产品需要在画面角落里强制加水印或者限定上传到自家服务器才能转码本质原因就是拿不到编码中间层。用 WebCodecs 之后水印是自己画的元数据是自己写的导出文件也在本地生成不需要看平台脸色。1.2 核心流程采集—编码—封装这套方案的完整链路可以拆成四段屏幕画面采集、逐帧读取、VideoEncoder 编码、MP4 封装。你可以把它类比成一条流水线屏幕流是原料VideoEncoder 是加工车间mp4-muxer 是打包车间。第一步用navigator.mediaDevices.getDisplayMedia拿到屏幕分享流浏览器会弹出原生选择框让用户选择分享整个屏幕、某个窗口还是某个浏览器标签页。第二步用MediaStreamTrackProcessor把视频轨道读取成一个个VideoFrame对象这一步相当于把“流水画面”拆成了“单张照片”。第三步把每个VideoFrame交给VideoEncoder编码编码器内部压缩成 H.264 码流通过output回调吐出EncodedVideoChunk。第四步把这些编码后的 chunk 交给 mp4-muxer封装成标准 MP4最终下载到本地。听上去不难但每一步都有不少细节。比如拿到流的宽高不一定等于屏幕分辨率码率和帧率要根据场景动态调整编码器忙不过来时还要丢帧降载。这些后续章节我会逐个展开。1.3 为什么可以做到无插件、无水印、直接导出 MP4先说“无插件”。WebCodecs 是浏览器原生的 Web API不需要安装任何扩展、插件或客户端程序也不像某些企业录屏方案需要先部署一个本地代理。只要浏览器支持 WebCodecs页面就能直接驱动编码器。再说“无水印”。很多第三方录屏服务会把生成的视频上传到服务器再在服务端叠加平台 logo 和时间水印然后返回带水印的文件给你。自己用 WebCodecs 实现时视频数据从头到尾只在本地内存和编码器之间流转没有任何平台有机会往画面里加水印。如果业务上确实需要水印也可以在编码前主动用 canvas 绘制到帧上再交给VideoEncoder等于把水印的控制权完全拿回来。最后是“直接导出 MP4”。因为我们可以自由选择编码器输出和封装器最通用的组合就是视频用 H.264、音频用 AAC然后封装成 mp4。这是所有播放器和视频平台都认的组合不需要二次转码。这也是相比 MediaRecorder 默认生成 webm 最大的优势。2. 环境准备与兼容性判断2.1 浏览器支持情况与功能检测动手之前先确认一件最重要的事目标浏览器到底支不支持 WebCodecs。目前 Chrome 和 Edge 的支持度最好Safari 和 Firefox 在较新版本里也逐渐跟上但编码器的具体能力仍有差异尤其是 H.264 硬件编码的可用性。代码里第一道保险是特性检测if (!(VideoEncoder in window)) { alert(当前浏览器不支持 WebCodecs请使用最新版 Chrome 或 Edge); }第二道保险是调用VideoEncoder.isConfigSupported。这个方法可以告诉你在当前浏览器上某个编码配置是否被支持比我们自己在文档里猜靠谱得多const config { codec: avc1.42001f, width: 1920, height: 1080, bitrate: 5_000_000, framerate: 30, }; const support await VideoEncoder.isConfigSupported(config); console.log(support.supported, support.config);avc1.42001f是 H.264 Baseline Profile 的 codec 字符串兼容性最好。如果希望更高的压缩效率可以再试试avc1.64001fHigh Profile。但不管用哪个我都建议用isConfigSupported先探测不要写死。实际项目中我遇到过同一台机器的 Chrome 和 Edge 对同一段 codec 字符串的支持结果不一样的情况。另外整个流程必须运行在用户手势触发的函数里因为getDisplayMedia会弹原生分享选择框浏览器不允许在非用户交互下自动弹出。2.2 项目依赖mp4-muxerWebCodecs 只负责编码不负责封装 MP4。编码器输出的是一个个EncodedVideoChunk需要有人把 GOP、时间戳、SPS/PPS 这些信息按照 MP4 的 box 结构组装成一个完整文件。自己手写 MP4 封装不是不可能但 ftyp、moov、mdat、stbl 这些 box 的嵌套关系以及 codec config 的写入细节多且容易出错不推荐在业务里重复造轮子。我用的库是 mp4-muxernpm 上直接安装npm i mp4-muxer它支持在浏览器里用 ESM 引入也可以直接用 script 标签加载。这个库最方便的地方是它会自动从VideoEncoderoutput 回调的第二个参数里读取 decoderConfig并解析出 H.264 需要的 avcC 描述我们不需要手工去处理 SPS/PPS 的二进制。如果项目对包体积敏感可以在开始录制时才动态import(mp4-muxer)避免首屏加载无关字节。2.3 最简页面结构示例页面只需要三个按钮开始录制、停止录制、下载。再加一行状态文本就够了。核心是逻辑UI 越简单越不容易干扰调试。button idstart开始录制/button button idstop disabled停止录制/button button iddownload disabled下载 MP4/button p idstatus未录制/p如果想让用户看到实时预览可以在页面上放一个video autoplay muted把getDisplayMedia返回的MediaStream直接赋给video.srcObject。屏幕流的预览不会产生额外编码开销放心用。3. 第一步获取屏幕画面流3.1 getDisplayMedia 参数详解获取屏幕流的代码是整条链路的第一环const stream await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 30, max: 60 }, }, audio: true, });这里的video约束里frameRate写ideal和max是告诉浏览器“我希望能有 30 帧最多 60 帧”实际帧率由系统能力和屏幕内容动态决定。比如屏幕静止的时候很多系统会自动降帧省电录出来的文件帧率可能低于目标值这很正常。audio: true在某些系统上可以捕获系统声音尤其在 Chrome 里配合 macOS 或 Windows 的效果比较稳定。但需要注意系统音频捕获能力会受到浏览器版本和系统权限策略的影响企业在域控环境下有时会被策略直接禁用。我建议把这个能力做成可选项录制时检测音频轨道是否存在如果不存在就用静音轨占位避免视频 Track 和 Audio Track 时间线对不上。还有一个细节值得提在不希望弹出选择框时焦点跳到别处的情况下可以使用 Chrome 的CaptureController来设置焦点行为const controller new CaptureController(); controller.setFocusBehavior(no-focus-change); const stream await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true, controller, });这个能力适合做课程录制工具用户选完窗口后焦点不要跑到系统弹窗里打断正在演示的操作。3.2 MediaStreamTrackProcessor 逐帧读取拿到MediaStream之后下一步是把视频轨道“拆”成一帧帧VideoFrame。现代 Chrome 支持MediaStreamTrackProcessor它把一个视频轨道包装成一个ReadableStream你可以像读普通字节流一样读视频帧const videoTrack stream.getVideoTracks()[0]; const processor new MediaStreamTrackProcessor({ track: videoTrack }); const reader processor.readable.getReader(); async function pumpFrames() { while (recording) { const { value: frame, done } await reader.read(); if (done) break; if (encoder.encodeQueueSize 10) { frame.close(); continue; } encoder.encode(frame, { keyFrame: false }); frame.close(); } }这里有一个必须养成的习惯每一帧用完立即调用frame.close()。VideoFrame底层持有的是 SharedArrayBuffer 或 GPU 资源不主动关闭内存会一路涨到标签页崩溃。我第一次写的时候忘了 close录了 8 分钟内存冲到 3GB直接把页面卡死了。encodeQueueSize 10是背压策略。编码器处理不过来时队列会越堆越长这时最好的办法是主动丢帧而不是继续叠帧导致内存爆炸。屏幕录制丢几帧对用户体验影响不大比卡死强得多。3.3 兼容性备用方案从 video 元素取帧MediaStreamTrackProcessor在部分浏览器里仍被归为较新的能力如果线上环境要兼容没有它的情况还有一个更保守的方案把同一个MediaStream赋给一个隐藏的video元素播放起来后用requestVideoFrameCallback在每一帧到来的回调里创建VideoFrameconst video document.createElement(video); video.srcObject stream; video.muted true; await video.play(); function pumpFromVideo() { if (!recording) return; const frame new VideoFrame(video, { timestamp: performance.now() * 1000 }); encoder.encode(frame, { keyFrame: false }); frame.close(); video.requestVideoFrameCallback(pumpFromVideo); } video.requestVideoFrameCallback(pumpFromVideo);requestVideoFrameCallback是专门跟随视频帧率的回调不会每帧重复触发性能上也可以接受。区别是MediaStreamTrackProcessor更接近底层延迟更小video方案多了一层播放器走管线的成本。我的建议是先用video方案把整个流程跑通再做性能优化再考虑切换到MediaStreamTrackProcessor。这样每一步都是可控的排错也容易。4. 第二步VideoEncoder 编码参数4.1 H.264 编码器配置创建编码器的代码不复杂但参数要细心const encoder new VideoEncoder({ output: (chunk, meta) { muxer.addVideoChunk(chunk, meta); }, error: (e) console.error(编码错误, e), }); encoder.configure({ codec: avc1.42001f, width: videoWidth, height: videoHeight, bitrate: 5_000_000, framerate: 30, latencyMode: realtime, });width和height最好从videoTrack.getSettings()里读不要自己用screen.width或window.innerWidth猜测屏幕缩放比例和窗口尺寸都会导致实际的采集分辨率变化。我自己就踩过一次代码里写死了 1920x1080但用户在高分屏上把浏览器窗口缩到 1280 后录出来的视频上下被裁剪了。bitrate的单位是 bit/s5_000_000就是 5 Mbps。latencyMode: realtime表示编码器走实时低延迟模式不会为了追求压缩率去缓存多帧这对录屏场景是合适的。如果你是在做离线视频处理可以改成quality输出效率更高但会有额外的处理延迟。4.2 码率、分辨率和帧率的取舍屏幕录制的特点是大面积静态内容H.264 对这种画面压缩率很高。但不同场景的码率需求差异很大我给一个我自己常用的参考表录制场景推荐分辨率推荐帧率推荐码率静态文档、PPT1080p15-302-4 Mbps操作演示、网页滚动1080p304-8 Mbps代码编辑、文字密集原生分辨率15-302-5 Mbps视频播放、动画演示2K/4K30-6010-20 Mbps码率给太低文字边缘会出现明显的马赛克和振铃给太高导出文件体积成倍增长但视觉提升有限。我的经验是优先保证文字清晰度从 5 Mbps 起步录 30 秒导出检查一次再根据产物调整。另一个不太容易被注意到的点是帧率幻觉。很多人以为录屏帧率越高越好于是无脑拉 60fps。但对操作演示这种场景观众注意力在鼠标移动和界面变化上30fps 已经完全够用。60fps 会让体积涨 30%-50%收益却很难感知。除非录的是游戏画面否则建议优先用 30fps。4.3 关键帧间隔与手动关键帧MP4 播放器要能在进度条上快速跳转必须依赖关键帧I 帧。如果关键帧太少用户把进度条拖到中间播放器要等很久才能解码出画面。反过来关键帧太密集文件体积又会上涨。WebCodecs 的VideoEncoderConfig里有一些浏览器支持不一的 keyframe 相关字段为了兼容性最稳妥的方式是自己控制每 N 帧强制一个关键帧。let frameIndex 0; function pumpFrames() { // 每 150 帧强制一个关键帧30fps 下就是 5 秒一个 const isKeyFrame frameIndex % 150 0; encoder.encode(frame, { keyFrame: isKeyFrame }); frameIndex; }我个人的习惯是 3-5 秒一个关键帧。给后期剪辑的话可以缩短到 2 秒只是给学员在线看5 秒足够了。太大的 I 帧会挤占码率预算所以别太贪。4.4 编码队列背压策略VideoEncoder.encodeQueueSize表示还没编码完的帧数。如果这个值持续增长说明编码速度跟不上采集速度这在低端电脑上很常见。强行把所有帧都塞进队列内存和延迟都会失控。处理思路有三种丢帧、降帧率、降分辨率。实操中我用的组合是“先丢帧 再降码率”队列超过阈值就丢帧连续多次超过阈值就把bitrate降一档。这样用户体验是录制的流畅度略有下降但不会出现长时间卡顿或崩溃。if (encoder.encodeQueueSize 10) { // 情况严重降低一档码率 degradeQuality(); }降低码率不需要重建编码器可以调用encoder.configure更新配置但要确保新的宽高等参数和采集尺寸一致否则编码器会报错。5. 第三步MP4 封装与文件下载5.1 接入 mp4-muxer封装这一步直接决定了最后能不能得到一个播放器可识别的 MP4。初始化 muxer 时需要告诉它视频编码格式、分辨率以及目标输出方式import { Muxer, ArrayBufferTarget } from mp4-muxer; let muxer new Muxer({ target: new ArrayBufferTarget(), video: { codec: avc, width: videoWidth, height: videoHeight, }, fastStart: in-memory, firstTimestampBehavior: offset, });codec: avc表示视频轨是 H.264。fastStart: in-memory会让 muxer 在 finalize 时把 moov 盒子放到文件头部这样播放器打开文件就能立刻开始播放不用等整个文件下载完。代价是最后会在内存里重新组装一遍完整的 ArrayBuffer这种方案对 5 分钟以内的短视频完全没问题。firstTimestampBehavior: offset是我强烈建议开启的选项。屏幕采集的VideoFrame自带时间戳这个时间戳可能来自采集设备的时钟不是从 0 开始的。如果不做偏移封装出来的 MP4 时间线会非常混乱可能出现首帧画面丢失、播放器无法定位等问题。设置成offset后muxer 会把所有 chunk 的时间戳统一偏移到从 0 开始。在VideoEncoder的 output 回调里直接把 chunk 和 meta 都丢给 muxeroutput: (chunk, meta) { muxer.addVideoChunk(chunk, meta); },meta参数里带decoderConfig里面包含 H.264 的 avcC 描述信息mp4-muxer 会自己处理不需要我们解析二进制。5.2 avcC description 的处理很多第一次用 WebCodecs 封装 MP4 的人会遇到一个奇怪的问题录出来的 MP4 在浏览器里能播放放到 Windows 自带的播放器或某些老旧播放器里就是黑屏或绿屏。这种情况八成是 MP4 里的 avcC 盒子没写好。avcC 盒子里装着 H.264 的 Profile、Level、SPS、PPS 等信息解码器需要靠它才能正确初始化解码上下文。WebCodecs 在编码器首次输出时会把 decoderConfig.description 作为 ArrayBuffer 传给 metamp4-muxer 会在内部把它写入 avcC。只要你在 output 回调里把meta完整传给muxer.addVideoChunk(chunk, meta)这个流程是自动的。如果 meta 里始终没有 description就要检查 codec 配置是否有效、编码器是否真的初始化成功了。一般来说Chrome 和 Edge 都会提供完整 description。5.3 导出下载与进度提示录制结束后的收尾顺序很重要不能直接 muxer.finalize必须等编码器把队列里的帧全部处理完async function stopRecording() { recording false; await encoder.flush(); muxer.finalize(); const { buffer } muxer.target; saveBlob(buffer); }encoder.flush()会等待所有待编码的帧完成编码并触发 output 回调。muxer.finalize()会写入 moov 等元数据完成 MP4 的收尾。保存文件最简单的方式是用 Blob 加 a 标签触发下载function saveBlob(arrayBuffer) { const blob new Blob([arrayBuffer], { type: video/mp4 }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download recording-${Date.now()}.mp4; a.click(); URL.revokeObjectURL(url); }如果文件很大或者你希望用户选择保存位置也可以用 File System Access APIconst handle await window.showSaveFilePicker({ suggestedName: recording.mp4, }); const writable await handle.createWritable(); await writable.write(arrayBuffer); await writable.close();这种方式的体验更像桌面软件不经过下载列表直接落到用户指定目录。但注意它需要 HTTPS 环境且用户在保存弹窗里手动确认。6. 进阶录制音频与长时间录制6.1 音频采集的两种思路很多录屏功能对画质的认可度往往取决于声音是否清晰。视频画面的链路我们前面已经跑通了音频的难点在于getDisplayMedia返回的音频轨道不能直接交给 WebCodecs 编码必须先通过 Web Audio API 转成 PCM 数据再封装成AudioData交给AudioEncoder。第一种思路是继续走 WebCodecs 路线链路是屏幕流的 audio track →AudioContext的createMediaStreamSource→AudioWorkletNode里拿 PCM → 组装AudioData→AudioEncoder编码成 AAC → 交给同一个 muxer。这种方式优点是可以精确控制编码参数并和视频轨在同一个时间轴上封装最终得到一个带声音的 MP4。第二种思路是偷懒方案让视频走 WebCodecs音频单独用MediaRecorder录成一个 webm 或 mp3录完之后再找工具把两条轨道合并。但这需要额外的后处理工具对纯前端来说并不现实。所以我实际走的还是第一条思路虽然代码量多一些但整套流程在同一套 API 体系内可控性最好。6.2 AudioEncoder 处理链路音频编码器的配置长这样const audioEncoder new AudioEncoder({ output: (chunk, meta) { muxer.addAudioChunk(chunk, meta); }, error: console.error, }); audioEncoder.configure({ codec: mp4a.40.2, // AAC-LC sampleRate: 48000, numberOfChannels: 2, bitrate: 128000, });在AudioWorkletProcessor的process方法里我们拿到的是多个声道的 Float32Array。需要把这些 PCM 数据整理成AudioData核心代码骨架是这样class PCMCaptureProcessor extends AudioWorkletProcessor { process(inputs) { const input inputs[0]; if (input input[0]) { const channel0 input[0]; const channel1 input[1] || input[0]; // 组装成 AudioData 后交给 audioEncoder // 注意 timestamp 要按采样数换算成微秒 } return true; } }AudioData的构造需要传入format、sampleRate、numberOfFrames、numberOfChannels、timestamp和data其中timestamp的单位是微秒。这部分代码量不小而且不同浏览器的 AudioWorklet 输入通道布局有细微差异我建议封装成独立的AudioRecorder模块不要和视频编码逻辑混在同一个文件里。在初始化 muxer 的时候如果打算加音频轨记得在配置里补上音频信息let muxer new Muxer({ target: new ArrayBufferTarget(), video: { codec: avc, width: videoWidth, height: videoHeight }, audio: { codec: aac, numberOfChannels: 2, sampleRate: 48000 }, });6.3 长录制时的内存优化前面一直用ArrayBufferTarget它的特点是实现简单、适合短视频。但如果要录 1 小时以上按 5 Mbps 估算一个小时的文件约 2.25GB全部堆在内存里肯定不现实。长时间录制的方案是走FileSystemWritableFileStream边编码边把数据写进文件系统。mp4-muxer 本身支持面向 Stream 的 target 设计可以把每个EncodedVideoChunk持续写入一个可写文件流而不是攒到内存里。这种模式对录制时长几乎无上限内存占用稳定。使用流式写入时要注意fastStart的选择。内存模式可以用in-memory方便生成 moov 在前的文件但流式场景更推荐fragmented也就是分片 MP4播放器不需要等 moov 就能边下边播。代价是一部分老旧播放器对 fragmented MP4 的兼容性稍差。如果你的产物最终要在各种播放器里打开我建议还是控制在 30 分钟内用内存方案最省心。另外用户点击“停止共享”或系统主动断开屏幕流时视频轨道会触发ended事件这时候要主动完成停止流程videoTrack.addEventListener(ended, () { stopRecording(); });这个事件很容易被忽略。用户点了一下系统里的“停止共享”代码如果还在pumpFrames里傻等录制永远不会正常结束最终导出的文件也是损坏的。7. 常见问题排查表与避坑经验7.1 问题速查表在实际开发和灰度测试中我遇到最多的问题集中在兼容性、内存和时间戳三类整理成表方便排查现象可能原因解决办法VideoEncoder是 undefined浏览器版本过低或禁用了 WebCodecs升级 Chrome/Edge或降级到 MediaRecorderisConfigSupported返回 falsecodec 字符串不被支持换avc1.64001f再试或用 VP9/AV1录制出来的视频没有声音音频轨没采集到或系统策略禁用确认audio: true检查系统输入权限画面明显卡顿编码队列堆积CPU 跟不上丢帧、降帧率、降码率拖动进度条很慢关键帧太少每 60-90 帧强制一个关键帧播放器提示文件损坏没执行flush就finalize等encoder.flush()完成后再finalize文件很大明显超过预期码率过高或关键帧太密集检查码率和 I 帧间隔导出后绿屏或黑屏avcC description 缺失或时间戳偏移异常确认 meta 完整传入 muxer启用firstTimestampBehavior: offset7.2 我踩过的 3 个坑第一个坑是忘记frame.close()。这是最隐蔽也最致命的问题因为录制的前几分钟内存表现正常等时间一长内存曲线开始失控最终标签页崩溃。排查时我用 Chrome 任务管理器看到了Shared memory那一项疯狂上涨。所有VideoFrame都必须在使用完后close()包括被丢弃的帧。第二个坑是时间戳没有做偏移。有一次录完导出前几秒画面是正常的但进度条一拖就黑屏播放器时间也显示得很奇怪。后来发现是VideoFrame自带的 timestamp 来自采集设备的时钟不是从 0 开始的。只要 muxer 设了firstTimestampBehavior: offset这个问题就消失了。千万不要自己在外面手动减一个起始时间直接用 muxer 的偏移能力更稳定。第三个坑是在output回调里做了耗时操作。我刚接入 mp4-muxer 时在回调里把 chunk 转成 ArrayBuffer 并 push 到数组同时在页面上更新“已录制时长”。结果画面一复杂就掉帧原因是output回调的执行频率很高任何同步 I/O 或者 DOM 操作都会拖慢编码线程。正确的做法是 output 里只做数据传递UI 更新放到单独的定时器里每 500ms 刷新一次就够了。7.3 后续可以怎么扩展这套方案跑通后可以扩展的方向很多。要做水印和文字标注可以在编码前用 canvas 把帧画一遍再new VideoFrame(canvas)水和文案的位置、透明度都可以自己控制。要录制指定区域可以用VideoFrame的visibleRect参数裁剪只把屏幕的一部分编码进去。想录制多个音频源比如系统声音和麦克风分开存可以走多 AudioEncoder 的思路但 MP4 多音轨封装需要 muxer 的额外支持复杂度会高一些。如果想把这套能力产品化我建议再加上“录制前设备检测”开工前先探测一次编码器能力把不支持的浏览器用户挡在录制按钮之前而不是等录完才发现文件打不开。实际上我自己在这个项目里最深的体会是WebCodecs 把视频编码的“黑盒”打开了一条缝真正决定效果优劣的还是那些老生常谈的工程问题——内存管理、时间戳、背压、兼容性降级。把这些细节处理好纯前端录制高清 MP4 绝对可行而且比集成任何第三方录屏 SDK 都要省钱、可控。如果时间允许下一步可以把音频采集的 AudioWorklet 模块单独抽出来优化把长时间录制的流式写入也补上这样整套方案就从“能跑”变成了“靠谱”。
返回列表