Skip to main content
YK logo
← Back to projects

Streamer Wall — Concurrent Twitch Stream Monitor

A grid of 50 live, real Twitch embeds with per-tile hover-to-unmute and automatic quality swapping

Overview

Streamer Wall is a monitoring wall of concurrent Twitch streams — a grid where every tile is a real, independent Twitch player, not a static thumbnail. All tiles autoplay muted at low quality; hovering a tile unmutes it and bumps it to full quality, moving away re-mutes it and drops it back down. It's seeded with 50 channels from a Twitch collection, with the grid sizing itself automatically to whatever channel list you give it.


The problem

Fifty simultaneous video players is not the same problem as fifty thumbnails. Each tile is a genuine Twitch Embed JS player decoding real video, which means the naive version of this idea — mount 50 iframes and hope for the best — would either fall over on anything but a powerful machine or produce a wall of noise if every stream played audio at once. The actual problem was resource and attention management across a grid of players that are all, individually, fully capable of demanding CPU, GPU, and bandwidth.

Twitch's embed parent restriction

Twitch's embed player requires the page's own hostname to be passed explicitly as the parent parameter, or it refuses to load — a deliberate anti-embedding control. That needed handling correctly for both local development and deployment without hardcoding a hostname that would break the moment the app moved to a different domain.


What I built

Per-tile hover state driving mute and quality together

StreamTile.tsx wraps one Twitch Embed player and, on hover, calls the embed's player API to unmute and bump it to full quality in the same interaction, then reverses both on mouse-leave. Tying mute and quality to the same hover event rather than treating them separately was the right call — there's no situation where you'd want full-quality video muted, or audio playing at potato quality, so coupling them removes a whole class of inconsistent intermediate states.

A grid that sizes itself from the data

App.tsx lays tiles out in a grid sized to the nearest square root of however many channels are in channels.ts, with FIXED_COLS/FIXED_ROWS overrides available when you want a specific shape (a strict 10×5, say) rather than whatever the nearest square works out to. Editing the channel list is a one-line change to an array — the grid recalculates itself rather than needing a layout change alongside it.

Reading the embed hostname at runtime, not baking it in

Rather than hardcoding parent=localhost or a specific production domain, the app reads window.location.hostname at runtime and passes that to the Twitch embed. That means the same build works unmodified in local development and after deployment to any domain — the embed parent is derived from wherever the page actually is, not configured per environment.

Honest handling of a genuinely heavy page

The README is upfront about what running all 50 tiles actually costs: heavy CPU/GPU load from video decoding and significant sustained bandwidth even muted at the lowest quality, with the explicit advice to trim the channel list to 10–20 on older hardware. Offline channels dim automatically rather than being pulled from the grid or shown as broken, using Twitch's own embed offline placeholder. Documenting the resource reality of a project rather than glossing over it is a small thing, but it's the difference between someone running this and being confused why their fans spin up, versus knowing exactly what they signed up for.


Engineering decisions

Minimal hand-rolled types over a wrapper library

There's no React wrapper for the Twitch Embed SDK in wide use, so twitch-embed.d.ts provides minimal hand-written TypeScript types for the global SDK loaded via a <script> tag in index.html, rather than pulling in a heavier community package for a fairly small surface area of the API actually needed (mount, mute, set quality). For a personal tool with a narrow, well-understood integration point, writing the fifteen lines of types directly was less overhead than vetting and wiring up a third-party wrapper.

Letting the browser do less work by default

Every tile starts muted and at the lowest available quality, with the expensive state (unmuted, full quality) only active on direct hover — effectively one tile at a time under normal use, rather than 50 tiles all decoding at top quality simultaneously. This is the actual solution to the resource problem: not clever virtualisation, just making sure the expensive state is opt-in per tile rather than the default for all fifty at once.


Stack

Layer Choice
Framework React · TypeScript · Vite
Integration Twitch Embed JS SDK
Linting oxlint

Reflections

The real design problem here wasn't the grid layout or even the Twitch integration — both are fairly mechanical once you know the embed API. It was accepting that 50 real video players is a genuinely heavy page and designing the interaction model (muted-and-low-quality by default, full experience only on direct hover) around that constraint rather than around what would look best in a screenshot. Being honest about the resource cost in the README, rather than letting someone discover it the hard way, is the same instinct applied to documentation instead of code.