Cube Scripts

WebRTC

Set up STUN and TURN so every player is reachable.

Cube Monitoring uses WebRTC to send video from a watched player to the admin. Media flows peer to peer - your server only relays the handshake.

You can change the connection settings in Config.StunServers and Config.RTCConfig, and the TURN credentials in server/config.lua. Read more about these at Mozilla's documentation.

Why you need a TURN server#

STUN is enough when two peers can reach each other directly. It fails when a player is behind symmetric NAT, CGNAT, or a restrictive mobile or corporate network - common enough that a public server hits it daily.

When that happens the window stays on "Connecting..." and then fails with "The player's network is blocking a direct connection."

A TURN server fixes this by relaying the media when no direct path exists.

Warning

Without TURN, a percentage of your players will never be watchable. The resource ships with public STUN only and no TURN credentials.

Cloudflare#

Cube Monitoring has built-in support for Cloudflare's Realtime TURN server, which generates short-lived credentials on demand.

1

Enable dynamic credentials#

server/config.lua
ServerConfig.Turn.dynamic.enabled = true
ServerConfig.Turn.dynamic.service = "cloudflare"
2

Create the TURN key#

  1. Create a Cloudflare account
  2. Go to Realtime → TURN Server
  3. Press Create
  4. Optionally set a name
  5. Press Create
  6. Save the Turn Token ID and API Token somewhere safe - you need them in the

    next step, so keep the page open

3

Add your credentials#

server/config.lua
ServerConfig.Turn.dynamic.cloudflare = {
    tokenId = "6tixd89deru5dn3in1la93am7yhhbq3g",
    apiToken = "becst6d09fk3q6wumib9jf4zou6ppcn9mecr7a1ne2povhu4tedwyuu49q8bqn6y",
}

Press Finish in the Cloudflare dashboard, then restart the resource.

Note

These values sit in server/config.lua, a server script. They are never sent to a client and cannot be extracted from a player's game files.

4

Tune the cache#

server/config.lua
ServerConfig.Turn.dynamic = {
    enabled = true,
    service = "cloudflare",
    ttl = 3600,
    cacheMargin = 300,
}

ttl is how long Cloudflare keeps the credentials valid, in seconds. cacheMargin is how many seconds before expiry a fresh set is requested.

Credentials are fetched once and reused for every stream until they near expiry, so a busy server makes very few API calls. With Config.Debug on you will see a [DEBUG] TURN credentials cached for 3300s line.

Tip

Dynamic servers are added to your STUN list, not swapped for it. Direct connections still take the fast path and only fall back to the relay when needed.

Metered#

You can also use a service like metered. Create an account, get your credentials, press Show ICE Servers Array and copy the contents.

Metered gives you JavaScript, so convert each entry to Lua - urls stays the same, username and credential keep their names:

server/config.lua
ServerConfig.Turn.servers = {
    {
        urls = "turn:global.relay.metered.ca:80",
        username = "YOUR_USERNAME",
        credential = "YOUR_CREDENTIAL",
    },
    {
        urls = "turn:global.relay.metered.ca:80?transport=tcp",
        username = "YOUR_USERNAME",
        credential = "YOUR_CREDENTIAL",
    },
    {
        urls = "turn:global.relay.metered.ca:443",
        username = "YOUR_USERNAME",
        credential = "YOUR_CREDENTIAL",
    },
    {
        urls = "turns:global.relay.metered.ca:443?transport=tcp",
        username = "YOUR_USERNAME",
        credential = "YOUR_CREDENTIAL",
    },
}

Leave ServerConfig.Turn.dynamic.enabled = false when configuring TURN this way.

Self-hosted coturn#

If you run your own VPS, coturn is the usual choice. A minimal /etc/turnserver.conf:

conf
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
user=cube:a-long-random-password
realm=yourdomain.com
external-ip=YOUR_PUBLIC_IP
no-multicast-peers

Open UDP and TCP 3478, TCP 5349, and the relay range (49152-65535 by default) on your firewall.

server/config.lua
ServerConfig.Turn.servers = {
    {
        urls = "turn:yourdomain.com:3478",
        username = "cube",
        credential = "a-long-random-password",
    },
    {
        urls = "turns:yourdomain.com:5349?transport=tcp",
        username = "cube",
        credential = "a-long-random-password",
    },
}
Tip

Always include a turns: entry on port 443 or 5349. It is the only variant that survives networks blocking everything except HTTPS.

Advanced settings#

config.lua
Config.RTCConfig = {
    iceTransportPolicy = "all",
    iceCandidatePoolSize = 64,
    bundlePolicy = "max-bundle",
}
SettingPurpose
iceTransportPolicy"all" tries direct first and falls back to relay. "relay" forces every stream through TURN - slower and more expensive, but the fastest way to prove TURN works.
iceCandidatePoolSizeHow many candidates are gathered up front. Higher connects faster at a small cost.
bundlePolicy"max-bundle" keeps everything on one transport, the most NAT-friendly option.

Testing your TURN server#

Use the Trickle ICE tool:

  1. Remove the default servers
  2. Add your TURN URL, username and credential
  3. Press Gather candidates

You want at least one candidate of type relay. If you only see host and srflx, your TURN server is unreachable or the credentials are wrong.

To prove the whole path end to end, set iceTransportPolicy = "relay" and watch a player. If the stream works, TURN is doing its job. Set it back to "all" afterwards.

Troubleshooting#

The stream stays on "Connecting..." and then fails#

No working relay path. Configure TURN, then verify it with Trickle ICE.

It works for some players but not others#

The failing players are behind restrictive NAT. This is exactly what TURN is for - STUN alone will never cover them.

The stream connects but the image is black#

Not a network problem. The capture pipeline binds the game backbuffer asynchronously, polls for real pixels and forces a fresh keyframe when they arrive. A persistent black frame usually means the target is on a loading screen or fully minimised.

Cloudflare credentials fail#

Check the server console for [ERROR] TURN credentials failed. The usual causes are a swapped tokenId / apiToken, a revoked token, or the server having no outbound HTTPS access to rtc.live.cloudflare.com.

TURN works in Trickle ICE but not in game#

Confirm ServerConfig.Turn.dynamic.enabled matches how you configured it. With it disabled the Cloudflare keys are ignored entirely; with it enabled your static servers list is still used as well.