# TCP Command API

Exaplay 3 exposes a plain-text TCP command interface intended for show control systems, touch panels, and other devices that cannot easily send HTTP or OSC packets. It accepts simple comma-delimited commands terminated by a carriage-return and is designed to stay open as a persistent connection.

## Connection

| Parameter           | Value                         |
| ------------------- | ----------------------------- |
| **Protocol**        | TCP (raw socket)              |
| **Default Port**    | `8100`                        |
| **Configuration**   | Config → Network → TCP Listen |
| **Line terminator** | `\r` (CR) or `\r\n` (CRLF)    |
| **Encoding**        | ASCII / UTF-8                 |

The server accepts multiple simultaneous connections. Each command produces exactly one response line (or multiple lines ending with `END` for list commands).

### Changing the Listen Address/Port

The **TCP Listen** config field accepts one or more entries (one per line), each in `ADDRESS:PORT` format. Use `*` as the address to listen on all network interfaces:

```
*:8100
```

To listen on a specific interface only:

```
192.168.1.5:8100
```

## Command Format

```
COMMAND,COMPOSITION_ID[,ARGUMENT]<CR>
```

* Fields are separated by commas.
* The first field is always the command name.
* Commands that target a composition require the composition's **variable name** (e.g. `comp_1`) as the second field.
* Retrieve variable names with `get:complist`.
* Responses end with `\r\n`.

## Global Commands

These commands do not require a composition ID.

| Command          | Response                             | Description                     |
| ---------------- | ------------------------------------ | ------------------------------- |
| `hello`          | `hallo`                              | Connection test / keep-alive    |
| `get:ver`        | `Exaplay,<version>`                  | Get engine version string       |
| `get:complist`   | One line per composition, then `END` | List all compositions           |
| `get:projname`   | `<project name>`                     | Get the loaded project name     |
| `exit` or `quit` | `OK`                                 | Close the connection gracefully |

### `get:complist` response format

```
<varname>,<display name>
<varname>,<display name>
...
END
```

**Example session:**

```
→ get:complist
← comp_main,Main Show
← comp_lobby,Lobby Loop
← END
```

***

## Composition Commands

All composition commands use the format `COMMAND,COMPID[,ARGUMENT]`.

### Transport

| Command                      | Arguments         | Response | Description                |
| ---------------------------- | ----------------- | -------- | -------------------------- |
| `play,COMPID`                | —                 | `OK`     | Start playback             |
| `pause,COMPID`               | —                 | `OK`     | Pause playback             |
| `stop,COMPID`                | —                 | `OK`     | Stop playback              |
| `set:cuetime,COMPID,SECONDS` | `SECONDS` — float | `OK`     | Seek to position (seconds) |

### Audio

| Command                | Arguments               | Response  | Description            |
| ---------------------- | ----------------------- | --------- | ---------------------- |
| `get:vol,COMPID`       | —                       | `<0–100>` | Get volume (integer %) |
| `set:vol,COMPID,VALUE` | `VALUE` — integer 0–100 | `OK`      | Set volume             |

### Display

| Command                  | Arguments               | Response  | Description                  |
| ------------------------ | ----------------------- | --------- | ---------------------------- |
| `get:alpha,COMPID`       | —                       | `<0–100>` | Get master alpha (integer %) |
| `set:alpha,COMPID,VALUE` | `VALUE` — integer 0–100 | `OK`      | Set master alpha             |

### Playback Options

| Command                 | Arguments            | Response   | Description            |
| ----------------------- | -------------------- | ---------- | ---------------------- |
| `get:loop,COMPID`       | —                    | `0` or `1` | Get loop state         |
| `set:loop,COMPID,VALUE` | `VALUE` — `0` or `1` | `OK`       | Enable/disable looping |

### Status and Info

| Command                  | Response format                         | Description                      |
| ------------------------ | --------------------------------------- | -------------------------------- |
| `get:name,COMPID`        | `<name>`                                | Get display name                 |
| `get:type,COMPID`        | `timeline`, `cuelist`, or `composition` | Get composition type             |
| `get:itemcount,COMPID`   | `<count>`                               | Number of media items            |
| `get:status,COMPID`      | `STATUS,TIME,FRAME,CUEINDEX,TOTAL`      | Full playback status (see below) |
| `get:duration,COMPID`    | Depends on type (see below)             | Total duration                   |
| `get:playingitem,COMPID` | Depends on type (see below)             | Currently playing item           |
| `get:cuename,COMPID`     | `<name>`                                | Name of the currently active cue |

**`get:status` response fields:**

| Field      | Values                               | Description                                                    |
| ---------- | ------------------------------------ | -------------------------------------------------------------- |
| `STATUS`   | `0` stopped, `1` playing, `2` paused | Playback state                                                 |
| `TIME`     | float (seconds)                      | Current playback position                                      |
| `FRAME`    | integer                              | Current frame number (`TIME × 60`)                             |
| `CUEINDEX` | integer                              | Active cue index (timeline) or current item position (cuelist) |
| `TOTAL`    | float (seconds)                      | Total duration of current item (cuelist only)                  |

