You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 02:13:12