Skip to content

security-union/opus-web-audio-polyfill

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

opus-web-audio-polyfill

Real-time Opus audio encoding/decoding for the web using AudioWorklet.

What is this?

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.

Why?

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.

Architecture

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.

Files

  • 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)

How it works

Encoder

  1. Captures audio from MediaStream via AudioWorklet
  2. Buffers PCM samples in lock-free ring buffer
  3. Opus encoder runs in worklet thread
  4. Emits encoded Opus packets via MessagePort
  5. You send packets over network

Decoder

  1. You receive Opus packets from network
  2. Post packets to worklet via MessagePort
  3. Opus decoder runs in worklet thread
  4. Decoded PCM goes to lock-free ring buffer
  5. AudioWorklet pulls samples and plays them

Integration (videocall-rs example)

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

Encoder Usage

// 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

Decoder Usage

// 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

Message Protocol

Encoder Messages

// Initialize
{ type: 'init', options: { sampleRate: 48000, channels: 1, ... } }

// Start encoding
{ type: 'start' }

// Stop encoding
{ type: 'stop' }

// Flush buffers
{ type: 'flush' }

// Cleanup
{ type: 'close' }

Decoder Messages

// Initialize
{ type: 'init', options: { outputBufferSampleRate: 48000, ... } }

// Send Opus packet
{ type: 'decode', data: Uint8Array }

// Drain buffers
{ type: 'drain' }

Output Messages (from encoder)

// Encoded Opus packet
{ type: 'data', data: Uint8Array }

// Ready to receive audio
{ type: 'ready' }

Technical Details

FreeQueue Implementation

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

Audio Processing

  • 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)

Opus Configuration

  • 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

Why Minified Versions?

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 Support

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)

Building from Source

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.js

Common Issues

"AudioWorklet is not defined"

You're not on HTTPS or localhost. AudioWorklet requires secure context.

"SharedArrayBuffer is not defined"

Missing COOP/COEP headers. Add:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

"Module not found: /encoderWorker.min.js"

Path is wrong or file not copied to build output. Check your bundler config.

Choppy audio / dropouts

  • Ring buffer too small → increase buffer size
  • CPU overloaded → reduce quality or sample rate
  • Network jitter → implement jitter buffer

Audio delay / latency

  • Large buffer sizes → reduce buffer size
  • High frame duration → reduce frame duration
  • Network latency → can't fix, it's physics

Debugging

Enable logging

Most implementations have debug flags. Look for:

{ debug: true, verbose: true }

Monitor buffer levels

FreeQueue exposes available_read() and available_write():

// In worklet
const available = this.ringBuffer.available_read();
console.log(`Buffer level: ${available} samples`);

Check for underruns/overruns

  • Underrun: decoder has no data to play (glitches/silence)
  • Overrun: encoder buffer full (drops audio)

Both indicate buffer size or network issues.

Alternatives

  • 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.

Real-World Usage

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

License

Check the original source. Opus itself is BSD-licensed. Emscripten runtime is MIT/University of Illinois. This wrapper code is probably MIT too.

Credits

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)

Contributing

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published