**`get:duration` response:**

* **Timeline:** `DURATION` — total timeline length in seconds (float)
* **CueList:** `COUNT,TOTAL` — number of items and sum of all clip lengths (float seconds)

**`get:playingitem` response:**

* **CueList:** `INDEX,NAME,FILE` — 1-based item index, friendly name, source file path
* **Timeline:** `INDEX,CUENAME,TIME` — cue variable index, cue name, current time (float seconds)

***

## Timeline Commands

These commands are only valid when the composition is a **Timeline**.

| Command                    | Arguments                    | Response                     | Description                         |
| -------------------------- | ---------------------------- | ---------------------------- | ----------------------------------- |
| `set:cue,COMPID,INDEX`     | `INDEX` — cue variable index | `OK`                         | Jump to cue by index and trigger it |
| `get:cuelist,COMPID`       | —                            | One line per cue, then `END` | List all cues                       |
| `get:cueinfo,COMPID,INDEX` | `INDEX` — cue variable index | `INDEX,NAME,OFFSET,ACTIVE`   | Details about a specific cue        |

**`get:cuelist` (Timeline) response format:**

```
<index>,<name>,<time-offset seconds>
<index>,<name>,<time-offset seconds>
...
END
```

**`get:cueinfo` (Timeline) response fields:** `INDEX,NAME,OFFSET_SECONDS,IS_ACTIVE`

***

## CueList Commands

These commands are only valid when the composition is a **CueList**.

| Command                    | Arguments                     | Response                      | Description                     |
| -------------------------- | ----------------------------- | ----------------------------- | ------------------------------- |
| `set:cue,COMPID,INDEX`     | `INDEX` — 1-based item number | `OK`                          | Play item at the given position |
| `next,COMPID`              | —                             | `OK`                          | Advance to the next item        |
| `prev,COMPID`              | —                             | `OK`                          | Go back to the previous item    |
| `get:cuelist,COMPID`       | —                             | One line per item, then `END` | List all items                  |
| `get:cueinfo,COMPID,INDEX` | `INDEX` — 1-based item number | `INDEX,NAME,FILE,DURATION`    | Details about a specific item   |

**`get:cuelist` (CueList) response format:**

```
<1-based index>,<name>,<file path>
<1-based index>,<name>,<file path>
...
END
```

> **Note:** Timeline cue indices match the cue's **variable index** (arbitrary integer). CueList item indices are always **1-based sequential** numbers.

***

## Error Responses

All error responses begin with `ERR,`:

| Error                       | Meaning                                                     |
| --------------------------- | ----------------------------------------------------------- |
| `ERR,unknown_command`       | Command string not recognised                               |
| `ERR,composition_not_found` | No composition with the given variable name exists          |
| `ERR,missing_argument`      | Required argument was not supplied                          |
| `ERR,cue_not_found`         | The cue/item index does not exist                           |
| `ERR,none_playing`          | Requested info about a playing item, but nothing is playing |
| `ERR,no_next_item`          | `next` reached the end of the cue list                      |
| `ERR,already_at_first`      | `prev` called when the first item is already playing        |
| `ERR,cuelist_empty`         | `next`/`prev` called on an empty cue list                   |
| `ERR,unsupported_type`      | Command is not applicable to this composition type          |
| `ERR,no_project`            | No project is currently loaded                              |
| `ERR,exception`             | An internal error occurred                                  |

***

## Example Sessions

### Connection test

```
→ hello
← hallo
```

### Discover and play a composition

```
→ get:complist
← comp_main,Main Show
← comp_lobby,Lobby Loop
← END

→ play,comp_main
← OK

→ get:status,comp_main
← 1,12.3456,740,0,0.0000
```

### Work with a CueList

```
→ get:type,comp_lobby
← cuelist

→ get:cuelist,comp_lobby
← 1,Opening,/media/opening.mp4
← 2,Act 1,/media/act1.mp4
← 3,Closing,/media/closing.mp4
← END

→ set:cue,comp_lobby,2
← OK

→ get:playingitem,comp_lobby
← 2,Act 1,/media/act1.mp4

→ next,comp_lobby
← OK
```

### Work with a Timeline

```
→ get:type,comp_main
← timeline

→ get:cuelist,comp_main
← 1,Intro,0.0000
← 2,Scene A,30.0000
← 3,Scene B,90.0000
← END

→ set:cue,comp_main,2
← OK
```

### Adjust volume and alpha

```
→ set:vol,comp_main,75
← OK

→ get:vol,comp_main
← 75

→ set:alpha,comp_main,50
← OK
```

### Close the connection

```
→ exit
← OK
```

***

## Connecting from Common Show Control Systems

### Medialon / Alcorn McBride

Both support raw TCP socket commands natively. Configure a **TCP Device** pointing to the Exaplay machine on port `8100` and map commands directly.

### Crestron / AMX

