Skip to content

On a rooted TV

With the Homebrew Channel’s SSH server on, the tools install over root SSH instead of Developer Mode. Nothing expires, so the Developer Mode timer below isn’t needed. Uninstall the Developer Mode app before restarting a rooted TV: if it’s still installed at startup, the TV can delete every sideloaded app, the Homebrew Channel included.

Terminal window
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/tv_root # once
npm run root-ssh # installs the key, switches the tools to root SSH
npm run deploy
npm run install-ipk -- some-app.ipk # any other webOS package

root-ssh logs in with the Homebrew Channel’s default password the first time and installs the key. After the next restart the TV only accepts the key. It also sets tv.root in config.json, which makes deploy, install-ipk and the hourly updater use scp and the TV’s installer service (luna-send) over SSH. LG’s ares-install doesn’t work here because the Homebrew Channel’s SSH server refuses the second command it sends.

tv/init.d/ holds startup hooks for a rooted TV; npm run tv-setup copies them to /var/lib/webosbrew/init.d/ and starts them, and installs tv/bin/ and Input Hook’s keybinds. The Homebrew Channel runs the hooks at every boot.

  • 20-close-devtools-port: closes the debug port (below).
  • 40-launcher-helper: starts tv/helper/helper.js (Node, root), which records which apps are on screen for the Recently Used row (/home/root/.config/minimal-launcher/recent.json), serves the phone remote, and runs the sleep timer and Night mode (below).
  • 55-allow-admanager: Life’s Good Without Spying’s “stop ad services” option swaps LG’s ad manager (/usr/sbin/admanager) for a stub, and Disney+, ESPN and Peacock then hang on their loading screens. This hook removes just that stub at boot, and watches for a few minutes in case it comes back; the app’s other blocks stay on. Pressing Apply in Life’s Good Without Spying stubs it again: restart the TV or run npm run tv-setup. Its log is /run/minimal-launcher/allow-admanager.log.
  • 50-watchdog: every 5 minutes, starts the power-on watcher and the helper again if either has stopped. Its log is /run/minimal-launcher/watchdog.log.
  • 30-launcher-on-wake: opens the launcher when the TV is turned on with the remote (the TV reports why it turned on: getPowerOnReason says remoteKey), and for 20 seconds takes the screen back if a device woken with the TV grabs it over HDMI-CEC. When something else turned the TV on, such as a console over HDMI-CEC, it stays where that put it. Its log is /run/minimal-launcher/launcher-on-wake.log; 30-launcher-on-wake states prints power states as they change.

tv/bin/commercial-mute toggles the TV’s mute: muting starts a 2½-minute timer that unmutes it again, and a press while muted unmutes at once. It uses com.webos.service.audio/master/muteVolume, the mute the remote’s button toggles (com.webos.audio has a separate one). Input Hook swallows a key it runs a command for, and kills what the command leaves behind, so the timer runs detached with setsid. tv/lginputhook/keybinds.json puts it on the remote’s Mute button (code 113), next to Home and Exit → launcher (773 and 174), 1 → LG’s home bar (2) and the dot → the sidebar (1038). npm run tv-setup installs both. To change the length, edit the 150 in the keybinds file.

Pressing Left on the first tile of any row slides out a menu (Up/Down to move, OK or Right to choose, Back or Left to go back):

  • Inputs: switch straight to one.
  • Picture & sound (rooted TVs): the picture mode and sound mode of the apps and of each input (LG keeps one of each per input), from the modes the TV offers.
  • Sleep timer (rooted TVs): 30, 60, 90 or 120 minutes, with a TV notification a minute before. The helper on the TV runs it, so it works whatever app is on screen.
  • Launcher settings: theme, icons, the glass color (with glass icons on), Night mode, when the screensaver starts and what it shows, a button to start it now, 12- or 24-hour clock, °F or °C, showing hidden apps. Saved with the layout backup.
  • HDMI settings (rooted TVs): per input, Deep Color (needed for 4K HDR at full quality) and PC mode; and SIMPLINK (HDMI-CEC) and Auto Power Sync for the whole TV. These are the TV’s own settings, read and changed through com.webos.settingsservice as root.
  • System: LG’s settings app; turning the TV off and rebooting it, each after a second OK. Turning off darkens the screen at once (tv/bin/power-off switches the panel off, then powers the TV down), so it feels as quick as the remote’s power button.

From the hour picked in Launcher settings (9, 10 or 11 PM, or midnight) until 7 AM, the launcher dims itself, the screensaver included. On a rooted TV the helper also turns on the TV’s Eye Comfort Mode (warmer colors) and sets energy saving to medium (a dimmer screen), whatever is on screen, and in the morning puts back what they were before. Both are TV-wide settings. While night mode is on, the earlier values are kept in /home/root/.config/minimal-launcher/night.json.

