Evan Panahi

Software, games, and whatever I’m reading.

Theme
Essay

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.

16 August 2026

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 with TWITCH_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:

  1. The Host proxy rewrites every non-asset path into /[domain]/…, so a webhook or /api/live route needs the matcher to exclude /api/ (one line in src/proxy.ts).
  2. 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.