Use the Crestron **TCPClient** symbol (SIMPL+) or the **TCPClient** class (SIMPL# Pro). Send commands as ASCII strings with `\r\n` terminators.

**SIMPL+ example:**

```
TCP_CLIENT ExaplayClient;

FUNCTION Init()
    ExaplayClient.Connect("192.168.1.5", 8100);
END_FUNCTION

FUNCTION PlayComp(STRING compId)
    STRING cmd[64];
    cmd = "play," + compId + "\x0D\x0A";
    ExaplayClient.SendData(cmd);
END_FUNCTION
```

**SIMPL# Pro example:**

```csharp
var client = new TCPClient("192.168.1.5", 8100, 4096);
client.ConnectToServer();

void Send(string cmd) =>
    client.SendData(Encoding.ASCII.GetBytes(cmd + "\r\n"), cmd.Length + 2);

// Map touch-panel button
Send("play,comp_main");
Send("set:cue,comp_main,3");
Send("next,comp_main");
```

### Q-SYS (QSC)

Q-SYS cores run a **Lua 5.3** environment with a built-in `TcpSocket` object. The following snippet opens a persistent connection to Exaplay and wires up Named Controls:

```lua
-- Q-SYS Lua: Exaplay TCP control
local EXAPLAY_IP   = "192.168.1.5"
local EXAPLAY_PORT = 8100
local sock         = TcpSocket.New()
local connected    = false

local function connect()
  sock:Connect(EXAPLAY_IP, EXAPLAY_PORT)
end

sock.EventHandler = function(s, evt, err)
  if evt == TcpSocket.Events.Connected then
    connected = true
    print("Exaplay: connected")
  elseif evt == TcpSocket.Events.Disconnected then
    connected = false
    Timer.CallAfter(connect, 5)   -- auto-reconnect after 5 s
  elseif evt == TcpSocket.Events.Data then
    local resp = s:Read(s:Lines())
    print("Exaplay response: " .. resp)
  end
end

local function send(cmd)
  if connected then sock:Write(cmd .. "\r\n") end
end

Controls["Play"].EventHandler  = function() send("play,comp_main")  end
Controls["Stop"].EventHandler  = function() send("stop,comp_main")  end
Controls["Next"].EventHandler  = function() send("next,comp_main")  end
Controls["SetCue"].EventHandler = function()
  send(string.format("set:cue,comp_main,%d",
       math.floor(Controls["SetCue"].Value)))
end

connect()
```

Q-SYS also has a built-in `HttpClient` for one-shot REST requests:

```lua
HttpClient.Download({
  Url     = "http://192.168.1.5:8123/cue/trigger",
  Method  = "POST",
  Headers = { ["Content-Type"] = "application/json" },
  Data    = '{"name":"Scene 1 - Opening"}',
  EventHandler = function(tbl, code, data, err)
    if code == 200 then print("Cue fired OK")
    else print("HTTP error: " .. tostring(err)) end
  end
})
```

### CueLab

CueLab (by Avolites/Cogenta) outputs **OSC** natively — configure it in **Settings → Network → OSC Output**:

| CueLab Action field | Value                      |
| ------------------- | -------------------------- |
| Destination IP      | `192.168.1.5`              |
| Destination Port    | `8000`                     |
| OSC Address         | `/exaplay/cue/trigger`     |
| Argument (string)   | cue name, e.g. `"Opening"` |

Other useful OSC addresses to map to CueLab actions:

```
/exaplay/play            → start composition
/exaplay/stop            → stop composition
/exaplay/cue/go          → advance to next cue
/exaplay/cue/back        → step back one cue
/exaplay/fade <seconds>  → fade to black
```

CueLab can also fire **Web Request** actions — set Method `POST`, URL `http://192.168.1.5:8123/cue/trigger`, body `{"name":"My Cue"}`.

### Python (testing / scripting)

```python
import socket

HOST = "192.168.1.5"
PORT = 8100

with socket.create_connection((HOST, PORT)) as sock:
    def send(cmd):
        sock.sendall((cmd + "\r\n").encode())
        return sock.recv(4096).decode().strip()

    print(send("hello"))             # hallo
    print(send("get:complist"))      # comp_main,Main Show\r\nEND
    print(send("play,comp_main"))    # OK
```

### netcat (quick diagnostics)

```bash
nc 192.168.1.5 8100
hello
get:complist
get:status,comp_main
exit
```

***

## Firewall

Ensure TCP port `8100` is open on the Exaplay machine:

```batch
netsh advfirewall firewall add rule name="Exaplay TCP Control" ^
  dir=in action=allow protocol=TCP localport=8100
```

***

## See Also

* [HTTP REST API](https://github.com/vioso/docs/blob/main/exaplay3/developer/rest-api.md) — JSON-based HTTP API (port 8123) with equivalent seeking and transport commands
* [UDP Command API](/v3/developer-reference/udp-api.md) — fire-and-forget datagram variant of this API


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.exaplay.one/v3/developer-reference/tcp-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
