简体   繁体   中英

What is the best way to stream audio to the browser, chunk by chunk, with javacript

I'm trying to make a web-radio station and I would like to frequently change songs and overlay sound freely. I'd like to rate-limit the audio so the feed can be altered before its sent out. I'd also like to serve continuous content if that is possible

So far, we have a close to successful attempt with websockets, but the quality is a bit messy

Here's the code:
server.js

const express     = require('express');
const app         = express()
const http        = require('http')
const server      = http.createServer(app)
const { Server }  = require("socket.io")
const io          = new Server(server)
const fs          = require('fs')

const SRC_PATH      = 'src.wav'
const PACKET_SIZE   = 6400
let   PACKET = 0

function getpacket(socket){
    const file_descriptor     = fs.openSync(SRC_PATH, 'r', null)
    const read_offset         = PACKET * PACKET_SIZE
    const buffer              = Buffer.alloc(PACKET_SIZE)
    const buffer_write_offset = 0
    const num_bytes_to_read   = PACKET_SIZE
    const num_bytes_read      = fs.readSync(file_descriptor, buffer, buffer_write_offset, num_bytes_to_read, read_offset)
    fs.closeSync(file_descriptor)
    console.log(`Sending packet ${PACKET}`)
    socket.emit("data", buffer)
    PACKET++
}

app.use('/', express.static('.'))

io.on('connection', (socket) => {
    console.log("connected...")
    socket.on("get", ()=>{getpacket(socket)})
})

server.listen(3000, () => {
  console.log('listening on *:3000');
})

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Testing</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
</head>

<body>
    <div onclick="listen()">Click to Listen</div>

    <script>
    const socketio     = io()
    const SAMPLE_RATE  = 32000 // samples/second
    
    async function listen(){
        // Set up the new audio context
        const audioContext = new AudioContext()

        socketio.once("data", (arrayBuff)=>{
            const buffer = new Uint8Array(arrayBuff)
            addTobuffer(buffer, audioContext)
        })

        requestData()
    }

    function requestData(){
        socketio.emit("get")
    }

    async function addTobuffer(data, audioContext){
        // Set up the new audio source
        const audioSource  = await audioContext.createBufferSource()
        // create audio buffer from data,
        const audioBuffer  = await createAudioBuffer(audioContext,data) 
        // Asign the data buffer to the audioSource
        audioSource.buffer = audioBuffer
        // Connect the audio source to the audio context
        audioSource.connect(audioContext.destination)

        audioSource.start(0)
        // wait until just before the end and then get more data
        const packetLength    = (data.length/SAMPLE_RATE)*1000-10
        await new Promise(resolve=>setTimeout(resolve,packetLength))

        socketio.once("data", (arrayBuff)=>{
            const buffer = new Uint8Array(arrayBuff)
            addTobuffer(buffer, audioContext)
        })
        requestData()
    }

    async function createAudioBuffer(audioContext,data){
        /* uint8 pcm to float */
        const number_of_channels = 1
        const number_of_bytes    = data.length
        const audioBuffer        = audioContext.createBuffer(number_of_channels, number_of_bytes, SAMPLE_RATE)
        const nowBuffering       = audioBuffer.getChannelData(0)
        for (let index=0; index<number_of_bytes;index++){
            const thirtytwofloat = new Float32Array(1)
            thirtytwofloat[0]    = (data[index]-(255/2))/255
            nowBuffering[index]  = thirtytwofloat[0]
        }
        return audioBuffer
    }

    </script>

</body>
</html>

And to generate the strangely formatted PCM WAV:

ffmpeg -i src.mp3 -ar 32000 -ac 1 -acodec pcm_u8 src.wav

Is there a way to get cleaner audio output? Thank you in advance for your help!

Ok, so it looks like many radio stations like Today's Hits (which can be found in the howler.js radio example ), stream audio by using the live streaming standard

First they send an http message "content-type: audio/mpeg", as well as a few other messages like Transfer-Encoding to signal the browser that a stream is coming and to keep the connection to the server. We can see the http messages if we look at the.network tab of the developers console on safari and examine the requests

According to the MDN docs there are a handful of formats that are standardized for streaming, wav doesn't look to be one of them at the moment, but mp3 is though

Here's a working example...
server.js

const EventEmitter = require('events')
const schedule     = require('node-schedule')
const express      = require('express')
const http         = require("http")
const fs           = require('fs')

const app          = express()

const SAMPLE_SIZE = 32000                         // samples/sec
const PACKET_SIZE = SAMPLE_SIZE                   // 1 second worth of data
const UPDATE_TIME = '* * * * * *'                 // every second
const PATH        = "./src.mp3"
let   PACKET_NUM  = 0
const eventEmitter= new EventEmitter ()

async function getpacket(req,res){
  const file_descriptor     = fs.openSync(PATH, 'r', null)
  const read_offset         = PACKET_NUM * PACKET_SIZE
  const buffer              = Buffer.alloc(PACKET_SIZE)
  const buffer_write_offset = 0
  const num_bytes_to_read   = PACKET_SIZE
  const num_bytes_read      = fs.readSync(file_descriptor, buffer, buffer_write_offset, num_bytes_to_read, read_offset)
  fs.closeSync(file_descriptor)
  console.log(`Sending packet ${PACKET_NUM} to ${req.socket.remoteAddress}`) // safari sometimes requests two streams at the same time
  res.write(buffer)
}

app.get("/", (req,res)=>{
  res.sendFile("index.html",{root: '.'})
})

app.get("/src.mp3", async (req,res)=>{
    res.writeHead(200,"OK",{"Content-Type":"audio/mpeg"})

    const updateHandler = () =>{ getpacket(req,res) }
    eventEmitter.on("update", updateHandler) // On update event, send another packet

    req.socket.on("close",()=>{
      eventEmitter.removeListener("update",updateHandler)
      console.log(`Client ${req.socket.remoteAddress} disconected from server`)
    })
})

// This creates a schedule to make an update event on every second
schedule.scheduleJob(UPDATE_TIME, function(){
  PACKET_NUM+=1
  eventEmitter.emit("update")
})

const server = http.createServer(app)
server.listen(3000)

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Testing</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>

<body>
    <audio controls id="music"></audio>

    <div onclick="listen()">Click to Listen</div>
    <div onclick="stop()">Click to Stop</div>

    <script>    
    async function listen(){
       let music = document.getElementById('music')
       music.src="/src.mp3"
       music.play()
    }

    async function stop(){
       let music = document.getElementById('music')
       music.pause()
       music.src=""
    }
    </script>

</body>
</html>

And to generate the mp3 file from the wav file

ffmpeg -i src.wav -ab 128k src.mp3

If server.js is running on your machine you can go to http://localhost:3000/src.mp3 to see the live stream, and http://localhost:3000/ to see the example of it's support in the browser

It is possible to update src.mp3 live with ffmpeg

Packages and helpful resources

Some ffmpeg insights for live editing

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM