Skip to content

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

445 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CouchSwarm

github.com/TypicalHog/couchswarm · created by TypicalHog, 2026 · released under The Unlicense, MIT or Apache 2.0, your choice.

CouchSwarm plays torrents you choose. It hosts, indexes and provides no content, and its creator does not endorse using it to share or stream anything you have no right to.

A shared movie room built with React, WebTorrent, and playsvideo. Vercel runs Next.js with a Turso/libSQL database. The helper app retrieves video from ordinary torrent peers and delivers verified pieces to room participants over WebRTC, with TURN fallback. Each browser maintains its own buffer and follows the host’s authoritative timeline.

Helper app and Vercel

On Windows, download public/downloads/CouchSwarm-Helper-win-x64.zip, extract it, and open helper-package/CouchSwarm Helper.exe. On Linux x64, download public/downloads/CouchSwarm-Helper-linux-x64.tar.gz, extract it, and run helper-package/couchswarm-helper from a terminal. Either package includes Node; no separate runtime installation is needed. In your room, choose Connect your helper, create a pairing link, and paste it into the helper. The host’s helper serves every guest who doesn’t run their own. A guest can pair their own helper the same way to download from torrent peers directly instead of through the host; guests without one only need the room invite link. Neither package is committed: run npm run build:helper for the Windows ZIP or npm run build:helper:linux for the Linux tarball (or host a release), and set COUCHSWARM_HELPER_DOWNLOAD_URL or COUCHSWARM_HELPER_DOWNLOAD_URL_LINUX before the matching download button appears. A site can publish one build or both; each button shows only when its own URL is set.

Keep the helper open while watching. While it is sharing it asks Windows not to sleep, so a helper PC nobody is watching on keeps serving viewers; closing the lid and sleeping on purpose still work. It supports one room with up to 12 viewers and places no limit on torrent size, so keep an eye on free disk space for a very large movie. Movie pieces are downloaded on demand and shared with viewers and torrent peers; while anyone is watching, uploads to torrent peers are held to 256 KB/s so the swarm does not compete with the room for your upload, and the limit lifts once nobody is connected. Keep downloads when I close is on by default: the movie stays on disk in the download folder, which is %LOCALAPPDATA%/CouchSwarm/downloads unless Browse points somewhere else. That folder must be on an NTFS drive: exFAT and FAT32 cannot store a movie in pieces, so the app refuses such a folder instead of reserving the whole movie on the first write. Every kept torrent gets its own torrent-<info hash> subfolder there, single-file torrents included. Each browser selects the whole video and the helper fetches whatever browsers request, so a kept download ends up complete unless the room leaves before the swarm delivers all of it. A kept movie is checked against its piece hashes every time it is loaded, so restarting the helper, or leaving a movie and coming back to it, reads the whole download again before playback resumes; on a slow drive that takes a while. Clear that checkbox for the old behaviour, a temporary room-* folder removed when you stop sharing or close — inside your chosen folder, or %LOCALAPPDATA%/CouchSwarm/downloads when you choose none. A forced process kill may leave one behind; remove it only while the helper is stopped. The folder and the checkbox are saved in %LOCALAPPDATA%/CouchSwarm/settings.json, and anything the helper writes to its error output is timestamped into %LOCALAPPDATA%/CouchSwarm/helper.log, which is discarded once it passes 256 KiB; that log is where a helper that will not start says why. Pairing links expire in five minutes and work once; restarting the helper requires a fresh link. The current Windows build is unsigned.

The Linux package is the same helper without a window: extract the tarball and run ./couchswarm-helper in a terminal, and keep that terminal open while you watch. Everything named Windows above — SmartScreen, the sleep request, the NTFS requirement, the %LOCALAPPDATA% paths — describes the Windows package. This one downloads by default into $XDG_DATA_HOME/CouchSwarm/downloads, or ~/.local/share/CouchSwarm/downloads when that variable is unset, and timestamps anything it writes to its error output into $XDG_STATE_HOME/CouchSwarm/helper.log, or ~/.local/state/CouchSwarm/helper.log; that log is where a helper that will not start says why. The package’s own README.txt repeats those paths.

