Real-time Opus audio encoding/decoding for the web using AudioWorklet.
Web Audio API + Opus codec = this library.
Two AudioWorklet processors that handle Opus encoding and decoding in real-time. No WebCodecs API required. No fancy dependencies. Just pure Web Audio API.
Safari doesn't support Opus in WebCodecs (as of 2025). Chrome does, but inconsistently. You want real-time voice/audio over WebRTC? You need Opus. This bridges the gap.
Browser Audio Input → encoderWorker → Opus Packets → Network
Network → Opus Packets → decoderWorker → Browser Audio Output
Uses AudioWorklet (runs in audio rendering thread) for low-latency processing. Implements lock-free ring buffers (FreeQueue) for zero-copy audio transfer.
encoderWorker.js- Source encoder worklet (~1.7MB unminified)decoderWorker.js- Source decoder worklet (~1.0MB unminified)encoderWorker.min.js- Production encoder (~377KB minified)decoderWorker.min.js- Production decoder (~227KB minified)
- Captures audio from MediaStream via AudioWorklet
- Buffers PCM samples in lock-free ring buffer
- Opus encoder runs in worklet thread
- Emits encoded Opus packets via MessagePort
- You send packets over network
- You receive Opus packets from network
- Post packets to worklet via MessagePort
- Opus decoder runs in worklet thread
- Decoded PCM goes to lock-free ring buffer
- AudioWorklet pulls samples and plays them
In videocall-rs/yew-ui, the minified workers are:
- Copied to build root via Trunk:
<link data-trunk rel="copy-file" href="./scripts/encoderWorker.min.js" /> - Loaded as AudioWorklet modules:
context.audio_worklet().add_module("/encoderWorker.min.js") - Instantiated as AudioWorkletNode with processor name
- Communicated with via MessagePort
// In Rust/WASM via web_sys
let context = AudioContext::new_with_context_options(&options)?;
let worklet = AudioWorkletNode::new_with_options(
&context,
"encoder-worklet", // registered processor name
&options
)?;
// Connect audio input
media_stream_source.connect_with_audio_node(&worklet)?;
// Listen for encoded packets
let port = worklet.port()?;
port.set_onmessage(Some(&handler)); // receives Opus packets// Similar setup
let worklet = AudioWorkletNode::new_with_options(
&context,
"decoder-worklet", // registered processor name
&options
)?;
// Connect to speakers
worklet.connect_with_audio_node(&context.destination())?;
// Send Opus packets
let port = worklet.port()?;
port.post_message(&opus_packet)?; // sends packets to decoder// Initialize
{ type: 'init', options: { sampleRate: 48000, channels: 1, ... } }
// Start encoding
{ type: 'start' }
// Stop encoding
{ type: 'stop' }
// Flush buffers
{ type: 'flush' }
// Cleanup
{ type: 'close' }// Initialize
{ type: 'init', options: { outputBufferSampleRate: 48000, ... } }
// Send Opus packet
{ type: 'decode', data: Uint8Array }
// Drain buffers
{ type: 'drain' }// Encoded Opus packet
{ type: 'data', data: Uint8Array }
// Ready to receive audio
{ type: 'ready' }Lock-free single-producer/single-consumer FIFO using SharedArrayBuffer. Based on boost::lockfree::spsc_queue design.
- Uses Atomics for read/write indices
- One extra buffer slot to distinguish full vs empty
- Zero memory allocation during runtime
- Supports multi-channel audio
- Sample Rate: Configurable (typically 48kHz)
- Frame Size: 128 samples default (AudioWorklet quantum)
- Channels: 1 (mono) or 2 (stereo)
- Buffer Size: Configurable ring buffer (typically 8192-16384 samples)
- Bitrate: Configurable (typically 32-128 kbps)
- Complexity: 0-10 (affects CPU vs quality tradeoff)
- Application: VOIP, Audio, or LowDelay
- Frame Duration: 2.5ms to 60ms
The full source is 2.7MB+ because it includes:
- Opus codec compiled to WASM
- Emscripten runtime
- Ring buffer implementation
- Worklet processor code
Minified versions strip:
- Whitespace and comments
- Debug symbols
- Unnecessary polyfills
- Dead code
Results in ~600KB total for both encoder and decoder. Still chunky, but acceptable for real-time audio apps.
| Browser | AudioWorklet | SharedArrayBuffer | Status |
|---|---|---|---|
| Chrome 66+ | ✓ | ✓ | Fully supported |
| Firefox 76+ | ✓ | ✓ | Fully supported |
| Safari 14.1+ | ✓ | ✓ | Fully supported |
| Edge 79+ | ✓ | ✓ | Fully supported |
Requirements:
- HTTPS or localhost (AudioWorklet security requirement)
- Cross-Origin-Opener-Policy: same-origin (for SharedArrayBuffer)
- Cross-Origin-Embedder-Policy: require-corp (for SharedArrayBuffer)
If you need to modify the workers:
# Install dependencies (emscripten, opus, etc)
# This is project-specific, check original build scripts
# Build
npm run build # or whatever the build command is
# Minify
# Usually terser or uglify-js
terser encoderWorker.js -c -m -o encoderWorker.min.js
terser decoderWorker.js -c -m -o decoderWorker.min.jsYou're not on HTTPS or localhost. AudioWorklet requires secure context.
Missing COOP/COEP headers. Add:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Path is wrong or file not copied to build output. Check your bundler config.
- Ring buffer too small → increase buffer size
- CPU overloaded → reduce quality or sample rate
- Network jitter → implement jitter buffer
- Large buffer sizes → reduce buffer size
- High frame duration → reduce frame duration
- Network latency → can't fix, it's physics
Most implementations have debug flags. Look for:
{ debug: true, verbose: true }FreeQueue exposes available_read() and available_write():
// In worklet
const available = this.ringBuffer.available_read();
console.log(`Buffer level: ${available} samples`);- Underrun: decoder has no data to play (glitches/silence)
- Overrun: encoder buffer full (drops audio)
Both indicate buffer size or network issues.
- WebCodecs API: Native Opus support, but Safari doesn't support it
- MediaRecorder API: Can encode to Opus, but not real-time enough
- Native apps: Use platform APIs, but then it's not web
- Server-side transcoding: Adds latency and infrastructure cost
This library exists because none of the above work for real-time web audio.
Used in videocall-rs for WebRTC audio in Safari and as fallback for Chrome. Handles thousands of concurrent calls in production.
Typical use case:
- P2P voice/video calls
- Live streaming audio
- Real-time music collaboration
- Game voice chat
- WebRTC applications
Check the original source. Opus itself is BSD-licensed. Emscripten runtime is MIT/University of Illinois. This wrapper code is probably MIT too.
Built on top of:
- Opus codec - Jean-Marc Valin et al. (IETF RFC 6716)
- Emscripten - Alon Zakai et al.
- Web Audio API - W3C Audio Working Group
- FreeQueue - Google Chrome team (lock-free queue implementation)
This is a dependency, not the main project. If you find bugs, fix them and submit PRs. Keep it simple. Keep it fast.
Built for videocall-rs. Works everywhere. Ships minified.