web录音封装
因为工作需求,需要一个录音功能,所以封装了一个录音类(库)
一、实现目标
我们希望封装一个具备以下特性的录音工具类:
✅ 支持 开始 / 停止录音
✅ 支持录音状态管理(
idle、recording、stopped、error)✅ 可通过回调函数监听录音状态与错误
✅ 录制结果可获取为 Blob 或 URL
✅ 自动清理媒体资源,防止内存泄漏
✅ 基于 TypeScript,类型安全、可扩展
二、核心实现思路
浏览器提供了两大关键 API:
**
navigator.mediaDevices.getUserMedia()**:请求麦克风权限并返回音频流(MediaStream)**
MediaRecorder**:将音频流录制为二进制数据(Blob)
我们基于这两者构建一个面向对象封装,负责完整的录音生命周期管理。
三、完整代码实现
// src/AudioRecorder.ts
// 定义录音器的状态类型,利用 TypeScript 的字面量类型提高代码可读性和健壮性
type RecorderStatus = 'idle' | 'recording' | 'stopped' | 'error';
// 定义构造函数中可以传入的选项
interface AudioRecorderOptions {
onStatusChange?: (status: RecorderStatus) => void;
onError?: (error: Error) => void;
}
export class AudioRecorder {
private mediaStream: MediaStream | null = null;
private mediaRecorder: MediaRecorder | null = null;
private audioChunks: Blob[] = [];
private audioBlob: Blob | null = null;
private audioUrl: string | null = null;
private status: RecorderStatus = 'idle';
// 回调函数
private onStatusChange: ((status: RecorderStatus) => void) | null = null;
private onError: ((error: Error) => void) | null = null;
constructor(options?: AudioRecorderOptions) {
if (options) {
this.onStatusChange = options.onStatusChange || null;
this.onError = options.onError || null;
}
}
private setStatus(status: RecorderStatus) {
this.status = status;
this.onStatusChange?.(this.status);
}
/**
* 开始录音
*/
public async start(): Promise<void> {
if (this.status === 'recording') {
console.warn('Recorder is already recording.');
return;
}
try {
// 1. 请求麦克风权限并获取媒体流
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 2. 创建 MediaRecorder 实例
this.mediaRecorder = new MediaRecorder(this.mediaStream);
// 3. 清空上一次的录音数据
this.audioChunks = [];
this.audioBlob = null;
this.audioUrl = null;
// 4. 定义 dataavailable 事件:当有音频数据块可用时触发
this.mediaRecorder.addEventListener('dataavailable', (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data);
}
});
// 5. 定义 stop 事件:当录音停止时触发
this.mediaRecorder.addEventListener('stop', () => {
// 将所有音频块合成为一个 Blob 对象
this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' }); // 或者使用 'audio/webm' 等
// 创建一个 URL 以便在 <audio> 标签中播放
this.audioUrl = URL.createObjectURL(this.audioBlob);
// 释放媒体流资源
this.cleanup();
this.setStatus('stopped');
});
// 6. 开始录音
this.mediaRecorder.start();
this.setStatus('recording');
} catch (err) {
const error = err as Error;
console.error('Error starting recording:', error);
this.onError?.(error);
this.setStatus('error');
// 清理以防部分初始化成功
this.cleanup();
}
}
/**
* 停止录音
*/
public stop(): void {
if (!this.mediaRecorder || this.status !== 'recording') {
console.warn('Recorder is not recording or not initialized.');
return;
}
// stop() 会触发上面定义的 'stop' 事件
this.mediaRecorder.stop();
}
/**
* 获取录音状态
*/
public getStatus(): RecorderStatus {
return this.status;
}
/**
* 获取录音文件的 Blob 对象
*/
public getAudioBlob(): Blob | null {
return this.audioBlob;
}
/**
* 获取可播放的录音文件 URL
*/
public getAudioUrl(): string | null {
return this.audioUrl;
}
/**
* 清理资源,停止麦克风轨道
*/
private cleanup(): void {
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = null;
}
this.mediaRecorder = null;
}
/**
* 公共的销毁方法,用于外部调用(例如 Vue 组件卸载时)
*/
public dispose(): void {
if (this.status === 'recording') {
this.stop();
}
this.cleanup();
console.log('Recorder disposed.');
}
}四、功能拆解与设计亮点
1️⃣ 状态管理更清晰
使用 TypeScript 的 字面量类型(**RecorderStatus**) 来定义录音状态,有效避免硬编码和拼写错误:
type RecorderStatus = 'idle' | 'recording' | 'stopped' | 'error';2️⃣ 生命周期安全管理
录音涉及硬件资源(麦克风),一旦不释放就可能占用系统设备或导致浏览器报错。
我们通过 cleanup() 方法统一释放资源:
private cleanup(): void {
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = null;
}
this.mediaRecorder = null;
}
这在 Vue 或 React 组件卸载时尤为重要,可搭配:
onUnmounted(() => recorder.dispose());3️⃣ 录音结果可直接播放或上传
录音结束后,会自动生成一个可播放的本地 URL:
this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
this.audioUrl = URL.createObjectURL(this.audioBlob);
你可以直接绑定到 <audio> 标签播放:
<audio :src="recorder.getAudioUrl()" controls></audio>
或将 audioBlob 上传至服务器:
const formData = new FormData();
formData.append('file', recorder.getAudioBlob(), 'recording.wav');
await fetch('/upload', { method: 'POST', body: formData });
五、使用实例
// main.ts 或组件内逻辑中
import { AudioRecorder } from './AudioRecorder';
// 创建实例
const recorder = new AudioRecorder({
onStatusChange: (status) => {
console.log('状态变化:', status);
statusText.textContent = status;
},
onError: (err) => {
console.error('录音错误:', err);
alert('录音失败:' + err.message);
},
});
// 获取 DOM 元素
const startBtn = document.getElementById('startBtn')!;
const stopBtn = document.getElementById('stopBtn')!;
const audioPlayer = document.getElementById('audioPlayer') as HTMLAudioElement;
const statusText = document.getElementById('statusText')!;
// 按钮事件
startBtn.addEventListener('click', async () => {
await recorder.start();
});
stopBtn.addEventListener('click', () => {
recorder.stop();
// 延迟一点时间,确保 audioBlob 已生成
setTimeout(() => {
const url = recorder.getAudioUrl();
if (url) {
audioPlayer.src = url;
audioPlayer.play();
}
}, 500);
});
<div>
<p>录音状态:<span id="statusText">idle</span></p>
<button id="startBtn">开始录音</button>
<button id="stopBtn">停止录音</button>
<br /><br />
<audio id="audioPlayer" controls></audio>
</div>
六、扩展建议
🎚️ 支持音量检测:通过
AudioContext+AnalyserNode实时绘制音量波形。🕐 支持录音时长限制:在
start()时设置定时器自动停止。💾 添加持久化功能:可将 Blob 保存到 IndexedDB,实现断点续录。
🧩 支持多格式导出:通过
MediaRecorder.mimeType指定格式(如audio/webm、audio/mp3)。🔒 权限检测与降级提示:在调用前检测
navigator.mediaDevices是否存在。