- JavaScript 63.9%
- CSS 21.8%
- HTML 6.5%
- Nix 6.1%
- Dockerfile 1.7%
1. Making it so focus mode can be resized and the size is stored. 2. Allowing the arrow keys to rotate through streams in focus and fullscreen mode |
||
|---|---|---|
| src | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| flake.nix | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| shell.nix | ||
| vite.config.js | ||
MultiStream
Watch multiple Twitch and YouTube streams at the same time in a single browser tab.
Features
- Multi-stream grid — watch Twitch channels and YouTube videos side-by-side
- Drag to reorder — grab the ⠿ handle on any stream card and drag it into position
- Per-stream audio controls — mute/unmute each stream individually
- Solo mode — focus audio on one stream and silence the rest with a single click
- Global mute / unmute — silence or restore all streams at once (
M/U) - Streams start muted — new streams are added silently so you stay in control
- Account sign-in — sign in to Twitch and YouTube so embedded players recognise your subscriptions and YouTube Premium (skips ads)
- Profiles — save named groups of streams (e.g. "Valorant", "Hermitcraft") and switch between them instantly
- Persistent state — open streams, layout, and profiles are saved to
localStorageand restored on reload - Clear All — remove all active streams in one click
- Flexible layouts — auto-fill grid or fixed 1–4 column layouts
Supported URL formats
| Platform | Example |
|---|---|
| Twitch channel | twitch.tv/channelname |
| YouTube video | youtube.com/watch?v=VIDEO_ID |
| YouTube short URL | youtu.be/VIDEO_ID |
| YouTube live | youtube.com/live/VIDEO_ID |
| YouTube embed | youtube.com/embed/VIDEO_ID |
Twitch embeds require the page to be served over HTTP/HTTPS — they will not work when opened as a local
file://URL.
Keyboard shortcuts
| Key | Action |
|---|---|
A |
Open the Add Stream dialog |
M |
Mute all streams |
U |
Unmute all streams |
Esc |
Close any open dialog |
Getting started
Prerequisites
- Node.js 18 or later (24 recommended)
- npm 9 or later (bundled with Node.js)
Install and run
# 1. Install dependencies
npm install
# 2. Start the dev server (hot reload at http://localhost:5173)
npm run dev
Available npm scripts
| Script | Description |
|---|---|
npm run dev |
Dev server with hot reload → http://localhost:5173 |
npm run build |
Production bundle → dist/ |
npm run preview |
Serve the production build → http://localhost:4173 |
npm start |
Build then serve (production preview) |
Nix
Two Nix entry points are provided — use whichever matches your setup.
shell.nix — classic Nix (nix-shell)
Uses your system nixpkgs channel.
# Enter the dev environment (auto-installs npm deps on first run)
nix-shell
# Or run a single command without entering the shell
nix-shell --run "npm run build"
flake.nix — modern Nix (nix develop)
Pins nixpkgs to nixos-unstable for fully reproducible builds.
# Enter the dev environment
nix develop
# Or run a single command
nix develop --command npm run build
Both shells provide Node.js 24 (with npm) and print available commands on entry. If node_modules/ is absent they run npm install automatically.
Reproducible nix build
flake.nix also exposes a packages.default output that builds the dist/ folder hermetically via buildNpmPackage. Before using it you need to supply the correct npmDepsHash:
# 1. Make sure package-lock.json is up to date
npm install --package-lock-only
# 2. Compute the hash
nix-prefetch-npm-deps package-lock.json
# 3. Paste the printed hash into flake.nix → npmDepsHash = "sha256-...";
# 4. Build — outputs to ./result/
nix build
Docker
Quick start
# Build the image and start the container
docker compose up -d
# Tail logs
docker compose logs -f
# Stop and remove the container
docker compose down
The app will be available at http://localhost:3000.
Build and run manually
# Build the image
docker build -t multistream:latest .
# Run the container
docker run -d \
--name multistream \
-p 3000:3000 \
--restart unless-stopped \
multistream:latest
Image details
The Dockerfile uses a two-stage build to keep the final image small (~173 MB):
| Stage | Base image | Purpose |
|---|---|---|
builder |
node:24-alpine |
Install npm dependencies and run vite build |
runner |
node:24-alpine |
Install serve, copy dist/, expose port 3000 |
The runner stage contains only the compiled static assets and the static file server — no source code, Vite, or dev tooling.
Changing the port
Edit docker-compose.yml and update the left side of the port mapping:
ports:
- "8080:3000" # now reachable at http://localhost:8080
Or pass it directly with docker run:
docker run -d -p 8080:3000 multistream:latest
Rebuilding after source changes
docker compose up -d --build
Project structure
multistream/
├── index.html # HTML shell (markup only)
├── src/
│ ├── main.js # Application logic (ES module)
│ └── style.css # All styles
├── public/ # Static assets served as-is
├── dist/ # Production build output (generated)
├── package.json # npm scripts and dependencies
├── vite.config.js # Vite configuration
├── shell.nix # Classic Nix dev shell
├── flake.nix # Flake-based Nix dev shell + build
├── Dockerfile # Two-stage Docker build
├── docker-compose.yml # Docker Compose service definition
└── .dockerignore # Files excluded from Docker build context
Dependencies
| Package | Role |
|---|---|
| SortableJS | Drag-and-drop reordering of stream cards |
| Vite | Dev server and production bundler |
| serve | Static file server used in the Docker image |