I built an in-browser Roku TV remote with ~80 lines of TypeScript. Here's how Roku's ECP API actually works
I built an in-browser Roku TV remote with ~80 lines of TypeScript. Here’s how Roku’s ECP API actually works
我用大约 80 行 TypeScript 代码构建了一个浏览器版 Roku 电视遥控器,以下是 Roku ECP API 的工作原理
Roku ships an HTTP API on every device they sell. It has no authentication, no API key, no documentation page on a marketing site — but it powers every third-party Roku remote app on the App Store and Play Store. It’s called ECP (External Control Protocol) and once you’ve seen it, you’ll wonder why the rest of the smart-TV world isn’t this simple. I needed an in-browser remote for HiRemote’s Hisense Roku TV landing page — the idea being a visitor who lands on the page can press buttons in the page itself without first installing the iOS app. Three quirks made it harder than the marketing-page version suggests; here’s the actual implementation.
Roku 在其销售的每台设备上都内置了一个 HTTP API。它没有身份验证,没有 API 密钥,也没有营销页面上的文档说明——但 App Store 和 Play Store 上所有的第三方 Roku 遥控器应用都依赖它。它被称为 ECP(外部控制协议),一旦你了解了它,你就会纳闷为什么其他智能电视厂商不能做得这么简单。我需要为 HiRemote 的海信 Roku 电视落地页制作一个浏览器遥控器,目的是让访问者无需安装 iOS 应用,直接在网页上就能操作按钮。有三个小问题让实现过程比预想的要复杂,以下是具体的实现方案。
1. Discovery — SSDP is overrated, the user types the IP
1. 设备发现——SSDP 被高估了,直接让用户输入 IP
Every Roku tutorial starts with “use SSDP multicast on 239.255.255.250:1900”. This is true but useless from a browser: browsers can’t send UDP. You can’t run SSDP from JavaScript. For a browser-based remote, the pragmatic solution is: ask the user for their TV’s IP. On the iPhone app side we use Bonjour. On the web page side we just show a one-time input box, then localStorage it. Roku TVs always run ECP on port 8060, so once you have the IP, the base URL is fixed: const baseUrl = http://${tvIp}:8060. The only gotcha here is that the user’s browser is HTTPS but the TV is HTTP. Modern browsers block mixed-content requests, so you have to either accept this and let the request fail gracefully, or proxy through your own backend. We chose the first option — the input box explains the limitation and tells the user to also try the iOS app which doesn’t have this restriction.
每一个 Roku 教程都以“使用 239.255.255.250:1900 上的 SSDP 多播”开头。这虽然没错,但在浏览器中毫无用处:浏览器无法发送 UDP 数据包,你无法通过 JavaScript 运行 SSDP。对于基于浏览器的遥控器,务实的解决方案是:直接询问用户的电视 IP。在 iPhone 应用端,我们使用 Bonjour 协议;而在网页端,我们只需显示一个一次性的输入框,然后将其存入 localStorage。Roku 电视始终在 8060 端口运行 ECP,因此一旦获得 IP,基础 URL 就固定了:const baseUrl = http://${tvIp}:8060。这里唯一的问题是用户的浏览器处于 HTTPS 环境,而电视是 HTTP。现代浏览器会拦截混合内容请求,所以你必须要么接受请求失败并优雅地处理,要么通过自己的后端进行代理。我们选择了前者——输入框会解释这一限制,并建议用户尝试没有此限制的 iOS 应用。
2. Buttons — every command is a POST to a path
2. 按钮操作——每个命令都是对路径的 POST 请求
The control surface is one of the cleanest API designs I’ve seen in a consumer device: POST http://<tv-ip>:8060/keypress/<KeyName>. <KeyName> is one of about 30 documented strings: Home, Up, Down, Left, Right, Select, Back, Play, Pause, Rev, Fwd, VolumeUp, VolumeDown, VolumeMute, PowerOn, PowerOff, plus a few platform-specific ones for Roku TV (channel up/down, input switching). No body, no headers, no auth. Just the POST:
async function sendKey(key: RokuKey) { return fetch(${baseUrl}/keypress/${key}, { method: "POST", }) }
For typing into search boxes (Netflix login, YouTube search), there’s /keypress/Lit_<urlEncodedChar> — one POST per character. Cleaner than building a virtual keyboard, ugly that it isn’t batched, but it works.
这是我在消费电子设备上见过的最简洁的 API 设计之一:POST http://<tv-ip>:8060/keypress/<KeyName>。<KeyName> 是大约 30 个已记录字符串中的一个:Home、Up、Down、Left、Right、Select、Back、Play、Pause、Rev、Fwd、VolumeUp、VolumeDown、VolumeMute、PowerOn、PowerOff,以及一些 Roku 电视特有的指令(频道加减、输入切换)。无需请求体、无需 Header、无需验证,只需一个 POST 请求:
async function sendKey(key: RokuKey) { return fetch(${baseUrl}/keypress/${key}, { method: "POST", }) }
对于在搜索框中输入文字(如 Netflix 登录、YouTube 搜索),可以使用 /keypress/Lit_<urlEncodedChar>——每个字符发送一个 POST 请求。虽然不如构建虚拟键盘优雅,且不支持批量发送显得有些丑陋,但它确实有效。
3. Direct-app-launch — the surprisingly useful one
3. 直接启动应用——出奇好用的功能
The endpoint nobody talks about: POST http://<tv-ip>:8060/launch/<channel-id>. Channel IDs are stable Roku-assigned numbers. 12 is Netflix, 13 is Prime Video, 837 is YouTube, 291097 is Disney+. Posting to /launch/12 boots Netflix on the TV — no D-pad navigation needed. This is the killer feature for a remote that lives on a phone or in a browser: you skip the entire “navigate the home screen” UX that makes physical Roku remotes annoying. One tap → on Netflix. Full list of channel IDs is in the device’s response to GET /query/apps (returns XML, so use DOMParser not JSON.parse).
这是一个没人提及的端点:POST http://<tv-ip>:8060/launch/<channel-id>。频道 ID 是 Roku 分配的固定数字。12 是 Netflix,13 是 Prime Video,837 是 YouTube,291097 是 Disney+。向 /launch/12 发送 POST 请求即可在电视上启动 Netflix,无需使用方向键导航。这是手机或浏览器遥控器的杀手级功能:你跳过了让实体 Roku 遥控器变得繁琐的“主屏幕导航”体验。一键点击,直接进入 Netflix。频道 ID 的完整列表可以通过 GET /query/apps 获取(返回的是 XML,所以要使用 DOMParser 而不是 JSON.parse)。
4. Putting it together
4. 整合代码
type RokuKey = | "Home" | "Back" | "Select" | "Up" | "Down" | "Left" | "Right" | "VolumeUp" | "VolumeDown" | "VolumeMute" | "PowerOn" | "PowerOff" | "Play" | "Pause" | "Rev" | "Fwd"
class RokuRemote {
constructor(private tvIp: string) {}
private base() { return `http://${this.tvIp}:8060` }
press(key: RokuKey) { return fetch(`${this.base()}/keypress/${key}`, { method: "POST" }) }
type(text: string) {
return Promise.all([...text].map(ch =>
fetch(`${this.base()}/keypress/Lit_${encodeURIComponent(ch)}`, { method: "POST" })
))
}
launchApp(channelId: number) { return fetch(`${this.base()}/launch/${channelId}`, { method: "POST" }) }
}
That’s the whole remote. Render a D-pad with onClick={() => remote.press("Up")} and you have a working web-based Roku remote in 80 lines.
这就是整个遥控器的核心。通过 onClick={() => remote.press("Up")} 渲染一个方向键,你就能在 80 行代码内实现一个可用的网页版 Roku 遥控器。
TL;DR: Roku’s HTTP control protocol is plain POST /keypress/<KeyName>. No auth. 80 lines of TypeScript = working remote. Discovery is the only genuinely hard part for a browser context, and “ask the user for the IP” is the right answer there. The end result is live at hiremote.app/hisense-roku-tv-remote — bring your own Roku-TV IP and try it without installing anything.
总结: Roku 的 HTTP 控制协议就是简单的 POST /keypress/<KeyName>,无需验证。80 行 TypeScript 代码即可实现遥控功能。在浏览器环境下,设备发现是唯一真正的难点,而“让用户输入 IP”是目前最正确的解决方案。最终成果已上线至 hiremote.app/hisense-roku-tv-remote —— 准备好你的 Roku 电视 IP,无需安装任何东西即可体验。