See DEPLOYMENT.md for the Vercel database, helper downloads, and relay setup. npm run build:helper builds the portable ZIP on Windows x64 with Node 24 LTS, and npm run build:helper:linux the tarball on Linux x64. npm run test:remote checks pairing and real TCP → WebRTC transfers; npm run test:relay checks a configured live TURN server with relay-only connections.

Run locally

Requires Node.js 22.13 or a later 22, or Node.js 24: the two lines CI tests, and the range package.json holds a Vercel deployment to, so a new Node major is never picked up untested.

npm.cmd install
npm.cmd run dev

That serves http://localhost:3001 and needs no environment: the schema is applied before the dev server starts, into .local/rooms.db unless TURSO_DATABASE_URL names another database. Migrations added under drizzle/ are applied on the next start, so a database that has fallen behind catches up with its rooms intact; only a start that fails with Applied migration changed needs .local/rooms.db deleted and recreated, and that discards every room it holds. Streaming uses a service worker, so use HTTPS or localhost; plain HTTP on a LAN address cannot stream torrents. CouchSwarm needs Chrome or Edge 116+, Firefox 124+, or Safari 17.4+; older browsers are refused with a message instead of failing mid-stream.

Local development helper

The dev server serves the website only. A local room reaches a helper two ways: pair the helper app above through Connect your helper, which works against http://localhost:3001 like any other origin, or run the standalone npm run helper behind a proxy that serves its /torrent-helper/ routes from the website’s own origin. Without either, the site probes /torrent-helper/health, gets nothing, and falls back to ordinary browser WebTorrent peers. The connection panel shows Your helper when connected; the standalone helper also reports its native torrent peer count there, while the paired app shows that count in its own window. Use Reconnect to movie after a helper error.

The browser opens a helper session using its existing room credential. The helper reads the room's source from the room API, obtains metadata through ordinary torrent discovery, and returns that torrent's metadata — rebuilt without the metadata URL hints and web seeds it strips — plus an opaque web seed URL. Each browser carries the source's own HTTPS web seeds and wss: trackers to its torrent itself, so a web-seed-only movie still plays while a helper is paired. Each browser still verifies piece hashes, maintains its own buffer, processes MKV locally, and follows the existing shared timeline. Multiple viewers in the same room reuse one helper download. Requested ranges prioritize the pieces required for playback and seeking, but each browser selects the whole video, so the helper is asked for all of it in the end. That selection restarts at the piece a read begins in whenever the read lands behind it or past a piece it has not reached, so a late join or a seek fills forward from the playhead, and the pieces it skipped are taken once nothing ahead is left. The paired helper additionally keeps a few 32 MB windows selected past recent requests so its swarm download stays pipelined, the window a viewer moved to most recently ahead of the ones it left.

The helper supports two active room torrents and 24 viewer leases, with no cap on torrent size. It waits up to 90 seconds for metadata. Normal leaving releases the lease; an inactive lease expires after two minutes. Once its last lease ends the download is kept for about a minute so a reconnecting viewer reuses it; after that the native client stops and its generated .torrent-cache/session-* directory is deleted. A leftover session-* directory older than an hour is removed the first time the standalone helper loads a torrent after it starts; the paired Windows helper does the same for a leftover room-* directory, but only while Keep downloads when I close is cleared, so one left by a forced kill stays until you clear that checkbox again or remove it yourself. A forced process kill can leave a cache directory behind; remove those directories only while the helper is stopped. On Windows both helpers mark each file that shares a piece with the video as sparse before writing it, so the tail pieces an MKV player reads first do not make NTFS allocate the whole file; the paired app refuses a download folder on a drive that is neither NTFS nor ReFS, because sparse marking silently fails there. If the torrent fails after it started serving (a full drive, say), the paired Windows helper reports the reason and reloads it, and after three failed loads it asks for the movie to be chosen again; the standalone npm run helper reports the reason and drops the torrent, so use Reconnect to movie to load it again. Torrent data is excluded from Git and deployment packages. UPnP and NAT-PMP router changes are disabled.

