如何在浏览器中录音并将录音片段上传
背景
在现代 Web 开发中,浏览器支持通过 Web API 进行音频录制,本文将详细介绍如何使用 MediaRecorder
API 进行浏览器端音频录制,并通过 HTTP 请求上传录音片段。我们将从以下几个方面进行介绍
一、获取音频输入权限
在开始录音之前,我们需要确认用户设备上是否有音频输入设备(如麦克风)。这通过 navigator.mediaDevices.enumerateDevices()
方法来获取设备列表,并检查是否包含类型为 audioinput
的设备。
public async check(): Promise<number> {
try {
const devices = await window.navigator.mediaDevices.enumerateDevices();
const hasAudioInput = devices.some((device) => device.kind === 'audioinput');
console.log('[SpeechBase] 音频权限', hasAudioInput ? 1 : 0);
return hasAudioInput ? 1 : 0;
} catch (err) {
console.error('[SpeechBase] 异常', err);
return 0;
}
}
enumerateDevices()
:列出设备列表,查找是否有音频输入设备。- 返回值:
1
表示有音频设备,0
表示没有。
二、初始化录音设备并开始录音
在用户设备有音频输入权限的前提下,我们使用 getUserMedia()
获取音频流,并创建一个 MediaRecorder
实例来录制音频。MediaRecorder
会根据传入的音频流开始录制,并支持在录制结束后获取录音数据。
public async initialize(config: { onStop: (buffers: Blob[]) => void; onError: (msg: string) => void }) {
const { onStop, onError } = config;
try {
this.stopTracks();
this.buffers = [];
console.log('[SpeechBase] start initialize');
this.userMedia = await window.navigator.mediaDevices.getUserMedia({ audio: true });
console.log('[SpeechBase] init userMedia');
this.recorder = new window.MediaRecorder(this.userMedia, { audioBitsPerSecond: 64000 });
console.log('[SpeechBase] init recorder');
this.recorder.ondataavailable = (event: any) => {
this.buffers.push(event.data);
};
this.recorder.onstop = () => {
onStop(this.buffers);
this.buffers.length = 0;
};
return true;
} catch (error) {
this.userMedia = null;
this.recorder = null;
console.error('[SpeechBase] createVoice has error', error);
onError && onError('打开录音失败');
}
}
getUserMedia({ audio: true })
:请求访问音频设备。MediaRecorder
:用于录制音频流,支持不同的音频比特率配置。ondataavailable
:每当录制到一个音频片段时,会触发此回调并将数据添加到buffers
数组中。
三、停止录音并上传音频片段
录音完成后,我们需要将音频数据上传到服务器。我们使用 axios
向服务器发送 HTTP 请求,将录制的音频片段上传。音频数据需要转换为 Blob
格式并指定其 MIME 类型。
public async recognizeAudio(options: { url: string; params: any; data: Blob[] }): Promise<any> {
const { url, params, data } = options;
const blob = new Blob(data, { type: 'audio/webm; codecs=opus' });
try {
const response = await axios.post(url, blob, {
headers: {
'Content-Type': 'audio/x-raw',
'param': JSON.stringify(params),
},
timeout: 5000,
});
console.log('[SpeechBase]: ', response.data);
return response.data;
} catch (error) {
console.error('[SpeechBase]: ', error);
throw error;
}
}
new Blob(data, { type: 'audio/webm; codecs=opus' })
:将录音片段转换为Blob
对象,并指定 MIME 类型。axios.post(url, blob)
:发送 POST 请求,将音频数据上传到服务器。
四、开始录音和停止录音
我们提供了两个方法:start()
和 close()
,分别用于开始和停止录音。start()
方法启动录音,而 close()
方法停止录音并触发 onStop
回调,将录音数据传递给服务器。
public async start() {
if (this.recorder && this.recorder.state === 'inactive') {
this.buffers = [];
this.recorder.start();
return;
}
console.warn('[SpeechBase] Recorder is already recording or not initialized.');
}
public async close() {
if (this.recorder && this.recorder.state === 'recording') {
console.log('[SpeechBase]: stop');
this.recorder.stop();
this.buffers = [];
return;
}
console.warn('[SpeechBase] Recorder is already inactive or not initialized.');
}
this.recorder.start()
:开始录音。this.recorder.stop()
:停止录音,触发onstop
事件并将音频数据上传。
五、销毁资源的重要性
每次调用 initialize
方法时,都需要确保音频轨道被正确地停止。如果不调用 stopTracks()
,会导致一些问题:
- 多重音频流问题:每次
initialize
时,如果前一个录音会话未结束并且没有停止音频轨道,那么会存在多个音频流并行运行。这不仅会浪费资源,还可能导致应用的性能下降,甚至造成浏览器崩溃。 - 内存泄漏:音频流(
userMedia
)和录音器(MediaRecorder
)如果没有正确停止和销毁,可能会导致内存泄漏。每次初始化都可能增加额外的音频流和录音器对象,而这些对象在未停止时会持续占用内存。 - 音频设备冲突:如果前一次的音频设备(如麦克风)没有被释放,可能会造成冲突。下一次尝试获取麦克风权限时,浏览器可能会报错,或者无法正确初始化录音设备。 为了避免这些问题,我们需要在每次调用
initialize
方法之前,先调用stopTracks()
来停止之前的音频轨道,释放占用的资源。
public stopTracks() {
if (this.userMedia && this.userMedia.getTracks) {
this.userMedia.getTracks().forEach((item: any) => item.stop && item.stop());
this.userMedia = null;
}
}
this.userMedia.getTracks().forEach((item) => item.stop())
:停止音频轨道,确保不再占用硬件资源。this.userMedia = null
:释放音频流对象。
六、stop & dispose 区别
在音频录制和资源管理的上下文中,dispose
和 stop
都是用于释放或结束资源的操作,但它们有不同的使用场景和目的。以下是这两个方法的区别:
1. stop
stop
方法通常是用来停止正在进行的某个操作或过程。在音频录制的场景中,stop
通常用于停止正在录制的音频流或停止某个正在进行的活动,而不一定涉及到完全销毁或清理所有相关资源。
在你的代码中,stop
方法主要用来停止录音操作。它会调用 MediaRecorder
的 stop()
方法,停止录音,并触发 onstop
回调,通常会将音频数据(Blob[]
)传递给后续处理函数。
public async close() {
if (this.recorder && this.recorder.state === 'recording') {
console.log('[SpeechBase]: stop');
this.recorder.stop(); // 停止录音
this.buffers = []; // 清空缓存的音频数据
return;
}
console.warn('[SpeechBase] Recorder is already inactive or not initialized.');
}
- 作用:停止音频录制,并触发相关的回调或后续操作。
- 用途:停止正在进行的活动(例如录音或视频播放)。
2. dispose
dispose
方法通常是用于彻底清理和释放资源的操作。它不仅停止当前的操作,还会确保所有与该操作相关的资源都被释放和清理掉,以防止内存泄漏或不必要的资源占用。dispose
通常意味着一个对象生命周期的结束,确保不再占用任何资源。dispose
方法用于完全销毁录音相关的资源,确保在不再使用时释放掉所有的音频流、录音器以及其他占用的资源。
public dispose() {
console.log('[SpeechBase]: dispose');
this.close(); // 停止录音
this.recorder = null; // 释放录音器
this.stopTracks(); // 停止音频轨道并释放资源
}
- 作用:彻底清理和销毁资源,确保对象的所有资源都被释放。
- 用途:对象生命周期结束时使用,通常用于防止内存泄漏和资源冲突。
主要区别
方法 | 作用 | 使用场景 |
---|---|---|
stop | 停止正在进行的活动(如录音、播放),通常不释放对象或资源。 | 用于停止当前操作(如停止录音、停止视频播放等)。 |
dispose | 释放所有与对象相关的资源并销毁对象,确保没有剩余的内存或资源占用。 | 用于清理和销毁对象,通常在对象不再需要时调用,避免资源泄漏。 |
举例
stop
:只需要停止录音但不打算销毁所有相关资源时,可以使用stop
方法。例如,用户停止录音时,我们仅需要停止录音并触发回调处理结果,但录音器对象 (recorder
) 和音频轨道 (userMedia
) 仍然可以被重新使用。dispose
:不再需要录音功能,或者用户退出了录音模块时,应该调用dispose
方法彻底销毁所有与录音相关的资源,防止内存泄漏或不必要的资源占用。如果每次初始化和销毁都不清理资源,可能会导致多个音频流并行存在,影响系统性能,甚至造成崩溃。
总结
stop
是用来停止某个活动(如录音或播放),但它不会销毁资源,适用于短期的停止操作。dispose
是用来销毁对象并释放所有资源,适用于对象生命周期结束时的清理工作,确保没有资源泄漏。
六、综合实现:录音并上传
通过创建一个 SpeechEngine
类,我们封装了上述录音和上传的功能,并可以灵活地配置服务器的 URL 和请求参数。
class SpeechEngine extends SpeechBase {
public srOpts: ISROpts = {
server: {
sr: ServerOpt.http,
url: `${location.protocol}`,
},
onResult: () => {},
};
private isRecording: boolean = false;
public async sr(srOpts: ISROpts) {
if (this.isRecording) {
return;
}
this.isRecording = true;
this.srOpts = { ...this.srOpts, ...srOpts };
const { onResult, server, serverParams } = this.srOpts;
await this.initialize({
onStop: async (data: any) => {
const { sr = '', url = '' } = server || {};
if (sr === ServerOpt.http && url) {
try {
if (this.isRecording) {
return;
}
const result = await this.recognizeAudio({ url, params: serverParams, data });
onResult(result);
} catch (error) {
if (this.srOpts.onError) { this.srOpts.onError('网络异常'); }
}
}
},
onError: (msg: string) => {
this.isRecording = false;
if (this.srOpts.onError) { this.srOpts.onError(msg); }
},
});
if (this.isRecording) {
this.start();
this.srOpts.onStart && this.srOpts.onStart();
} else {
this.dispose();
}
}
}
通过这种方式,我们实现了浏览器端录音和录音数据上传的完整流程。SpeechEngine
类管理录音的启动、停止、数据上传等功能,并通过回调通知应用程序录音结果。