浏览器媒体 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 在这些能力之上提供房间、发布、订阅、事件和统计接口。项目回答应沿实际链路说明:
- 用户如何获得短期身份凭据并进入指定房间。
- 本地设备怎样采集、预览并发布,远端媒体怎样订阅和展示。
- 权限拒绝、网络中断、重复进入或设备变化如何影响界面状态。
- 退出时如何结束业务状态、停止相关媒体并释放实例。
- 哪些指标和日志支持质量判断,哪些工作由 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. 参考资料
- W3C:Media Capture and Streams
- WHATWG HTML:Media elements
- W3C:Web Audio API
- W3C:MediaStream Recording
- MDN:getUserMedia()
- MDN:getDisplayMedia()
- MDN:MediaStreamTrack.stop()
- MDN:MediaRecorder
- MDN:dataavailable event
- MDN:HTMLMediaElement.play()
- MDN:Web Speech API
- WebRTC:Getting started
- TRTC:Web SDK Quick Start
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明