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:
| Requirement | Why |
|---|---|
| Ubuntu on x86-64 | The 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. |
| systemd | The service is managed by a systemd unit that the installer writes. Without systemctl the installer stops. |
| root access, over SSH | The installer creates a service account, directories and cryptographic keys. |
| Inbound TCP 1935 and 8080 | 1935 is where publishers connect. 8080 is where the starter configuration serves playback. Both are settings you can change later. |
| A correct clock | Run systemd-timesyncd or chrony. Licensing, playlist timestamps and DVR all depend on the clock being sane. |
| An encoder | OBS 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.
| Path | Mode and owner | What lives there |
|---|---|---|
/etc/ettix-stream | 2770 root:ettix-stream | The configuration file, and NOTICE.md (third-party licence notices, which several linked licences require to be shipped with the binary). |
/var/lib/ettix-stream | 0700 ettix-stream | The installation identity, the licence lease and the encryption master key. |
/var/log/ettix-stream | 0750 ettix-stream | Reserved 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:
| Field | Value |
|---|---|
| Server | rtmp://SERVER/live |
| Stream Key | channel1?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:
| What | Behaviour while unlicensed |
|---|---|
| New RTMP publishes | Refused, with the licence reason in the log. |
| Publishers already connected | Disconnected after a 60-second grace period. |
| HLS playback | 503 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 recording | Stopped after the current segment, with journals checkpointed cleanly. Nothing is ever deleted. |
| DVR playback and export | Refused. |
/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 API | Fully 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.