如何实现MediaRecorder流实时编码为MP3且避免浏览器卡顿?
实时将MediaRecorder流编码为MP3的可行方案
问题背景
当前实现是把MediaRecorder的流存为Blob,录制完成后转WAV再用lamejs编码成MP3,5分钟的录制会导致主线程卡顿。试过用ScriptProcessorNode的方案,但该API已被废弃;直接在MediaRecorder的ondataavailable回调里用lamejs编码,又因为数据类型不匹配失败。不想用WebWorker或体积过大的ffmpeg库,求可行方案。
附之前尝试的代码:
const dataArray = [] // globally load lamejs const mp3Encoder = new lamejs.Mp3Encoder( 1, 44000, 128 ) const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const recorder = new MediaRecorder(stream, { audioBitsPerSecond: 9000, }) recorder.ondataavailable = async ({ data }) => { const mp3Data = mp3Encoder.encodeBuffer(data) // 这里类型不匹配,无法编码 dataArray.push(mp3Data) } recorder.onstop = async () => { mediaStream.getAudioTracks().forEach(track => track.stop()) const mp3Data = mp3Encoder.flush() dataArray.push(mp3Data) } // 播放/下载时 // const blob = new Blob(dataArray, { type: 'audio/mp3' })
可行方案:用AudioWorklet实现实时MP3编码
MediaRecorder输出的Blob是压缩后的音频数据(默认是webm),无法直接给lamejs编码,lamejs需要原始PCM样本。正确的做法是直接从媒体流抓取PCM数据,用AudioWorklet在后台线程处理编码,既避免主线程卡顿,又符合现代Web API规范。
步骤1:编写AudioWorkletProcessor脚本
创建一个单独的mp3-encoder-worklet.js文件,负责处理PCM数据并调用lamejs编码:
// 引入lamejs(确保lamejs能在AudioWorklet环境运行,可使用UMD版本或打包嵌入) importScripts('path/to/lame.min.js'); class Mp3EncoderWorklet extends AudioWorkletProcessor { constructor() { super(); // 初始化lamejs编码器:单声道、44.1kHz采样率、128kbps比特率 this.encoder = new lamejs.Mp3Encoder(1, 44100, 128); this.port.onmessage = (e) => { if (e.data === 'flush') { // 收到flush指令,输出剩余编码数据 const mp3Data = this.encoder.flush(); this.port.postMessage({ type: 'mp3Data', data: mp3Data }); } }; } process(inputs) { // 获取输入的PCM数据(单声道取第一个输入的第一个通道) const inputData = inputs[0][0]; if (!inputData) return true; // 将Float32Array格式的PCM转换为lamejs需要的Int16Array const int16Data = new Int16Array(inputData.length); for (let i = 0; i < inputData.length; i++) { // 把[-1, 1]范围的浮点值转换为[-32768, 32767]的整数 const sample = Math.max(-1, Math.min(1, inputData[i])); int16Data[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; } // 编码PCM数据为MP3 const mp3Data = this.encoder.encodeBuffer(int16Data); if (mp3Data.length > 0) { this.port.postMessage({ type: 'mp3Data', data: mp3Data }); } return true; } } registerProcessor('mp3-encoder-worklet', Mp3EncoderWorklet);
步骤2:主线程代码实现
在主页面中加载AudioWorklet,连接媒体流并收集MP3数据:
let audioContext; let workletNode; const mp3Chunks = []; async function startRecording() { // 获取媒体流 const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // 初始化AudioContext(需用户交互触发,避免浏览器限制) audioContext = new AudioContext({ sampleRate: 44100 }); await audioContext.audioWorklet.addModule('mp3-encoder-worklet.js'); // 创建媒体流源节点 const sourceNode = audioContext.createMediaStreamSource(stream); // 创建自定义Worklet节点 workletNode = new AudioWorkletNode(audioContext, 'mp3-encoder-worklet'); // 监听Worklet发送的MP3数据 workletNode.port.onmessage = (e) => { if (e.data.type === 'mp3Data') { mp3Chunks.push(new Uint8Array(e.data.data)); } }; // 连接节点:源 -> Worklet -> 输出(可选,如需监听录制声音) sourceNode.connect(workletNode); workletNode.connect(audioContext.destination); } async function stopRecording() { // 发送flush指令,获取剩余编码数据 workletNode.port.postMessage('flush'); // 等待最后一批数据 await new Promise(resolve => { workletNode.port.onmessage = (e) => { if (e.data.type === 'mp3Data') { mp3Chunks.push(new Uint8Array(e.data.data)); resolve(); } }; }); // 停止音频上下文和媒体流 await audioContext.close(); const stream = workletNode.context.createMediaStreamDestination().stream; stream.getAudioTracks().forEach(track => track.stop()); // 生成MP3 Blob并下载 const mp3Blob = new Blob(mp3Chunks, { type: 'audio/mp3' }); const url = URL.createObjectURL(mp3Blob); const a = document.createElement('a'); a.href = url; a.download = 'recording.mp3'; a.click(); URL.revokeObjectURL(url); // 重置数据 mp3Chunks.length = 0; }
关键说明
- 数据格式转换:AudioWorklet输出的PCM是Float32Array,必须转换为lamejs要求的Int16Array,范围映射要准确。
- AudioWorklet环境:lamejs需适配AudioWorklet环境,建议使用UMD版本或通过打包工具将其与Worklet脚本合并。
- 用户交互要求:AudioContext必须由用户交互(如按钮点击)触发,否则浏览器会阻止初始化。
- 采样率匹配:确保AudioContext的采样率与lamejs编码器的采样率一致(示例中为44100Hz)。
内容的提问来源于stack exchange,提问作者CZ workman
相关产品推荐
相关产品推荐

