Streaming on Twitch — and Teaching eap.gg to Notice
A future-me guide: how the stream is set up, and how eap.gg could show the schedule and embed the broadcast when it's live.
This is a note to a future version of me who hasn't streamed in months and has forgotten everything. Part one is the stream itself. Part two is what eap.gg could do about it — a schedule, a live embed — and how I'd build that on the current Next.js site.
Part 1 — The stream
Account. Twitch account with 2FA turned on (required to stream). In Creator Dashboard → Settings → Stream, copy the primary stream key — treat it like a password; anyone with it can broadcast as you. Reset it if it ever leaks.
Software. OBS Studio. On first run, the auto-configuration wizard is genuinely fine — pick "optimize for streaming", log in with the Twitch account, and let it measure upload bandwidth. Then sanity-check Settings → Output:
- Encoder: hardware (NVENC / Apple VT / AMF) if available; otherwise x264 at
veryfast. - Bitrate: 6000 kbps at 1080p60 is the ceiling that reliably works for a non-partner channel; drop to 936p60 or 720p60 at 4500 kbps if the encoder or upload struggles. Keyframe interval 2 s. Rate control CBR.
- Audio: 160 kbps AAC, 48 kHz. Mic on its own track with a noise gate; desktop/game audio on another so a VOD can be remixed.
Scenes. Three is plenty: Starting soon (static card + music), Game (capture + webcam + small overlay), BRB. Add a "stream ending" scene later if it becomes a habit. Keep overlays out of the game's HUD corners.
Test before anyone watches. Append ?bandwidthtest=true to the stream key in OBS, hit Start Streaming, and open inspector.twitch.tv — it shows dropped frames, bitrate stability, and keyframe issues without going live. Remove the flag afterwards.
Channel setup (once). Category + tags in Stream Manager; enable VOD storage and clips; write the About panels; turn on stream schedule in Creator Dashboard (this matters for part 2 — the schedule there becomes the source of truth for the site).
Go-live checklist. Title and category set → mic unmuted → right scene → Start Streaming → post the link. Afterwards, the VOD is what makes the site interesting a week later.
Part 2 — Teaching eap.gg to notice
Three levels of integration, in increasing order of effort. All of them live only on eap.gg — the domain registry (web/src/lib/domains.ts) is the switch, so eap.me and eap.dev stay untouched.
Level 0 — what already exists
Essays can embed a channel, VOD, or collection via the twitchEmbed block ("Adding a Twitch Embed" covers it). Good for a post-stream write-up: paste the VOD, done. Nothing to build.
Level 1 — the schedule (a calendar that maintains itself)
Don't hand-edit a schedule in Sanity; the Creator Dashboard schedule already exists and Twitch exposes it two ways:
GET https://api.twitch.tv/helix/schedule?broadcaster_id=…— JSON segments (start_time,end_time,title,category,is_recurring, vacation). Needs an app access token (client-credentials withTWITCH_CLIENT_ID/TWITCH_CLIENT_SECRET; the token is cacheable for weeks).GET https://api.twitch.tv/helix/schedule/icalendar?broadcaster_id=…— an RFC 5545 iCalendar feed, no auth. Perfect for an "Add to your calendar" link.
Site side: a small getSchedule() in src/lib/twitch.ts (fetch with next: { revalidate: 300 }), rendered as a short "Streaming" list on eap.gg. The broadcaster id comes once from GET /helix/users?login=<name> and goes into the registry (DOMAINS.gg.twitch = { login, broadcasterId }) or env.
Level 2 — embed the stream when it's live
Two halves: knowing it's live, and showing it.
Knowing. Simplest: GET /helix/streams?user_login=<name> returns a non-empty data array iff live. Wrap it in isLive() with a ~60 s revalidate; on eap.gg the page can render the live card without any client JS. Better: EventSub webhooks for stream.online / stream.offline (condition broadcaster_user_id) hitting a Route Handler that verifies the HMAC-SHA256 signature over message-id + timestamp + body (Twitch-Eventsub-Message-Signature: sha256=…, secret 10–100 chars), answers the one-time webhook_callback_verification challenge with the raw challenge as plain text, replies inside a few seconds, and calls revalidateTag('twitch-live'). Start with polling; upgrade to EventSub when the delay annoys you.
Showing. The player is an iframe:
<iframe
src="https://player.twitch.tv/?channel=<name>&parent=eap.gg&muted=true"
allowfullscreen loading="lazy" title="Live on Twitch"></iframe>parent must name every host that embeds it — eap.gg (www redirects to apex, so it doesn't need one), plus localhost in development. Add frame-src https://player.twitch.tv https://embed.twitch.tv to the CSP headers in next.config.ts. Chat can be a second iframe (https://www.twitch.tv/embed/<name>/chat?parent=eap.gg) but video-only reads better on this site.
Two things the current architecture needs before any of this:
- The Host proxy rewrites every non-asset path into
/[domain]/…, so a webhook or/api/liveroute needs the matcher to exclude/api/(one line insrc/proxy.ts). - Design freeze. A "Live now" card at the top of the eap.gg Stream is new UI. The least invasive version reuses existing DS pieces (a card with a
TypeTag-style "LIVE" label, the iframe, one line of title/category). It still counts as a freeze exception — decide it consciously in the session that builds this, don't drift into it.
Env vars for that session: TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET, TWITCH_BROADCASTER_ID, TWITCH_EVENTSUB_SECRET (all server-only; none NEXT_PUBLIC_).
After the stream
When Journals land (Plan 2), a stream naturally becomes a Journal entry: date, game, VOD embed, one paragraph. The schedule tells people when; the Stream tells them what happened. That's the whole loop.