React定时向Flask后端发送音频的正确实现(含WebSocket方案)
问题分析与解决方案
你的核心问题在于:浏览器MediaRecorder原生不支持MP3编码,强行指定audio/mpeg-3会导致生成的音频片段格式不完整;同时HTTP POST传输独立片段的方式不利于后续合并为完整可用的MP3文件。下面是基于WebSocket的完整实现方案,确保生成的MP3格式有效。
关键问题拆解
- 编码格式不兼容:大多数浏览器的
MediaRecorder仅支持webm/opus等原生格式,直接输出MP3会缺失文件头、尾部等关键结构。 - 片段传输与合并:每隔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)
核心逻辑说明
- 前端编码选择:使用
audio/webm; codecs=opus确保浏览器能生成格式有效的音频片段。 - WebSocket传输:通过ArrayBuffer传输音频数据,比HTTP POST更适合实时片段传输,减少请求开销。
- 后端转码处理:收集所有片段后,用FFmpeg合并并转码为标准MP3,确保文件结构完整、可正常播放。
- 资源清理:转码完成后自动清理临时文件,避免磁盘空间浪费。
内容的提问来源于stack exchange,提问作者MacY
相关产品推荐
相关产品推荐

