音视频API的代码审查与普通业务接口不同,它涉及真实硬件设备的访问、系统权限的授予,以及底层采集与编码的异步回调。一个看似能正常跑通的录制功能,可能因为轨道未停止而导致摄像头指示灯常亮,也可能因为AudioContext没有关闭而持续占用音频设备。审查时不能只看主流程是否返回预期结果,而要沿着资源从创建到销毁的完整路径逐一核对。

一、资源生命周期与释放审查
音视频API中最常见的问题是资源泄漏。以getUserMedia为例,调用成功后返回的MediaStream包含一个或多个MediaStreamTrack,这些轨道直接对应底层摄像头或麦克风资源。如果页面停止使用后只移除<video>元素的srcObject,而不显式调用track.stop(),部分浏览器会保持设备占用,导致系统隐私指示灯无法熄灭,甚至影响其他应用的设备访问。
审查资源释放时,需要关注两条路径:正常结束和异常退出。正常结束路径包括用户点击停止按钮、路由切换、组件卸载等,应当统一调用一个cleanup函数来停止所有轨道。异常退出路径包括权限被系统收回、设备拔出、页面进入后台等,这些场景可能触发track的ended事件,也可能不触发,需要同时监听track.onended并执行相同的清理逻辑。比较稳妥的写法如下:
function attachStream(stream) {
const video = document.querySelector('video');
video.srcObject = stream;
const tracks = stream.getTracks();
tracks.forEach(track => {
track.addEventListener('ended', () => cleanupTracks(tracks));
});
}
function cleanupTracks(tracks) {
tracks.forEach(track => {
if (track.readyState !== 'ended') {
track.stop();
}
});
}
function teardownStream(stream) {
if (!stream) return;
const tracks = stream.getTracks();
cleanupTracks(tracks);
const video = document.querySelector('video');
if (video) video.srcObject = null;
}
上面代码将清理逻辑集中到cleanupTracks函数,并在正常teardownStream和track.onended两条路径中复用,避免只在一处释放。审查时如果发现stop()调用出现在多个组件或页面中且没有统一入口,应当要求重构,否则后续需求变化时非常容易漏改。
AudioContext同样需要重点检查。Web Audio API中,创建一个AudioContext会占用系统音频输出资源,部分平台还会阻止页面进入空闲状态。页面关闭音频播放后必须调用audioContext.close()。审查中如果看到只将音量归零或者暂停振荡器,但没有关闭上下文,就应当标记为资源泄漏。另一个常见错误是闭包中保存oscillator节点引用却没有start或stop,或者stop后没有断连节点,导致整个图无法被垃圾回收。
二、错误处理与权限状态审查
音视频API的调用结果高度依赖运行环境,同样的代码在桌面浏览器和移动端浏览器、HTTPS和HTTP页面、系统权限开启和关闭状态下表现可能完全不同。因此审查时不能默认getUserMedia一定成功,必须逐行确认失败分支是否完整。
navigator.mediaDevices.getUserMedia返回一个Promise,常见拒绝类型包括NotAllowedError、NotFoundError、NotReadableError和OverconstrainedError。仅使用一个通用catch打印错误日志是不够的,因为不同错误对应的用户提示和恢复策略不同。例如NotAllowedError通常说明用户拒绝了权限,需要引导用户到浏览器地址栏或系统设置中开启;NotFoundError可能表示设备未连接,需要提示检查硬件;OverconstrainedError则说明请求的分辨率、帧率或设备ID不符合当前设备能力,需要降级约束重新尝试。审查时建议要求使用error.name分支处理,而不是依赖error.message字符串比较,因为不同浏览器的message文案不稳定。
async function startCamera() {
const constraints = {
video: { width: { ideal: 1920 }, height: { ideal: 1080 } },
audio: true
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
return stream;
} catch (error) {
switch (error.name) {
case 'NotAllowedError':
showPermissionGuide();
break;
case 'NotFoundError':
showNoDeviceTip();
break;
case 'OverconstrainedError':
return startCameraWithFallback();
default:
showUnknownError();
}
}
}
async function startCameraWithFallback() {
const fallback = { video: true, audio: true };
return navigator.mediaDevices.getUserMedia(fallback);
}
此外,getDisplayMedia用于屏幕共享时,权限模型与getUserMedia不同。某些浏览器中用户每次共享都会弹出选择器,且一旦用户点击取消会立刻抛出NotAllowedError,但该错误并不代表权限被永久拒绝。审查时需要确认屏幕共享取消后不会弹出生硬的权限错误提示,而是静默退出或温和提示可重试。
MediaRecorder的审查重点则在于状态检查。MediaRecorder实例有inactive、recording、paused三个状态,调用start、pause、resume、stop之前必须先判断当前状态,否则可能抛出InvalidStateError。比如stop()在inactive状态下调用会直接报错,但很多组件在页面卸载时会无条件调用stop以避免泄漏,这反而会制造异常。正确做法是在stop前检查mediaRecorder.state !== 'inactive'。
三、时序与状态机审查
音视频API大量使用事件驱动,数据并非同步返回。MediaRecorder的数据通过ondataavailable事件分片输出,Web Audio的节点则需要在正确的时机连接和断开。审查时如果发现依赖固定延时来保证数据完整,比如停止录制后setTimeout 500毫秒再上传,通常说明没有正确使用事件语义。
MediaRecorder的正确采集方式是监听ondataavailable把每个Blob累积到数组,并在onstop事件中合并它们生成最终文件。stop触发后,最后一个分片会以dataavailable事件形式派发,但这个派发顺序可能晚于stop语句返回,因此不能在调用stop后立刻读取数组。正确写法是:
const chunks = [];
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp8' });
recorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: recorder.mimeType });
const url = URL.createObjectURL(blob);
uploadBlob(blob);
URL.revokeObjectURL(url);
};
function stopRecording() {
if (recorder.state !== 'inactive') {
recorder.stop();
}
}
另一个常见时序问题是轨道结束与播放元素状态的同步。当远端WebRTC连接断开或本端设备拔出时,track可能触发ended事件,但<video>元素的readyState不会自动回到初始状态,如果继续读取video.currentTime或调用play()可能得到异常。审查时应要求在track.onended回调中同步更新UI状态和播放控件,而不是只依赖视频元素的pause事件。
Web Audio中,AudioBufferSourceNode是一次性节点,调用start后到达音频末尾会自动结束,不能再复用。很多开发者会在循环播放需求中反复调用同一个source.start,第二次调用就会抛错。审查时需要确认每次播放都新建节点,并在onended中释放引用。此外,使用AudioContext.decodeAudioData解码音频文件时,旧版本浏览器要求用回调函数而非Promise返回结果,如果项目需要兼容老版本Safari,必须同时处理两种接口形态。
四、性能与兼容性审查
音视频功能对CPU和内存的压力远高于普通页面逻辑,审查代码时要从约束参数、数据路径和对象回收三个方面评估性能影响。
采集阶段,getUserMedia默认返回设备支持的最高分辨率,在低端设备上可能导致编码开销过大。审查时应该确认是否根据设备能力设置合理的ideal或max约束,而不是无脑请求1920x1080。同时,不要把多个独立的getUserMedia调用串联在一起后又保留前面不需要的流,每个页面最好只有一个活跃的输入流,并复用该流创建不同输出目标。
传输和录制阶段,MediaRecorder的mimeType选择直接影响兼容性和文件大小。video/mp4在部分桌面浏览器中不被支持,而video/webm;codecs=vp9在高分辨率下编码速度较差。审查时需要看到类似的选型代码,并且有降级回退逻辑:
function getMimeType() {
const candidates = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm',
'video/mp4'
];
for (const type of candidates) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return '';
}
Web Audio中的性能审查则集中在节点连接管理。每创建一个节点就会消耗一部分处理资源,如果不断创建GainNode或BiquadFilter而没有在停止后断开连接,音频图会越来越大,即使原本的source已经结束,节点仍然保留在图中。审查时应当要求每个临时节点都有明确的connect和disconnect配对。对于长期运行的场景,优先使用参数自动化调度,如gain.setValueAtTime配合linearRampToValueAtTime,而不是用定时器频繁修改参数值。
兼容性方面还要注意两点。一是AudioContext在移动端浏览器中初始状态常为suspended,必须等待用户手势后调用audioContext.resume(),审查时要确认播放按钮事件中包含了恢复上下文逻辑;二是在处理二进制数据时,Blob和ArrayBuffer的转换不能假设所有浏览器都支持Blob.arrayBuffer(),稳妥做法是先使用FileReader或response.arrayBuffer。这些细节往往藏在音视频API的调用缝隙里,评审时逐项对照比事后压测更能发现稳定性和兼容性问题。
音视频代码审查的核心,是把每一次资源创建都当作一次必须闭环的事务。从getUserMedia返回的流到AudioContext建立的节点图,从MediaRecorder的状态迁移到各浏览器的能力差异,只有将这些入口和出口都纳入审查清单,才能让音视频功能在上线后稳定运行,而不是依赖用户端的偶现反馈来反向修复。