The helper serves a remote control page on port 8765 for phones on the home network: a D-pad with OK and Back, Home (the launcher), LG’s home bar, volume, the commercial mute, play controls, power, and your apps and inputs. Buttons go through the TV’s own network remote service, using the client key the computer was approved with. npm run tv-setup prints a private link (http://<tv>:8765/?k=…): opening it once on a phone saves the key in a cookie, and the page refuses anyone without it.

Turning the TV on from the phone: the page lives on the TV, and a TV that’s off soon stops answering on the network, so on its own the page can only turn the TV off. The relay in relay/ fixes that. It runs on an always-on computer on the home network that has Docker (a home server or NAS), and serves the same page on the same port, passing everything through to the TV. When the TV is off it still loads the page (showing “TV is off”), and the power button wakes the TV with a Wake-on-LAN packet. For that, the TV’s Mobile TV On setting must be on (Settings → All Settings → General → Mobile TV On, “Turn on via Wi-Fi”), and the TV should be on a wired connection. Set relay.host in config.json, then, with the TV on:

Terminal window
npm run relay-setup

It copies the relay to that computer over SSH, with the TV’s address, its MAC address and the remote’s key, starts it with Docker Compose, and prints the relay’s private link (http://<server>:8765/?k=…, the same key as the TV’s). If your SSH user can’t run Docker there, it says how to start it (with sudo, or as the tv-remote project in a Docker manager such as Arcane or Dockge: point relay.dir at its projects folder). Run it again after changing relay/; changes to the page reach the relay by themselves.

Type on the TV: with a text box open on the TV (one that brings up LG’s own keyboard, as the web browser and many apps do), what you type on the phone appears there as you type, and Enter submits it. Apps that draw their own keyboard, such as YouTube and most streaming apps, don’t take it, so two buttons search directly: Search YouTube opens YouTube’s results for the text, the way a voice search does, and Search Plex lists matching movies, shows and episodes from your server on the phone, with posters; tap one to open its page in PlxNative on the TV (with plex.appId set to another Plex app, that app just opens). The helper runs the Plex search, so the server’s token never reaches the phone.

The hourly sync reads your Watchlist from plex.tv, matches each title on your server, and writes the list to /home/root/.config/minimal-launcher/watchlist.json on the TV, so a Watchlist change doesn’t need a reinstall. Titles on the server open their page in PlxNative; the rest are marked as not on it.

On a rooted TV the launcher copies its order, hidden apps, Favorites and theme to /home/root/.config/minimal-launcher/layout.json (through the Homebrew Channel’s root service) whenever they change. A fresh install restores them from there. sync also keeps a copy in backup/layout.json and packages it, for when the TV itself was reset.

With plex.appId set to PlxNative, a Continue Watching card resumes that title, and a Recently Added card opens its page, using PlxNative’s launch links. On webOS before 7.4 a link only works on a cold start, so the launcher closes PlxNative first.

webOS opens the web app manager’s remote debugging port (9998) on every network interface. On a rooted TV it lets anyone on the network script the Homebrew Channel, which has root. The kernel has no firewall, so a startup hook in /var/lib/webosbrew/init.d/20-close-devtools-port bind-mounts a copy of /etc/systemd/system/scripts/webapp-mgr.sh without --remote-debugging-port and restarts the web app manager. (--remote-debugging-address=127.0.0.1 was tried first; the web app manager ignores it.) To reopen the port for debugging until the next restart:

Terminal window
umount /etc/systemd/system/scripts/webapp-mgr.sh && systemctl restart webapp-mgr

LG Input Hook, a root-only app from the Homebrew Channel, can make the remote’s Home button open the launcher. Install it, open it once (that starts it, and it then starts itself at boot), and put this in /home/root/.config/lginputhook/keybinds.json on the TV:

{
"773": {"action": "launch", "id": "com.minimal.launcher"},
"174": {"action": "launch", "id": "com.minimal.launcher"},
"2": {"action": "launch", "id": "com.webos.app.home"},
"1038": {"action": "exec", "command": "/home/root/minimal-launcher/bin/launcher-sidebar"}
}

773 is Home on LG’s infrared remotes (checked on an AKB75675313) and 174 is Exit; both open the launcher, so leaving an app or the TV’s settings lands on the launcher instead of live TV. Pressed on the launcher itself, either one goes back to the top of the home screen and closes the sidebar. 2 is the 1 button; it opens LG’s own home bar (com.webos.app.home) as a fallback. 1038 is the . (dot) button; tv/bin/launcher-sidebar launches the launcher with its sidebar open from anywhere, and on the launcher it opens or closes the sidebar. Input Hook reloads the file when it changes. To find another button’s code, open Input Hook and watch its log while pressing the button. Don’t press “Save changes” in Input Hook’s own screen unless it shows your keybinds, because it overwrites the file with what it shows.