<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hisuperdev</title>
    <description>The latest articles on DEV Community by hisuperdev (@hisuperdev).</description>
    <link>https://dev.to/hisuperdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3949607%2F09f6f7c5-1106-4653-87d1-1b32f87bb954.png</url>
      <title>DEV Community: hisuperdev</title>
      <link>https://dev.to/hisuperdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hisuperdev"/>
    <language>en</language>
    <item>
      <title>How I built a single iOS app that controls all four Hisense smart-TV platforms</title>
      <dc:creator>hisuperdev</dc:creator>
      <pubDate>Thu, 28 May 2026 13:27:48 +0000</pubDate>
      <link>https://dev.to/hisuperdev/how-i-built-a-single-ios-app-that-controls-all-four-hisense-smart-tv-platforms-30ef</link>
      <guid>https://dev.to/hisuperdev/how-i-built-a-single-ios-app-that-controls-all-four-hisense-smart-tv-platforms-30ef</guid>
      <description>&lt;p&gt;Hisense ships TVs with four different operating systems — VIDAA,&lt;br&gt;
Roku TV, Google TV, and Fire TV. The official "Remote for Hisense&lt;br&gt;
TV" you find in their app store only works with VIDAA. When I lost&lt;br&gt;
my own remote, I had a Google TV-flavored Hisense, so I built a&lt;br&gt;
single-app solution that pairs with whichever platform the user has.&lt;/p&gt;

&lt;p&gt;Here's how each of the four local-network protocols works, and how&lt;br&gt;
the app picks the right one without asking the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Local protocol&lt;/th&gt;
&lt;th&gt;Pairing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VIDAA&lt;/td&gt;
&lt;td&gt;MQTT over TLS&lt;/td&gt;
&lt;td&gt;Direct local connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roku TV&lt;/td&gt;
&lt;td&gt;ECP over HTTP&lt;/td&gt;
&lt;td&gt;Open by default on port 8060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google TV&lt;/td&gt;
&lt;td&gt;gRPC, TLS)&lt;/td&gt;
&lt;td&gt;4-digit PIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fire TV&lt;/td&gt;
&lt;td&gt;ADB-over-Wi-Fi&lt;/td&gt;
&lt;td&gt;One-time TV-side prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All four are LAN-only — no cloud involvement once paired.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. VIDAA — MQTT over TLS
&lt;/h2&gt;

&lt;p&gt;VIDAA TVs accept TLS connections on port 36669 and speak MQTT&lt;br&gt;
underneath. The control message format publishes button presses as&lt;br&gt;
small JSON payloads — &lt;code&gt;{"key": "KEY_VOLUMEUP"}&lt;/code&gt; etc. — to predictable&lt;br&gt;
topics scoped by the TV's MAC address. Other developers in the Home&lt;br&gt;
Assistant community have written about this in detail; my Swift&lt;br&gt;
implementation is on top of that prior work.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Roku TV — ECP is HTTP
&lt;/h2&gt;

&lt;p&gt;Almost too easy. Roku publishes the External Control Protocol&lt;br&gt;
publicly. Every Roku and Hisense Roku TV listens on port 8060:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl -X POST http://192.168.1.100:8060/keypress/Home&lt;br&gt;
curl -X POST http://192.168.1.100:8060/keypress/VolumeUp&lt;br&gt;
curl http://192.168.1.100:8060/query/active-app&lt;br&gt;
curl http://192.168.1.100:8060/query/device-info&lt;br&gt;
curl -X POST http://192.168.1.100:8060/launch/12&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No pairing. No PIN. If your phone and the TV are on the same LAN,&lt;br&gt;
you can drive it from a curl one-liner. This is why I shipped an &lt;a href="https://hiremote.app/hisense-roku-tv-remote#remote" rel="noopener noreferrer"&gt;in-browser Roku remote&lt;/a&gt; — for Hisense Roku TV owners, you don't actually need an app at all. The page implements ECP from JavaScript and runs in any modern browser. Cannibalises my own install funnel for that segment, but it's the right call for the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Google TV — gRPC dance
&lt;/h2&gt;

&lt;p&gt;Google TV uses gRPC over TLS with cert pinning. Pairing exchanges a&lt;br&gt;
4-digit PIN through paired proto messages — the Android TV Remote v2&lt;br&gt;
protocol. The .proto definitions are discussed in various developer&lt;br&gt;
communities; from there it's standard gRPC client work.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Fire TV — ADB-style local connection
&lt;/h2&gt;

&lt;p&gt;Fire TV runs Android and supports network-mode ADB on port 5555.&lt;br&gt;
After the user enables developer-friendly settings on the TV, the&lt;br&gt;
app authorizes once via a permission prompt on the TV screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform detection via mDNS
&lt;/h2&gt;

&lt;p&gt;The app browses for service types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;_androidtvremote._tcp → Google TV&lt;/li&gt;
&lt;li&gt;_roku-rsp._tcp → Roku TV
&lt;/li&gt;
&lt;li&gt;_amzn-wplay._tcp → Fire TV&lt;/li&gt;
&lt;li&gt;Port-scan port 36669 fallback → VIDAA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first responder wins. The user never picks "what kind of Hisense&lt;br&gt;
TV is this" unless detection fails.&lt;/p&gt;

&lt;p&gt;The implementation ships as &lt;a href="https://apps.apple.com/us/app/remote-for-hisense-tv/id6740401390" rel="noopener noreferrer"&gt;Remote for Hisense TV on the App Store&lt;/a&gt;, free with a Pro tier ($9.99/year) that adds the Apple Watch app, Lock Screen widget, and removes ads. The marketing site with deeper troubleshooting articles is at &lt;a href="https://hiremote.app" rel="noopener noreferrer"&gt;hiremote.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to answer iOS protocol questions in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>iot</category>
      <category>tv</category>
      <category>ios</category>
    </item>
    <item>
      <title>I built an in-browser Roku TV remote with ~80 lines of TypeScript. Here's how Roku's ECP API actually works</title>
      <dc:creator>hisuperdev</dc:creator>
      <pubDate>Sun, 24 May 2026 21:50:15 +0000</pubDate>
      <link>https://dev.to/hisuperdev/i-built-an-in-browser-roku-tv-remote-with-80-lines-of-typescript-heres-how-rokus-ecp-api-56ni</link>
      <guid>https://dev.to/hisuperdev/i-built-an-in-browser-roku-tv-remote-with-80-lines-of-typescript-heres-how-rokus-ecp-api-56ni</guid>
      <description>&lt;p&gt;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 &lt;strong&gt;ECP&lt;/strong&gt; (External Control Protocol) and once you've seen it, you'll wonder why the rest of the smart-TV world isn't this simple.&lt;/p&gt;

&lt;p&gt;I needed an in-browser remote for &lt;a href="https://hiremote.app/hisense-roku-tv-remote" rel="noopener noreferrer"&gt;HiRemote's Hisense Roku TV landing page&lt;/a&gt; — 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Discovery — SSDP is overrated, the user types the IP
&lt;/h2&gt;

&lt;p&gt;Every Roku tutorial starts with "use SSDP multicast on &lt;code&gt;239.255.255.250:1900&lt;/code&gt;". This is true but useless from a browser: browsers can't send UDP. You can't run SSDP from JavaScript.&lt;/p&gt;

&lt;p&gt;For a &lt;em&gt;browser-based&lt;/em&gt; 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 &lt;code&gt;localStorage&lt;/code&gt; it. Roku TVs always run ECP on port &lt;code&gt;8060&lt;/code&gt;, so once you have the IP, the base URL is fixed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tvIp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:8060`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Buttons — every command is a POST to a path
&lt;/h2&gt;

&lt;p&gt;The control surface is one of the cleanest API designs I've seen in a consumer device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST http://&amp;lt;tv-ip&amp;gt;:8060/keypress/&amp;lt;KeyName&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;KeyName&amp;gt;&lt;/code&gt; is one of about 30 documented strings: &lt;code&gt;Home&lt;/code&gt;, &lt;code&gt;Up&lt;/code&gt;, &lt;code&gt;Down&lt;/code&gt;, &lt;code&gt;Left&lt;/code&gt;, &lt;code&gt;Right&lt;/code&gt;, &lt;code&gt;Select&lt;/code&gt;, &lt;code&gt;Back&lt;/code&gt;, &lt;code&gt;Play&lt;/code&gt;, &lt;code&gt;Pause&lt;/code&gt;, &lt;code&gt;Rev&lt;/code&gt;, &lt;code&gt;Fwd&lt;/code&gt;, &lt;code&gt;VolumeUp&lt;/code&gt;, &lt;code&gt;VolumeDown&lt;/code&gt;, &lt;code&gt;VolumeMute&lt;/code&gt;, &lt;code&gt;PowerOn&lt;/code&gt;, &lt;code&gt;PowerOff&lt;/code&gt;, plus a few platform-specific ones for Roku TV (channel up/down, input switching).&lt;/p&gt;

&lt;p&gt;No body, no headers, no auth. Just the POST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RokuKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/keypress/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For typing into search boxes (Netflix login, YouTube search), there's &lt;code&gt;/keypress/Lit_&amp;lt;urlEncodedChar&amp;gt;&lt;/code&gt; — one POST per character. Cleaner than building a virtual keyboard, ugly that it isn't batched, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Direct-app-launch — the surprisingly useful one
&lt;/h2&gt;

&lt;p&gt;The endpoint nobody talks about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST http://&amp;lt;tv-ip&amp;gt;:8060/launch/&amp;lt;channel-id&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Channel IDs are stable Roku-assigned numbers. &lt;code&gt;12&lt;/code&gt; is Netflix, &lt;code&gt;13&lt;/code&gt; is Prime Video, &lt;code&gt;837&lt;/code&gt; is YouTube, &lt;code&gt;291097&lt;/code&gt; is Disney+. Posting to &lt;code&gt;/launch/12&lt;/code&gt; boots Netflix on the TV — no D-pad navigation needed.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Full list of channel IDs is in the device's response to &lt;code&gt;GET /query/apps&lt;/code&gt; (returns XML, so use &lt;code&gt;DOMParser&lt;/code&gt; not &lt;code&gt;JSON.parse&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Putting it together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RokuKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Back&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Down&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VolumeUp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VolumeDown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VolumeMute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PowerOn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PowerOff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Play&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pause&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fwd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RokuRemote&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;tvIp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;base&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tvIp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:8060`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RokuKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;/keypress/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;/keypress/Lit_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;launchApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;/launch/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole remote. Render a D-pad with &lt;code&gt;onClick={() =&amp;gt; remote.press("Up")}&lt;/code&gt; and you have a working web-based Roku remote in 80 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Roku's HTTP control protocol is plain &lt;code&gt;POST /keypress/&amp;lt;KeyName&amp;gt;&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;The end result is live at &lt;a href="https://hiremote.app/hisense-roku-tv-remote" rel="noopener noreferrer"&gt;hiremote.app/hisense-roku-tv-remote&lt;/a&gt; — bring your own Roku-TV IP and try it without installing anything.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>iot</category>
      <category>tv</category>
    </item>
  </channel>
</rss>
