跳到主要内容

浏览器媒体 API

浏览器媒体 API分别负责音视频的采集、处理、录制、播放和实时传输。拿到麦克风流不等于录好了文件,录到音频也不等于识别出了文字。做语音输入、视频预览或在线通话时,先分清这些职责,才能解释权限、失败和资源释放分别发生在哪里。

面试讨论媒体项目,重点是讲清数据从哪里来、经过哪些处理、交给谁消费,以及用户取消或离开后怎样停止。浏览器已经提供的能力和业务代码自己的职责,应分开说明。

1. 面试核心​

先记住这几条,再读正文
  • 采集、录制、语音识别和播放是不同步骤。getUserMedia() 返回轨道集合,MediaRecorder 产出编码数据,识别服务才把语音转换为文字。卡 · 媒体对象与职责
  • 媒体权限与异步生命周期需要一起处理。安全上下文、设备、约束和用户授权都可能影响采集;用户取消页面操作后,迟到的流仍要释放。卡 · 取消后到达的摄像头流
  • 暂停播放、禁用轨道、停止轨道和停止录制作用不同。清理时应按资源所有权处理轨道、录制器、播放器与对象 URL。卡 · 暂停、静音与释放
  • 录制结果要等最后的数据事件完成后再使用。分片时间不是精确时钟,单个分片也不保证可以独立播放;长录制要控制内存和上传积压。卡 · 等待最后一个数据事件
  • play() 是可能失败的异步操作。媒体容器、编码、浏览器能力、加载与自动播放策略共同影响结果,不能只看文件后缀。卡 · 播放、格式与缓冲
  • 音频优化必须对应验收指标。降噪、回声消除和自动增益不是同一个问题,听感、识别质量、延迟与设备成本需要一起比较。卡 · 降噪、回声与验收
  • 实时音视频 SDK 不替代应用的权限、身份、状态和清理设计。介绍 TRTC 等项目时,要说明自己负责的链路及故障处理,不把 SDK 内部能力说成自己实现。卡 · 综合题

2. 媒体对象与职责​

MediaStreamTrack 表示一条音频或视频轨道;MediaStream 把相关轨道组织在一起。例如一个摄像头加麦克风的流,可以包含一条视频轨道和一条音频轨道。它表示持续产生的媒体,不是一个已经完成的文件。

需求主要接口或组件产出或作用
摄像头、麦克风采集navigator.mediaDevices.getUserMedia()包含轨道的 MediaStream
用户选择屏幕或窗口getDisplayMedia()用户选择的显示内容流
将流录成数据MediaRecorder多次 dataavailable 交付的 Blob
音频节点处理与分析Web Audio API音频处理图、分析结果或输出流
语音转文字SpeechRecognition 或独立识别服务识别结果与错误事件
文字转语音speechSynthesis合成语音播放
本地或远程媒体播放<audio>、<video>播放状态、时间与错误事件
实时音视频连接RTCPeerConnection 或上层 SDK传输轨道、连接状态与统计

Web Audio API 用相互连接的音频节点处理音频,例如调整增益、滤波或分析音量。它不是语音识别模型,也不意味着任意滤波都能可靠去除噪声。

getDisplayMedia() 需要用户选择共享目标,调用通常还要求短暂的用户激活;不能把一次摄像头授权理解为以后可以静默共享屏幕。屏幕共享的可用音频来源也依赖浏览器和操作系统。

3. 采集权限与生命周期​

3.1 约束、设置与失败​

getUserMedia() 需要安全上下文和相应许可。开发中的可信本地地址可以例外支持,生产页面应使用 HTTPS;嵌入页面还受顶层页面和 Permissions Policy 限制。界面应由明确的用户操作开始采集,并说明采集对象。

约束描述希望获取什么媒体。ideal 表达偏好,exact 表达必须满足的要求;例如把一个设备不支持的分辨率设为精确要求,可能直接失败。getSettings() 返回实际应用的设置,不能把请求参数直接当成最终结果。

