Repository navigation
Phone app protocol
How the TiviBro phone app talks to TiviBro on the TV. For anyone building or checking the Android app; service/companion.js is the TV's side and scripts/companion/tb-phone.js a complete client in Node.
- Off until the user switches it on in TiviBro: Settings > Phone app > Phone app connection.
- While on, TiviBro's helper keeps running in the background, also when TiviBro is closed. It listens on TCP 47810 (the API, plain HTTP on the home network) and UDP 47810 (discovery). If 47810 is taken it uses 47816, 47817 or 47818, and says so in discovery.
- After the TV restarts, or if webOS stops it, the helper is back within 5 minutes (a persistent webOS activity calls it). An install removes the package's own activity, so
install.shstarts the helper again and adds a second 5-minute activity (tivibro-keepalive) that isn't part of the package; the helper ends either one when the connection is turned off.TIVIBRO_NO_KEEPALIVE=1skips the second.
Send the text tivibro-discover-v1 as a UDP broadcast to port 47810. Each TV answers to the sender with JSON:
{ "app": "TiviBro", "api": 1, "id": "3f9c0a7d12e4b6c8", "name": "Living room", "port": 47810, "version": "0.6.0-alpha.30" }GET /v1/hello gives the same, plus pairing (whether the TV is showing a code right now) and now (the TV's clock, in ms). Nothing else is open without pairing.
On the TV: Settings > Phone app > Pair a phone. The TV shows a 12-character code (XXXX-XXXX-XXXX, letters and digits without 0, O, 1, I and L) and a QR code for 10 minutes. The QR code is a link:
https://tivibro.app/pair#v=1&h=<ip>:<port>&id=<tv id>&c=<code>&n=<tv name>
Everything is after the #, so a phone camera opening it never sends the code to tivibro.app. The page there opens the app when it is installed.
The code never crosses the network:
-
code= the code without dashes or spaces, in capitals. -
kp= PBKDF2-SHA256(code, salttivibro-pair-v1:<tv id>, 100,000 rounds, 32 bytes). -
POST /v1/pairwith{ "nonce": <random, base64, 16 to 64 characters>, "phone": { "name", "model", "platform" }, "proof": hex HMAC-SHA256(kp, "tivibro-pair-v1\n<tv id>\n<nonce>\n<phone name>") }. - The TV answers
{ "ok": true, "tv": { "id", "name", "api", "version" }, "phone": <phone id>, "key": { "iv", "data", "tag" }, "proof" }.- Check
proof= hex HMAC-SHA256(kp, "tivibro-paired-v1\n\n"): the TV knew the code too. - The phone's key is AES-256-GCM(kp, iv) with additional data
tivibro-key-v1\n<phone id>: 32 random bytes. Keep it in the phone's secure storage, out of backups.
- Check
Errors (JSON { "error", "code" }): closed (no code on the TV, it ran out, or it already paired a phone), wrong (with left, tries left), full (12 phones already), busy (too many tries from this address in a minute). Five wrong codes end that code.
Every other call carries four headers:
| Header | Value |
|---|---|
X-TiviBro-Phone |
the phone id |
X-TiviBro-Time |
ms, on the TV's clock (keep the difference to now from hello) |
X-TiviBro-Nonce |
random, base64, 16 to 64 characters, new for every request |
X-TiviBro-Sig |
hex HMAC-SHA256(key, "tivibro-req-v1\n\n\n\n\n") |
The TV refuses a time more than 5 minutes off (clock, with its now), a nonce it has seen (replay), a bad signature (signature) and a phone it doesn't know (unpaired). Every answer to a known phone carries X-TiviBro-Sig = hex HMAC-SHA256(key, "tivibro-res-v1\n\n\n"); drop answers without it. The one the TV can't sign is unpaired (it no longer has the key): say so, but let the user decide to pair again, since anyone on the network could send it.
| Call | Answer |
|---|---|
GET /v1/info |
name, model, webos, version, mac and macs (for Wake-on-LAN), wol (Turn on via Wi-Fi), bt (the TV's Bluetooth address) and woble (its Bluetooth advertising setting is on) for waking over Bluetooth, ips, tivibro: { open, visible, editor }
|
POST /v1/open |
opens TiviBro on the TV |
POST /v1/editor |
{ "state": "ready", "url" }: the web editor, signed in for this phone (open it in a WebView). starting: TiviBro is opening or starting it; ask again every two seconds. |
POST /v1/forget |
removes this phone from the TV |
All signed like the calls above. TiviBro carries out each command once; if TiviBro is closed, a command opens it and waits for it (up to 40 seconds).
| Call | Body | What it does |
|---|---|---|
POST /v1/key |
{ "key": "ok" } |
A remote button: ok, hold (OK held), up, down, left, right, back, chup, chdown, playpause, stop, rew, ff, info, red, green, yellow, blue, num0 to num9. |
POST /v1/action |
{ "action": "guide" } |
Straight to: guide, search, settings, live, movies, shows, last, fav, favorites, recents, audio, picture, exit, restart, reconnect, list, panel, osd, menu. |
POST /v1/play |
{ "k": "<channel key>" } |
Plays that channel full screen. |
POST /v1/text |
{ "text": "michigan" } |
Searches for it on the TV. |
POST /v1/volume |
{ "dir": "up" } |
The TV's own volume: up, down, mute. Works with TiviBro closed. Answers { "volume": { "level", "muted" } }. |
POST /v1/blewake |
{ "op": "register", "address": "aa:bb:cc:dd:ee:ff" } |
Turning the TV on over Bluetooth, as LG's ThinQ app does. The phone makes up its own Bluetooth address once and registers it with the TV (luna://com.webos.service.asc/registerDevice, type ThinQ); to wake the TV it advertises LG's manufacturer data (company 0x00C4: its address, then 07 00 and the TV's bt address) with service UUID 0000feba-0000-1000-8000-00805f9b34fb. op: register answers { registered, msg }, status answers { registered, woble }, unregister answers { ok }. Works with TiviBro closed. A bad address or op is 400 with code: "bad". |
GET /v1/state?since=<rev>&wait=1 |
What TiviBro shows: { rev, open, state: { tab, full, playing: { k, name, num, logo, group }, now: { t, s, e }, next, paused, vod } }. With wait=1 and the current rev, the TV answers when it changes, or after 20 seconds. |
|
GET /v1/lineup?rev=<rev> |
The channel list as TiviBro shows it: { rev, groups: [{ id, name, keys }], channels: [{ k, n, num, logo, fav, now, na, dead }] } (na: 1: a channel only a group shows, not All; dead: 1: its last tries on the TV failed while other channels play, shown as "not playing lately" until it plays again). pending: true while TiviBro is making it (ask again in a second), same: true when rev is current, closed: true when TiviBro is closed. |
POST /v1/ask with { "what": "...", "args": { ... } }, signed like every call. TiviBro on the TV answers (it is opened first when closed), and the phone waits for it: { ok: true, data }. TiviBro's own refusals come back as 409 with code: "tv" and a reason to show. { pending: true } means TiviBro didn't answer in time (it may still be opening); ask again. With "noOpen": true and TiviBro closed, the answer is { closed: true } and nothing opens (the app's automatic backups).
| what | args | data |
|---|---|---|
guide |
group (a group id from the lineup, empty for all), at (ms, default now), offset, limit (up to 200) |
{ total, offset, pending, rows: [{ k, a (catch-up), dead (not playing lately), p: [{ t, s, e, r (reminder set) }] }] }: what is on at at and the two after. A row without p is still loading on the TV (pending counts them); ask again. |
schedule |
k |
{ k, arch (catch-up days), progs: [{ t, s, e, d (description), r }], descPending }: from as far back as catch-up goes to a week ahead. |
remind |
k, s (the programme's start), on
|
{ on }. The TV's own reminder, shown also while TiviBro is closed. |
fav |
k, on
|
{ on }. The lineup follows. |
onnow |
none |
{ on: [movie], soon: [movie], looking }, or { off, msg } without a TMDB token on the TV. A movie: { id, title, year, genre, runtime, rating, plot, poster, backdrop, logo, live, chans: [{ k, n, logo, s, e, r }] }, each movie once with every channel showing it. |
vis |
op list with group (blank: the channel k's own group), or save with group and changes ({ key: true (show) or false (hide) }) |
list: { group, title, rows: [{ k, n, num, logo, on, g }] }, every channel of the group, hidden ones too (on 0), g 1 when it has guide data. save: { msg }; the lineup follows. |
chan |
k, op (info, hide with on, rename with name (empty: the provider's), group with group and on, newgroup with name, follow with on: follow the team whose channel it is, for My teams) |
{ k, name, provider, renamed, hidden, group, groups: [{ id, name, has }], team: { id, name, on } or null, msg }: the channel after the change. The lineup follows. |
settings (sealed) |
op: sections, page (sec, path: the page rows followed in), toggle (i, on), choice (i, v: the choice's index), cycle (i, dir), action (i); pin when Settings is locked |
{ sections: [{ id, title }] }, a page { title, trail, rows: [{ i, kind, label, sub, value, on, options, cur, swatch }] } (kind: head, info, toggle, choice, cycle, page, action), or after a change { msg, page }. { locked: true, msg } when the TV's PIN is needed or wrong. |
vodcats |
kind (movie, series, or media for My media) |
{ cats: [{ id, n, count, adult, locked }] }, or { none: true } when the playlist has none (for media: no My media sources). _all is All movies or All shows, _cw Continue watching, _my My list. My media's are continue, favorites and all. voditems, vodinfo and vodplay take kind: 'media' the same way; a My media movie plays from the TV's My media screen. |
voditems |
kind, cat or q (search), offset, limit (up to 300) |
{ total, offset, items: [{ id, n, c (cover), r (rating), y (year), p (percent watched) }] }, locked: true for a category behind the PIN. |
vodinfo |
kind, id
|
A movie: { id, n, c, plot, genre, cast, year, dur, r, media, pos, len } (pos and len in seconds, where it stopped). A show: { id, n, c, plot, genre, cast, year, r, seasons: [{ s, eps: [{ id, e, t, dur, plot, pos, len }] }] }. |
vodplay |
kind, id, ep (an episode id, for a show), fromStart
|
{ title }. Plays it on the TV (from where it stopped unless fromStart). |
input |
id (from the state's inputs), value, submit
|
{ ok }. Types into a text box on the TV's screen; submit saves a rename box. |
playlist |
link (the provider's full link), name, use (switch to it) |
{ msg }. Adds the playlist like the web editor's Add a playlist. |
backup |
{ backup }: everything (settings, every playlist with its login, edits, restore points), the same as the web editor's backup before it is sealed with a passphrase. |
|
restore |
backup |
{ playlists }. TiviBro restarts with it. |
logs (sealed) |
{ text }: TiviBro's log, the app's (this and earlier sessions) and the network helper's, as Report a problem makes it: provider, guide and server names replaced. The last 600,000 characters. |
GET /v1/state also lists the text boxes on the TV's screen: state.inputs: [{ id, label, type (text, url or password), value, has (a password is filled in), submit, focus }]. A password's text is never sent.
playlist, input, backup, restore, settings and logs are refused unless sealed, since they carry logins or private details. A sealed request's body is { "sealed": { iv, data, tag } }: the question, AES-256-GCM encrypted under HMAC-SHA256(key, "tivibro-seal-v1") with additional data tivibro-seal-req-v1\n<phone id>\n<nonce> (the request's own nonce, so a sealed body can't be moved to another request). The answer to a sealed request is sealed the same way with tivibro-seal-res-v1\n<phone id>\n<nonce>; errors from before the TV could open the request come back plain. The signature covers the sealed body as sent.
When the TV is off nothing answers. Send Wake-on-LAN magic packets to the mac from info (broadcast, UDP 9), every few seconds until hello answers. The TV needs Turn on via Wi-Fi (or Mobile TV On) switched on.
TiviBro by OhMyBahGosh · MIT licensed · Not affiliated with LG Electronics or TiviMate · Found a mistake? Open an issue.
Getting started
On the TV
- The set-up tour
- Channel logos
- The remote
- The guide
- Groups and the menu
- Watching fullscreen
- Sound the TV can't play
- Other versions of a channel
- Event channels: pay-per-view and sports passes
- My teams
- Catch-up
- Movies and shows
- Movies on now
- My media
- Search
- Favorites, hiding and your own groups
- Reminders
- Watchlist
- Settings
- Updates
- Parental controls
Every setting
- Settings: Playlists
- Settings: Guide
- Settings: Channels and groups
- Settings: Channel and group names
- Settings: Reminders and watchlist
- Settings: Playback
- Settings: My media
- Settings: API keys
- Settings: Remote and buttons
- Settings: Appearance and start screen
- Settings: Parental controls
- Settings: Backups and restore points
- Settings: Phone app and web editor
- Settings: Updates, about and diagnostics
Web editor
- Turning on the web editor
- Drafts and Send to TV
- Channels and groups
- Smart clean-up
- Channel and group names
- The programme guide
- Reminders and watchlist
- Playlists
- My media in the editor
- Backups and restore points
Help
Project