ares-install needs a working ssh into the TV, and this TV only has root
access (no Developer Mode, no ssh currently reachable). The Homebrew
Channel's own "Add repository" dialog accepts any URL that returns
{"packages": [...]}, which is the same schema its own gen-manifest.js
uses per app — confirmed by reading the app's source directly rather
than guessing at the format.
make-manifest.py now also writes out/repo.json, the existing manifest
wrapped in that shape, so publishing is: build the ipk, run the script
with --base-url pointing at wherever the release assets will live,
attach ipk + icon + repo.json there, then paste the repo.json URL into
the TV once. No ares, no ssh, no review queue.
Root elevation afterwards is unaffected either way: "Grant root access"
in the app calls elevate-service over the Luna bus from inside the
running app, which was already ssh-independent.
Corrected two inaccuracies in the process: webosbrew/repo doesn't exist
(the real project is webosbrew/apps-repo), and the Homebrew Channel has
no "install from file" UI — Browse-and-install or the /install Luna
service are the only ways in, both of which need a URL, not a local
path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
210 lines
8.1 KiB
Markdown
210 lines
8.1 KiB
Markdown
# Building, testing and publishing
|
|
|
|
## What you need
|
|
|
|
| For | Install |
|
|
| --- | --- |
|
|
| the native service | the [openlgtv buildroot NDK](https://github.com/openlgtv/buildroot-nc4/releases) — or Docker, see below |
|
|
| packaging | `npm install` (ares-cli), or `npm install -g @webosose/ares-cli` |
|
|
| the tests | a host C compiler, Python 3, Node (optional: `flatbuffers`, `jsdom`) |
|
|
|
|
### The toolchain
|
|
|
|
The NDK is a **Linux** toolchain — there is no macOS or Windows build of it. On
|
|
Linux, unpack and relocate it once:
|
|
|
|
```sh
|
|
tar xf arm-webos-linux-gnueabi_sdk-buildroot-x86_64.tar.gz -C "$HOME"
|
|
"$HOME/arm-webos-linux-gnueabi_sdk-buildroot/relocate-sdk.sh"
|
|
```
|
|
|
|
Everywhere else, build the native part in a container:
|
|
|
|
```sh
|
|
./tools/docker-build.sh # -> build/native/audiocap-service
|
|
```
|
|
|
|
That bakes the SDK into an image, so it downloads once and later builds start
|
|
immediately. There are aarch64 and x86_64 SDK builds and the image picks
|
|
whichever matches the container, so on Apple Silicon it runs natively rather
|
|
than under emulation. Only the compile happens in the container; packaging and
|
|
deployment run on the host, where the TV is reachable.
|
|
|
|
Register the TV with ares once, using the Homebrew Channel's ssh (port 9922,
|
|
root):
|
|
|
|
```sh
|
|
ares-setup-device --add tv \
|
|
--info "{'host':'192.168.1.20','port':9922,'username':'root'}"
|
|
```
|
|
|
|
## Build and deploy
|
|
|
|
```sh
|
|
./tools/build.sh # cross-compile, stage, package -> out/*.ipk
|
|
./tools/build.sh install # ares-install on device "tv"
|
|
./tools/build.sh launch
|
|
./tools/build.sh logs
|
|
```
|
|
|
|
With the container toolchain it is two steps, since `build.sh` only knows how
|
|
to drive a local NDK:
|
|
|
|
```sh
|
|
./tools/docker-build.sh && ./tools/build.sh package
|
|
```
|
|
|
|
`DEVICE=livingroom ./tools/build.sh install` targets a different device;
|
|
`WEBOS_SDK=/opt/webos-sdk ./tools/build.sh` a differently placed NDK.
|
|
|
|
The same commands exist as npm scripts (`npm run build`, `npm run deploy`, …)
|
|
if that is more your habit. `build.sh` puts `node_modules/.bin` on PATH first,
|
|
so a local `npm install` of ares-cli is enough — no global install needed.
|
|
|
|
### What the packaging step does
|
|
|
|
`ares-package` takes two directories:
|
|
|
|
```text
|
|
build/stage/app frontend/, minus js/mock.js and its <script> tag
|
|
build/stage/service servicefiles/ plus the compiled audiocap-service
|
|
```
|
|
|
|
The service is declared native in `servicefiles/services.json`
|
|
(`"engine": "native"`), which is what makes webOS exec the binary rather than
|
|
look for a Node entry point.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
./test/run-tests.sh
|
|
```
|
|
|
|
Everything runs on the host — no TV involved:
|
|
|
|
| Suite | What it proves |
|
|
| --- | --- |
|
|
| syntax check | `service.c` and `main.c` compile against stub Luna/glib headers |
|
|
| `verify_flatbuf.py` | the hand-rolled FlatBuffers encoder matches what the official Python runtime decodes — union tags, defaults, vector contents |
|
|
| `verify_rtp.py` | the RTP sink and `host/lgtv-audiocap-receiver.py` agree: header layout, sequencing, timestamps, big-endian payload, MTU, and the SDP text |
|
|
| `engine_smoke.c` | capture → DSP → fan-out → sockets, including all 44 bytes of the WAV header and the status document |
|
|
| `ui_smoke.js` | the real `index.html` loaded in jsdom and driven like a remote |
|
|
|
|
The two optional dependencies:
|
|
|
|
```sh
|
|
python3 -m venv /tmp/fbvenv && /tmp/fbvenv/bin/pip install flatbuffers
|
|
npm install # jsdom
|
|
```
|
|
|
|
Without them those two suites print SKIP and the rest still runs.
|
|
|
|
To open the UI in a desktop browser — `js/mock.js` stands in for the Luna bus:
|
|
|
|
```sh
|
|
npm run serve # http://localhost:8000
|
|
```
|
|
|
|
## Publishing to the Homebrew Channel
|
|
|
|
Two ways to get an ipk into the Browse tab. Neither one touches `ares-install`
|
|
or ssh — the app installs itself once the TV can reach a URL.
|
|
|
|
### Your own repository (no review, no waiting)
|
|
|
|
The Homebrew Channel's *Settings → Repositories → Add repository* accepts any
|
|
URL that returns `{"packages": [...]}`, where each entry is the same manifest
|
|
[`make-manifest.py`](../tools/make-manifest.py) already writes. Point one at
|
|
your own git host's release assets and the app shows up in Browse with no
|
|
submission process at all — this is what `--repo-out` (on by default) is for.
|
|
|
|
1. Bump `version` in `frontend/appinfo.json`, `servicefiles/package.json` and
|
|
`package.json`.
|
|
2. Build the ipk: `./tools/docker-build.sh && ./tools/build.sh package` (or
|
|
`./tools/build.sh` on Linux with the NDK installed).
|
|
3. Create a release tagged e.g. `v1.0.0` and attach three files to it: the
|
|
ipk, `frontend/assets/icon.png`, and a repo index generated with
|
|
`--base-url` set to that release's asset URL:
|
|
|
|
```sh
|
|
python3 tools/make-manifest.py \
|
|
--base-url https://git.crylia.de/Crylia/lgtv_audio_cap/releases/download/v1.0.0
|
|
# -> out/manifest.json (one app entry)
|
|
# -> out/repo.json (that entry wrapped as {"packages": [...]})
|
|
```
|
|
|
|
Attach `out/repo.json` itself too — its own download URL is what you paste
|
|
into the TV, and it must match `--base-url` exactly or the ipk/icon links
|
|
inside it point at the wrong place.
|
|
4. On the TV: Homebrew Channel → gear icon → *Add repository* → paste the
|
|
`repo.json` release URL → back out to Browse → find *Audio Cap* → Install.
|
|
|
|
Shipping an update later is the same four steps with a bumped version and a
|
|
new release tag; the Channel diffs against the installed version and offers
|
|
an update once the repo URL is already added.
|
|
|
|
### The official repo (public listing, reviewed)
|
|
|
|
Submit once to [webosbrew/apps-repo](https://github.com/webosbrew/apps-repo)
|
|
and anyone with a stock Homebrew Channel install can find it without adding a
|
|
custom repository. That project wants a `.yml` descriptor pointing at your own
|
|
hosted `manifest.json` (the un-wrapped file from step 3 above) — see its README
|
|
for the submission format.
|
|
|
|
---
|
|
|
|
`rootRequired: true` in the manifest tells the Homebrew Channel the service
|
|
needs elevating, from either path. The app can also do it on demand —
|
|
*System → Grant root access* runs the Channel's `elevate-service` itself, over
|
|
the Luna bus, so it works whether SSH is enabled on the TV or not.
|
|
|
|
## How the service is put together
|
|
|
|
```text
|
|
main.c registers on the bus, runs the glib loop
|
|
service.c the Luna methods and the status subscription
|
|
engine.c the capture thread: read a block, analyse it, hand it to every sink
|
|
config.c load/merge/atomic-save of config.json
|
|
dsp.c peak/RMS envelopes and the 16-band analysis
|
|
capture/ one file per backend, all dlopen-based
|
|
sinks/ one file per output
|
|
net/ RTP, FlatBuffers, the shared stream server
|
|
common/ JSON, logging, ring buffer, audio format
|
|
```
|
|
|
|
Three rules hold the design together:
|
|
|
|
**One format inside.** Everything between a backend and a sink is interleaved
|
|
signed 16-bit little-endian PCM at the configured rate. Backends convert on the
|
|
way in, sinks convert on the way out. Nothing in the middle branches on sample
|
|
type.
|
|
|
|
**Sinks must never block.** The engine calls every sink from the capture thread,
|
|
in order, and a sink that stalls stalls capture. Anything that can wait — a TCP
|
|
client that stopped reading, a HyperHDR host that is switched off — buffers
|
|
internally and drops the oldest audio instead. The Flatbuffers sink connects
|
|
non-blockingly and finishes the handshake on later blocks; sends are bounded to
|
|
200 ms.
|
|
|
|
**Audio libraries are `dlopen`ed, never linked.** A TV without `libpulse` must
|
|
still run the ALSA backend, and one with neither must still run the test tone
|
|
and start up cleanly. `ldd` on the binary shows glib, luna-service2, libc — no
|
|
audio.
|
|
|
|
### Adding a sink
|
|
|
|
1. Write `native/src/sinks/sink_yours.c` with an `open` that reads its own key
|
|
out of the settings object, plus `write`, `status` and `close`.
|
|
2. Define `const sink_driver_t sink_driver_yours` at the bottom and declare it
|
|
in `sinks/sink.h`.
|
|
3. Add it to the table in `sinks/sink.c` and to `DEFAULTS_JSON` in `config.c`.
|
|
4. Add its fields to `SINK_FIELDS` in `frontend/js/app.js`.
|
|
|
|
The UI needs nothing else: it builds the Outputs panel from `listSinks`.
|
|
|
|
### Adding a capture backend
|
|
|
|
The same shape in `native/src/capture/`, with an `available()` that answers
|
|
honestly on a TV that lacks the library, and an optional `describe()` that adds
|
|
its own fields to the diagnostics.
|