Locking the control channel

(We are not security experts — that caveat applies to this whole section.)

Port 9010 is the kiosk’s control channel. It does exactly what the tablet does: start and stop sessions, mute, pause, change volume and language, take the kiosk out of service.

It works out of the box, in every scenario, with nothing configured. With one PC or two, you need no setup to get a tablet running. Read on to learn what you have and when to change it — not because anything is waiting for you.

How it decides who may drive the kiosk

One rule applies to each connection separately:

Where the tablet connects from What happens

the Unreal machine itself

allowed straight in. Nothing to configure — a touchscreen wired to the kiosk PC is, by definition, someone standing at the kiosk

anywhere on a network

must present the token, or it is refused

That is the whole model. Notice what is not there: no setting opens or closes the port, and there is no way to get it wrong. A tablet on the same box needs no token. A tablet on another box always needs one, and no configuration can waive that.

The token, and the one thing to decide about it

A fresh install already has a matching token on both sides, so a two-box kiosk connects with no setup. The value is the same in tablet.bat and Unreal:

promethist-kiosk-default

That value is printed here, in a document we hand to partners. Be clear about what this buys you: it stops port scanners, bots, and anyone idly poking your network. It stops nobody who has read this page. It is a lock that only keeps out people not looking for you.

Replace it if the network is not exclusively yours — a venue’s LAN, guest Wi-Fi, a shared office, or anywhere you would not leave an unlocked laptop. Update two places with one value:

; Saved/PromethistLinkConfig.ini on the Unreal box
[PromethistKioskTablet]
AuthToken=<your value>
:: tablet.bat on the tablet box
set "TABLET_TOKEN=<your value>"

powershell -c "[guid]::NewGuid().ToString()" generates a good one. Until you replace it, Unreal logs a warning the first time a tablet connects over the network. An installation still on the default is never quietly on the default.

Use letters, digits and hyphens only. The token travels through a URL and a .bat file, and four characters do not survive: & ends the value, % is eaten by cmd, while " and # break the line building the URL. All four fail identically with "invalid token", even when the two files look identical. Comparison is case-sensitive.

Do not pass it as -KioskTabletToken= on the command line. It works, but Unreal writes the entire command line to its startup log, leaking the token into every kiosk log file. The ini file is read silently, ensuring the token is never logged.

If you are streaming (D and E), read this one

In streamed shapes, a tunnel or reverse proxy typically runs on the Unreal box and forwards traffic to it. Remote traffic thus arrives looking like it came from the Unreal machine, taking the "same box, come straight in" path where the token is never requested.

Apply both of these independent fixes:

  • Lock the tunnel, which the other door section covers anyway — --basic-auth on ngrok, or basic auth on your proxy.

  • Make Unreal demand the token from everybody, local connections included:

    ```ini
    [PromethistKioskTablet]
    RequireTokenFromLocal=true
    ```
    With this set, the tablet on the same box needs the token too. This is fine, because `tablet.bat`
    already ships with it.

Optional: shut the port completely

If a box must never be reachable on 9010 — no tablet anywhere except this machine, ever — you can close it at the network level instead of relying on the token:

[PromethistKioskTablet]
BindAddress=127.0.0.1

Nothing off that machine can then open a connection. You do not need this; the per-connection rule above already refuses anyone without the token. It exists for people who prefer the port simply not answer.

Checking what you actually have

Every start prints a block in the Unreal log. It is the fastest way to check a deployment you did not set up, and the most useful thing to send us when asking for help:

=== Promethist kiosk: deployment summary ===
  control channel : port 9010, bind=every interface
  from this PC    : allowed without a token
  from the network: token required
  guarded by      : the PUBLISHED default token - replace it if this network is not yours
  tablet page     : served on port 8080
  streaming       : no - local kiosk
  settings file   : ...\Saved\PromethistLinkConfig.ini
============================================

The token never appears in it, so the block is safe to screenshot.

Do not put the token in config.json. This is the trap this section steers you around, and it is not obvious: Unreal serves config.json on the same port as the page, to anybody who asks. A token inside is fetched via one GET /config.json by anyone scanning the port — published by the very thing protecting it. Instead, tablet.bat puts it in the page URL’s fragment (http://kiosk:8080/#token=…). A browser never sends a fragment to a server: not in requests, not in logs, not in a Referer.

The token field in config.json is still correct in one case: you serve the tablet page yourself, from somewhere the public cannot fetch it.

Two failure modes to recognise, because they look completely different:

What you see What it is

Tablet fogged, banner says unauthorised; Unreal logs Rejecting a tablet hello: bad token

the two values disagree. Compare them character by character — and check for a trailing comment on the AuthToken= line, which becomes part of the token

Unreal logs using the DEFAULT AuthToken

a tablet connected over the network using the token printed in this guide. Fine on a network that is purely yours; replace it otherwise

Three things a token does not do:

  • It is not encryption. On a plain http:///ws:// link it crosses the wire in the clear, so an untrusted path needs the tunnel to supply TLS.

  • It does not protect the page on 8080, which remains readable to anyone who can reach it. This is deliberate: because the token lives in the fragment, the page is merely sample code containing no secrets and possessing no ability to connect.

  • It is not hidden from the tablet box itself. The launcher passes the URL on the browser’s command line, and the browser saves the address in its kiosk profile history. Anyone with an account or disk image of that machine can read it. This locks out the network, not someone standing at the kiosk with a keyboard. If the tablet box is not physically yours, treat the token as known and put a real boundary in front of the ports.

The other door: who may open the stream at all (D and E)

The token above locks the kiosk’s controls. In a streamed installation, there is a second door nobody thinks about: the stream page itself. By default, anyone who learns its address can open it. This is the correct default for a public demo link, where strangers are exactly who you want.

A kiosk is not that. In D and E, the only browser that should ever open your stream is the one bolted to your wall. Anything else knocking on that address is either a mistake or someone snooping. Two things happen if you leave it open:

  • someone can occupy your single viewer slot, preventing your own screen from connecting;

  • if you run a TURN relay, they receive its password just by loading the page (see above).

The fix is one setting, and it is not in our software — it belongs to whatever publishes the address:

If you reach it through Put the lock here

ngrok

--basic-auth (or its OAuth option) on the tunnel

your own nginx / Caddy / reverse proxy

HTTP basic auth, a client certificate, or an IP allow-list

a VPN between the kiosk and the host

nothing further — the address is not public in the first place

Any of these restricts "anyone on the internet" to "our kiosk". A username and password in the URL is sufficient for an unattended machine: your kiosk browser opens a single address on boot, forever.

Do this only for kiosk installations. If the same host also serves a demo link for customers, lock the kiosk link but leave the demo open. They are two addresses, not one setting.

The big screen’s page, when you are streaming

In the streaming shapes (D and E), the big screen is also a web page: the Pixel Streaming player. It is a different page from the tablet, served by the signalling server instead of Unreal, and it is not part of the kiosk-tablet repo.

You want our build of that player, not the stock Epic one. Epic’s library disables several things a talking digital human needs by default:

  • Echo cancellation and noise suppression. The Pixel Streaming library hardcodes echoCancellation: false, autoGainControl: false and noiseSuppression: false on the microphone. The avatar’s voice comes out of a speaker next to that microphone, so without echo cancellation the avatar hears itself and starts talking to itself. Ours patches getUserMedia to force these back on. This alone is reason enough not to start from the stock player.

  • A blocked microphone is invisible. The library swallows the browser’s "no, you may not use the microphone" rejection without firing any event. From the page’s perspective, nothing happened and the visitor simply talks to a statue. Ours wraps the call, notices, and shows a hint.

  • Plus single-user gating, cursor auto-hide, and the ?agentRef= / ?level= / ?region= URL parameters that pick which agent, scene and backend a page connects to.

One thing in our build you will probably want to turn off for a kiosk is the idle timeout, which drops inactive viewers. That is correct for a demo link — freeing the slot when somebody wanders off with the tab open — but wrong for a kiosk. A browser bolted to a wall is supposed to stay connected all day, whether or not a human is in front of it.

You get both the built page and its source. The finished player is already in the package you receive, sitting beside the signalling server that serves it. If you change nothing, there is nothing to install or build. The TypeScript it was built from is included because the page is meant to be forked: your branding, your landing screen, your idle behaviour.

This is the same arrangement as the tablet: take the working code, edit it if you want to, and never wait for us to change things for you.

We do not recommend starting from Epic’s own player. You would have to reimplement every fix listed above, starting with echo cancellation. Get that wrong, and the avatar holds a conversation with itself.

Audio/video sync is a separate thing, and it lives in Unreal — not in the page. Pixel Streaming sends audio and video as independent streams by default. This is the lowest-latency choice, but lets them drift apart on a jittery network so the avatar’s voice stops matching its mouth. We turn sync on, and it is on by default in our component:

Knob

bSyncAudioVideoTracks on UPromethistLinkComponentdefault true

What it drives

the PixelStreaming2.WebRTC.DisableAudioSync console variable, set at BeginPlay

Override without rebuilding

a SyncAudioVideoTracks= line in Saved/PromethistLinkConfig.ini

Cost

some receiver-side buffering latency — the browser holds frames back to line the two up

Mechanically, this puts both tracks in the same media stream, letting the browser’s WebRTC stack align them from their RTP timestamps. The sync happens in the browser, but nothing in the page turns it on. Flip the Unreal-side knob and every viewer gets it. There is nothing to implement in a custom player, and nothing you can do from a page if it is off.

It must be set before the first viewer connects (Pixel Streaming reads it when creating the track IDs), which is why it applies at BeginPlay rather than when someone joins.

Turning it off is a legitimate choice for a same-room kiosk on a LAN, where there is no jitter to correct and the latency is pure cost. The .ini override lets you A/B test it on a deployed machine without a rebuild.

The one thing you will want to change: our page opens on a landing screen. A #connectOverlay shows branding and a Connect button that the visitor clicks to start. This is correct for a browser link, but wrong for a kiosk. The kiosk should display the avatar when a visitor walks up — nobody should have to press Connect on a wall.

That click is not purely decorative, so do not just delete it. Browsers block audio until a user gesture, and the click satisfies that rule. The kiosk launcher solves this differently: passing --autoplay-policy=no-user-gesture-required on the browser command line removes the requirement entirely, making an unattended auto-start possible. Skip the landing screen and pass that flag; skipping it without the flag produces a silent avatar.