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

Flask实现跨浏览器实时WAV音频流传输问题排查

跨浏览器兼容的实时WAV音频流传输方案

问题根源

  • Safari(含iOS端)会先发一个Range: bytes=0-1的探测请求,用于获取音频元数据(如WAV头信息),而桌面Chrome/Firefox直接请求完整流。如果后端不处理这个探测请求,Safari会判定响应无效,拒绝播放。
  • 实时WAV流需要正确的分块传输编码和响应头配置,否则Safari会因格式校验失败无法播放。

Flask后端修改方案(app.py)

from flask import Flask, Response, request
import pyaudio
import struct

app = Flask(__name__)

# 麦克风音频参数(按需调整)
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100
CHUNK = 1024

# 生成标准44字节WAV文件头
def generate_wav_header(sample_rate, channels, bits_per_sample):
    byte_rate = sample_rate * channels * bits_per_sample // 8
    block_align = channels * bits_per_sample // 8
    data_size = 0  # 实时流无固定长度,设为0

    header = struct.pack(
        '<4sI4s4sIHHIIHH4sI',
        b'RIFF',
        36 + data_size,
        b'WAVE',
        b'fmt ',
        16,
        1,  # PCM格式标识
        channels,
        sample_rate,
        byte_rate,
        block_align,
        bits_per_sample,
        b'data',
        data_size
    )
    return header

WAV_HEADER = generate_wav_header(RATE, CHANNELS, 16)

def audio_generator():
    # 先发送WAV头,确保浏览器识别格式
    yield WAV_HEADER
    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT,
                    channels=CHANNELS,
                    rate=RATE,
                    input=True,
                    frames_per_buffer=CHUNK)
    try:
        while True:
            data = stream.read(CHUNK)
            yield data
    finally:
        stream.stop_stream()
        stream.close()
        p.terminate()

@app.route('/audio')
def audio_stream():
    range_header = request.headers.get('Range')
    # 处理Safari的0-1探测请求
    if range_header and range_header.startswith('bytes=0-1'):
        response = Response(WAV_HEADER[:2], status=206)
        response.headers['Content-Type'] = 'audio/wav'
        response.headers['Accept-Ranges'] = 'bytes'
        # 用超大值标识实时流无固定总长度
        response.headers['Content-Range'] = 'bytes 0-1/{}'.format(len(WAV_HEADER) + (1 << 60))
        return response
    # 处理完整流请求
    response = Response(audio_generator(), mimetype='audio/wav')
    response.headers['Transfer-Encoding'] = 'chunked'
    response.headers['Accept-Ranges'] = 'bytes'
    response.headers['Cache-Control'] = 'no-cache'
    return response

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, threaded=True)

前端修改方案(index.html)

<!DOCTYPE html>
<html>
<head>
    <title>实时音频流</title>
</head>
<body>
    <button id="playBtn">播放音频</button>
    <script>
        const playBtn = document.getElementById('playBtn');
        let audio;

        playBtn.addEventListener('click', () => {
            if (audio && !audio.paused) {
                audio.pause();
                playBtn.textContent = '播放音频';
                return;
            }

            // 用原生Audio对象,适配Safari
            audio = new Audio('/audio');
            audio.play().catch(err => {
                console.error('播放失败:', err);
            });
            playBtn.textContent = '暂停音频';

            audio.addEventListener('error', (e) => {
                console.error('音频错误:', e);
            });
        });
    </script>
</body>
</html>

关键响应头说明

  • Accept-Ranges: bytes: 告知浏览器服务器支持范围请求,是Safari播放流的必要配置。
  • Content-Range: bytes 0-1/{超大值}: 针对探测请求返回合法范围,用超大值标识实时流无固定长度,避免Safari因长度不匹配报错。
  • Transfer-Encoding: chunked: 启用分块传输,适配实时流无固定长度的特性。
  • Cache-Control: no-cache: 防止浏览器缓存音频流,保证实时性。

额外优化建议

  • 移动端浏览器大多要求HTTPS环境才能访问麦克风和播放流,本地测试可用localhost例外,正式部署需配置HTTPS。
  • 若需更稳定的长时流支持,可改用MediaSource API替代原生Audio对象:
    const playBtn = document.getElementById('playBtn');
    let mediaSource;
    
    playBtn.addEventListener('click', async () => {
        if (mediaSource) {
            mediaSource.endOfStream();
            mediaSource = null;
            playBtn.textContent = '播放音频';
            return;
        }
    
        mediaSource = new MediaSource();
        const audio = new Audio(URL.createObjectURL(mediaSource));
        
        mediaSource.addEventListener('sourceopen', async () => {
            const sourceBuffer = mediaSource.addSourceBuffer('audio/wav; codecs="1"');
            const response = await fetch('/audio');
            const reader = response.body.getReader();
            
            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                if (!sourceBuffer.updating) {
                    sourceBuffer.appendBuffer(value);
                }
            }
        });
    
        audio.play().catch(err => console.error(err));
        playBtn.textContent = '暂停音频';
    });
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 00:10:47