现象或错误应检查什么
navigator.mediaDevices 不可用上下文安全性、浏览器能力
NotAllowedError用户许可、系统许可、页面策略等限制
NotFoundError是否存在符合请求种类的设备
OverconstrainedError哪个必须满足的约束无法实现
NotReadableError授权后设备是否仍无法被读取
Promise 长时间不结束用户可能没有选择;不能假定必然很快成功或失败

错误名能缩小范围,但并不总能唯一证明原因。不要把所有失败都展示成“请刷新”。

3.2 取消后到达的摄像头流​

采集请求没有一个通用的 AbortSignal 参数可让业务取消权限弹窗。因此,页面中的“停止”既要释放已经拿到的流,也要让稍后成功的旧请求失效。

下面的独立页面只请求摄像头。generation 标识本次开始是否仍然有效,pending 防止重复发起未结束的请求:

<button id="start" type="button">开始预览</button>
<button id="stop" type="button">停止</button>
<p id="status" role="status">未开始</p>
<video id="preview" muted playsinline></video>
<script>
const preview = document.querySelector('#preview');
const startButton = document.querySelector('#start');
const status = document.querySelector('#status');
let stream = null;
let pending = false;
let generation = 0;

function release(value) {
value?.getTracks().forEach(track => track.stop());
}
function stopPreview() {
generation += 1;
preview.pause();
preview.srcObject = null;
release(stream);
stream = null;
status.textContent = '已停止';
}
async function startPreview() {
if (pending || stream) return;
const current = ++generation;
pending = true;
startButton.disabled = true;
status.textContent = '等待摄像头';
let acquired = null;
try {
if (!navigator.mediaDevices?.getUserMedia) {
throw new Error('当前环境不支持采集');
}
acquired = await navigator.mediaDevices.getUserMedia({
audio: false,
video: { width: { ideal: 640 }, height: { ideal: 360 } }
});
if (current !== generation) {
release(acquired);
return;
}
stream = acquired;
preview.srcObject = stream;
await preview.play();
if (current === generation) status.textContent = '预览中';
} catch (error) {
release(acquired);
if (current === generation) {
preview.srcObject = null;
stream = null;
status.textContent = `无法预览:${error.name}`;
}
} finally {
pending = false;
startButton.disabled = false;
}
}
startButton.addEventListener('click', startPreview);
document.querySelector('#stop').addEventListener('click', stopPreview);
window.addEventListener('pagehide', stopPreview);
</script>

点击停止会增加 generation。即使原来的授权随后成功,current 已经过期,得到的轨道会立刻停止,不会重新挂到 preview 上。若用户一直没有处理权限请求,pending 会保持为真;这里选择等待原请求结束,不不断弹出新的请求。

组件实现还需要在卸载时移除监听器,并管理设备断开时的 ended 事件。主动调用 track.stop() 会把该轨道的 readyState 设为 ended,但不会因此给它派发 ended 事件,所以自己的停止逻辑应直接更新界面。

3.3 暂停、静音与释放​

操作直接作用不能据此推断的事
video.pause()暂停播放器摄像头轨道已停止
video.muted = true关闭这个播放器的声音麦克风没有采集或没有发送
track.enabled = false该轨道输出静音或黑帧轨道生命周期已结束
track.stop()结束这一条轨道使用同一来源的其他轨道也结束
recorder.stop()结束录制并交付剩余数据输入轨道已停止
video.srcObject = null解除播放器对流的使用流中所有轨道都被释放

克隆轨道可能共享底层来源,但具有独立生命周期。停止一个克隆不等于停止其他克隆;设备是否还在使用,要看其他消费者是否仍持有活动轨道。负责创建或接管轨道的组件,应明确谁最终调用 stop()。

4. 录制与结果交付​

4.1 等待最后一个数据事件​

MediaRecorder 把输入流编码为可保存的媒体数据。录制器有 inactive、recording、paused 等状态;停止后会交付最后的数据,再触发 stop 事件。停止方法返回时,不能立刻假定所有数据已经到齐。

下面的函数只录制输入流中的音频轨道。调用方负责释放输入轨道;函数用 done 交付完成结果,用 stop() 请求结束:

function recordAudio(stream, { maxBytes = 16 * 1024 * 1024 } = {}) {
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
throw new RangeError('maxBytes must be a positive safe integer');
}
const tracks = stream.getAudioTracks();
if (!tracks.some(track => track.readyState === 'live')) {
throw new Error('No live audio track');
}
const mimeType = ['audio/webm;codecs=opus', 'audio/mp4']
.find(type => MediaRecorder.isTypeSupported(type));
const recorder = new MediaRecorder(
new MediaStream(tracks), mimeType ? { mimeType } : {}
);
const chunks = [];
let bytes = 0;
let failure = null;
const stop = () => {
if (recorder.state !== 'inactive') recorder.stop();
};
const done = new Promise((resolve, reject) => {
recorder.addEventListener('dataavailable', event => {
if (failure || event.data.size === 0) return;
bytes += event.data.size;
if (bytes > maxBytes) {
failure = new Error('Recording exceeds maxBytes');
chunks.length = 0;
stop();
return;
}
chunks.push(event.data);
});
recorder.addEventListener('error', event => {
failure = event.error || new Error('Recording failed');
});
recorder.addEventListener('stop', () => {
if (failure) reject(failure);
else resolve(new Blob(chunks, { type: recorder.mimeType }));
chunks.length = 0;
}, { once: true });
recorder.start(250);
});
return { stop, done };
}

done 成功时才得到完整 Blob。函数不硬编码“所有浏览器都支持 WebM”,而是检测候选格式;即使 isTypeSupported() 返回真,运行中也可能因资源等原因失败。异步错误通过 done 返回,调用方必须消费这个 Promise。

maxBytes 限制本例累计保留的分片,不是整个浏览器进程的内存上限。编码器或单个迟到分片仍可能占用额外内存;长录制应设计持续存储或上传、队列上限、超时与失败处理,不能把所有分片一直留在数组里。

4.2 输入轨道由谁释放​

下面的函数请求麦克风并录制约两秒,成功或失败后都释放自己取得的轨道:

async function captureClip() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true },
video: false
});
let timer;
let recording;
try {
recording = recordAudio(stream);
timer = setTimeout(recording.stop, 2000);
return await recording.done;
} finally {
clearTimeout(timer);
recording?.stop();
stream.getTracks().forEach(track => track.stop());
}
}

调用 captureClip() 后应处理返回的 Promise。约两秒是由定时任务请求停止,并非精确音频时长;用户没有处理授权时,本函数也不会在两秒后自动取消授权。交互式录音还应增加与摄像头示例类似的取消和过期结果处理。

recorder.start(250) 请求按时间片交付数据,但主线程忙、后台限制等都可能推迟事件。不能用“分片数乘 250”计算准确时长。一个分片可能依赖前面的容器信息,不能假定每片都是可独立播放的文件;同一次完整录制按顺序合并,与把多次独立录制直接拼接是不同情况。

5. 播放、格式与缓冲​

5.1 播放是异步状态变化​

play() 返回的 Promise 在开始播放时成功,也可能因权限、格式等问题失败。界面应根据实际结果显示播放状态,不能调用后立即把按钮改成“正在播放”。静音自动播放与有声自动播放的策略也不同。

录制得到的 Blob 可以创建对象 URL 交给播放器。下面的函数替换当前音频,并返回释放函数:

function attachRecording(audio, blob) {
const url = URL.createObjectURL(blob);
audio.src = url;
audio.controls = true;
let released = false;
return () => {
if (released) return;
released = true;
if (audio.getAttribute('src') === url) {
audio.pause();
audio.removeAttribute('src');
audio.load();
}
URL.revokeObjectURL(url);
};
}

调用方可在用户点击播放时执行并处理 audio.play(),替换录音或销毁组件时调用释放函数。不要在刚设置 src 后立刻撤销对象 URL;播放器还可能继续需要它。旧释放函数也不应清空后来已经替换的新音频,所以示例比较了当前 src。

跨标签页协调播放器时,通信事件只是让目标标签页调用自己的 pause() 等逻辑。通知机制与并发冲突边界见 浏览器上下文通信。

5.2 容器、编码与播放失败​

容器组织音视频轨道和时间等信息,编码决定媒体数据怎样压缩和还原。例如 MP4 是容器名称,不能只凭 .mp4 后缀判断其中每种编码都能在目标浏览器播放。

canPlayType() 可以探测给定 MIME 类型与编码描述的支持情况,但返回值不是完整文件播放成功的保证。服务端响应、文件是否完整、编解码能力、跨域使用方式和运行资源都可能影响实际结果。

preload 是加载提示;loadedmetadata 表示元数据可用,并不表示整个文件下载完成。播放器出现等待时,应结合网络请求、缓冲区间、错误事件和当前播放时间,区分下载不足、解码失败和播放策略问题。可见的进度条也不等于媒体字节已经全部加载。

普通文件播放、基于 Media Source Extensions 的分段追加和 WebRTC 实时轨道属于不同链路。前端需要根据点播、直播或通话的延迟与交互要求选择方案,不把 <video src="..."> 当成所有媒体协议的完整实现。

6. 语音输入与音频质量​

6.1 录音不等于语音识别​

语音输入通常先获得音频,再交给识别能力,最后把识别结果转换为可编辑文本。使用 Web Speech API 时,SpeechRecognition 通过事件返回识别结果;使用独立服务时,则要明确上传或流式协议、鉴权、结果顺序和断网处理。MediaRecorder 本身不会返回转写文本。

SpeechRecognition 的浏览器支持和识别实现存在差异,不能默认离线可用或音频始终留在本机。要核对目标环境、服务和用户授权;结果中间态、最终态、停止、异常和不支持时的文本输入降级也应有明确处理。

语音合成是反方向:speechSynthesis 把文字变成声音。项目中同时用了录音、识别和合成时,要分别说明各自的开始、结束和资源边界,不能用“Web Speech 一次处理了所有事”代替链路解释。

6.2 降噪、回声与验收​

回声消除主要处理扬声器声音被麦克风再次收录的问题,降噪尝试减少非目标背景噪声,自动增益调整音量水平。它们目的不同,也不能保证所有设备和噪声下都有效。

采集约束中的 echoCancellation、noiseSuppression、autoGainControl 可用于表达需求,再检查支持能力和实际设置。不要仅因代码里写了 noiseSuppression: true 就宣称已经实现可验收的降噪算法。

比较方案时保留相同的输入、设备、音量和网络条件,并至少观察:

  • 目标语音是否清楚,弱音和字尾是否被误删。
  • 回声、持续背景噪声和突发噪声分别有什么变化。
  • 识别结果的准确性与中间结果延迟是否改善。
  • CPU、内存、耗电与端到端延迟是否可接受。
  • 拒绝权限、设备切换、断网和后台恢复时能否正确结束或恢复。

AI 生成了一段处理代码,只能作为候选实现。要检查它是否真的使用了输入音频、输出是否接入实际链路,以及同一段样本在处理前后的结果。仅听一段安静环境录音,无法支持“复杂噪声已经解决”的结论。

7. 实时音视频与 SDK 边界​

WebRTC 提供实时媒体与数据连接能力。一个通话应用还需要交换协商信息的信令、身份与房间管理,以及网络连接建立和故障恢复。RTCPeerConnection 不会自动完成产品的登录、权限和业务规则;连接也不保证始终直接点对点,可能需要中继或媒体服务器。

TRTC 等 SDK 在这些能力之上提供房间、发布、订阅、事件和统计接口。项目回答应沿实际链路说明:

  1. 用户如何获得短期身份凭据并进入指定房间。
  2. 本地设备怎样采集、预览并发布,远端媒体怎样订阅和展示。
  3. 权限拒绝、网络中断、重复进入或设备变化如何影响界面状态。
  4. 退出时如何结束业务状态、停止相关媒体并释放实例。
  5. 哪些指标和日志支持质量判断,哪些工作由 SDK 或服务完成。

