Sunday, January 18, 2026

JavaScript Libraries for Video Streaming in 2025: A Complete Guide

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

  1. Extremely constrained devices — Microcontrollers, embedded systems without hardware video encoders
  2. Ultra-low-latency requirements — Sub-millisecond encode times, no buffering
  3. Educational purposes — Understanding video compression fundamentals
  4. 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
})

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:

  1. Shipping megabytes of WASM codecs
  2. Burning CPU on software decoding
  3. Running your own signaling server
  4. Fighting WebRTC's complexity

Today:

  1. WebCodecs gives native hardware codec access
  2. WebTransport provides modern low-latency transport
  3. webConnect.js/Trystero eliminate signaling servers
  4. 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

P2P Libraries

CellB Historical Resources

Classic JavaScript Codecs (still useful)

No comments: