← Projects
MIT · Open source

gtv

Google TV control library — device store, discovery, pairing, and a stateful remote session

The framework-agnostic core of the Google TV stack — discovery, pairing, a stateful session, and a shared device store that every client (CLI, MCP, app) builds on.

🌟 Features

  • 🔍 mDNS Discovery — finds every Google TV on the local network via dns-sd; returns pure data, no side effects
  • 🔐 Dependency-injected PairingonSecret is a callback you supply (terminal readline, MCP round-trip, Tauri dialog…); the library never touches I/O itself
  • 📡 Stateful SessioncreateSession() returns an EventEmitter that reduces the underlying protocol stream to a single observable SessionState; subscribe with .on("change", state => …)
  • One-shot HelperssendKey, launchApp, connect, withRemote for scripts that don't need a long-lived connection
  • 📱 App Catalog — curated APPS list with findApp, listApps, and appLink (builds the reliable market://launch?id=<package> URI)
  • 🗄️ Shared Config Store — reads and writes ~/.config/gtv/config.json; all consumers (gtv-cli, mcp-gtv, gtv-app) share one device registry
  • 🔑 Full Keycode SurfaceKEYS, KEY_LABELS, and re-exported RemoteKeyCode / RemoteDirection so consumers need only depend on @kud/gtv

🚀 Quick Start

npm install @kud/gtv

Discover and pair

import { discover, pair } from "@kud/gtv"

const [tv] = await discover()

await pair({
  host: tv.host,
  hostname: tv.hostname,
  port: tv.port,
  name: tv.name,
  onSecret: async () => promptUserForPin(), // PIN displayed on the TV screen
})

Drive a stateful session

import { createSession, KEYS } from "@kud/gtv"

const session = createSession() // uses the currently configured device
session.on("change", (state) => console.log(state))

session.sendKey(KEYS.home)
session.typeText("interstellar")
session.launchApp("market://launch?id=com.netflix.ninja")
session.stop()

One-shot commands

import { sendKey, launchApp, findApp, appLink, KEYS } from "@kud/gtv"

await sendKey(KEYS.mute)

const netflix = findApp("netflix")
if (netflix) await launchApp(appLink(netflix))

📖 API Reference

Discovery

ExportSignatureDescription
discover() => Promise<DiscoveredDevice[]>mDNS scan; returns every Google TV found on the LAN

Pairing

ExportSignatureDescription
pair(options: PairOptions) => Promise<PairResult>Full pairing flow; onSecret is called with the PIN entry callback

PairOptions:

FieldTypeRequiredDescription
hoststringIP address of the TV
hostnamestringHostname (from mDNS)
portnumberRemote port (default 6466)
namestringFriendly device name
onSecret() => Promise<string>Resolves the PIN shown on screen
onStatus(status: PairStatus) => voidProgress callback
savebooleanPersist device to config store (default true)

Session

ExportSignatureDescription
createSession(device?: Device) => SessionLong-lived, event-driven connection

Session extends EventEmitter:

MemberTypeDescription
stateSessionStateCurrent snapshot (connected, powered, volume, currentApp, …)
sendKey(keyCode: number, direction?: number) => voidSend a remote keypress
typeText(text: string) => voidIME text input
launchApp(link: string) => voidOpen an app by URI
stop() => voidTear down the connection
on("change", cb)Fires on every state update
on("error", cb)Fires on connection errors

One-shot helpers

ExportDescription
connect(device?)Opens a raw AndroidRemote, resolves when ready
withRemote(fn, device?)Runs fn(remote) then closes the connection
sendKey(keyCode, direction?, device?)One-shot key send
launchApp(link, device?)One-shot app launch

App catalog

ExportSignatureDescription
APPSAppEntry[]Curated list (Netflix, YouTube, Prime Video, Plex, Disney+, Spotify, …)
listApps() => AppEntry[]Returns APPS
findApp(query: string) => AppEntry | undefinedCase-insensitive match on id or display name
appLink(app: AppEntry) => stringBuilds market://launch?id=<package> URI

Config store

Config lives at ~/.config/gtv/config.json and is shared across all consumers.

ExportDescription
readConfigRead the full config file
listDevicesAll paired devices
getCurrentDeviceThe active device
findDevice(query)Find by host, name, or hostname
upsertDevice(device)Insert or update a device entry
setCurrentDevice(host)Mark a device as active
removeDevices(hosts)Delete one or more devices
readPreferencesRead the preferences block
writePreferences(prefs)Write the preferences block
CONFIG_PATHAbsolute path to the config file

Keycodes

ExportDescription
KEYSMap of friendly names → RemoteKeyCode values
KEY_LABELSMap of RemoteKeyCode values → display strings
RemoteKeyCodeRe-export from @kud/androidtv-remote
RemoteDirectionRe-export from @kud/androidtv-remote
setDebugEnable protocol-level debug logging

🔧 Development

gtv/
├── src/
│   ├── index.ts        # Public surface
│   ├── session.ts      # Stateful Session (EventEmitter)
│   ├── client.ts       # One-shot helpers
│   ├── discovery.ts    # mDNS via dns-sd
│   ├── pairing.ts      # Pairing flow
│   ├── config.ts       # Device store + preferences
│   ├── apps.ts         # App catalog + appLink
│   ├── keycodes.ts     # KEYS / KEY_LABELS
│   └── types.ts        # SessionState, VolumeState
└── dist/               # Compiled output (tsup)
git clone https://github.com/kud/gtv.git
cd gtv
npm install
npm run build
ScriptDescription
npm run buildCompile with tsup
npm run build:watchCompile in watch mode
npm run typecheckType-check without emitting
npm run cleanDelete dist/

Node.js ≥ 22 required.

🏗 Tech Stack

PackageRole
@kud/androidtv-remoteProtocol layer — TLS socket, protobuf codec, key/text/app-link send
tsupZero-config ESM bundler
typescriptType safety across the entire public API

MIT © kud — Made with ❤️