LITHOS / getting started / a box that runs itself

A box that runs itself

A Raspberry Pi or a mini-PC in a closet, translating every Sunday without anyone logging in. This page is the long version of that section of getting started: the two installers, what they change, and how a camera already in the room becomes the microphone.

The shape of it

Why a service, not a terminal

A server you start in a terminal — or in a screen session, or over SSH — is gone the moment the power blinks. It goes quietly: no error, no crash report, nothing in a log anybody reads. You find out on Sunday morning, from a room of people whose phones say the page can't be reached.

Installed as a service, the box comes back on its own — after a power cut, after a crash, after someone unplugs the wrong thing. That is the whole difference, and it is worth ten minutes once.

Two pieces can be installed this way, separately or on the same machine: the server (the engine and the pages everyone connects to) and the playout client (whatever drives speakers, or takes a microphone, in a particular room). A small building runs both on one Pi. A big one runs the server in a rack and a playout box in each room.

Prerequisite

First: Node on the box

Both installers need Node.js 22 or newer, installed system-wide. This trips up almost every Pi, for two reasons worth knowing before you start:

  • A Pi's own package manager offers Node 20, which is too old.
  • An nvm install doesn't count. nvm puts Node under your home directory, and the services run locked out of /home on purpose. Node works perfectly when you log in and is invisible to the service — which shows up as a service that starts, dies instantly and restarts forever, explaining nothing.

The installer checks for this before touching anything, and offers to fix it. It tells you exactly what that means first, because it is a permanent change to how the machine gets updates:

No usable system Node.js 22+ found (an nvm node under /home does not count:
the service runs with ProtectHome=true and cannot see it).

This can install it for you, which means, permanently, on this machine:
  · a new apt repository:  https://deb.nodesource.com/node_22.x
  · its signing key at:    /usr/share/keyrings/nodesource.gpg
  · the 'nodejs' package, replacing any distribution one

Install Node.js 22 now? [y/N]

Say no and it changes nothing, printing these instead — the same commands it would have run, so you can do it yourself or check it first. --with-node answers yes in advance, for an unattended install. On a machine that already has Node 22+, none of this appears at all.

sudo apt-get install -y curl ca-certificates gnupg

# NodeSource's signing key, converted to the binary form apt wants
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
  | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg

# The repository, pinned to packages that key signs — signed-by matters:
# without it the key would be trusted for every repository on the machine.
echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
  | sudo tee /etc/apt/sources.list.d/nodesource.list

sudo apt-get update
sudo apt-get install -y nodejs
node --version    # expect v22.x or newer

You will see the shorter curl … setup_22.x | sudo bash - in NodeSource's own docs. It ends up in the same place, but it pipes a script you have not read into a root shell — and on a machine that runs a service for a whole building, spelling out the two things being added (a key, and one repository pinned to it) is worth four extra lines.

Step one

1 · The server

Download lithos-server-node-<version>.tar.gz, extract it, and run the installer:

tar xzf lithos-server-node-0.7.0.tar.gz
cd lithos-server-node-0.7.0
sudo ./install.sh

It creates a dedicated lithos-server account, installs the program files and the service, and then stops. It does not start the server, on purpose: the example PIN can't do anything, and a server that comes up unable to work looks broken rather than unconfigured.

Open the settings file it wrote, set an operator PIN and a provider key, then start it:

sudo nano /etc/lithos-server/server.env
sudo systemctl start lithos-server
journalctl -u lithos-server -f

The log prints the address for the congregation and the secure one for staff pages. That's the same server you'd get from a terminal — it simply also comes back by itself now.

Where everything lives

Three directories, deliberately separate:

  • /opt/lithos-serverprogram files. Replaced wholesale on upgrade. Nothing of yours is in here.
  • /etc/lithos-serveryour settings. server.env, plus keys/ and tls/ if you use them. Never overwritten.
  • /var/lib/lithos-serveryour data. Rooms, languages, playout assignments, transcripts, the voice cache. This is the one to back up.

Two API keys, and a switch

A key can go straight into server.env, or into its own file named by LT_KEY_FILE_*. The second is better — the secret sits alone instead of inside a settings file — and it buys something else. Name two key files and the operator page grows a switch between them:

LT_KEY_FILE_FREE=/etc/lithos-server/keys/free
LT_KEY_FILE_PAY=/etc/lithos-server/keys/pay
LT_KEY_MODE=pay

That exists for a specific reason. If you keep a free-tier key for rehearsals and a billed one for the service, something has to choose when the box starts at boot — and a service has nobody to ask. Whichever it defaults to, being on the wrong one is invisible: the free key fails under a real congregation, and the billed key costs money quietly. So the live key is printed in the log at every start and shown on the operator page, with a button to move off it. A switch there applies to sessions started afterwards, and is remembered across restarts. Name one key file, or none, and no switch appears.

Step two

2 · Speakers in a room

The playout client drives the speakers, and registers them with the host so you can assign rooms and languages from Playout devices. Same shape as the server:

sudo apt-get install -y alsa-utils
tar xzf lithos-playout-node-0.7.0.tar.gz
cd lithos-playout-node-0.7.0
sudo ./install.sh

It reads the sound cards actually present and writes them into /etc/lithos-playout/devices.json — one output each, named after the card:

{
  "devices": [
    { "name": "Audio",    "role": "output", "alsa": "plughw:CARD=Audio" },
    { "name": "Audio_1",  "role": "output", "alsa": "plughw:CARD=Audio_1" },
    { "name": "vc4hdmi0", "role": "output", "alsa": "plughw:CARD=vc4hdmi0" }
  ]
}

