How to play a live stream of raw Opus packets (2‑byte BE length‑prefixed) in the browser (HTML/JS)?
08:14 06 Sep 2025

I’m capturing Opus frames live from a Discord Lavalink fork before encryption and dumping them to disk (and/or streaming them over WebSocket). Each packet is written as:

2‑byte big-endian length (N)
N bytes of Opus payload (48 kHz, ~20 ms per frame, Discord voice compatible)

Example write logic (Kotlin, trimmed to the relevant part):

override fun retrieveOpusFrame(buf: ByteBuf) {
    // Duplicate the buffer to copy bytes for dumping
    val ro = buffer.duplicate()
    ro.flip()
    val len = ro.remaining()
    val bytes = ByteArray(len)
    ro.get(bytes)

    // Send frame to Discord
    buf.writeBytes(buffer.flip())

    // Dump frame to file: [2-byte big-endian length][Opus payload]
    out.write((len ushr 8) and 0xFF)
    out.write(len and 0xFF)
    out.write(bytes)
}

The resulting .opusdump file is just a sequence of raw Opus packets (no Ogg/WebM container).

Example .opusdump (first 64 bytes, using: Get-Content -Path .\file.opusdump -Encoding Byte -TotalCount 64 | Format-Hex):

00000000   00 03 FC FF FE 00 03 FC FF FE 00 03 FC FF FE 00
00000010   03 FC FF FE 00 03 FC FF FE 00 03 FC FF FE 00 03
00000020   FC FF FE 00 03 FC FF FE 00 03 FC FF FE 00 03 FC
00000030   FF FE 00 03 FC FF FE 00 03 FC FF FE 00 03 FC FF

I want to play this audio in real time in a plain HTML/JS front-end (Chrome/Firefox), ideally by streaming packets to the browser as they’re produced. Ideally, the browser would handle playback directly from raw Opus frames or small WebM segments; I want to avoid server-side conversion or transcoding.

What I’ve tried

  • Capturing frames to a .opusdump file as shown above.

  • Reading browser documentation on Media Source Extensions (MSE) and Web Audio.

  • I haven’t yet tried containerizing or decoding in JS/WASM because I’m unsure where to start.

Main question:

  • What is the recommended approach to play live raw Opus packets from Discord in a browser in real time? Can they be fed directly to Web Audio/WebCodecs, or do they need to be wrapped in a container like WebM?

Follow-ups:

  • If a container is needed, are there any Java/Kotlin/Node libraries to wrap raw Opus frames into WebM segments suitable for MSE (SourceBuffer with type 'audio/webm; codecs=opus')?

  • If decoding client-side, what is the best approach to handle timing, buffering, and playback (e.g., using WASM libopus or similar)?

  • Can FFmpeg process these length-prefixed raw Opus frames directly, or is a small demuxer needed first?

  • Are there any Discord-specific Opus considerations (channels, preskip, granule positions) when remuxing or decoding outside of Discord?

Constraints

  • Front-end: plain HTML/JS (no native app).
  • Server: JVM (Kotlin), but I’m open to a small Node sidecar if it simplifies MSE/WebM segmenting.
java kotlin web-audio-api opus