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

如何让React应用中的音频录制功能正常运行?

React语音录制onstop回调无输出问题排查与修复

我正尝试为React应用添加语音录制功能,但未达到预期效果。在ondataavailable方法中使用console.log()能看到每秒持续增长的blobs数组,但在onstop方法中执行console.log()却无任何输出。相关代码如下:

if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
  const mediaRecorder = new MediaRecorder(stream)

  const audio = document.querySelector('audio')
  const microphoneBtn = document.querySelector('.microphone-btn')
  microphoneBtn?.addEventListener('click', function () {
    mediaRecorder.start(1000)
    setRecordingState(mediaRecorder.state)
  })
  //start recording
  let chunks = []
  mediaRecorder.ondataavailable = e => {
    chunks.push(e.data)
  }

  console.log(chunks)

  //stop recording
  const stopBtn = document.querySelector('.stop-audio-recording')
  stopBtn?.addEventListener('click', function () {
    mediaRecorder.stop()
    setRecordingState(mediaRecorder.state)
  })
  mediaRecorder.addEventListener('stop', e => {
    console.log(chunks)
    const blob = new Blob(chunks, { type: "audio/ogg; codecs=opus" })
    chunks = []

    const audioUrl = window.URL.createObjectURL(blob)
    audio.src = audioUrl
  })
  mediaRecorder.onstop = e => {
    console.log(chunks)
    const blob = new Blob(chunks, { type: "audio/ogg; codecs=opus" })
    chunks = []

    const audioUrl = window.URL.createObjectURL(blob)
    audio.src = audioUrl
  }
}).catch(err => console.log(err.message))

问题原因

  1. DOM引用失效:React组件更新时会重新渲染DOM,通过document.querySelector获取的按钮/音频元素引用会失效,导致stop按钮的点击事件未正确触发mediaRecorder.stop(),自然onstop回调不会执行。
  2. 事件绑定冗余与作用域问题:同时使用addEventListener('stop')和onstop赋值属于冗余操作;且chunks仅初始化一次,多次录制会累积旧数据,也未在每次录制前重置。
  3. 实例状态丢失:MediaRecorder实例直接定义在getUserMedia的回调中,组件重新渲染后可能丢失实例引用,导致后续操作失效。

修复方案

改用React的useRef管理DOM元素、MediaRecorder实例和数据缓存,结合useEffect处理媒体设备初始化与清理,符合React组件生命周期规范:

import { useState, useRef, useEffect } from 'react';

function AudioRecorder() {
  const [recordingState, setRecordingState] = useState('inactive');
  // 用useRef保存跨渲染周期的引用
  const mediaRecorderRef = useRef(null);
  const chunksRef = useRef([]);
  const audioRef = useRef(null);

  useEffect(() => {
    let stream;

    const initMedia = async () => {
      if (!navigator.mediaDevices?.getUserMedia) return;
      try {
        stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        const mediaRecorder = new MediaRecorder(stream);
        mediaRecorderRef.current = mediaRecorder;

        // 监听数据片段
        mediaRecorder.ondataavailable = (e) => {
          chunksRef.current.push(e.data);
          console.log('当前录制片段数:', chunksRef.current.length);
        };

        // 监听录制停止事件
        mediaRecorder.onstop = () => {
          console.log('录制结束,所有片段:', chunksRef.current);
          const audioBlob = new Blob(chunksRef.current, { type: "audio/ogg; codecs=opus" });
          const audioUrl = window.URL.createObjectURL(audioBlob);
          if (audioRef.current) audioRef.current.src = audioUrl;
          // 重置片段缓存
          chunksRef.current = [];
        };
      } catch (err) {
        console.error('媒体设备初始化失败:', err.message);
      }
    };

    initMedia();

    // 组件卸载时清理资源
    return () => {
      if (mediaRecorderRef.current?.state === 'recording') {
        mediaRecorderRef.current.stop();
      }
      stream?.getTracks().forEach(track => track.stop());
    };
  }, []);

  const startRecording = () => {
    if (!mediaRecorderRef.current) return;
    chunksRef.current = []; // 每次录制前清空缓存
    mediaRecorderRef.current.start(1000);
    setRecordingState('recording');
  };

  const stopRecording = () => {
    if (!mediaRecorderRef.current || mediaRecorderRef.current.state !== 'recording') return;
    mediaRecorderRef.current.stop();
    setRecordingState('inactive');
  };

  return (
    <div>
      <audio ref={audioRef} controls />
      <button onClick={startRecording} disabled={recordingState === 'recording'}>
        {recordingState === 'recording' ? '录制中' : '开始录制'}
      </button>
      <button onClick={stopRecording} disabled={recordingState !== 'recording'}>
        停止录制
      </button>
    </div>
  );
}

export default AudioRecorder;

修复要点

  • useRef管理引用:用useRef存储DOM元素、MediaRecorder实例和chunks数组,确保组件重新渲染时引用不丢失。
  • useEffect初始化与清理:在useEffect中完成媒体设备初始化和事件绑定,仅执行一次;组件卸载时停止录制并释放媒体流,避免内存泄漏。
  • React事件绑定:用JSX的onClick替代原生addEventListener,适配React的渲染机制。
  • 录制状态重置:每次开始录制前清空chunks缓存,避免旧数据干扰。

内容的提问来源于stack exchange,提问作者Chisom Ozor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 11:22:23