The helper uses TCP and WebRTC for piece transfers, with UDP trackers and DHT for discovery. uTP transfers are disabled because the installed client waits through several uTP retries before falling back to TCP, delaying startup against TCP-only peers.

Run npm run helper for that standalone process. It listens only on 127.0.0.1:3791; COUCHSWARM_ORIGIN selects the room website (default http://localhost:3001; it is not the website’s own COUCHSWARM_PUBLIC_ORIGIN, which sets the helper pairing-link origin) and COUCHSWARM_HELPER_PORT changes the listen port. COUCHSWARM_HELPER_CACHE moves its torrent cache directory (default .torrent-cache in this folder); it does not change the Windows helper app's download folder, which that app stores itself. Serve its /torrent-helper/ routes through a reverse proxy at the same HTTPS origin as the website, preserving Host and Origin, to use it remotely. Room credentials and opaque per-viewer capabilities restrict access; foreign browser origins are rejected. This is a local companion, not a public multi-tenant torrent service. Do not expose the listener as an unauthenticated public gateway. Public deployment needs an operator access policy and bandwidth quotas.

Set COUCHSWARM_HELPER_OFFLINE=1 before starting npm run helper to run the helper without DHT or trackers and to keep private-network peer hints and tracker URLs in a source. Use it only for local test fixtures; leave it unset for normal development.

For remote friends, use the paired helper app above and a publicly reachable website; a localhost invite works only on this computer. Pairing and connection setup use outbound HTTPS polling, so the host needs no tunnel or router port forwarding. That outbound access has to be direct: the helper reads neither the Windows proxy settings nor HTTP_PROXY, and TCP peers, UDP trackers, DHT and WebRTC cannot cross an HTTP or SOCKS proxy anyway, so a proxy-only network cannot run a helper even where the room itself opens in the browser. Direct WebRTC is tried first; a configured TURN server supplies the relay fallback. Vercel functions do not run the native torrent process.

HTTPS .torrent URLs must return the file directly (no redirect), use public internet addresses, and be at most 4 MiB. Magnet metadata URL hints, third-party web seeds, private-network tracker URLs, and private-network peer hints are stripped from the native helper; the browser drops the same metadata URL hints, which are fetched before any peer is contacted, and retains its normal web-seed support on every path, helper or not. The native helper announces only to udp: and wss: trackers; http:, https: and plain ws: tracker URLs are dropped with no error, so a torrent that announces only over HTTP is left with DHT — plus, under the standalone npm run helper, the two WebSocket trackers it adds — to find peers with. A private torrent gets none of those from WebTorrent, so one that announces only over HTTP is refused instead of waiting out the metadata timeout. A udp: tracker name is rewritten to the address that was checked, so a later DNS answer cannot move the announce, and peers offered by a tracker, the DHT or ut_pex are refused at connect time unless they are on a public internet address, which also means a seeder on your own LAN is not used. A torrent is refused when a file that shares a piece with the video has a folder or file name Windows cannot create (a reserved device name such as CON, NUL, AUX or COM1, or a name ending in a dot or a space), a name Windows would store as an existing file, a path longer than 250 characters once joined to the folder the helper downloads to (a shorter folder chosen with Browse can make the same torrent fit), or — in a multi-file torrent — a name WebTorrent cannot request as a web seed path (#, ?, %, or a control character). Other filenames, including spaces and Unicode, are supported.

Watching together

Paste a magnet or HTTPS .torrent URL, create the room, and share the invite link. Guests choose a name and enable playback on their device. The host can start after everyone buffers eight seconds (or the remaining video). A three-second scheduled start and periodic drift correction keep viewers close to the shared timeline. Host pause, seek, source changes, a new guest, buffer loss, or a disconnected host pause the room. Seeking requires everyone to buffer again. A buffer stall resumes on its own three seconds after everyone is ready again; every other pause waits for the host. The host can also press Space to play or pause for the room, as long as no field, button or dialog has focus. Beneath the player, the file row carries a bar over the whole movie showing which of its pieces this device has saved, which are arriving, and which are still to come, so a download that is ahead of the playhead — or behind it, after a seek — can be seen rather than inferred from a percentage.

Rooms support 12 active participants and expire 24 hours after they are created, once no one is on the couch. An expired room is deleted outright - members, helpers and signalling rows with it - by the next request that reaches it, and by the rooms anybody creates next, oldest first and at most a hundred per new room. Invite secrets and participant credentials are hashed in the room database (Turso on Vercel, a local libSQL file in development). Each tab keeps its own access credential and the room invite in session storage; a host also keeps a re-claim key in local storage, which outlives Leave on purpose so they can come back to a room they are still hosting. While that key is there, anyone who opens the same invite in the same browser profile rejoins as host, able to control playback, remove people and rotate the invite, so clear this site's data after hosting on a shared computer. Rotating the invite retires the old key, and a key is swept a day after its room was created. Invitees cannot control playback. Room APIs use revision checks and heartbeat sequence numbers to reject stale updates. Clients poll every second while a movie is loaded and every two seconds in an empty lobby, pause if contact is lost for 3.5 seconds (a little longer on slow connections, and up to 11 while a request is still waiting on its answer, so a reply that is merely slow does not stop the movie), and back polling off toward eight seconds once contact has been gone for twelve; small drift is corrected with playback speed and larger drift by seeking. Network timing means this is best-effort synchronization, not a frame-accurate guarantee.

Data and privacy

CouchSwarm has no accounts and collects nothing for itself, but a room does hold and hand on more than the video. What is kept where:

  • Room database (Turso on Vercel, .local/rooms.db in development): the room name, the torrent source, the name each participant chose, hashed invite secrets, participant credentials and helper pairing tokens, the helper's own status text, and — while a guest is connecting to a paired helper — the WebRTC offer and answer that set that connection up, which name the participants' IP addresses. All of it goes when the room is cleaned up, as described above.
  • Your browser: the room invite and this tab's access credential in session storage, a host's re-claim key in local storage until they leave, and the torrent store described under Torrent requirements.
  • The helper's computer: the movie in the download folder, %LOCALAPPDATA%/CouchSwarm/settings.json, and %LOCALAPPDATA%/CouchSwarm/helper.log; on Linux, the movie in the download folder and ~/.local/state/CouchSwarm/helper.log.
  • The relay: a TURN server sees the IP addresses of the clients it relays between, and the temporary username it is given carries the member or helper id it was minted for. As deploy/turn/compose.yaml runs coturn, its log names a username or an address only when something fails — a credential that is wrong or has expired, a broken connection, a rate limit — and Docker keeps the last 30 MB of that log; started with --verbose, coturn logs every session's.
  • Everyone else: torrent peers, trackers and the DHT see the IP address and the info hash of whoever is in the swarm, browser or helper, as they do for any torrent client. Connection setup asks a STUN server for a public address — Google's unless COUCHSWARM_STUN_URLS names another — and a Vercel deployment with Analytics and Speed Insights enabled collects whatever Vercel documents for those. Both are handed the page's path alone: the room id, the invite and a pairing code are cut from every URL they report.

Torrent requirements

  • Browsers connect to WebRTC-compatible peers and web seeds. The local helper acts as a web seed and connects to traditional (non-WebRTC) torrent peers over TCP, discovering them via UDP trackers and DHT; without a helper, a WebTorrent-capable seeder or reachable web seed must already be available.
  • CouchSwarm adds public WebSocket trackers to discover WebRTC peers even when a magnet lists only UDP trackers. Extra trackers cannot supply missing seeders or connect a browser directly to ordinary torrent clients. Fully browser-only use requires an existing WebRTC swarm, an HTTPS web seed, or someone seeding their local files from a browser. Adding support for arbitrary traditional swarms requires a separate torrent bridge. These trackers are also announced when a paired helper delivers the movie.
  • MKV, MP4, M4V, MOV, and WebM files are selectable. MKV uses playsvideo: on-demand remuxing into fragmented MP4 and conversion of supported audio formats (including AC3, EAC3, MP3, FLAC, and Opus) to AAC. DTS, TrueHD and MP2 are not among them: the demuxer cannot identify those tracks, so a release that carries one first plays on a supported track behind it - one in the main track's language when there is one, and a commentary only when nothing else is left - and one that carries nothing else is refused with a message naming the formats. A file that path cannot carry — VP8 video, or LPCM and Vorbis audio — is handed straight to the browser, which plays many such releases itself. Each participant runs this locally; the full movie is not copied into a conversion buffer. MKV needs a browser that can construct MediaSource in a dedicated worker — Chrome, Edge, or Safari on a computer or iPad, inside the browser floor above; Firefox and iPhone Safari cannot, so CouchSwarm now refuses MKV there with a clear message instead of failing with a ReferenceError.
  • Subtitles are each participant's own: pick an .srt, .ass, .ssa or .vtt file from the torrent, or upload one from your device, and everyone else keeps what they chose. A subtitle track stored inside an MKV is never offered, and bitmap subtitles (.sub with its .idx, .sup) cannot be displayed.
  • Video is passed through without re-encoding. Devices must support the video's codec (for example, HEVC support depends on browser and hardware). The helper bridges torrent transport; no server video transcoder is included.
  • Indexed MKVs use their cue table to start and seek without scanning the entire movie. MKVs with missing or damaged cues can need a longer initial scan.
  • .torrent URLs and web seeds must permit browser requests (CORS).
  • The largest compatible video is selected first. When a torrent holds more than one video — a season pack, say — a Video in this torrent dropdown appears under the player listing each file by name, with its full path as a tooltip; the host picks, and the whole room switches together.
  • Keep the tab open. Downloaded pieces are uploaded to other peers; leaving keeps this client's torrent store so a reconnect resumes where it stopped, and anything a closed tab left behind is cleaned up the next time you open a movie. Only one CouchSwarm movie tab can run per browser profile, and a room can be open in only one tab; a second tab is refused with a message saying another tab already has the movie — or the room — open, and telling you to close that tab first.

Checks

npm.cmd test
npm.cmd run test:rooms
npm.cmd run test:mkv
npm.cmd run test:helper
npm.cmd run test:migrate
npm.cmd run test:patches
npm.cmd run test:remote
npm.cmd run typecheck
npm.cmd run lint
npm.cmd run build

npm run test:offline runs every suite that needs no server - test, test:mkv, test:helper, test:migrate and test:patches - after checking that each test file belongs to one of the scripts. npm run test:rooms and npm run test:remote use the running local server at http://localhost:3001; set TEST_ORIGIN if your printed URL differs. Set COUCHSWARM_PACKAGED_TEST=1 to run test:remote against the built work/helper-package agent instead of helper/. The room integration test creates isolated rooms and checks invite access, host permissions, readiness, scheduled starts, buffer stalls, stale reports, seeking, source replacement, and leaving. The MKV check uses actual synthetic video/audio, reads an MKV over HTTP ranges, verifies a small cue-table read, remuxes its first and later segments, and checks both tracks and timestamps. Real WebRTC media playback still depends on an available swarm and the browser's codec support.

Helper tests seed synthetic files over loopback TCP, retrieve magnet metadata, and download the full single-file and multi-file payloads through a webseed-only consumer with piece-hash verification. They cover first-byte and mid-file seeking, HEAD, invalid ranges, separate viewer capabilities, shared downloads, cache deletion, origin/room authentication, idle expiry, the request body size limit, and rejection of private-network torrent URLs. No public movie torrent or browser UI is used by these tests. WebTorrent exposes no bind address, so the synthetic seeders here and the two clients in npm test listen on every interface for the few seconds a check runs; nothing advertises the ephemeral port and Windows Firewall may prompt once.

Sources

The build copies the matching WebTorrent service worker into public/sw.min.js. Drizzle migrations define the room schema; npm run db:migrate applies them, and predev and build run it for you.

playsvideo is pinned to 0.4.7; the Mediabunny fork it asks for is vendored at one commit as vendor/mediabunny-1.38.1-cd3cd2b.tgz, which a mediabunny devDependency and an override point every install at, so installing needs no GitHub access and the lockfile carries that tarball's integrity hash (see vendor/README.md). A scoped build transform makes its embeddedSubtitlePolicy: 'off' skip eager subtitle extraction, preventing unrelated whole-movie reads from competing with the torrent buffer, narrows its per-fragment console logging to error and warning lines, and raises the hls.js forward buffer from 30 seconds to 60: the playlist playsvideo writes carries no bitrate, so hls.js never takes its size-based target and stopped at a flat 30 seconds however much of the movie was already saved. Its ceiling, maxMaxBufferLength, is set to the same 60, because that ceiling is the only thing hls.js lowers when a browser's media buffer fills first, and from its default of 600 the first two reductions would change nothing while each one drops a fragment to be remuxed again. The same transform plans an MKV's segments from the cue points of its video track alone: mkvmerge and FFmpeg index subtitle events as well, and a segment starting at one re-cuts the video from the keyframe before it, so the picture froze for seconds at a time while the sound ran on. It also closes the seams in a converted soundtrack: playsvideo converts AC-3 and E-AC-3 to AAC a segment at a time, each in its own ffmpeg run, and every run opened on the encoder's 1,024-sample priming frame and ended on a half-finished window, so each seam, every few seconds, played about 24 ms of silence and noise, and the sound ran 21 ms behind the picture. Each run now also converts at least 3,072 samples of real audio either side of its segment, cut on a grid of whole source and AAC frames, and keeps only the frames between its cuts, so neighbouring runs meet frame for frame as one continuous encode would; the packet length comes from the codec (1,536 samples for AC-3, and what the frame header says for E-AC-3, MP3 and FLAC), so 44.1 kHz AC-3, stamped unevenly to the millisecond, fits the grid too, while a segment whose packets are not where the grid puts them, or that the grid would leave without sound, is converted as before, as is Opus. A soundtrack that starts after the film's first segment no longer throws the player into a reset loop: that segment had no audio, so the init segment cut from it declared no audio track and every later append failed. The stretch before the soundtrack's first packet is now filled with silent AAC frames, the ones hls.js fills its own gaps with, so every segment carries the track and the picture before the sound plays. A soundtrack that ends before the film's last segment no longer stops the picture where the sound stops: each segment past its end was handed the soundtrack's last packet, from before the segment, and hls.js dropped the fragment or found it added nothing, so the stretch after the last packet is filled the same way; the segment that reaches the soundtrack's end notes it, and the segments after it look no further. Both cover converted soundtracks and AAC-LC played as it is; a soundtrack in another format played as it is (HE-AAC, Opus, FLAC or MP3) that starts that late or ends that early still fails. scripts/playsvideo-loader.cjs reads the patch strings from scripts/playsvideo-patches.json, whose keys are also the dist files the build routes through it, and npm run test:patches fails when an upgrade moves one of those files or changes a patched string, so review this transform when upgrading playsvideo. Every npm audit fix that is lockfile-only is applied on each refresh, so what npm audit still reports is what CouchSwarm accepts. npm audit --omit=dev reduces to one chain: ip@2.0.1 (GHSA-2p57-rm9w-gvfp) through bittorrent-tracker; its index pulls in the UDP tracker-server parser that imports it, so the module is loaded in Node and left out of the browser bundle, but that parser calls only ip.toString(), nothing here calls the misclassifying isPublic or isPrivate, and CouchSwarm never runs a tracker server, so the advisory is unreachable here. npm’s proposed fix is a semver-major webtorrent downgrade and is not applied. The rest are development-only: esbuild@0.18.20 (GHSA-67mh-4wv8-2f99) through drizzle-kit’s @esbuild-kit shim, which nothing but db:generate runs, and whose only fix is a semver-major drizzle-kit downgrade. npm ls reports an extraneous @emnapi/runtime package; it is the wasm32 optional fallback for a skipped native binding and is expected. ip-set, reached through WebTorrent, ships a preinstall of npx only-allow pnpm; under npm it is a no-op, but it fetches only-allow from the registry on every fresh install, outside the lockfile's integrity hashes and outside an offline cache. npm ci --ignore-scripts avoids it at the cost of node-datachannel's prebuilt binding, which must then be restored with npm rebuild node-datachannel.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages