Getting started

This page takes you from a downloaded release tarball to a stream you can watch, in nine steps. Nothing is assumed beyond a server you can log into and an encoder. Every command below is one you can copy and run.

What you need before you start

ETTIX STREAM ships as a single binary with a shell installer. The installer refuses to run anywhere it has not been tested, so check these first:

RequirementWhy
Ubuntu on x86-64The installer refuses any other distribution or architecture rather than half-working. Debian is allowed with a warning because it is closely related. The binary itself has no distribution dependency, so a hand installation elsewhere is possible if you know what you are doing.
systemdThe service is managed by a systemd unit that the installer writes. Without systemctl the installer stops.
root access, over SSHThe installer creates a service account, directories and cryptographic keys.
Inbound TCP 1935 and 80801935 is where publishers connect. 8080 is where the starter configuration serves playback. Both are settings you can change later.
A correct clockRun systemd-timesyncd or chrony. Licensing, playlist timestamps and DVR all depend on the clock being sane.
An encoderOBS Studio or ffmpeg. Both are covered in step 7.

Throughout this page, SERVER means your server's hostname or address as seen from outside, and 1.0.0 stands in for the version you downloaded. Replace them as you go.

The nine steps

Verify what you downloaded

Two separate questions have to be answered before you run anything as root. The signature answers is this an Ettix build? The checksum answers is this the file the checksum names? They are different questions and both files are published so that both can be asked.

V=1.0.0; A=ettix-stream-$V-linux-amd64.tar.gz
curl -fsSLO https://ettix.com/downloads/ettix-stream/$V/$A
curl -fsSLO https://ettix.com/downloads/ettix-stream/$V/$A.sha256
curl -fsSLO https://ettix.com/downloads/ettix-stream/$V/$A.sig
curl -fsSLO https://ettix.com/downloads/ettix-release-signing.pub

Check the signature first. The public key is pinned once, in a file you write yourself, so that a substituted key cannot quietly make a substituted tarball verify:

echo "ettix-releases@ettix.com $(cat ettix-release-signing.pub)" > allowed
ssh-keygen -Y verify -f allowed -I ettix-releases@ettix.com -n file -s "$A.sig" < "$A"

Then check the digest:

sha256sum -c "$A.sha256"

Both the tarball and its checksum file are signed. Signing only the checksum would let anyone who can replace the tarball leave a valid signature attached to a file nobody now holds.

Release signing has not yet been exercised in production. No production signing key existed at the time this documentation was written, and a fake one was deliberately not invented for testing, because a fake key teaches the verification step to pass. If no .sig is published alongside your version, the checksum is the whole of what you can verify. Say so to Ettix support rather than assuming the signature is optional.

The tarball's digest is also recorded in the release notes and in CHANGELOG.md, so you can compare against a second source without having to ask anybody.

Unpack, read the installer, then run it

Unpack and change into the directory. The installer must be run from inside it: it installs the binary and the unit file that are sitting beside it, and it stops if they are not there.

tar xzf "$A" && cd "ettix-stream-$V-linux-amd64"
sha256sum -c SHA256SUMS
less install.sh

SHA256SUMS covers every file in the release, so that check confirms the unpacked contents as well as the archive. Reading install.sh is not a formality. It runs as root, it creates the system account your service runs as, and it generates the keys your recordings will depend on. It is written in plain /bin/sh and commented throughout precisely so that reading it is realistic. Piping a script like this straight into a shell is not the documented method and never will be.

Now see what it intends to do, before it does any of it:

sudo sh install.sh --dry-run

Every line of the plan is printed with a would: prefix and nothing on the machine is changed. That option exists so that "read it before running it" is a step you can actually take seriously on a machine you care about: you can compare what the script says it will do against what it prints it is about to do. When you are satisfied:

sudo sh install.sh

Re-running the installer is safe. Every step checks for what it would create and reports rather than replacing it, because "I ran the installer twice" must never mean a replaced installation identity or a replaced encryption master key. On an upgrade it goes further: it validates your existing configuration with the new binary before touching anything, and stops with the running version untouched if the new release would not accept it.

What the installer just did

It is worth knowing exactly what changed, because two of these things are the parts you can lose.

A service account. A system user and group named ettix-stream, with no home directory and /usr/sbin/nologin as its shell. The server never runs as root; it binds ports 80, 443 and 1935 through CAP_NET_BIND_SERVICE instead.

Three directories.

PathMode and ownerWhat lives there
/etc/ettix-stream2770 root:ettix-streamThe configuration file, and NOTICE.md (third-party licence notices, which several linked licences require to be shipped with the binary).
/var/lib/ettix-stream0700 ettix-streamThe installation identity, the licence lease and the encryption master key.
/var/log/ettix-stream0750 ettix-streamReserved for log files. This build has no file-logging setting: log configures only the level and the format, and the server writes to standard error, which systemd records in the journal.

The mode on /etc/ettix-stream is 2770 rather than the tighter 0750 for a specific reason. The administration API writes the configuration file back — saving from the web interface, enabling a stream, editing a destination — and it does so by writing a temporary file beside it and renaming, which needs write permission on the directory, not merely on the file. With 0750 every such request failed with a permission error that looked like a bug in the API. The trade is real and deliberate: a compromised streaming process can rewrite its own configuration. The file itself stays 0640 root:ettix-stream, so nothing outside the service account can read your stream keys.

The binary, at /usr/local/bin/ettix-stream, mode 0755. Before installing it the script verifies ettix-stream.sha256 from the release directory. An operator who checked the tarball has not yet checked the binary inside it, and if that checksum file is missing the installer says so loudly rather than proceeding quietly.

A starter configuration at /etc/ettix-stream/config.json, written only if no file is already there. It defines one stream with the id channel1, an RTMP push source on the live application, a playback listener on :8080 and the administration listener on 127.0.0.1:8081.

A generated stream key inside that configuration. The example configuration published in the documentation and in every release tarball carries the placeholder change-this-stream-key; the installer replaces it with 24 random bytes from the system generator. A fresh installation carrying the published placeholder would accept a broadcast from anyone who had read the manual. The server refuses to start on the placeholder value, but an installation that will not start is a worse first experience than one that starts with a real key.

The installation identity, created by running ettix-stream init as the service account. This writes the installation id and its keypair into /var/lib/ettix-stream, and — only when a stream is configured to encrypt — an encryption master key. A key you are told to back up but which protects nothing dilutes the instruction that matters, so one is not created speculatively.

The systemd unit, at /etc/systemd/system/ettix-stream.service, enabled and started. It is a hardened unit: ProtectSystem=strict makes the whole filesystem read-only to the service, so every path the server writes has to be listed in ReadWritePaths. The installer reads your configured DVR destinations out of the configuration and adds them, rather than relying on you to remember. If you add a DVR destination later you must add it there yourself and run systemctl daemon-reload, or the first recording fails with a permission error that looks nothing like a configuration mistake.

Finally it printed firewall guidance: 1935/tcp for publishers, 80 and 443/tcp for playback and ACME, and a reminder that the administration listener should stay on loopback. Those playback ports are where you will end up once you have put TLS in front of the server; until then the starter configuration is on 8080, so that is the port to open for the walkthrough below.

Confirm the service is up:

systemctl status ettix-stream
journalctl -u ettix-stream -f

Leave that journalctl running in a second terminal for the rest of this page. Publishing failures are deliberately indistinguishable to the publisher — a wrong key, an unknown stream and a busy stream all produce the same refusal — so the server's log is the only place the actual reason appears.

Reach the administration interface

This is the step that stops most new operators, and the answer is not obvious from the outside: the administration interface is not reachable from another machine, by design.

http.admin_listen defaults to 127.0.0.1:8081, which is the loopback interface only. Pointing a browser at http://SERVER:8081 from your laptop will fail, and it is not broken. Every /api/v1/* route is served on that listener and nowhere else; the public playback listener carries only media, the browser player SDK, /health and /ready.

The reason is that the administration API is privileged. It can stop a broadcast, rewrite your configuration and delete recorded media. A fresh installation has no credential configured at all — there is no default administrator and no bundled password, because a shipped credential is one that every installation in the world shares. Binding a privileged, unauthenticated API to a public interface for even the few minutes it takes to set a password is not a risk the default is willing to take on your behalf.

Reach it over SSH port-forwarding instead. From your own machine:

ssh -N -L 8081:127.0.0.1:8081 user@server

-L 8081:127.0.0.1:8081 makes port 8081 on your laptop an entrance to port 8081 on the server's own loopback interface, carried inside the SSH connection you have already authenticated. -N asks for no shell, so the command does nothing except hold the tunnel open. Leave it running and open:

http://127.0.0.1:8081

You will find the dashboard, and this documentation at http://127.0.0.1:8081/docs/. You are reading it there now if you have got this far.

Forward the playback listener at the same time, so that step 8 works from the same browser without opening a firewall:

ssh -N -L 8081:127.0.0.1:8081 -L 8080:127.0.0.1:8080 user@server

Do not change admin_listen to 0.0.0.0:8081 to make this easier. ettix-stream config check refuses a non-loopback administration listener unless you have also set http.admin_allow_remote and configured http.admin_auth, and even then it is only appropriate behind a firewall, a VPN or an authenticating proxy. admin_allow_remote is an acknowledgement that you have done that work; it is not a substitute for it.

Create an administration credential

On the loopback default the interface will let you in with no credential. Create one anyway before you go any further: it is what makes the interface safe to reach any other way, and it is what lets you give a colleague access without giving them a shell on the server.

Passwords are never written into the configuration in the clear. The binary produces the hash:

ettix-stream admin hash --user ops

It prompts twice on the terminal with echo turned off, and prints one JSON object on standard output. The password is deliberately not accepted as a command-line flag: a password on a command line ends up in your shell history, in /proc/<pid>/cmdline where every user on the box can read it, and in the audit log. The name must start with a letter or digit and contain only letters, digits, ., _ and -, up to 64 characters. The password must be at least 12 characters — not a claim about what makes a good password, but the floor below which hashing is theatre.

What it prints looks like this:

{"username": "ops", "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$...$...", "read_only": false}

Paste that object into http.admin_auth.users in /etc/ettix-stream/config.json. The file is 0640 root:ettix-stream, so you will need sudo:

sudo nano /etc/ettix-stream/config.json

The result should read:

"http": {
  "listen": ":8080",
  "admin_listen": "127.0.0.1:8081",
  "cors_origins": ["*"],
  "admin_auth": {
    "users": [
      {"username": "ops", "password_hash": "$argon2id$v=19$m=19456,t=2,p=1$...$..."}
    ]
  }
}

Check it, then restart:

sudo ettix-stream config check
sudo systemctl restart ettix-stream

config check reads /etc/ettix-stream/config.json unless you pass --config, validates the whole file and reports every problem it finds in one pass rather than stopping at the first. Unknown field names are errors, so a typo in a key is caught here rather than being silently ignored.

A restart is required rather than a reload. A reload (systemctl reload, which sends SIGHUP) applies stream and log changes at runtime, but listener and HTTP settings — which is what admin_auth is — are read when the server starts. The reload would report success and change nothing, which is worse than refusing.

For automation rather than a person, ask for an API key instead:

ettix-stream admin hash --api-key ci-deploy --read-only

That generates the secret itself, prints Authorization: Bearer ci-deploy.<secret> once on standard error and the JSON object for http.admin_auth.api_keys on standard output. ETTIX stores only the hash and cannot print the secret again. --read-only restricts the credential to requests that do not change anything, and it works for --user too.

ettix-stream status and ettix-stream streams list query the administration API over HTTP but do not attach a credential. Once http.admin_auth is configured they receive HTTP 401. Use the web interface, or query the API directly with an API key:

curl -H "Authorization: Bearer ci-deploy.SECRET" http://127.0.0.1:8081/api/v1/status

Find the stream key the installer generated

The key is in the configuration file, on the stream the starter configuration defines:

sudo sed -n 's/.*"stream_key": "\([^"]*\)".*/\1/p' /etc/ettix-stream/config.json

If jq is installed, this is less dependent on the file's layout:

sudo jq -r '.streams[].source.rtmp.stream_key' /etc/ettix-stream/config.json

The administration interface will not show it to you. Every view of the configuration it offers renders secrets as *** and says so, which is why the file on the server is the only place the key can be read.

A stream key is a bearer credential: anyone holding it can broadcast on your channel. Printing it to a terminal puts it in your scrollback and, depending on your shell, in your history file. Clear both once the encoder has it, and treat a key that has been pasted into a chat window as one that needs replacing.

Note the stream's id as well — channel1 in the starter configuration. You need both: the id says which stream you are publishing to, and the key proves you are allowed to.

Publish to it

A publisher connects to the RTMP application named by source.rtmp.app (live) and publishes under the stream's id (channel1). The key travels as a ?key= parameter.

OBS Studio

In Settings → Stream, choose Service Custom... and fill in exactly two fields:

FieldValue
Serverrtmp://SERVER/live
Stream Keychannel1?key=YOUR_STREAM_KEY

The stream id goes in OBS's Stream Key field, not in the Server URL, because that is how OBS splits the RTMP address. Then Start Streaming. Leave OBS's own encoder settings alone for a first test; the defaults produce H.264 video and AAC audio, which is what the server accepts.

ffmpeg

This sends a test pattern and a tone, and needs no camera, capture card or media file. It is the fastest way to prove the path end to end:

ffmpeg -re \
  -f lavfi -i "testsrc2=size=1280x720:rate=30" \
  -f lavfi -i "sine=frequency=440:sample_rate=48000" \
  -c:v libx264 -preset veryfast -profile:v high -pix_fmt yuv420p \
  -b:v 3000k -maxrate 3000k -bufsize 6000k \
  -g 60 -keyint_min 60 -sc_threshold 0 \
  -c:a aac -b:a 128k -ar 48000 -ac 2 \
  -f flv "rtmp://SERVER/live/channel1?key=YOUR_STREAM_KEY"

To send a file you already have, in real time rather than as fast as the disk allows:

ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -ar 48000 -ac 2 \
  -f flv "rtmp://SERVER/live/channel1?key=YOUR_STREAM_KEY"

-re is what makes ffmpeg pace itself at the source's own rate. Without it ffmpeg pushes the whole file at once and the server sees an implausible live stream.

-g 60 with a 30 fps source puts a keyframe every two seconds. Segments are cut at the first keyframe at or after the target segment duration (four seconds by default), so a GOP that divides the target gives you segments of the length you asked for. A GOP longer than the target is not cut in half — segments simply become one GOP long, and the stream reports gop_exceeds_target so you can see why.

RTMP ingest accepts H.264 video and AAC audio only. HEVC, AV1 and VP9 over enhanced RTMP, and MP3 or Speex audio, are dropped and counted, and the server logs the codec once per connection so the reason is visible rather than looking like silence. If nothing plays and the publisher thinks it is connected, check the codec first.

If the publisher is refused

Every refusal looks identical to the publisher: one NetStream.Publish.BadName, whether the key was wrong, the stream does not exist, the stream is disabled, or another encoder already holds it. That is deliberate — it stops the protocol being used to enumerate your configuration — and it means the reason is only in the server's log:

journalctl -u ettix-stream -f

One more thing worth knowing: because the stream defines no publisher accounts, the RTMP password field is also accepted as the stream key. That is what makes tools which only offer a password field work. As soon as a stream defines publisher accounts, one field cannot mean two things and the key must be in ?key=.

Watch it

Playback is HLS, on the public listener, at a URL built from the stream id:

http://SERVER:8080/hls/channel1/index.m3u8

Port 8080, not 80, because that is what the starter configuration's http.listen says. Give it a few seconds after the publisher connects: the playlist needs a segment or two before a player has anything to fetch.

Confirm the server agrees that something is arriving:

ettix-stream streams list

That prints the stream's state, the resolution and frame rate read from the video itself, the bitrate, the viewer count and the number of segments held. A stream in state live with a growing segment count is working.

ffplay and VLC

ffplay http://SERVER:8080/hls/channel1/index.m3u8
vlc http://SERVER:8080/hls/channel1/index.m3u8

These are the least ambiguous test: they do the HLS work themselves and depend on nothing in a browser. If a player works and a browser does not, the problem is in the browser, not in the server.

In a browser

Safari plays HLS natively, so the playlist URL pasted into the address bar is enough there. Chrome, Firefox and Edge do not, and need a JavaScript player. ETTIX ships one, together with a page for exercising it:

http://127.0.0.1:8081/player/example.html

That page lives on the administration listener, so reach it through the SSH tunnel from step 4 — it is a development tool with a token field and a button that forces a credential renewal, and the listener whose job is to serve media to the public is not where that belongs. Set Playback origin to http://127.0.0.1:8080 if you forwarded port 8080 as well, or to http://SERVER:8080 if you can reach that directly, leave Stream as channel1, and press Live.

The page and the media are on two different origins here, so the browser applies cross-origin rules. The starter configuration sets "cors_origins": ["*"], which permits any origin and is why this works out of the box. Before you put anything real behind it, narrow that list to the origins that should embed your player — see the Security page.

Activate the licence

Until this is done the installation is unlicensed, and the streaming engine is in restricted mode. It is worth knowing exactly what that means, because the symptoms are specific and none of them is a crash:

WhatBehaviour while unlicensed
New RTMP publishesRefused, with the licence reason in the log.
Publishers already connectedDisconnected after a 60-second grace period.
HLS playback503 with a Retry-After. The segments already in memory are kept, not purged — they have already been delivered, and keeping them makes recovery immediate.
DVR recordingStopped after the current segment, with journals checkpointed cleanly. Nothing is ever deleted.
DVR playback and exportRefused.
/health and /ready/health stays 200, because the process is alive and you must be able to reach it to fix the licence. /ready answers 503, so a load balancer drains this server rather than sending viewers to one that will refuse them.
Administration interface and APIFully available.

That last row is the point. An installation whose licence has lapsed stays recoverable from wherever you are: you can read the error, read the installation id out to support and install a licence, without anyone standing next to the machine. Returning to a valid licence lifts the restriction immediately and sources reconnect on their own.

The installer printed your installation id when it ran ettix-stream init. If it has scrolled away, ask for it again — init refuses to replace an existing identity, so re-running it is safe and simply prints what is already there:

sudo -u ettix-stream ettix-stream init

Then, in the administration interface you opened in step 4, go to the License screen. Sign in at https://ettix.com, open Licensing, add this installation id, bind it to a licence, and paste the activation token back into that screen. Those routes are never gated by the licence they report on, which is what makes this possible on a server that is refusing everything else.

The same thing over the API, for a scripted installation:

curl -X POST http://127.0.0.1:8081/api/v1/license/activate \
  -H "Authorization: Bearer ci-deploy.SECRET" \
  -H 'content-type: application/json' \
  -d '{"token":"ACTIVATION_TOKEN"}'

The same route accepts a whole lease instead of a token, as {"lease":"..."}, which is what an installation with no outbound network to Ettix.com uses. Supply one or the other, never both.

Activation is done from the administration interface or its API. There is no ettix-stream license command in this build's CLI, and any instruction that names one is out of date. ettix-stream --help lists the commands that do exist.

One diagnostic is worth running once, on any build you are given, before you spend time on an activation that cannot succeed:

ettix-stream version --json

If license_key_ids is empty, that binary has no compiled-in licence authority keys and cannot verify any lease. It will install, start, ingest and serve perfectly, and fail only at activation. That is a defect in the build, not in your configuration; report it to Ettix rather than retrying the token.

What next

You now have a licensed server carrying one stream. Three things are worth doing before it carries anything you care about.

Put TLS in front of playback and narrow cors_origins to the origins that should embed your player. Both are on the Security page, along with viewer authorisation and how to protect the administration listener if loopback plus SSH is not workable for your team.

Decide what the server should record. The DVR page covers destinations, retention, the ReadWritePaths requirement that catches everyone once, and the verification and repair tools. If you enable encryption, that page is also where the master key backup is explained, and that backup is the difference between a recoverable recording and an unrecoverable one.

Decide what you will watch. The Monitoring page covers the status API, the metrics worth alerting on, and what /ready means to a load balancer.

Beyond those: Configuration is the full field-by-field reference for config.json; Streaming covers additional streams, SRT sources, pull sources and the tuning that affects latency; and Troubleshooting is organised by symptom, for when something that was working has stopped.