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

Next.js中MediaRecorder录屏时麦克风音频录制异常求助

问题分析与解决方案

核心问题排查

  1. AudioContext 激活限制:浏览器要求AudioContext必须通过用户交互(如点击按钮)触发激活,否则会处于suspended状态,导致麦克风音频无法正常处理。
  2. 音频音量不平衡:系统音频与麦克风音频直接混合时,系统音量通常远大于麦克风,导致麦克风声音被掩盖,出现“不清晰”或“录不到”的情况。
  3. 重复停止逻辑:原代码在recorder.onstop回调中再次调用recorder.stop(),触发重复停止操作,导致录制流程异常。
  4. 状态管理不严谨:媒体流、录制片段等变量用useState存储,存在重置风险;未正确处理资源释放,可能引发内存泄漏。

具体修复步骤

  • 确保AudioContext正确激活:在用户点击开始录制后创建并主动恢复AudioContext,规避浏览器的自动暂停策略。
  • 添加音量控制节点:给麦克风音频增加GainNode,手动调整音量平衡,避免被系统音频覆盖。
  • 移除重复停止调用:删除onstop回调中的recorder.stop(),避免逻辑冲突。
  • 优化状态存储:用useRef存储媒体流、录制片段等可变数据,避免组件重渲染时重置;录制结束后主动清理所有资源。

修改后的完整代码

'use client'

import React, { useState, useRef, useCallback } from 'react';

const ScreenRecorder: React.FC = () => {
  const [recording, setRecording] = useState<boolean>(false);
  const [mediaUrl, setMediaUrl] = useState<string>();
  // 使用useRef存储可变状态,避免组件重渲染丢失数据
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const uStreamRef = useRef<MediaStream | null>(null);
  const mStreamRef = useRef<MediaStream | null>(null);
  const audioContextRef = useRef<AudioContext | null>(null);
  const mediaPartsRef = useRef<Blob[]>([]);

  const startRecording = useCallback(async () => {
    try {
      // 重置录制片段
      mediaPartsRef.current = [];
      // 获取屏幕共享流(包含系统音频)
      const screenStream = await navigator.mediaDevices.getDisplayMedia({
        video: true,
        audio: true,
      });
      mStreamRef.current = screenStream;

      // 获取麦克风流,启用降噪、回声消除等优化
      const micStream = await navigator.mediaDevices.getUserMedia({
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true,
        },
      });
      uStreamRef.current = micStream;

      // 创建并激活AudioContext
      const audioContext = new window.AudioContext();
      audioContextRef.current = audioContext;
      // 浏览器安全限制:必须通过用户交互激活AudioContext
      if (audioContext.state === 'suspended') {
        await audioContext.resume();
      }

      // 创建音频源节点
      const micSource = audioContext.createMediaStreamSource(micStream);
      const systemSource = audioContext.createMediaStreamSource(screenStream);

      // 给麦克风添加音量增益,平衡与系统音频的音量
      const micGain = audioContext.createGain();
      micGain.gain.value = 2.5; // 根据实际场景调整,范围1-3
      micSource.connect(micGain);

      // 创建目标节点合并音频
      const destination = audioContext.createMediaStreamDestination();
      micGain.connect(destination);
      systemSource.connect(destination);

      // 合并音频与视频流
      const combinedStream = new MediaStream([
        ...destination.stream.getAudioTracks(),
        ...screenStream.getVideoTracks(),
      ]);

      // 使用WebM格式,兼容性更好
      const recorder = new MediaRecorder(combinedStream, {
        mimeType: 'video/webm; codecs=vp9,opus',
      });
      mediaRecorderRef.current = recorder;

      recorder.start();
      setRecording(true);

      recorder.onstop = () => {
        const blob = new Blob(mediaPartsRef.current, { type: 'video/webm' });
        const url = URL.createObjectURL(blob);
        setMediaUrl(url);

        // 清理AudioContext资源
        audioContext.close();
        audioContextRef.current = null;
      };

      recorder.addEventListener('dataavailable', (e) => {
        if (e.data.size > 0) {
          mediaPartsRef.current.push(e.data);
        }
      });

      // 监听屏幕共享结束事件,自动停止录制
      screenStream.getVideoTracks()[0].onended = () => {
        stopRecording();
      };
    } catch (error) {
      console.error('启动录制失败:', error);
      setRecording(false);
    }
  }, []);

  const stopRecording = useCallback(() => {
    const recorder = mediaRecorderRef.current;
    const micStream = uStreamRef.current;
    const screenStream = mStreamRef.current;

    if (recorder && recorder.state !== 'inactive') {
      recorder.stop();
    }

    // 停止所有媒体轨道
    micStream?.getTracks().forEach(track => track.stop());
    screenStream?.getTracks().forEach(track => track.stop());

    // 重置所有引用状态
    mediaRecorderRef.current = null;
    uStreamRef.current = null;
    mStreamRef.current = null;
    setRecording(false);
  }, []);

  return (
    <div style={{ padding: '20px' }}>
      <h2>录制状态: {mediaRecorderRef.current?.state || '未开始'}</h2>
      {!recording && (
        <button onClick={startRecording} style={{ padding: '8px 16px' }}>
          开始录制
        </button>
      )}
      {recording && (
        <button onClick={stopRecording} style={{ padding: '8px 16px', backgroundColor: '#ff4444', color: 'white' }}>
          停止录制
        </button>
      )}
      {mediaUrl && (
        <div style={{ marginTop: '20px' }}>
          <h3>录制预览:</h3>
          <video src={mediaUrl} controls autoPlay style={{ maxWidth: '100%' }} />
        </div>
      )}
    </div>
  );
};

export default ScreenRecorder;

额外优化说明

  • 麦克风质量优化:启用echoCancellation、noiseSuppression等参数,自动优化麦克风录制效果。
  • 格式兼容性:选择video/webm格式,相比MP4,MediaRecorder对WebM的支持更普遍,录制效率更高。
  • 自动停止机制:监听屏幕共享轨道的onended事件,当用户手动停止屏幕共享时,自动结束录制流程。
  • 资源清理:录制结束后关闭AudioContext、停止所有媒体轨道,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Quoc Vo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 00:40:53