Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TFT Live Overlay

Your Teamfight Tactics rank, LP swings and recent placements on stream — straight from the Riot API. Ranked and Double Up.

License: MIT Electron 43 Windows · macOS · Linux OBS · Streamlabs

The app window on the Overlay page: the sidebar shows a live Diamond IV at 12 LP, a Ladder control switches between Ranked and Double Up, and the live preview below it renders the real overlay card The overlay card at Diamond I taking a first place: the placement strip shifts, the LP figure rolls from 45 to 83 and the bar closes on Master

A won game landing. Captured from the real overlay — not a mockup.


A desktop app that puts your live rank on your stream. It talks to Riot directly, so there's no third-party service in the path adding its own rate limit, its own outage, or another account to sign up for. Point it at your Riot ID, add one Browser Source, done.

Install

Prebuilt installers are available for Windows, MacOS and Linux.

Your OS says the app is unrecognised or can't be opened

The builds aren't code-signed — a Windows certificate is a paid yearly subscription and macOS notarisation needs an Apple Developer account, neither of which a free overlay app justifies. Nothing is wrong with the download; both systems simply don't recognise the publisher.

Windows SmartScreen shows "Windows protected your PC" → More infoRun anyway
macOS Right-click the app → OpenOpen. Double-clicking gives you no Open button, only Cancel. If macOS insists it's damaged: xattr -dr com.apple.quarantine "/Applications/TFT Live Overlay.app"
Linux The AppImage needs the executable bit: chmod +x tft-live-overlay-linux-x86_64.AppImage