API 名称依版本而变,例如 TRTC Web SDK 4.x 与 5.x 的生命周期接口不同,应按项目实际版本说明。不能把另一个版本的示例拼进自己的调用链。SDK 使用者仍需管理自建的播放器、对象 URL、监听器和额外轨道,也不应在浏览器中放置用于签发身份凭据的服务端密钥。

8 · 面试题卡片​

本页题目与参考回答

8.1 网页语音输入怎样从麦克风得到文字?​

考察重点:采集、录制、识别与文本状态的职责。

相关内容:媒体对象与职责、录音不等于语音识别。

参考回答

采集获得音频,识别能力才把音频转成文字,录制只是把媒体保存为编码数据。 可以使用目标环境支持的 SpeechRecognition,也可以把音频交给独立识别服务。还要说明权限、结果中间态与最终态、取消、断网和不支持时的降级;不能把 MediaRecorder 的 Blob 当成转写文本。

8.2 用户点了停止,授权成功后为什么还会打开摄像头?​

考察重点:异步请求、过期结果与轨道生命周期。

相关内容:取消后到达的摄像头流。

参考回答

停止界面操作不等于取消了尚未完成的采集请求。 用请求代次或组件存活状态判断结果是否仍有效;过期请求一旦取得流,立即停止其轨道,不再绑定播放器。已经取得的流也要释放,权限请求持续等待时不能不断重复请求。

8.3 停止录制、暂停播放和停止轨道有什么区别?​

考察重点:对象职责与资源所有权。

相关内容:暂停、静音与释放、输入轨道由谁释放。

参考回答

停止录制结束编码数据的收集,暂停播放只影响播放器,停止轨道才结束这一条轨道。 还要解除播放器引用、撤销不再使用的对象 URL、清理监听器。停止一条轨道不保证共享来源的其他轨道结束,释放应由明确的所有者负责。

8.4 MediaRecorder 每 250 毫秒产生一片,能直接按片计时和逐片播放吗?​

考察重点:分片交付、计时、媒体容器与内存。

相关内容:等待最后一个数据事件、输入轨道由谁释放。

参考回答

不能保证分片精确按时到达,也不能保证每片都可独立播放。 用实际时间或媒体时间衡量时长,按顺序处理同一次录制的数据,并等最后的数据事件和 stop 完成。长录制需要持续存储或上传及队列上限,避免无限积累 Blob。

8.5 项目中的视频使用什么格式,播放失败时怎样排查?​

考察重点:容器与编码、加载、播放策略及状态。

相关内容:播放、格式与缓冲。

参考回答

先说明实际容器、编码和播放链路,再结合能力、网络与播放器错误定位。 文件后缀不能证明编码受支持,canPlayType 也不是完整播放保证。检查真实响应、文件完整性、缓冲与 play() 拒绝原因,区分格式、加载和自动播放策略,而不是统一归因于网速。

8.6 怎样判断前端语音降噪方案有效?​

考察重点:音频处理目标、受控对照与质量代价。

相关内容:降噪、回声与验收。

参考回答

先区分背景噪声、回声和音量问题,再用相同输入比较处理前后的结果。 检查目标语音是否保留、识别准确性、延迟、CPU 与耗电,并覆盖不同噪声、设备和网络条件。配置了降噪选项或拿到 AI 代码,都不能代替真实链路和验收证据。

8.7 使用 TRTC 的项目,怎样说明自己做了哪些工作?​

题型:综合题

考察重点:SDK 与业务职责、状态和故障处理。

相关内容:实时音视频与 SDK 边界、采集权限与生命周期。

参考回答

按项目实际版本说明身份与房间、采集与发布、订阅与展示、异常恢复及退出清理。 交代自己实现的业务状态、权限交互和诊断,明确哪些传输或媒体能力由 SDK 提供。用实际故障与测量支持选择,不把调用 SDK 等同于自己实现编解码和传输协议。

9. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明