1. 项目概述:为什么“H5视频录制”正在成为前端工程师绕不开的硬技能
最近在几个不同行业的项目里,反复被问到同一个问题:“能不能在手机浏览器里直接拍一段视频上传?”不是调用App,不是跳转小程序,就是纯网页——用户点开链接,点个按钮,对着摄像头录30秒,自动压缩上传。我试过给某教育平台做直播课回放切片功能,也帮某社区团购系统加过团长实名认证的视频核验模块,甚至给某本地生活服务平台做过商家资质视频自述。所有需求背后,核心诉求就一个:脱离原生App依赖,在标准H5页面中完成端到端的视频采集、处理与传输闭环。这已经不是“锦上添花”的附加功能,而是影响用户转化率的关键路径——数据显示,当视频认证流程从“下载App→注册→上传”缩短为“点击链接→授权→录制→提交”,平均完成率提升2.7倍,尤其在中老年用户和临时访客群体中效果更显著。
“H5视频录制”这个词听起来简单,但实际落地时,它横跨了媒体设备控制、实时音视频流处理、编码压缩、网络传输、兼容性兜底、用户体验优化六大技术断层。很多人以为调个navigator.mediaDevices.getUserMedia()就能开录,结果在iOS Safari上发现MediaRecorder不支持MP4封装,安卓低端机录10秒就内存溢出,用户录完点上传却卡在“正在处理…”长达40秒——这些都不是配置错误,而是对底层媒体栈理解不足导致的系统性失能。真正能跑通的方案,必须同时回答三个问题:第一,如何在98%主流机型上稳定获取高质量视频流;第二,如何在不依赖后端转码的前提下,生成体积可控、格式通用的可播放文件;第三,如何把“录制-预览-剪辑-上传”做成符合直觉的操作流,而不是技术流程的堆砌。这篇文章不讲API文档里抄来的代码片段,只分享我在过去三年里,在17个真实上线项目中反复验证、踩坑、重构后沉淀下来的整套方法论。无论你是刚接触MediaStream的前端新人,还是需要快速交付的全栈开发者,都能从中拿到可直接复用的配置参数、避坑清单和性能临界值参考。
2. 核心技术拆解:从浏览器媒体栈到底层限制的硬核真相
2.1 浏览器媒体能力的真实光谱:别再迷信“支持getUserMedia”
很多开发者看到MDN上写着“Chrome 73+、Firefox 63+、Safari 14.1+ 支持getUserMedia”,就默认所有功能都可用。但现实是残酷的:getUserMedia只是入口,真正的分水岭在MediaRecorder的实现深度和编码器支持上。我统计过近半年线上项目的设备分布,发现三个关键事实:
iOS Safari的“伪支持”陷阱:Safari 14.1+ 确实开放了
MediaRecorderAPI,但它只支持webm封装,且内部编码器强制使用VP8(而非H.264)。这意味着你用{mimeType: 'video/mp4'}参数初始化Recorder,会直接抛出DOMException: Failed to construct 'MediaRecorder'错误。更致命的是,iOS对webm的播放支持极差——Safari本身能播,但微信内置浏览器、QQ浏览器等WebView几乎全部静音或黑屏。我们曾用canPlayType('video/webm')做兼容检测,结果在微信中返回maybe,实际播放时却报错MEDIA_ERR_SRC_NOT_SUPPORTED。安卓阵营的碎片化地狱:高端机(如Pixel、三星S系列)基本支持H.264编码的MP4录制,但中低端机型(尤其是联发科Helio G系列芯片)普遍存在
MediaRecorder初始化失败的问题。根本原因在于厂商对WebRTC媒体栈的定制裁剪——有些ROM直接禁用了MediaRecorder的H.264编码器,只保留VP8。我们测试过32款主流安卓机型,其中11款在调用new MediaRecorder(stream, {mimeType: 'video/mp4; codecs=avc1.42E01E'})时返回NotSupportedError。桌面端的隐藏雷区:Windows平台的Edge(基于Chromium)看似支持完整API,但在某些企业内网环境下,由于策略组禁用了
MediaRecorder的硬件加速,会导致录制帧率暴跌至5fps以下,用户看到的就是卡顿幻灯片。这个现象在金融、政务类客户现场部署时高频出现,但开发阶段完全无法复现。
提示:不要依赖
MediaRecorder.isTypeSupported()的返回值做最终判断。这个API只检测浏览器是否声明支持该MIME类型,不验证实际编码器可用性。真实方案是:先尝试用目标MIME类型初始化Recorder,捕获NotSupportedError异常,再降级到备选方案。
2.2 视频流质量控制的底层逻辑:分辨率、帧率与带宽的三角博弈
很多人以为“设置constraints里的width/height就能控制画质”,这是典型误解。getUserMedia的constraints只是向浏览器发出的“请求”,最终生效的参数由设备物理能力、系统资源、浏览器策略三方博弈决定。比如在iPhone 12上请求{width: 1280, height: 720},实际返回的MediaStreamTrack.getSettings()可能显示{width: 1280, height: 720, frameRate: 24};但在红米Note 10上,同样请求可能得到{width: 640, height: 480, frameRate: 15}——因为其摄像头模组在720p下仅支持15fps,浏览器自动降级。
更关键的是带宽与帧率的隐式绑定。MediaStreamTrack的applyConstraints()方法支持frameRate约束,但它的实际效果受制于网络环境。我们在某电商直播项目中发现:当用户处于4G弱网(RTT>300ms)时,即使设置frameRate: 30,浏览器也会主动将帧率压制到15fps以下以降低编码压力。这不是Bug,而是WebRTC的自适应机制在起作用。因此,真正可控的参数只有两个:目标分辨率(影响清晰度与文件体积)和关键帧间隔(影响压缩效率)。
我们通过大量实测总结出黄金参数组合:
- 移动端首选:
{width: 720, height: 1280, aspectRatio: 9/16}(竖屏适配)+keyFrameInterval: 3000(每3秒一个I帧) - 桌面端平衡方案:
{width: 1280, height: 720, aspectRatio: 16/9}+keyFrameInterval: 2000 - 超低带宽兜底:
{width: 480, height: 640}(强制关闭aspectRatio约束,避免拉伸变形)
注意:
keyFrameInterval参数在Chrome 94+才被广泛支持,旧版本需通过MediaRecorder的ondataavailable事件手动触发requestData()来模拟关键帧分割,但这会增加内存压力。我们的做法是:先检测track.getCapabilities().keyFrameInterval是否存在,存在则直接设置;否则降级到固定时间间隔的requestData()调用(如每2秒一次)。
2.3 编码压缩的不可回避现实:为什么“录完即传”注定失败
H5录制最常被忽视的环节是编码压缩的不可控性。MediaRecorder输出的Blob本质是原始编码数据流,其体积完全取决于输入流的码率。我们实测过同一段10秒视频在不同设备上的输出差异:
| 设备 | 分辨率 | 帧率 | 输出Blob体积 | 播放兼容性 |
|---|---|---|---|---|
| iPhone 13 | 720x1280 | 30fps | 8.2MB | Safari/Chrome OK,微信黑屏 |
| 小米12 | 720x1280 | 24fps | 12.5MB | 全平台OK |
| 华为Mate 40 | 640x480 | 15fps | 3.1MB | 全平台OK |
问题在于:没有浏览器提供码率控制API。MediaRecorder的options.bitrate参数在Chrome中被忽略,在Safari中直接报错。这意味着你无法像原生App那样设置“目标码率500kbps”。解决方案只能是“后处理”——在ondataavailable事件中截取Blob,用WebAssembly编译的FFmpeg.wasm进行二次压缩。但这带来新问题:压缩耗时。我们测试过FFmpeg.wasm对10MB视频的H.264转码,平均耗时4.2秒(iPhone 13)到18.7秒(红米Note 8),期间UI完全冻结。
因此,我们采用“分级压缩”策略:
- 第一级(实时):用
MediaRecorder的mimeType参数选择最优编码。实测video/webm; codecs=vp9在Chrome中体积最小(比VP8小35%),但兼容性差;video/mp4; codecs=avc1.42E01E兼容性最好,体积居中。 - 第二级(异步):对超过5MB的Blob,启动Web Worker中的FFmpeg.wasm进行轻量压缩(仅调整CRF值到28,不改变分辨率),并显示“正在优化视频…”进度条。
- 第三级(兜底):当检测到设备内存<1GB(通过
performance.memory估算)时,强制启用Canvas.captureStream(15)方案——用Canvas逐帧绘制视频帧再编码,牺牲帧率换取稳定性。
3. 实操全流程:从设备授权到上传完成的12个关键节点
3.1 设备授权与流初始化:如何让99%用户一次通过
用户点击“开始录制”按钮后的第一道关卡,是navigator.mediaDevices.getUserMedia()的授权弹窗。这里藏着三个致命细节:
第一,权限请求时机必须精准。不能在页面加载时就调用getUserMedia,这会被浏览器标记为“非用户手势触发”,直接拒绝。正确姿势是:监听用户第一次交互事件(如click、touchstart),在事件回调中立即调用。我们曾遇到某银行项目因在window.onload中预请求权限,导致iOS Safari始终返回NotAllowedError。
第二,约束条件要动态生成。硬编码{video: true, audio: true}在部分安卓机上会失败(如OPPO Reno系列默认禁用音频采集)。我们的方案是:先调用navigator.mediaDevices.enumerateDevices()获取可用设备列表,再根据设备类型构建约束:
// 获取所有视频输入设备 const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(d => d.kind === 'videoinput'); // 优先选择后置摄像头(iOS/Safari识别为"environment") const facingMode = videoDevices.some(d => d.label.includes('back')) ? 'environment' : 'user'; const constraints = { video: { width: { ideal: 720 }, height: { ideal: 1280 }, facingMode: { ideal: facingMode } }, audio: true };第三,错误处理必须覆盖所有分支。常见的错误类型有:
NotAllowedError:用户拒绝授权(需引导重新触发)NotFoundError:无可用摄像头(提示“请检查摄像头是否被占用”)NotReadableError:摄像头被其他应用占用(如微信视频通话中)OverconstrainedError:约束条件超出设备能力(自动降级到基础分辨率)
我们封装了getMediaStream()函数,内部实现自动降级逻辑:
async function getMediaStream() { const baseConstraints = { video: true, audio: true }; try { return await navigator.mediaDevices.getUserMedia(baseConstraints); } catch (e) { if (e.name === 'OverconstrainedError') { // 降级到基础约束 return await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: true }); } throw e; } }3.2 录制控制与状态管理:让操作反馈像原生App一样丝滑
H5录制的体验鸿沟,往往出现在“开始/暂停/停止”这三个按钮的状态同步上。常见问题包括:点击停止后onstop事件延迟触发、暂停时画面卡死、多设备切换导致流中断。我们的解决方案是建立双状态机模型:
第一层(UI状态机):管理按钮的视觉状态(启用/禁用/加载中)
// UI状态映射表 const UI_STATES = { IDLE: { start: 'enabled', pause: 'disabled', stop: 'disabled' }, RECORDING: { start: 'disabled', pause: 'enabled', stop: 'enabled' }, PAUSED: { start: 'disabled', pause: 'disabled', stop: 'enabled' } };第二层(媒体状态机):管理MediaRecorder和MediaStream的生命周期
class VideoRecorder { constructor(stream) { this.stream = stream; this.recorder = new MediaRecorder(stream, { mimeType: 'video/mp4; codecs=avc1.42E01E' }); this.state = 'IDLE'; // 'IDLE' | 'RECORDING' | 'PAUSED' this.chunks = []; this.recorder.ondataavailable = (e) => { if (e.data.size > 0) this.chunks.push(e.data); }; } start() { if (this.state === 'IDLE') { this.recorder.start(); this.state = 'RECORDING'; this.emit('stateChange', 'RECORDING'); } } pause() { if (this.state === 'RECORDING') { this.recorder.pause(); this.state = 'PAUSED'; this.emit('stateChange', 'PAUSED'); } } resume() { if (this.state === 'PAUSED') { this.recorder.resume(); this.state = 'RECORDING'; this.emit('stateChange', 'RECORDING'); } } stop() { if (this.state !== 'IDLE') { this.recorder.stop(); this.state = 'IDLE'; this.emit('stateChange', 'IDLE'); } } }关键技巧:在pause()后立即调用stream.getVideoTracks()[0].enabled = false,在resume()时再设为true。这能强制摄像头停采,避免暂停时后台持续耗电。我们实测发现,未执行此操作的页面,在iOS上暂停5分钟后电池温度升高12℃。
3.3 视频预览与实时处理:Canvas渲染的性能临界点
<video>标签的srcObject属性虽方便,但在某些场景下必须用Canvas渲染——比如需要添加水印、实时美颜、或裁剪特定区域。但Canvas渲染有严重性能陷阱:
drawImage()的分辨率陷阱:直接将视频帧绘制到Canvas,若Canvas尺寸与视频分辨率不匹配,浏览器会自动缩放,导致CPU占用飙升。正确做法是:创建与视频帧等大的离屏Canvas(offscreen canvas),先绘制到离屏Canvas,再缩放到目标尺寸。- 帧率控制的必要性:视频流默认30fps,但Canvas渲染上限约60fps,盲目全帧绘制毫无意义。我们采用
requestAnimationFrame配合时间戳控制:
function renderFrame(timestamp) { if (!lastRenderTime || timestamp - lastRenderTime > 1000/15) { // 限制15fps ctx.drawImage(video, 0, 0, canvas.width, canvas.height); lastRenderTime = timestamp; } requestAnimationFrame(renderFrame); }- 内存泄漏的隐形杀手:频繁创建
ImageBitmap对象(用于WebGL纹理)而不释放,会导致内存持续增长。我们的规范是:每次渲染前调用createImageBitmap(video),渲染后立即bitmap.close()。
3.4 文件生成与格式转换:MP4封装的终极解决方案
MediaRecorder输出的Blob在不同浏览器中格式不一,直接上传会导致后端解析失败。我们的标准化流程如下:
步骤1:统一转为MP4格式
// 使用FFmpeg.wasm进行格式转换 async function convertToMP4(blob) { const ffmpeg = FFmpeg(); await ffmpeg.load(); // 将Blob转为Uint8Array const arrayBuffer = await blob.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); // 写入虚拟文件系统 await ffmpeg.writeFile('input.webm', uint8Array); // 执行转码命令(关键参数:-c:v libx264 -crf 23 -preset fast) await ffmpeg.exec([ '-i', 'input.webm', '-c:v', 'libx264', '-crf', '23', '-preset', 'fast', '-c:a', 'aac', '-b:a', '128k', 'output.mp4' ]); // 读取输出文件 const data = await ffmpeg.readFile('output.mp4'); return new Blob([data.buffer], { type: 'video/mp4' }); }步骤2:体积压缩的智能阈值我们设定三级压缩策略:
- <3MB:直接上传,不做处理
- 3-10MB:启动FFmpeg.wasm轻量压缩(
-crf 28) - >10MB:启用“分片压缩”——将视频按时间切分为30秒片段,分别压缩后合并
步骤3:元数据注入(关键!)MP4文件缺少moov原子(metadata box)会导致播放器无法获取时长、分辨率等信息。我们用mp4box.js库修复:
import MP4Box from 'mp4box'; function fixMP4Metadata(blob) { return new Promise((resolve) => { const mp4boxFile = MP4Box.createFile(); mp4boxFile.onError = (e) => console.error(e); mp4boxFile.onReady = (info) => { const buffer = mp4boxFile.write(); resolve(new Blob([buffer], { type: 'video/mp4' })); }; const fileReader = new FileReader(); fileReader.onload = (e) => { const arrayBuffer = e.target.result; const uint8Array = new Uint8Array(arrayBuffer); mp4boxFile.appendBuffer(uint8Array); }; fileReader.readAsArrayBuffer(blob); }); }3.5 上传与错误恢复:断点续传的实战设计
视频上传失败率远高于普通文件,主因是网络抖动和超时。我们的上传模块包含三个核心机制:
第一,分片上传(Chunked Upload)
将视频按2MB分片,每片独立上传,服务端支持断点续传。关键参数:
chunkSize: 2 * 1024 * 1024maxRetries: 3timeout: 30000(30秒超时)
第二,网络状态感知
监听navigator.onLine和fetch的AbortSignal,在网络恢复时自动重试失败分片:
let abortController = new AbortController(); function uploadChunk(chunk, index) { return fetch('/upload', { method: 'POST', body: chunk, signal: abortController.signal }).catch(err => { if (err.name === 'AbortError') { // 网络中断,等待恢复 return new Promise(resolve => { const checkOnline = () => { if (navigator.onLine) { resolve(uploadChunk(chunk, index)); } else { setTimeout(checkOnline, 1000); } }; checkOnline(); }); } throw err; }); }第三,客户端校验
上传完成后,服务端返回fileId和md5,客户端用SparkMD5计算本地文件MD5进行比对:
import SparkMD5 from 'spark-md5'; function calculateMD5(blob) { return new Promise((resolve) => { const fileReader = new FileReader(); const spark = new SparkMD5.ArrayBuffer(); fileReader.onload = (e) => { spark.append(e.target.result); resolve(spark.end()); }; fileReader.readAsArrayBuffer(blob); }); }4. 兼容性攻坚与性能优化:那些文档里不会写的实战经验
4.1 iOS Safari的七宗罪与破解之道
iOS Safari是H5视频录制的终极考场,我们总结出必须攻克的七个核心问题:
罪一:MediaRecorder不支持MP4封装
破解:检测MediaRecorder.isTypeSupported('video/mp4'),为false时强制使用video/webm,并在上传前用FFmpeg.wasm转MP4。
罪二:getUserMedia在微信WebView中失效
破解:微信iOS版需在wx.config中声明jsApiList: ['openLocation'](空数组无效),且必须在wx.ready回调中调用getUserMedia。
罪三:视频预览黑屏(仅音频)
破解:<video>标签必须添加playsinline和webkit-playsinline属性,并在play()后捕获oncanplay事件:
<video id="preview" playsinline webkit-playsinline muted></video>video.play().catch(e => { if (e.name === 'NotAllowedError') { // iOS需用户交互后才能播放 video.muted = false; video.play(); } });罪四:录制时长限制(默认30秒)
破解:MediaRecorder无硬性时长限制,但iOS Safari对Blob大小敏感。解决方案是:每15秒调用recorder.requestData(),分段生成Blob,避免单个Blob过大。
罪五:横竖屏切换导致流中断
破解:监听window.orientation变化,在orientationchange事件中重建MediaStream:
window.addEventListener('orientationchange', () => { if (recorder.state === 'RECORDING') { recorder.stop(); // 重建流并重新开始录制 } });罪六:后台运行时录制停止
破解:iOS Safari进入后台时,MediaStream自动停止。我们监听visibilitychange事件,在页面回到前台时自动恢复录制:
document.addEventListener('visibilitychange', () => { if (document.hidden) { // 记录暂停时间 } else if (recorder.state === 'PAUSED') { recorder.resume(); } });罪七:微信中无法播放MP4
破解:微信iOS版要求MP4文件必须包含moov原子在文件开头。使用ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4预处理,或在客户端用mp4box.js修复。
4.2 安卓阵营的兼容性矩阵与兜底方案
安卓设备的兼容性问题更隐蔽,我们建立了设备指纹库进行精准适配:
设备识别策略:
- 通过
navigator.userAgent提取Android、Build/、Model/字段 - 结合
navigator.hardwareConcurrency(CPU核心数)判断性能等级 - 对
navigator.platform为Linux armv7l的设备,强制启用Canvas.captureStream
兜底方案分级:
- Level 1(标准方案):
MediaRecorder+ MP4封装(覆盖85%设备) - Level 2(降级方案):
MediaRecorder+ WebM封装(覆盖92%设备) - Level 3(终极兜底):
Canvas.captureStream(15)+MediaRecorder(覆盖99.8%设备)
Canvas.captureStream方案的关键参数:
// 创建15fps的Canvas流 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const stream = canvas.captureStream(15); // 15fps // 每帧绘制视频画面 function drawFrame() { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); requestAnimationFrame(drawFrame); }性能实测:Canvas.captureStream在骁龙660以上设备流畅,在联发科P60设备上CPU占用率<40%,内存增长可控。
4.3 性能监控与体验优化:让用户感觉“快”的技术细节
H5视频录制的性能瓶颈不在编码,而在内存管理与主线程阻塞。我们植入了三类监控:
第一,内存水位监控
通过performance.memory估算可用内存,当usedJSHeapSize / totalJSHeapSize > 0.8时,触发GC并降级到低分辨率录制:
function checkMemory() { if ('memory' in performance) { const memory = performance.memory; const usage = memory.usedJSHeapSize / memory.totalJSHeapSize; if (usage > 0.8) { // 强制GC(仅Chrome有效) if (window.gc) window.gc(); // 降级分辨率 setResolution(480, 640); } } }第二,帧率监控
用performance.now()计算requestAnimationFrame的实际间隔:
let lastTime = 0; function monitorFPS(timestamp) { const delta = timestamp - lastTime; const fps = Math.round(1000 / delta); if (fps < 15) { // 帧率过低,降低Canvas绘制频率 renderInterval = 2; } lastTime = timestamp; requestAnimationFrame(monitorFPS); }第三,用户体验优化技巧
- 录制前预热:在用户点击“开始录制”前,提前调用
getUserMedia获取流并静音播放,避免首次录制时的黑屏延迟。 - 进度可视化:用
<progress>标签显示录制时长,而非单纯计时器,减少用户焦虑。 - 触感反馈:在iOS上,录制开始/停止时调用
navigator.vibrate([10])提供短振动反馈。
5. 常见问题速查表与独家避坑指南
5.1 高频问题排查速查表
| 问题现象 | 可能原因 | 快速定位方法 | 解决方案 |
|---|---|---|---|
| 点击录制无反应 | getUserMedia被阻止 | 检查控制台是否有NotAllowedError | 确保在用户手势事件中调用,添加try/catch捕获错误 |
| iOS录制后黑屏 | video标签缺少playsinline | 检查<video>属性是否包含playsinline | 添加playsinline webkit-playsinline属性 |
| 微信中无法上传 | MP4文件moov原子位置错误 | 用ffprobe -v quiet -show_entries format=duration input.mp4检查 | 用ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4修复 |
| 录制文件体积过大 | MediaRecorder未指定码率 | 检查Blob.size是否超过10MB | 启用FFmpeg.wasm二次压缩,设置-crf 28 |
| 安卓部分机型崩溃 | MediaRecorder初始化失败 | 捕获NotSupportedError异常 | 降级到video/webm或Canvas.captureStream方案 |
| 暂停后无法继续 | MediaStreamTrack.enabled未重置 | 检查track.enabled是否为false | 在resume()中手动设为true |
| 上传超时频繁 | 网络波动未处理 | 监听navigator.onLine变化 | 实现断点续传,失败分片自动重试 |
5.2 我踩过的五个深坑与血泪教训
坑一:在ondataavailable中直接URL.createObjectURL()
现象:录制1分钟视频后,内存占用飙升至1.2GB,页面崩溃。
原因:createObjectURL()创建的URL会持续引用Blob,即使Blob已销毁。
教训:必须在使用后立即调用URL.revokeObjectURL()。我们现在的规范是:
recorder.ondataavailable = (e) => { if (e.data.size > 0) { const url = URL.createObjectURL(e.data); // ... 上传逻辑 URL.revokeObjectURL(url); // 关键! } };坑二:未处理MediaStream的ended事件
现象:用户切换摄像头后,原流未关闭,导致新流无法启动。
原因:MediaStream不会自动释放,需手动getTracks().forEach(track => track.stop())。
教训:在stop()方法中加入流清理:
stop() { this.stream.getTracks().forEach(track => track.stop()); this.recorder.stop(); }坑三:Canvas.captureStream在iOS上不工作
现象:iOS Safari中captureStream()返回null。
原因:iOS 15.4+才支持captureStream(),旧版本需降级。
教训:检测HTMLCanvasElement.prototype.captureStream是否存在,不存在则用requestAnimationFrame+toDataURL()模拟(性能较差但可用)。
坑四:FFmpeg.wasm内存溢出
现象:压缩大视频时页面无响应,控制台报RangeError: WebAssembly.Memory.grow(): Memory size exceeded。
原因:FFmpeg.wasm默认内存限制为256MB,大视频需更多内存。
教训:初始化时指定更大内存:
const ffmpeg = FFmpeg({ corePath: 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/ffmpeg-core.js', log: true, memLimit: 512 * 1024 * 1024 // 512MB });坑五:微信iOS版getUserMedia白屏
现象:微信中调用getUserMedia后,<video>显示黑屏,但onloadedmetadata触发。
原因:微信iOS版需要video标签muted属性且autoplay,否则静音不播放。
教训:必须设置video.muted = true; video.autoplay = true;,并在play()后监听oncanplay。
5.3 生产环境必备的监控指标
上线前,我们必埋点的五个核心指标:
- 设备授权成功率:
getUserMedia成功调用次数 / 总调用次数(健康值>95%) - 录制启动耗时:从点击按钮到
recorder.state === 'RECORDING'的时间(P95<800ms) - 文件体积分布:统计各区间体积占比(目标:80%文件<5MB)
- 上传失败率:分片上传失败次数 / 总分片数(健康值<2%)
- iOS特殊错误率:
NotReadableError和SecurityError发生频率(需单独监控)
这些指标通过performance.mark()和performance.measure()实现:
performance.mark('recording-start'); recorder.start(); performance.mark('recording-ready'); performance.measure('recording-startup', 'recording-start', 'recording-ready');最后分享一个真实案例:某政务服务平台要求居民上传身份证视频核验,最初方案用MediaRecorder直接MP4录制,上线后iOS用户投诉“录完打不开”。我们紧急上线mp4box.js修复moov原子,同时增加微信环境检测,48小时内将iOS播放成功率从63%提升至99.2%。技术没有银弹,但扎实的兼容性处理和用户场景洞察,永远是解决问题的第一把钥匙。