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

React定时向Flask后端发送音频的正确实现(含WebSocket方案)

问题分析与解决方案

你的核心问题在于:浏览器MediaRecorder原生不支持MP3编码,强行指定audio/mpeg-3会导致生成的音频片段格式不完整;同时HTTP POST传输独立片段的方式不利于后续合并为完整可用的MP3文件。下面是基于WebSocket的完整实现方案,确保生成的MP3格式有效。

关键问题拆解

  1. 编码格式不兼容:大多数浏览器的MediaRecorder仅支持webm/opus等原生格式,直接输出MP3会缺失文件头、尾部等关键结构。
  2. 片段传输与合并:每隔10秒生成的音频片段需要统一收集后转码,WebSocket比HTTP更适合低延迟的实时片段传输。

前端React实现(WebSocket版)

import { useEffect, useRef } from 'react';
import io from 'socket.io-client';

const AudioRecorder = () => {
  const socketRef = useRef(null);
  const mediaRecorderRef = useRef(null);
  const intervalIdRef = useRef(null);
  const audioChunksRef = useRef([]);
  const clientIdRef = useRef(Math.random().toString(36).substring(2, 10)); // 唯一标识当前录制会话

  useEffect(() => {
    // 连接WebSocket服务
    socketRef.current = io('http://127.0.0.1:5000');

    socketRef.current.on('connect', () => {
      console.log('WebSocket连接成功,开始录制');
      startRecording();
    });

    socketRef.current.on('disconnect', () => {
      stopRecording();
    });

    return () => {
      stopRecording();
      socketRef.current.disconnect();
    };
  }, []);

  const startRecording = () => {
    navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(stream => {
        // 使用浏览器原生支持的webm/opus格式,保证片段有效性
        const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm; codecs=opus' });
        mediaRecorderRef.current = mediaRecorder;

        mediaRecorder.addEventListener('dataavailable', (event) => {
          if (event.data.size > 0) {
            audioChunksRef.current.push(event.data);
          }
        });

        mediaRecorder.addEventListener('stop', () => {
          // 将Blob转为ArrayBuffer通过WebSocket发送,比FormData更高效
          if (audioChunksRef.current.length > 0) {
            const blob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
            blob.arrayBuffer().then(buffer => {
              socketRef.current.emit('audio_chunk', {
                clientId: clientIdRef.current,
                data: buffer
              });
            });
            audioChunksRef.current = [];
          }
          // 停止后自动重启录制
          mediaRecorder.start();
        });

        mediaRecorder.start();

        // 每隔10秒触发一次片段发送
        intervalIdRef.current = setInterval(() => {
          if (mediaRecorder.state === 'recording') {
            mediaRecorder.stop();
          }
        }, 10000);
      })
      .catch(err => {
        console.error('音频捕获失败:', err);
      });
  };

  const stopRecording = () => {
    if (mediaRecorderRef.current) {
      mediaRecorderRef.current.stop();
      clearInterval(intervalIdRef.current);
      // 通知后端录制结束,触发转码
      socketRef.current.emit('recording_end', { clientId: clientIdRef.current });
    }
  };

  return (
    <div>
      <h3>音频录制中...</h3>
      <button onClick={stopRecording}>停止录制并生成MP3</button>
    </div>
  );
};

export default AudioRecorder;

后端Flask实现(WebSocket+FFmpeg转码)

需要先安装依赖:

pip install flask flask-socketio flask-cors ffmpeg-python
# 系统需安装FFmpeg:Ubuntu用apt install ffmpeg,Windows需添加FFmpeg到PATH
import os
import uuid
from flask import Flask
from flask_socketio import SocketIO, emit
from flask_cors import CORS
import ffmpeg

app = Flask(__name__)
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")

# 存储每个客户端的音频片段路径
client_chunks = {}
DATA_DIR = 'Data'  # 最终MP3存储目录
TEMP_DIR = 'Temp'  # 临时片段存储目录

# 初始化目录
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(TEMP_DIR, exist_ok=True)

@socketio.on('audio_chunk')
def handle_audio_chunk(data):
    client_id = data['clientId']
    audio_data = data['data']

    # 为当前客户端创建专属临时目录
    client_temp_dir = os.path.join(TEMP_DIR, client_id)
    os.makedirs(client_temp_dir, exist_ok=True)

    # 生成唯一片段文件名
    chunk_filename = f"{uuid.uuid4()}.webm"
    chunk_path = os.path.join(client_temp_dir, chunk_filename)

    # 保存音频片段
    with open(chunk_path, 'wb') as f:
        f.write(audio_data)

    # 记录当前客户端的所有片段路径
    if client_id not in client_chunks:
        client_chunks[client_id] = []
    client_chunks[client_id].append(chunk_path)

    emit('chunk_received', {'status': 'success'})

@socketio.on('recording_end')
def handle_recording_end(data):
    client_id = data['clientId']
    if client_id not in client_chunks or len(client_chunks[client_id]) == 0:
        emit('recording_result', {'status': 'error', 'message': '无有效音频片段'})
        return

    # 生成FFmpeg合并用的文件列表
    file_list_path = os.path.join(TEMP_DIR, f"{client_id}_filelist.txt")
    with open(file_list_path, 'w') as f:
        for chunk_path in client_chunks[client_id]:
            f.write(f"file '{os.path.abspath(chunk_path)}'\n")

    # 合并webm片段并转码为MP3
    try:
        output_path = os.path.join(DATA_DIR, f"{client_id}.mp3")
        ffmpeg.input(file_list_path, format='concat', safe=0) \
              .output(output_path, acodec='libmp3lame') \
              .run(overwrite_output=True)

        # 清理临时文件
        for chunk_path in client_chunks[client_id]:
            os.remove(chunk_path)
        os.remove(file_list_path)
        os.rmdir(os.path.join(TEMP_DIR, client_id))
        del client_chunks[client_id]

        emit('recording_result', {'status': 'success', 'filename': f"{client_id}.mp3"})
    except Exception as e:
        print(f"转码失败: {str(e)}")
        emit('recording_result', {'status': 'error', 'message': str(e)})

if __name__ == '__main__':
    socketio.run(app, debug=True)

核心逻辑说明

  1. 前端编码选择:使用audio/webm; codecs=opus确保浏览器能生成格式有效的音频片段。
  2. WebSocket传输:通过ArrayBuffer传输音频数据,比HTTP POST更适合实时片段传输,减少请求开销。
  3. 后端转码处理:收集所有片段后,用FFmpeg合并并转码为标准MP3,确保文件结构完整、可正常播放。
  4. 资源清理:转码完成后自动清理临时文件,避免磁盘空间浪费。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 21:35:07