Rename them to what the room calls them — "Balcony", "Cry room" — because those names are what you'll see on Playout devices. Do it before you assign rooms and languages: the name is half of how an assignment is remembered, so renaming afterwards loses it.

Then set the host and PIN, and start it:

sudo nano /etc/lithos-playout/playout.env
sudo systemctl start lithos-playout
journalctl -u lithos-playout -f

A room in another building

Nothing here assumes the host is nearby — the client makes one outbound connection. Two settings change:

  • LT_SERVER=auto will not work. Discovery is a network broadcast and stops at the subnet, so across a routed link or a separate VLAN it finds nothing at all. Give the host's address instead.
  • Set LT_BOX to the site"North Campus". It appears beside every device on Playout devices, and it is what keeps two rooms that both have a "Speaker" apart.

If the pages load in a browser but this client can't connect, look at LT_ALLOWED_HOSTS on the server: connections are checked against it, and a host reached by a name the server doesn't recognise is refused in a way that reads like a certificate problem and isn't one. The certificate guide covers that.

Step three, optional

3 · A camera as the microphone

No microphone to spare? If the room already has a camera on the network, its audio can be the input. Only the audio track is pulled — a 4K camera costs its audio bitrate and almost no CPU — and it appears on Playout devices like any other microphone, to be assigned to a room.

You need ffmpeg on the box, and the camera's RTSP address.

Finding the address

This is the hard part, and not your fault: the path is different for every manufacturer and the camera rarely tells you. The playout archive includes a finder:

./find-camera.sh                 # look for cameras on this network
./find-camera.sh --scan          # ...and sweep this subnet for RTSP
./find-camera.sh 192.168.1.50    # or probe one you already know

It looks for cameras that announce themselves, probes the paths manufacturers actually use, and prints the line to paste:

=== 192.168.1.50 (mDNS: UHD CAMERA)
    ✓ rtsp://192.168.1.50:554/1   (has audio)
      { "name": "Camera", "role": "input", "url": "rtsp://192.168.1.50:554/1" }

Note (has audio). It checks that separately from whether the stream opens, because a picture-only path opens perfectly happily and is useless as a microphone — a confusing half-hour to spend after you've already wired it up and assigned it to a room. Nothing is changed by running it; it only looks and prints. An NDI|HX camera works here directly, and needs nothing extra.

Add the line to devices.json, rename it for the room, and restart:

sudo nano /etc/lithos-playout/devices.json
sudo systemctl restart lithos-playout

If the camera sounds far too quiet

Don't reach for gain. This is worth a paragraph because it costs people months.

Some cameras and encoders send the same microphone on both channels with one of them inverted. Folding those to mono by averaging — which is what almost every tool does by default — makes them cancel each other out. On real hardware that removes 20 to 35 dB of voice: the feed arrives registered, connected, apparently healthy, and far too quiet to use. It looks exactly like a camera whose level is turned down, and turning the gain up only amplifies the wreckage.

Lithos measures the two channels instead of assuming, and says which case it found:

[device:input:Camera] stream channels: reading difference
  • difference — an inverted pair, being subtracted rather than averaged. This is the fix doing its job; the feed should sound normal.
  • left or right — the other channel was silent, so it's reading the live one alone.
  • sum — ordinary stereo. A quiet feed then really is a level problem, and the right place to fix it is at the camera or the desk.

Check that line before changing anything else. If you truly do need to lift a feed, LT_SOURCE_GAIN_DB exists — but it raises the noise with the voice, so it is a last resort, not a first move.

Later

Upgrading, and undoing

To upgrade, run the newer archive's installer. That's the whole procedure:

sudo ./install.sh

It replaces the program files and the service and never touches your settings or your data — not server.env, not devices.json, not the rooms and assignments. An upgrade that reverted your configuration would defeat the point of running it.

Because of that, the sound-card detection only runs on a first install. Adding USB adapters later? Move devices.json aside and re-run the installer to regenerate it — then re-add any camera line, and expect to reassign anything whose name changed.

To remove a service:

sudo ./install.sh --uninstall

It removes the service and the program files, and deliberately keeps your settings and data — those are your recordings and your room setup, and losing them to a tidy-up would be a poor trade. It prints how to delete them if you really mean to.

When something's wrong

When something's wrong

Everything a service does goes to the system log. These two lines answer most questions:

systemctl status lithos-server lithos-playout
journalctl -u lithos-server -u lithos-playout -f
The service starts and immediately dies, over and over
Almost always Node. Run /usr/bin/node --version — if that's missing or below 22, the service can't start, and an nvm Node under your home directory doesn't count. Re-run the installer and accept the Node offer, or install Node 22+ yourself.
Devices don't appear on Playout devices
Check the playout log for the host address it's using and whether the PIN was accepted. Across buildings, LT_SERVER=auto finds nothing — the address must be explicit. If the server's pages load but this client can't connect, check LT_ALLOWED_HOSTS on the server.
A speaker is assigned but nothing comes out
If the device shows as idle rather than offline, it's connected and waiting — nothing is playing for it yet, so start the room. If it says offline, the card name in devices.json no longer matches; aplay -L lists what's actually there.
The camera is connected but nobody can hear it
Find the stream channels: reading … line in the log first — see if the camera sounds far too quiet above. If it says sum and the feed is still weak, the level really is low and the camera or desk is the place to fix it.
Did it actually survive the reboot?
Don't assume — try it. sudo reboot, wait a minute, then systemctl is-active lithos-server lithos-playout. Both should say active. Far better to learn this on a Tuesday than on a Sunday.

Still stuck? Write to support@lithos.community with the output of systemctl status for the service that's misbehaving and the last page or so of its log.