如何在ReactJS(TypeScript)中分别录制麦克风和扬声器音频到独立文件
如何在ReactJS(TypeScript)中分别录制麦克风和扬声器音频到独立文件
嗨,我刚好在React+TypeScript项目里实现过完全一样的需求!其实核心就是利用浏览器原生的Web Audio API和MediaRecorder API,不用装额外的第三方库,就能轻松把麦克风输入和系统扬声器输出分别录成独立的音频文件。下面是我亲测可行的完整方案:
一、核心思路拆解
我们需要分别获取两个独立的媒体流,再给每个流单独创建录制实例:
- 麦克风输入流:通过
navigator.mediaDevices.getUserMedia获取 - 系统扬声器输出流:通过
navigator.mediaDevices.getDisplayMedia获取(这个API原本用于录屏,但我们可以只请求音频轨道)
然后给每个流绑定独立的MediaRecorder,分别控制录制状态,最后导出各自的音频文件。
二、完整React+TypeScript组件实现
直接上可运行的代码,我加了详细注释,你可以直接复制到项目里测试:
import { useState, useRef, useCallback } from 'react'; // 定义录制状态的TypeScript类型 type RecordingState = 'idle' | 'recording' | 'paused'; // 定义录制完成的文件类型 type RecordedFile = { blob: Blob; name: string; type: 'microphone' | 'system-audio'; }; const AudioRecorder: React.FC = () => { // 组件状态管理 const [recordingState, setRecordingState] = useState<RecordingState>('idle'); const [recordedFiles, setRecordedFiles] = useState<RecordedFile[]>([]); const [error, setError] = useState<string | null>(null); // 用useRef保存录制器实例(避免组件重渲染丢失引用) const micRecorderRef = useRef<MediaRecorder | null>(null); const systemAudioRecorderRef = useRef<MediaRecorder | null>(null); // 临时存储录制的音频数据块 const micRecordingBlobsRef = useRef<Blob[]>([]); const systemAudioRecordingBlobsRef = useRef<Blob[]>([]); // 初始化媒体流和录制器 const initRecorders = useCallback(async () => { try { setError(null); // 1. 获取麦克风媒体流 const micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false, }); // 2. 获取系统音频流(扬声器输出) const systemAudioStream = await navigator.mediaDevices.getDisplayMedia({ video: false, audio: true, }); // 3. 初始化麦克风录制器 const micRecorder = new MediaRecorder(micStream); micRecorder.ondataavailable = (e) => { if (e.data.size > 0) micRecordingBlobsRef.current.push(e.data); }; micRecorder.onstop = () => { // 生成完整的麦克风音频文件 const micBlob = new Blob(micRecordingBlobsRef.current, { type: 'audio/webm' }); setRecordedFiles(prev => [...prev, { blob: micBlob, name: `麦克风录制_${new Date().toISOString().slice(0, 19)}.webm`, type: 'microphone' }]); micRecordingBlobsRef.current = []; // 释放麦克风流资源 micStream.getTracks().forEach(track => track.stop()); }; micRecorderRef.current = micRecorder; // 4. 初始化系统音频录制器 const systemAudioRecorder = new MediaRecorder(systemAudioStream); systemAudioRecorder.ondataavailable = (e) => { if (e.data.size > 0) systemAudioRecordingBlobsRef.current.push(e.data); }; systemAudioRecorder.onstop = () => { // 生成完整的系统音频文件 const systemAudioBlob = new Blob(systemAudioRecordingBlobsRef.current, { type: 'audio/webm' }); setRecordedFiles(prev => [...prev, { blob: systemAudioBlob, name: `系统音频录制_${new Date().toISOString().slice(0, 19)}.webm`, type: 'system-audio' }]); systemAudioRecordingBlobsRef.current = []; // 释放系统音频流资源 systemAudioStream.getTracks().forEach(track => track.stop()); }; systemAudioRecorderRef.current = systemAudioRecorder; } catch (err) { setError(err instanceof Error ? err.message : '获取媒体流失败,请检查权限设置'); console.error('初始化录制器出错:', err); } }, []); // 开始录制 const startRecording = useCallback(() => { if (recordingState === 'recording') return; if (!micRecorderRef.current || !systemAudioRecorderRef.current) { setError('请先点击「初始化录制器」'); return; } micRecorderRef.current.start(); systemAudioRecorderRef.current.start(); setRecordingState('recording'); }, [recordingState]); // 暂停录制 const pauseRecording = useCallback(() => { if (recordingState !== 'recording') return; micRecorderRef.current?.pause(); systemAudioRecorderRef.current?.pause(); setRecordingState('paused'); }, [recordingState]); // 恢复录制 const resumeRecording = useCallback(() => { if (recordingState !== 'paused') return; micRecorderRef.current?.resume(); systemAudioRecorderRef.current?.resume(); setRecordingState('recording'); }, [recordingState]); // 停止录制 const stopRecording = useCallback(() => { if (recordingState === 'idle') return; micRecorderRef.current?.stop(); systemAudioRecorderRef.current?.stop(); setRecordingState('idle'); micRecorderRef.current = null; systemAudioRecorderRef.current = null; }, [recordingState]); // 下载录制的音频文件 const downloadFile = useCallback((file: RecordedFile) => { const url = URL.createObjectURL(file.blob); const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); URL.revokeObjectURL(url); }, []); return ( <div style={{ padding: '2rem', maxWidth: '600px', margin: '0 auto' }}> <h2>麦克风&系统音频独立录制</h2> {error && <p style={{ color: 'red' }}>⚠️ {error}</p>} <div style={{ gap: '1rem', margin: '1rem 0', display: 'flex', flexWrap: 'wrap' }}> <button onClick={initRecorders} disabled={recordingState !== 'idle'} style={{ padding: '0.5rem 1rem', cursor: 'pointer' }} > 初始化录制器 </button> <button onClick={startRecording} disabled={recordingState !== 'idle'} style={{ padding: '0.5rem 1rem', cursor: 'pointer', backgroundColor: 'green', color: 'white', border: 'none' }} > 开始录制 </button> {recordingState === 'recording' && ( <button onClick={pauseRecording} style={{ padding: '0.5rem 1rem', cursor: 'pointer', backgroundColor: 'orange', color: 'white', border: 'none' }} > 暂停 </button> )} {recordingState === 'paused' && ( <button onClick={resumeRecording} style={{ padding: '0.5rem 1rem', cursor: 'pointer', backgroundColor: 'blue', color: 'white', border: 'none' }} > 恢复 </button> )} <button onClick={stopRecording} disabled={recordingState === 'idle'} style={{ padding: '0.5rem 1rem', cursor: 'pointer', backgroundColor: 'red', color: 'white', border: 'none' }} > 停止录制 </button> </div> <div style={{ marginTop: '2rem' }}> <h3>已录制的文件</h3> {recordedFiles.length === 0 ? ( <p>还没有录制的文件哦</p> ) : ( <ul style={{ listStyle: 'none', padding: 0 }}> {recordedFiles.map((file, idx) => ( <li key={idx} style={{ margin: '0.5rem 0', padding: '0.5rem', border: '1px solid #eee' }}> <span> {file.type === 'microphone' ? '🎤 ' : '🔊 '} {file.name} </span> <button onClick={() => downloadFile(file)} style={{ marginLeft: '1rem', padding: '0.3rem 0.6rem', cursor: 'pointer' }} > 下载 </button> </li> ))} </ul> )} </div> </div> ); }; export default AudioRecorder;
三、关键细节说明
为什么用
useRef存MediaRecorder?
React组件重渲染时,useState变量会重新初始化,而useRef的引用会一直保留,这样我们在录制回调(比如onstop)里才能正确访问到录制器实例。音频格式说明
示例中用的是audio/webm格式,这是MediaRecorder支持的最通用格式,几乎所有现代浏览器都兼容,文件体积也比较小。如果需要转成MP3,可能需要额外的编码库,但日常场景下webm完全够用。权限注意事项
- 用户需要允许麦克风权限和屏幕录制权限(浏览器将系统音频归类到屏幕录制权限范畴)
- 开发时必须用
https或localhost,因为浏览器只在安全上下文里允许访问媒体设备API。
- 浏览器兼容性
支持Chrome 49+、Edge 79+、Firefox 25+、Safari 14.1+,覆盖绝大多数现代浏览器。
备注:内容来源于stack exchange,提问作者Praveen saxena
相关产品推荐
相关产品推荐

