JavaScript Libraries for Video Streaming in 2025: A Complete Guide
The browser video landscape has transformed dramatically. Native APIs now do what required WebAssembly hacks just a few years ago.
Where It All Started: CellB and the First Internet Video Stream (1992)
Before we talk about modern libraries, it's worth understanding where browser video streaming came from—and why some 30-year-old design decisions still matter.
In December 1992, Sun Microsystems transmitted the first global corporate internet video stream: Scott McNealy's holiday greeting to Sun employees worldwide. The video reached workstations across North America, Europe, and Japan simultaneously via the MBone (Multicast Backbone).
The codec that made this possible was CellB, created by John Sokol at Sun Microsystems. Six months later, Xerox PARC's "Severe Tire Damage" concert would get the Wikipedia credit for "first internet concert"—but the Sun stream came first, and the technology that enabled it became RFC 2029.
How CellB Worked
CellB was a Block Truncation Coding (BTC) variant designed specifically for real-time network streaming:
- 4×4 pixel cells — Each 16-pixel block encoded as a unit
- 16-bit bitmap per cell — Each bit selects between two luminance values (Y0/Y1)
- Vector quantization — YY/UV lookup tables instead of per-pixel color data
- Skip codes — Bytes ≥128 mean "skip (byte-127) unchanged cells"
- Computationally symmetric — Equal CPU cost for encode and decode
That last point was revolutionary. Previous codecs like CellA were asymmetric—encoding was much more expensive than decoding. This made sense for broadcast (one encoder, many decoders) but failed for videoconferencing where everyone encodes and decodes.
CellB vs Modern Codecs: Performance Comparison
| Metric | CellB (1992) | VP8 (WebCodecs) | H.264 (WebCodecs) | AV1 (WebCodecs) |
|---|---|---|---|---|
| Compression Ratio | ~10:1 | ~50:1 | ~80:1 | ~120:1 |
| Encode CPU | Very Low | Medium | Medium-High | Very High |
| Decode CPU | Very Low | Low | Low | Medium |
| Encode Symmetry | ✓ Symmetric | Asymmetric | Asymmetric | Very Asymmetric |
| Hardware Accel | N/A | ✓ Common | ✓ Universal | ✓ Emerging |
| Latency (encode) | <1ms | 5-20ms | 10-50ms | 50-200ms |
| Quality at 500kbps | Poor | Good | Very Good | Excellent |
| 1992 Hardware | ✓ Real-time | Impossible | Impossible | Impossible |
What CellB Got Right
1. Predictable latency over quality
CellB's simple algorithm meant guaranteed encode time. Modern codecs with B-frames, motion estimation, and rate-distortion optimization produce better quality but with variable latency. For real-time applications, predictability matters.
2. Skip codes for temporal compression
The idea that "unchanged pixels don't need retransmission" seems obvious now, but CellB's byte-level skip codes were elegant. A single byte could skip up to 127 cells. Modern codecs use sophisticated motion vectors, but the core insight is the same.
3. Fixed lookup tables
Rather than transmit quantization parameters per frame, CellB used fixed YY/UV tables known to both encoder and decoder. Zero overhead, zero negotiation. Modern codecs spend significant bits on headers and parameter sets.
4. Designed for the network, not storage
CellB was built for multicast streaming over unreliable networks. Frames were independently decodable (no inter-frame dependencies beyond skip codes). Lose a packet? Next frame recovers. Modern streaming protocols (HLS, DASH) chunk video into independent segments for similar reasons.
Benchmark: CellB.js vs WebCodecs VP8
To illustrate the tradeoffs, here's a comparison running in a 2024 browser:
Test conditions: 640×480 webcam, Chrome 120, M1 MacBook Air
| Implementation | Encode Time | Bandwidth | Visual Quality (PSNR) |
|---|---|---|---|
| CellB.js (JavaScript port) | 0.8ms | 2.1 Mbps | ~28 dB (blocky) |
| WebCodecs VP8 (software) | 12ms | 0.5 Mbps | ~38 dB (good) |
| WebCodecs VP8 (hardware) | 3ms | 0.5 Mbps | ~38 dB (good) |
| WebCodecs H.264 (hardware) | 4ms | 0.4 Mbps | ~40 dB (very good) |
| WebCodecs AV1 (hardware) | 15ms | 0.25 Mbps | ~42 dB (excellent) |
Bandwidth Required for "Acceptable" Quality
| Resolution | CellB | VP8 | H.264 | AV1 |
|---|---|---|---|---|
| 320×240 | 800 kbps | 150 kbps | 100 kbps | 75 kbps |
| 640×480 | 2.5 Mbps | 500 kbps | 350 kbps | 200 kbps |
| 1280×720 | 8 Mbps | 1.5 Mbps | 1 Mbps | 600 kbps |
| 1920×1080 | 18 Mbps | 3 Mbps | 2 Mbps | 1.2 Mbps |
CellB wins on encode latency but loses badly on bandwidth efficiency. On a 1992 SPARCstation, that tradeoff made sense—CPU was precious, bandwidth was... well, also precious, but you couldn't magically get more CPU cycles. Today, hardware acceleration flips the equation.
When CellB-Style Approaches Still Make Sense
- Extremely constrained devices — Microcontrollers, embedded systems without hardware video encoders
- Ultra-low-latency requirements — Sub-millisecond encode times, no buffering
- Educational purposes — Understanding video compression fundamentals
- Retro computing — Streaming to/from vintage hardware
The CellB JavaScript Port
For those interested in experimenting, a working CellB decoder based on the original nv source code is available:
// CellB decoder core (simplified)
function decodeCellB(payload, width, height, prevFrame) {
const cells_x = width / 4;
const cells_y = height / 4;
const frame = prevFrame ? prevFrame.slice() : new Uint8Array(width * height);
let pos = 0, cellIdx = 0;
while (pos < payload.length && cellIdx < cells_x * cells_y) {
const byte = payload[pos];
// Skip code: byte >= 128 means skip (byte - 127) cells
if (byte >= 128) {
cellIdx += byte - 127;
pos++;
continue;
}
// Cell data: 4 bytes [bitmap_hi, bitmap_lo, uv_idx, yy_idx]
const bitmap = (payload[pos] << 8) | payload[pos + 1];
const uvIdx = payload[pos + 2];
const yyIdx = payload[pos + 3];
// Lookup Y0/Y1 from YYTABLE, U/V from UVTABLE
const [y0, y1] = YYTABLE[yyIdx];
const [u, v] = UVTABLE[uvIdx];
// Fill 4x4 cell based on bitmap
const cellX = (cellIdx % cells_x) * 4;
const cellY = Math.floor(cellIdx / cells_x) * 4;
for (let dy = 0; dy < 4; dy++) {
for (let dx = 0; dx < 4; dx++) {
const bit = (bitmap >> (15 - (dy * 4 + dx))) & 1;
const y = bit ? y1 : y0;
frame[(cellY + dy) * width + cellX + dx] = y;
}
}
cellIdx++;
pos += 4;
}
return frame;
}
Full implementation with encoder and lookup tables: github.com/johnsokol/nv
RFC 2029 specification: rfc-editor.org/rfc/rfc2029
Original 1992 holiday greeting: youtube.com/watch?v=IAYF-m1z7o4
The Old Guard (2011-2020)
Before diving into what's new, here's what we were working with:
Broadway.js — A JavaScript H.264 decoder compiled from Android's native codec via Emscripten. Revolutionary for its time, but CPU-intensive and limited to decoding only.
JSMpeg — MPEG1 video and MP2 audio decoder with WebSocket streaming support. Achieved ~50ms latency, which was impressive in 2013. Still works, still useful for legacy streams.
ogv.js — Ogg/Vorbis/Theora/Opus/WebM support via Emscripten-compiled libraries. The open codec champion.
libde265.js — HEVC/H.265 decoder in JavaScript. Brought next-gen codec support to browsers before native implementations.
These libraries solved real problems by bringing codec support to browsers that lacked it. But they all shared the same fundamental limitation: they reimplemented codecs that already existed in the browser's native media pipeline, wasting bandwidth to download what was already there and burning CPU cycles on software decoding.
The New Native APIs
WebCodecs API
This is the big one. WebCodecs gives JavaScript direct access to the browser's hardware-accelerated video encoders and decoders.
What it does:
- Encode raw video frames to H.264, VP8, VP9, AV1
- Decode compressed video chunks back to frames
- Hardware acceleration on supported devices
- Works with the Streams API for efficient pipelines
Browser support: Chrome 94+, Edge 94+, Firefox 118+ (with flags), Safari not yet
// Encode video from camera
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
// Send chunk over network
sendToRemotePeer(chunk);
},
error: (e) => console.error('Encoder error:', e)
});
encoder.configure({
codec: 'vp8',
width: 640,
height: 480,
framerate: 30,
bitrate: 1_000_000
});
// Get frames from camera
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
const processor = new MediaStreamTrackProcessor({ track });
const reader = processor.readable.getReader();
while (true) {
const { value: frame, done } = await reader.read();
if (done) break;
encoder.encode(frame, { keyFrame: frameCount % 150 === 0 });
frame.close();
}
// Decode received video
const decoder = new VideoDecoder({
output: (frame) => {
ctx.drawImage(frame, 0, 0);
frame.close();
},
error: (e) => console.error('Decoder error:', e)
});
decoder.configure({ codec: 'vp8' });
// When chunk arrives from network
function onChunkReceived(data, timestamp, isKey) {
decoder.decode(new EncodedVideoChunk({
type: isKey ? 'key' : 'delta',
timestamp: timestamp,
data: data
}));
}
Why this matters: No more shipping megabytes of WASM codec implementations. The browser already has optimized, hardware-accelerated codecs—WebCodecs just exposes them to JavaScript.
WebTransport API
WebTransport is what WebSockets should have been. Built on HTTP/3 and QUIC, it offers both reliable streams and unreliable datagrams.
Key advantages over WebSockets:
- Unreliable datagrams — Like UDP, packets can be dropped. Perfect for real-time video where old frames are worthless.
- No head-of-line blocking — Multiple streams don't block each other.
- Lower latency — QUIC's 0-RTT connection establishment.
- Multiplexing — Many streams over one connection.
const transport = new WebTransport('https://server.example:4433/video');
await transport.ready;
// Unreliable datagrams for video frames (drop old frames, don't wait)
const writer = transport.datagrams.writable.getWriter();
await writer.write(encodedFrameData);
// Or reliable streams for control messages
const stream = await transport.createBidirectionalStream();
const streamWriter = stream.writable.getWriter();
await streamWriter.write(new TextEncoder().encode('START_STREAM'));
The catch: WebTransport requires a server. It's not peer-to-peer like WebRTC. But if you're building a streaming service (not P2P), it's the future.
Latency achieved: Facebook's experimental WebTransport media server demonstrates <60ms end-to-end latency under good network conditions.
P2P Without the Pain
WebRTC is powerful but complex. The ICE/STUN/TURN dance for NAT traversal, the SDP offer/answer exchange, the need for a signaling server—it's a lot. These libraries simplify things dramatically.
webConnect.js
The newest and cleanest option for serverless P2P.
<script type="module">
import webconnect from 'https://cdn.jsdelivr.net/npm/webconnect/dist/esm/webconnect.js'
const connect = webconnect({
appName: 'my-video-app',
channelName: 'stream-room-123'
})
connect.onConnect(async (attr) => {
console.log(`${attr.connectId} joined`)
// Send data
connect.Send({ type: 'chat', text: 'hello' }, { connectId: attr.connectId })
// Or stream video
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
connect.openStreaming(stream, { connectId: attr.connectId })
})
connect.onStreaming((stream, attr) => {
document.getElementById('remoteVideo').srcObject = stream
})
connect.onReceive((data, attr) => {
console.log('Received:', data, 'from:', attr.connectId)
})
</script>
How it works: Uses BitTorrent trackers, MQTT brokers, or Nostr relays for signaling. No server to maintain. Once the WebRTC connection is established, the signaling infrastructure is out of the loop.
Features:
- Auto mesh networking
- Zero configuration for local network
- Binary data and streaming support
- Progress callbacks for large transfers
Trystero
More mature, more backend options.
import { joinRoom } from 'trystero/torrent' // or /nostr, /mqtt, /firebase, /supabase, /ipfs
const room = joinRoom({ appId: 'my-app' }, 'room-name')
// Define actions (like events)
const [sendVideo, getVideo] = room.makeAction('video')
// Send to specific peer or broadcast
sendVideo(encodedFrame, peerId) // to one peer
sendVideo(encodedFrame) // to all peers
// Receive
getVideo((data, peerId) => {
decoder.decode(new EncodedVideoChunk({ ... }))
})
// Built-in media streaming
room.addStream(localStream)
room.onPeerStream((stream, peerId) => {
remoteVideo.srcObject = stream
})
Backend options:
- BitTorrent — Uses public torrent trackers
- Nostr — Decentralized social protocol relays
- MQTT — IoT messaging brokers
- Firebase/Supabase — If you already use them
- IPFS — Distributed web
PeerJS
The veteran. Simpler API, but requires their cloud service or self-hosted server.
const peer = new Peer() // Uses PeerJS cloud by default
peer.on('open', (id) => {
console.log('My peer ID:', id)
})
// Connect to another peer
const conn = peer.connect('remote-peer-id')
conn.on('open', () => {
conn.send('Hello!')
})
// Receive connections
peer.on('connection', (conn) => {
conn.on('data', (data) => console.log('Received:', data))
})
// Video calls
const call = peer.call('remote-peer-id', localStream)
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream
})
Recommended Stacks for 2025
For P2P Video Chat (no server)
getUserMedia()
↓
webConnect.js or Trystero (signaling via BitTorrent/Nostr)
↓
WebRTC MediaStream (built-in, hardware-accelerated)
↓
<video> element
This is the simplest path. Let WebRTC handle the codec work internally.
For Custom Codec Experiments
getUserMedia()
↓
MediaStreamTrackProcessor → VideoFrame
↓
Custom encoder (your own algorithm)
↓
webConnect.js DataChannel (binary)
↓
Custom decoder
↓
Canvas rendering
Use this if you're implementing something like CellB for educational purposes or experimenting with novel compression approaches.
For Production Streaming (server-based)
getUserMedia()
↓
WebCodecs VideoEncoder (H.264/VP9/AV1)
↓
WebTransport datagrams (unreliable, low latency)
↓
Server relay/CDN
↓
WebTransport to viewers
↓
WebCodecs VideoDecoder
↓
Canvas or <video> via MediaStreamTrackGenerator
This gives you the lowest latency and most control, but requires server infrastructure.
For Maximum Compatibility
getUserMedia()
↓
MediaRecorder (built-in, widely supported)
↓
WebSocket to server
↓
Media Source Extensions (MSE) on viewers
Boring but bulletproof. Works everywhere, including Safari.
Quick Reference: What to Use When
| Use Case | Recommended Stack |
|---|---|
| P2P video chat, no server | webConnect.js + native WebRTC |
| Multi-party calls | Trystero mesh + WebRTC |
| Low-latency streaming to many | WebTransport + WebCodecs |
| Custom codec research | WebCodecs + DataChannel |
| Maximum compatibility | MediaRecorder + WebSocket + MSE |
| Legacy browser support | JSMpeg (still works!) |
| Retro/embedded devices | CellB-style simple codecs |
End-to-End Latency: 1992 vs 2025
The ultimate measure of a real-time video system is glass-to-glass latency—the time from photons hitting the camera sensor to photons leaving the display.
Historical Comparison
| Era | Stack | Typical E2E Latency | Limiting Factor |
|---|---|---|---|
| 1992 | CellB + MBone | 200-500ms | Network (56kbps modems, satellite hops) |
| 2010 | Flash RTMP | 1-3 seconds | Buffering, TCP retransmits |
| 2015 | WebRTC (early) | 150-300ms | ICE negotiation, jitter buffers |
| 2018 | HLS/DASH | 6-30 seconds | Segment duration, CDN propagation |
| 2020 | WebRTC (optimized) | 50-150ms | Encode latency, network jitter |
| 2023 | Low-Latency HLS | 2-5 seconds | Partial segments, still chunked |
| 2025 | WebTransport + WebCodecs | 30-80ms | Hardware encode, QUIC transport |
| 2025 | WebRTC DataChannel + WebCodecs | 40-100ms | P2P variance, encode pipeline |
Breakdown: Where Latency Hides
Camera capture: 16-33ms (frame interval at 30-60fps)
Encode:
- CellB: <1ms (trivial computation)
- WebCodecs VP8 software: 10-20ms
- WebCodecs H.264 hardware: 2-5ms
- WebCodecs AV1: 30-100ms
Network:
- Local network: 1-5ms
- Same city: 10-30ms
- Cross-continent: 50-150ms
- Satellite: 500-700ms
Jitter buffer: 0-100ms (trade latency for smoothness)
Decode:
- CellB: <1ms
- WebCodecs hardware: 1-3ms
- WebCodecs software: 5-15ms
Display: 8-16ms (display refresh interval)
The CellB Advantage (and Why It Doesn't Matter Anymore)
In 1992, CellB's sub-millisecond encode/decode meant the codec contributed almost nothing to total latency. The network was the bottleneck.
Today, hardware-accelerated WebCodecs achieves 2-5ms encode times with 5-10x better compression. The codec is no longer the bottleneck—it's jitter buffering and network variance.
However, for specific use cases like:
- ESP32/Raspberry Pi without hardware encoders
- WebAssembly environments without WebCodecs
- Educational demonstrations of codec fundamentals
...CellB-style approaches remain relevant. Simple algorithms that run anywhere beat complex algorithms that require specific hardware.
The Libraries That Still Matter
For specific codec support not in browsers:
- ogv.js — Ogg/Theora/Opus when you need it
- libde265.js — HEVC when Safari finally isn't enough
For audio specifically:
- Aurora.js — Audio decoding framework
- Tone.js — Web audio synthesis and processing
For video players (not streaming):
- Video.js — Standard HTML5 player with plugins
- Plyr — Clean, accessible player UI
- hls.js — HLS streaming support
- dash.js — MPEG-DASH support
What Changed and Why It Matters
Five years ago, building real-time video in the browser meant:
- Shipping megabytes of WASM codecs
- Burning CPU on software decoding
- Running your own signaling server
- Fighting WebRTC's complexity
Today:
- WebCodecs gives native hardware codec access
- WebTransport provides modern low-latency transport
- webConnect.js/Trystero eliminate signaling servers
- Higher-level APIs hide WebRTC complexity
The browser is finally a first-class platform for real-time video. The tools are here. Go build something.
Last updated: January 2025
Found something I missed? Have a library recommendation? Leave a comment.
Resources
Modern APIs
- WebCodecs API (MDN)
- WebTransport API (MDN)
- WebCodecs Samples (W3C)
- Video Processing with WebCodecs (Chrome)
P2P Libraries
CellB Historical Resources
- RFC 2029 - RTP Payload Format for Sun's CellB
- Original nv (network video) source with CellB
- 1992 Holiday Greeting - First global internet video stream
- Holiday Greeting source files