Setup

  1. Account → enter your Riot ID (name + tag, no #) and pick your region.
  2. Paste a key from developer.riotgames.com. Personal keys expire every 24h — paste a fresh one in the same box when that happens, no restart needed.
  3. Save changes. Takes effect immediately.
  4. Overlay → pick the Ladder you want on screen: Ranked or Double Up.

Add it to OBS / Streamlabs

OverlayCopy URL → add a Browser Source pointing at it (http://localhost:3000/overlay.html).

The card is 370×108. Set the source slightly larger — say 400×130. The page background is transparent, so surplus source area is invisible, while a source smaller than the card clips it.

The Overlay page previews the card as you configure it. That preview is the real overlay in an iframe, not an approximation: what you see there is what OBS draws.

Want it bigger? Add ?scale=1.5 to the URL. That re-renders the card at the larger size and stays sharp — unlike resizing the Browser Source, which stretches an already-rendered 370×108 texture.

Want it still? ?motion=reduce turns the animation off, ?motion=os follows your machine's reduce-motion setting. Neither is the default — see How it works for why. If the card is unintentionally frozen, Help → Send a report: the overlay measures itself inside whichever browser drew it and writes a plain-language verdict you can paste into an issue.

What's on the card

Rank + LP Tier, division and LP. The figure rolls to its new value; a ▲/▼ marker shows the direction and fades.
Progress bar LP to the next tier. Dropped at Master+, where promotion is population-gated rather than an LP target.
Placement strip Last 5 finishes, newest first. Tonal, so it never competes with the LP readout.
Rank moments A division change gets a 1s accent. A tier change gets the full takeover.
Either ladder Ranked or Double Up, switched from the Overlay page.

Every tier gets its own material, not a hue rotation of one card: Grandmaster is bladed crimson steel with a hard fast glint, Challenger is engraved gold with no particles at all and a slow sheen that reveals the engraving.

All ten tiers of the overlay card side by side, from Iron through Challenger, each with its own frame material, particle behaviour and colour ramp

A tier change is the rarest thing that happens on a ranked stream and the most worth clipping, so it gets the whole card for two and a half seconds — and the card comes back in the colour of the tier you landed in. Frame, crest, particle behaviour and colour ramp all swap under the flare, rather than snapping over in one visible frame.

The overlay card promoting from Diamond I into Master and then demoting back: a flare takes the card over, a banner reads Promoted Master, and the card returns repainted in Master's purple before the demotion reverses it

Promotion into Master, then the demotion back out of it.

Double Up

Double Up has its own ranked ladder, and the card handles it as a first-class mode rather than a variant: same tiers, same materials, same LP bar, same promotion takeover. Pick it under Overlay → Ladder.

Both ladders are tracked the whole time, so switching is instant and neither one loses its session totals or its placement strip while you're on the other. The strip counts teams, 1st to 4th — Riot's API reports Double Up on the eight-player scale, where the winning pair holds places 1 and 2, so a raw "2" would read as a second place to your viewers when you actually won.

How it works

flowchart LR
  R["Riot API"] -->|"every 5s"| S["Express :3000<br/>poll loop · trackers · crest proxy"]
  S -->|"GET /api/rank · every 2.5s"| O["overlay.html"]
  O --> B["OBS / Streamlabs<br/>Browser Source"]
  W["App window<br/>settings · preview · test"] <-->|"IPC · live config"| S
Loading

Two poll loops at different rates, deliberately: server → Riot is user-configurable and defaults to 5s, sized against a personal key's 100 requests/2min, while overlay → server is a fixed 2.5s because a localhost request costs nothing. The window and the server are one process, so saving a new key or region takes effect on the next poll instead of needing a restart.

A few decisions worth the detour:

  • Browser Source, not window capture. Streamlabs renders a Browser Source off-screen in its own Chromium and composites it straight into the stream. Capturing a window instead adds a hop — GPU draws it, the OS compositor composites it, then OBS captures that — and alpha-transparent window capture is famously dependent on capture method and GPU driver. So the app is a GUI in front of a local HTTP server, not a window to point OBS at.

  • Both ladders are tracked at once, because the second one is free. Riot returns every queue's league entry in a single response, so polling Double Up alongside ranked costs no extra request — and the match documents that fill the placement strip already had to be fetched before their queue could be read, so Double Up games were being downloaded and thrown away. Switching ladders is therefore a read-side choice: no refetch, no reset, and each keeps its own session.

  • A poll is two awaited round-trips, so it can outlive its own config. Saving a new Riot ID mid-flight let the old identity's 404 land afterwards and overwrite fresh state. Each poll captures an identityEpoch and drops its result if it no longer matches.

  • Error strings are UI copy, not diagnostics. They render into a 222px-wide, 10.5px footer band, on stream, in front of viewers. Riot answers failures with a JSON body, and interpolating that into the thrown error put Riot API 401: {"status":{"message":"Forbid… on the card, truncated mid-object. Status codes now map to short sentences; the raw body goes to the log.

  • Motion is gated on a URL flag, not prefers-reduced-motion. It used to be the media query — and one OS checkbox (Windows' "Show animations", which every gaming perf guide tells you to turn off) silently deleted the sheen, the particles and every rank-change effect in every browser on that machine at once. It read as a broken build. The card is rendered for the viewers, so the operator's accessibility setting is the wrong signal.

  • The overlay can diagnose itself on a machine you don't have. It measures whether the animation clocks are advancing (not whether a frame looks different — the sheen is idle 89% of its cycle, which makes pixel diffing useless), whether each stylesheet returned rules, the Chromium version and the GPU, then POSTs that to /api/diag. Anything matching a Riot key is scrubbed on the way in and out, because the whole point of the file is that people send it to strangers.

  • Rank crests are proxied and normalised, not hotlinked. GET /api/crest/:tier fetches from Community Dragon and equalises every tier to the same visual area with sharp, so a plain object-fit: contain renders them at a consistent size.

Development

No bundler, no TypeScript, no build step — what's in src/ is what runs.

npm install
npm start
src/
├── shared/     tier + LP domain logic, loadable from Node and the browser alike
├── main/       Electron main process: lifecycle, tray, window, settings, IPC
├── preload/    the contextBridge surface (window.tftApp)
├── renderer/   the app window — Overlay, Account, Test, Help
├── overlay/    overlay.html and its styles/scripts — what OBS loads
└── server/     Express + Riot polling, split into riot/, tracking/, crest/, routes/

Closing the window hides it to tray; the server stays up so OBS never loses connection. Quit properly from the sidebar or the tray icon.

There's no automated test suite. The Test page (POST /api/test/event) drives mock rank and LP changes through the real code paths without spending API quota — that's how every overlay state gets exercised. src/server has no Electron dependency, so it also runs under plain Node:

node -e "require('./src/server').createOverlayServer({onStatusChange(){}}).start(3999)"
Building an installer, and cutting a release

npm run build runs electron-builder (configured in package.json) into dist/. Build on the OS you're targeting — cross-compiling NSIS or dmg needs tooling that isn't set up here, which is also why .github/workflows/release.yml runs one job per OS on GitHub's runners.

  1. Bump version in package.json and commit.
  2. Tag it as v + that exact version. electron-builder names the artifacts and the release from package.json, not from the tag, so a mismatch fails the workflow up front rather than shipping a v1.0.1 tag full of 1.0.0 files.
    git tag v1.0.1 && git push origin v1.0.1
  3. All three jobs upload into the same draft release. Nothing is downloadable until you publish it, so a failed Linux job can't leave a half-finished release in front of users.

No secrets to configure — GITHUB_TOKEN is provided automatically. Builds are unsigned; electron-builder picks up CSC_LINK / WIN_CSC_LINK with no workflow changes if you ever get certificates.

Regenerating the README's imagery

Every image above is a capture of the real Electron window driven through the real server, not a hand-built mockup — a mockup drifts from the app immediately.

npx electron docs/_gif-build/capture.js   # frames + docs/overlay-tiers.png
node docs/_gif-build/encode.js            # the two GIFs

docs/app-view.jpg is the exception — it's a screenshot of the whole app window, title bar included, so it isn't part of that pipeline. Retake it by hand whenever the window's chrome changes.

See docs/_gif-build/gif-capture.md for the multi-pass capture scheme and the gotchas it exists to work around.

About the npm warnings

Deprecation warnings (inflight, glob@7.x, boolean@3.2.0, tar@6.2.1) all come from inside electron-builder's dependency tree. They matter for npm run build, never for npm start.

npm audit reports 16 high severity on a fresh install. That's one advisory counted once per dependency path: a DoS in brace-expansion (GHSA-mh99-v99m-4gvg) where a crafted glob can force unbounded expansion. Every path reaches it through electron-builder, which is a devDependency; build.files is an explicit list, so nothing from that chain ships in an installer; and the patterns it expands come from this repo's own package.json, not from user input. Triggering it would mean handing your own build tool a hostile pattern by hand.

It's deliberately not patched, because it currently can't be. The fix only exists in brace-expansion 5.0.8+, and v5 changed its export from a callable function to an object — every consumer in the chain still calls it as a function, so an overrides entry forcing v5 breaks packaging outright. npm audit fix --force suggests downgrading electron-builder a major version, which doesn't clear the advisory either. This resolves itself when electron-builder updates its own chain.

If npm start says "Electron failed to install correctly": recent npm versions block dependency install scripts by default, and Electron's install script is what downloads its binary. package.json pre-approves electron and electron-winstaller via allowScripts, so a normal install shouldn't hit this. If a future dependency does: npm install-scripts approve <package>, then reinstall. npm install-scripts ls lists candidates.

Notes

  • Settings live in app.getPath('userData'), never in the project folder — it holds a Riot API key.
  • The port is fixed at 3000 and the overlay path is fixed at /overlay.html, so a Browser Source configured once keeps working across updates.
  • LP only moves once Riot finishes processing a match. Polling faster than a few seconds doesn't get you fresher data, it just spends quota.
  • On Linux, sharp prints a startup warning about Electron binary compatibility. Known, harmless, works anyway. Windows and macOS don't show it.

License

MIT — see LICENSE.

TFT Live Overlay isn't endorsed by Riot Games and doesn't reflect the views of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games and all associated properties are trademarks or registered trademarks of Riot Games, Inc.

About

Tracks TFT rank and LP with low latency and presents them in an animated overlay for Streamlabs and OBS (Browser Source)

Topics

Resources

Stars

Watchers

Forks

Releases

Sponsor this project

Contributors

Languages