bare-appletv-remote
bare-appletv-remote
Apple TV remote control library for the Bare runtime
Controls Apple TV via the Companion Link protocol — the same protocol used by the iOS Remote app. Handles discovery, pairing, session encryption, and remote commands including navigation, playback, touch gestures, and power.
Install
npm install bare-appletv-remote
Usage
As a library
const AppleTVRemote = require('bare-appletv-remote')
// First run: scans the network, pairs (showing a PIN on the TV screen),
// and saves credentials to ~/.appletv-credentials.json automatically.
// Subsequent runs: loads credentials from disk silently.
const remote = new AppleTVRemote({
onpin: async () => {
// called only when pairing is needed — return the PIN shown on screen
return myPromptFunction('Enter the PIN shown on the Apple TV: ')
}
})
await remote.ready()
// Navigation
await remote.up()
await remote.down()
await remote.left()
await remote.right()
await remote.click() // select focused item
await remote.menu() // menu / back button
await remote.back() // alias for menu()
// Playback
await remote.playPause()
// Volume
await remote.volumeUp()
await remote.volumeDown()
// Power
await remote.sleep()
await remote.wake() // Wake-on-LAN (requires Ethernet on the Apple TV)
// Touch gestures — coordinates are on a 0–1000 × 0–1000 surface.
// Swipes travel edge to edge by default (distance 1000, centered);
// short swipes may not register as directional on tvOS.
await remote.swipe('right') // fast-forward
await remote.swipe('left') // rewind
await remote.swipe('right', { distance: 500, steps: 20 })
// Low-level touch — useful for custom UI or continuous scrubbing
await remote.touchBegin(500, 500)
await remote.touchMove(700, 500)
await remote.touchEnd(700, 500)
await remote.close()
ready() loads (or obtains via pairing) the credentials; the encrypted session itself opens lazily on the first command and is then kept alive — the library answers the Apple TV's heartbeats and sends a keepalive every 25 s (the ATV drops idle Companion sockets after ~30 s). If the connection drops anyway (e.g. the ATV sleeps), it is re-established transparently on the next command, retrying up to three times with re-discovery in between.
If the Apple TV definitively rejects the stored credentials, the failing command throws an error with code: 'EREVOKED'. Re-pairing is never automatic — it needs a PIN from the TV screen — so handle it explicitly:
try {
await remote.playPause()
} catch (err) {
if (err.code === 'EREVOKED') {
await remote.repair() // prompts via onpin; reuses our identity so the ATV replaces the old pairing
await remote.playPause()
} else {
throw err
}
}
Options
| Option | Type | Default | Description |
|---|---|---|---|
onpin |
() => Promise<string> | string |
— | Called when pairing is needed. Return the PIN shown on screen. Required for first-time pairing. |
credentials |
Credentials |
— | Pass credentials directly, bypassing disk. |
credentialsFile |
string |
~/.appletv-credentials.json |
Override the credentials file path. |
host |
string |
— | Skip mDNS discovery and connect directly to this IP address. |
port |
number |
— | Port to use when host is set. |
idleTimeout |
number |
0 |
Milliseconds of inactivity before the session is closed. Default keeps it open indefinitely. |
debug |
boolean |
false |
Log protocol traffic. |
Events
remote.on('paired', () => console.log('Paired and saved.'))
remote.on('ready', () => console.log('Ready.'))
remote.on('close', () => console.log('Closed.'))
CLI
npm install -g bare-appletv-remote
# First run: scans, pairs, saves credentials
appletv pair
# Navigation
appletv up | down | left | right | click
# Playback & volume
appletv play
appletv volup | voldown
# Menu
appletv back
# Power
appletv sleep
appletv wake
Pairing troubleshooting
When pairing is initiated the Apple TV should display a PIN automatically. If nothing appears on screen:
Wake the TV first. The PIN dialog only appears when the TV is active. Press a button on the physical Siri Remote to wake it, then run
pairagain.The PIN appears as an overlay anywhere. It will appear over full-screen apps, the home screen, or even during video playback — you do not need to navigate to a specific screen first.
Enable remote access. On the Apple TV go to Settings → AirPlay & HomeKit and make sure Allow Access is not set to No One.
Remove a stale pairing. If the TV silently rejects the request (no PIN shown, but the handshake completes), a leftover pairing entry may be blocking it. On the Apple TV go to Settings → AirPlay & HomeKit → Remote App and Devices (or Settings → Remotes and Devices → Remote App and Devices) and remove any existing entry for this device, then pair again.
Delete local credentials and retry. If
~/.appletv-credentials.jsonexists from a previous attempt that did not complete, delete it before runningpairagain.
Advanced: scan and pair manually
const AppleTVRemote = require('bare-appletv-remote')
// Scan the network for Apple TVs
const devices = await AppleTVRemote.scan()
console.log(devices[0].name, devices[0].address)
// Pair with a specific device
const credentials = await AppleTVRemote.pair(devices[0], async () => getPin())
// Use the credentials
const remote = new AppleTVRemote({ credentials })
await remote.up()
await remote.close()
Protocol
Uses the Companion Link protocol (_companion-link._tcp mDNS service) with HAP (HomeKit Accessory Protocol) security:
- Pairing: SRP-6a (3072-bit) + Ed25519 long-term keys + ChaCha20-Poly1305
- Sessions: X25519 ephemeral DH + HKDF-SHA512 session keys
- Commands: Encrypted OPACK messages with HID event payloads
- Touch:
_hidTevents (press/move/release phases) on a 1000×1000 virtual touchpad surface - Wake: UDP Wake-on-LAN magic packet (MAC from mDNS
rpADTXT record)
Crypto via sodium-universal.
Acknowledgements
- appletv-remote by Alok Dhir — prior art and protocol reference
License
MIT