Load a stream to begin, then open a share link in another browser.
- Start the server on one machine:
node server.js prints the URLs to open. Open the page over http:// from that server, not from a local file.
- Load a stream here. The first viewer becomes the LEADER: the Identity block says LEADER — fetching from origin and every row in Segments reads ORIGIN (HTTP). That is correct — exactly one viewer must fetch from the origin.
- Open a share link (box above) in a second tab, in another browser (Chrome, Edge, Firefox, Safari) or on another computer on the same network. That viewer must show FOLLOWER — receiving from peers, list this viewer as ready in its Peers table, and its Segments rows must read PEER <id> via have (or via loading / via pull-through) with the timeline turning green. Its P2P share should reach 90–100 % after a few segments.
- Check the leader. Back here, Uploaded grows and the Peers table shows the follower with ↑ bytes. Only the leader's Origin (HTTP) counter keeps growing; followers stay at 0–1 origin segments. The status line above turns green on both sides.
- Test failover. Close the leader tab. Within about 10 seconds one follower turns into LEADER and playback continues on every viewer.
If a follower keeps showing ORIGIN:
- Signaling is not open, or Peers is empty → both viewers must use the same server address (use the share link, not
localhost from another machine) and the identical stream URL; the status line names the signaling URL in use.
- A peer stays connecting for more than 15 s → WebRTC is blocked between the machines (firewall, guest Wi-Fi client isolation, VPN, filtered mDNS). Try two tabs on one machine first; across networks add a TURN server:
?turn=turn:host:3478|user|pass.
- Peers are ready but segments still come from origin → look for busy, timeout or budget lines in the Event log: the peer link was slower than the origin for the current buffer, so the engine used HTTP rather than stall. The share climbs once the buffer fills.
- Nothing plays at all → the stream must be served with CORS headers (
Access-Control-Allow-Origin) that allow this page's origin, and the URL must be an http(s)://…m3u8.
How it works
- One swarm per stream. Every viewer of the same stream URL joins the same room on the signaling server (the room id is a hash of the URL). The server only relays connection set-up messages; no video ever passes through it.
- One origin fetcher. The viewer that joined first (lowest rank) is the LEADER and is the only one that downloads segments from the origin/CDN. Every other viewer is a FOLLOWER and asks peers first.
- Direct browser-to-browser links. Viewers open WebRTC data channels to each other (up to 12 per viewer, lowest ranks first). Peers announce which segments they hold (have) or are downloading (loading); a segment the leader is still downloading is relayed to followers progressively, and followers re-share what they received, so a large swarm forms a tree with the leader at the root.
- Never stall for P2P. Each fragment gets a time budget derived from the playback buffer minus the estimated origin time. If a peer is slow or busy, the follower falls back to the origin within that budget, so playback continues; the share climbs back once the buffer is full.
- Checked before playing. Every segment received from a peer is verified (container sniff, size and hash) before it reaches the player; a peer that sends bad data is banned for the session.
- Failover. If the leader leaves or crashes, the next lowest rank becomes leader within a few seconds; followers keep playing. A short signaling outage is resumed without changing roles.
- Same rendition, honest ABR. Followers mirror the leader's rendition so one origin download serves the whole swarm, and peer downloads are reported to the player as if they came from the origin, so adaptive bitrate does not overshoot.
Limits
- Streams: every distinct
.m3u8 URL is its own independent swarm; segments are never shared across streams. One signaling server handles up to 10,000 swarms and 5,000 simultaneous viewer connections by default (--max-rooms, --max-connections).
- Viewers per stream: 500 per swarm by default (
--room-max). Each viewer keeps at most 12 peer connections and serves up to 12 uploads at once; beyond the leader's direct peers, segments spread follower-to-follower, each hop adding roughly one segment transfer time (milliseconds on a LAN).
- Identical URL required: viewers must load the exact same stream URL. Per-viewer tokens in segment URLs break sharing unless the engine's
keyFor option normalizes them.
- Not shared: LL-HLS partial segments, streams served without CORS headers, non-http(s) sources, and objects over 64 MiB. iOS Safari plays natively without P2P.
- Reachability: peers must be able to connect to each other — same LAN, or STUN/TURN across NATs. Networks with client isolation or symmetric NAT need a TURN server (
?turn=); otherwise those viewers fall back to the origin.
- Memory: each viewer caches up to 150 MiB of segments (about 470 MiB worst case with in-flight objects); configurable in the engine options.
- Integrity: checks detect corruption, not a deliberate attacker: a malicious peer can craft a well-formed segment with a matching hash. Real protection needs origin-published segment digests.
Bottlenecks and how to handle them
- The leader's upload link is the main limit: it sends every segment to each of its direct peers, so it needs roughly stream bitrate × direct followers of upload bandwidth (e.g. 3 Mbit/s × 10 = 30 Mbit/s). On a LAN this is rarely an issue. Across the internet: put the leader on a wired/fast connection, lower
?maxPeers= so followers fan out among themselves, or choose the first viewer deliberately (start the stream on the best-connected machine first).
- The leader's origin link is the single download: if it is slow, followers see via loading deliveries slow down and then fall back to the origin. Handle: a well-connected leader; the engine's budgets already prevent stalls.
- Background tabs: browsers throttle timers in hidden tabs; the leader relays fastest when its tab is visible or on a dedicated machine (the upload pump is event-driven to minimize this).
- TURN relays: when peers can only connect through TURN, all P2P traffic costs relay bandwidth; size the TURN server for it or keep such viewers on the origin.
- The signaling server is lightweight (join and relay only, no media) and rate-limited per connection; it scales to thousands of connections per process. To go beyond, run more instances and keep every viewer of one stream on the same instance.
- Short buffers: the P2P budget is the buffer ahead minus the estimated origin time, so a small buffer forces early origin fallbacks. A longer
maxBufferLength in the player config (30 s here) raises the P2P share.