Audio capture and streaming app for webOS 5/6
Captures the TV's audio and sends it out over several transports. The
primary one is HyperHDR: RTP/L16 to a host-side loopback device, since
HyperHDR has no network audio input of its own. A second route renders
the spectrum on the TV and sends FlatBuffers images to port 19400
instead, for setups where touching the host's sound config is not an
option.
native/ the service: capture backends (PulseAudio, ALSA, exec,
test tone, all dlopen-based), DSP, and one file per sink
frontend/ D-pad driven UI at a fixed 1920x1080
servicefiles/ native service manifest plus the boot script
host/ RTP receiver and the loopback installer for the HyperHDR
machine
tools/ build/package, asset generation, Homebrew Channel
manifest, on-TV probe
test/ host-side suites: FlatBuffers and RTP verified against
real decoders, the engine end to end, the page in jsdom
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# Building, testing and publishing
|
||||
|
||||
## What you need
|
||||
|
||||
| For | Install |
|
||||
| --- | --- |
|
||||
| the native service | the [openlgtv buildroot NDK](https://github.com/openlgtv/buildroot-nc4/releases), `arm-webos-linux-gnueabi_sdk-buildroot` |
|
||||
| packaging | `npm install -g @webosose/ares-cli` |
|
||||
| the tests | a host C compiler, Python 3, Node (optional: `flatbuffers`, `jsdom`) |
|
||||
|
||||
Unpack the NDK and relocate it once:
|
||||
|
||||
```sh
|
||||
tar xf arm-webos-linux-gnueabi_sdk-buildroot.tar.gz -C "$HOME"
|
||||
"$HOME/arm-webos-linux-gnueabi_sdk-buildroot/relocate-sdk.sh"
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
### 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
|
||||
|
||||
1. Bump `version` in `frontend/appinfo.json`, `servicefiles/package.json` and
|
||||
`package.json`.
|
||||
2. `./tools/build.sh` and test the ipk on a real set.
|
||||
3. Attach the ipk and `frontend/assets/icon.png` to a release.
|
||||
4. Generate the manifest and attach that too:
|
||||
|
||||
```sh
|
||||
python3 tools/make-manifest.py \
|
||||
--base-url https://github.com/you/lgtv-audio-cap/releases/download/v1.0.0
|
||||
```
|
||||
|
||||
5. Submit the manifest URL to [webosbrew/repo](https://github.com/webosbrew/repo).
|
||||
|
||||
The manifest sets `"rootRequired": true`, which tells the Homebrew Channel the
|
||||
service needs elevating. The app can also do it on demand — *System → Grant root
|
||||
access* runs the Channel's `elevate-service`.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user