Flask将Pyaudio发送到浏览器。

9

我正在将我的服务器麦克风的音频发送到浏览器中(与this帖子类似,但进行了一些修改)。

一切都很顺利,直到您转到移动设备或Safari,完全不起作用。我已尝试使用类似howler的东西来处理前端,但没有成功(仍然可以在Chrome上和计算机上工作,但无法在手机上的Safari / Chrome等上运行)。<audio> ... </audio>在Chrome上很好,但只能在计算机上使用。

function play_audio() {
  var sound = new Howl({
    src: ['audio_feed'],
    format: ['wav'],
    html5: true,
    autoplay: true
  });
  sound.play();
}

如何发送一个实时的wav生成的音频流,使其在任何浏览器中都能正常工作?

编辑230203:

我已经将错误缩小到了头部(至少我认为是导致错误的原因)。

应该使用哪些头部来使所有浏览器都可以使用声音?

以这个简单的app.py为例:

from flask import Flask, Response, render_template
import pyaudio
import time

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html', headers={'Content-Type': 'text/html'})

def generate_wav_header(sampleRate, bitsPerSample, channels):
    datasize = 2000*10**6
    o = bytes("RIFF",'ascii')
    o += (datasize + 36).to_bytes(4,'little')
    o += bytes("WAVE",'ascii')
    o += bytes("fmt ",'ascii')
    o += (16).to_bytes(4,'little')
    o += (1).to_bytes(2,'little')
    o += (channels).to_bytes(2,'little')
    o += (sampleRate).to_bytes(4,'little')
    o += (sampleRate * channels * bitsPerSample // 8).to_bytes(4,'little')
    o += (channels * bitsPerSample // 8).to_bytes(2,'little')
    o += (bitsPerSample).to_bytes(2,'little')
    o += bytes("data",'ascii')
    o += (datasize).to_bytes(4,'little')
    return o

def get_sound(InputAudio):

    FORMAT = pyaudio.paInt16
    CHANNELS = 2
    CHUNK = 1024
    SAMPLE_RATE = 44100
    BITS_PER_SAMPLE = 16

    wav_header = generate_wav_header(SAMPLE_RATE, BITS_PER_SAMPLE, CHANNELS)

    stream = InputAudio.open(
        format=FORMAT,
        channels=CHANNELS,
        rate=SAMPLE_RATE,
        input=True,
        input_device_index=1,
        frames_per_buffer=CHUNK
    )

    first_run = True
    while True:
       if first_run:
           data = wav_header + stream.read(CHUNK)
           first_run = False
       else:
           data = stream.read(CHUNK)
       yield(data)


@app.route('/audio_feed')
def audio_feed():

    return Response(
        get_sound(pyaudio.PyAudio()),
        content_type = 'audio/wav',
    )

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

有一个长这样的 index.html 文件:

<html>
  <head>
    <title>Test audio</title>
  </head>
  <body>
    <button onclick="play_audio()">
      Play audio
    </button>
    <div id="audio-feed"></div>
  </body>
<script>

  function play_audio() {
    var audio_div = document.getElementById('audio-feed');
    const audio_url = "{{ url_for('audio_feed') }}"
    audio_div.innerHTML = "<audio controls><source src="+audio_url+" type='audio/x-wav;codec=pcm'></audio>";
  }

</script>
</html>

启动Flask开发服务器python app.py并使用Chrome进行测试,如果您有麦克风,您将听到输入声音(最好使用耳机,否则您会得到一个声音循环)。 Firefox也可以正常工作。
但是,如果您尝试在iPhone上的任何浏览器中使用相同的应用程序,则不会收到声音,并且在MacOS上使用safari也是如此。
没有任何错误,您可以在safari中看到音频的字节流被下载,但仍然没有声音。
是什么导致这种情况?我认为我应该在audio_feed响应中使用某些头文件,但是经过数小时的调试,我似乎找不到与此相关的任何内容。
编辑230309:
@Markus指出应遵循RFC7233 HTTP范围请求。那可能就是了。虽然firefox、chrome和可能更多桌面浏览器发送byte=0-作为头请求,但在iOS上使用的safari和浏览器发送byte=0-1作为头请求。

你有检查过支持和浏览器版本吗?https://caniuse.com/audio - Gonzalo Odiard
这里有什么相关的东西吗?https://dev59.com/T17Va4cB1Zd3GeqPNMxh - Gonzalo Odiard
1
你可以尝试使用不同的格式,如MP4,也可以查看Safari/Chrome等应用程序的权限,看看它们是否可以使用麦克风? - Tony
浏览器与麦克风本身无关,这只涉及服务器。 - destinychoice
1
在你的问题中,你说它在手机上“不起作用”,因此我认为这个问题也会影响到安卓系统。我建议你纠正这个陈述,并添加iOS标签。 - etuardu
显示剩余5条评论
1个回答

2

编辑于2023-03-12

事实证明,将音频实时流转换为mp3格式即可满足需求。您可以使用ffmpeg来完成此操作。可执行文件必须在服务器进程的执行路径中可用。这是一个已经测试过的工作草案,作为服务器的是Windows笔记本电脑,客户端为iPad上的Safari:

from subprocess import Popen, PIPE
from threading import Thread
from flask import Flask, Response, render_template
import pyaudio

FORMAT = pyaudio.paFloat32
CHANNELS = 1
CHUNK_SIZE = 4096
SAMPLE_RATE = 44100
BITS_PER_SAMPLE = 16

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html', headers={'Content-Type': 'text/html'})


def read_audio(inp, audio):
    while True:
        inp.write(audio.read(num_frames=CHUNK_SIZE))


def response():
    a = pyaudio.PyAudio().open(
        format=FORMAT,
        channels=CHANNELS,
        rate=SAMPLE_RATE,
        input=True,
        input_device_index=1,
        frames_per_buffer=CHUNK_SIZE
    )

    c = f'ffmpeg -f f32le -acodec pcm_f32le -ar {SAMPLE_RATE} -ac {CHANNELS} -i pipe: -f mp3 pipe:'
    p = Popen(c.split(), stdin=PIPE, stdout=PIPE)
    Thread(target=read_audio, args=(p.stdin, a), daemon=True).start()

    while True:
        yield p.stdout.readline()


@app.route('/audio_feed', methods=['GET'])
def audio_feed():
    return Response(
        response(),
        headers={
            # NOTE: Ensure stream is not cached.
            'Cache-Control': 'no-cache, no-store, must-revalidate',
            'Pragma': 'no-cache',
            'Expires': '0',
        },
        mimetype='audio/mpeg')


if __name__ == "__main__":
    app.run(host='0.0.0.0')

index.html 中将类型更改为 audio/mp3
<!DOCTYPE html>
<html>
  <head>
    <title>Test audio</title>
  </head>
  <body>
    <button onclick="play_audio()">
      Play audio
    </button>
    <div id="audio-feed"></div>
  </body>
<script>
  function play_audio() {
    var audio_div = document.getElementById('audio-feed');
    const audio_url = "{{ url_for('audio_feed') }}"
    audio_div.innerHTML = "<audio preload='all' controls><source src=" + audio_url + " type='audio/mp3'></audio>";
  }
</script>
</html>

免责声明:这只是一个基本的演示。每次调用audio_feed处理程序时,它都会打开一个音频-ffmpeg子进程。它不会缓存多个请求的数据,也不会删除未使用的线程和未消耗的数据。

致谢:如何在Python中实时将wav转换为mp3?


如果您有麦克风,那么问题中的代码是可重现的。 - destinychoice
@destinychoice,问题中发布的代码尚未实现RFC7233。很抱歉我的答案没有帮助到您解决这个问题。 - Markus
你的回答中的链接是在已经存在的文件上实现的。这可能是一种方法,但当实际文件大小未知时,我不确定如何做到这一点。 - destinychoice
这真不错,在iOS和Safari桌面上运行得非常好(其他浏览器也是如此)。我有些东西可以用了!然而,我无法弄清楚read_audio到底是做什么的? - destinychoice
1
在这个例子中,read_audio 被用作后台线程,不断地将音频数据块从 pyaudio 复制到 ffmpeg 管道中。这样,从 ffmpeg 管道读取输出的响应例程就与此任务解耦了。否则,等待从 ffmpeg 管道输出的数据可能会阻塞从 pyaudio 复制数据到管道中。希望这个解释有帮助! - Markus
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接