Appearance
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 dotted commands (comp1.play, system.showmode=on) — plus the classic comma form (play,comp1) — one per line, and is designed to stay open as a persistent connection.
Naming note: The Exaplay UI calls this composition type a Playlist. The wire protocol still uses the original
cuelistidentifier (returned byget:type, used inget:cuelist, etc.). This is intentional so existing integrations continue to work — treatcuelistandCueListin the protocol as synonyms for Playlist in the UI.
Connection
| Parameter | Value |
|---|---|
| Protocol | TCP (raw socket) |
| Default Port | 8100 |
| Configuration | Menu → Settings → Communication → TCP Listen |
| Line terminator | \r (CR), \n (LF) or \r\n (CRLF) — empty lines are ignored |
| Encoding | UTF-8 (control characters are removed) |
Up to 32 connections are served at once. An idle connection stays open indefinitely; a peer that disappears is detected by TCP keepalive after about 50 s. Each command produces exactly one response line (or multiple lines ending with END for list commands), and the engine never sends anything it was not asked for.
While a project is loading, commands are dropped without a reply (the connection stays open). A controller should read with a timeout and resend.
Changing the Listen Address/Port
The TCP Listen config field takes an ADDRESS:PORT entry. Use * as the address to listen on every network interface:
*:8100To listen on a specific interface only:
192.168.1.5:8100A change takes effect immediately — the listener re-binds. * is one socket on every address (0.0.0.0): it includes loopback, and an adapter that gets its address later is covered too. A specific address that cannot be bound yet (its network is still coming up, or another program holds the port) is retried every 5 s for up to five minutes.
Command Format
The protocol accepts two command syntaxes. The dotted syntax is the primary one — it addresses any object in the project tree systematically and every new capability lands there first. The comma syntax is the classic format; it remains fully supported for existing integrations.
Dotted syntax (primary)
OBJECT_PATH.VERB[=ARGUMENT]<CR>The path walks the project tree (composition, optionally deeper: media, layer, …), the last component is the verb:
comp_main.play
comp_main.cue.go=Blackout
comp_main.cue3.go
comp_main.get:status
comp_main.set:audio.volume=80
project.comp_main.media3.set:warp.enabled=1
system.showmode=on
system.pjlink=192.168.1.50,POWR,1See Dotted command syntax below for the full verb list.
Comma syntax (classic)
COMMAND,COMPOSITION_ID[,ARGUMENT]<CR>- Fields are separated by commas — no space after a comma (
play, comp1looks for a composition calledcomp1). - A field in double quotes may contain commas; the quotes are removed:
set:cue,comp1,"Act 1, Scene 2"plays the cue namedAct 1, Scene 2. - The first field is always the command name.
- Commands that target a composition take a composition reference as the second field (see below).
- Reply fields are not escaped: in a
get:complistline the name is everything after the first comma.
Addressing a composition
Wherever a command takes a composition reference (comp_main stands in for it in the tables below), both of these work:
- The composition's ID (its variable name):
comp1,comp2, … — shown in the composition's Inspector (the inline ID field holds the number: ID 11 →comp11) and listed byget:complist. The ID always wins if a name happens to match another composition's ID. - The composition's name as shown in the editor:
play,Main Show. Matching is case-insensitive, and_and space are interchangeable (play,main_showworks too). If several compositions share a name, the first in project order is used.
In the dotted syntax the same rule applies to the first path component (Main Show.cue.go=Blackout) — except that a name containing a . cannot be used there, because the dot separates path components; use the ID for such names.
For both syntaxes: responses end with \r\n.
Dotted command syntax (primary)
Any line whose first field contains a . is a dotted command — the same systematic addressing cue scripts use internally, resolved against the project object tree. The path pins the target object, so "cue X in composition Y" is always unambiguous. A write command answers OK and a read command returns the value.
Replies that start with ERR, are errors:
ERR,command_failed— the path or verb did not resolve, or the action failed (unknown cue,nextat the end of a Playlist, a Timeline-only getter on a plain composition, …).- A getter may return an error as its value:
ERR,none_playing(get:playingitem,get:cuename),ERR,cue_not_found(get:cueinfo=),ERR,no_devices(system.pjlink-all=). - With no project loaded every dotted command,
system.*included, answersERR,command_failed.
Always start with the composition ID (or a screen ID, or
system). A line whose first component is not a composition, a screen,systemorprojectis applied to the project itself: a mistypedset:audio.volume=80without a composition answersOKand writes a project valueaudio.volumeinstead of changing any volume.
Transport & cue verbs (on a composition)
| Dotted command | Response | Action |
|---|---|---|
comp_main.play | OK | Start playback |
comp_main.pause | OK | Pause playback |
comp_main.stop | OK | Stop playback |
comp_lobby.restart | OK | Play from start. Playlist: the current item goes back to its start and plays on — a looped clip starts over without stopping. Timeline: back to 0 and playing. Paused or stopped, it starts |
comp_main.set:time=45.5 | OK | Seek to position (seconds). Timeline: the timeline position. Playlist: the time within the current item — the same TIME that get:status reports |
comp_main.cue.go=5 | OK | Trigger cue by index (Timeline: variable index, Playlist: 1-based position) |
comp_main.cue.go=Blackout | OK | Trigger cue by name (case-insensitive, within this composition) |
comp_main.cue3.go | OK | Trigger a cue addressed as a tree node (cue3 = the cue's variable name) |
comp_lobby.next | OK | Playlist: play the next item (nothing playing → the first playable item). Timeline: jump to the next cue marker and trigger it |
comp_lobby.prev | OK | Playlist: play the previous item. Timeline: jump to the previous cue marker and trigger it |
comp_lobby.media3.play | OK | Playlist: play this item (media3 = the item's variable name) |
comp_lobby.media3.restart | OK | Playlist: play this item from its start. The item that is playing goes back to its start and plays on — a looped clip starts over without stopping; paused, it plays. Any other item starts from its start |
cue.go without an argument answers ERR,command_failed. At the end or start of a Playlist, next / prev answer ERR,command_failed (the classic form answers the more specific ERR,no_next_item / ERR,already_at_first).
Setter verbs
| Dotted command | Response | Action |
|---|---|---|
comp_main.set:vol=80 | OK | Set volume (0–100, decimals allowed; out-of-range values are clamped) |
comp_main.set:alpha=50 | OK | Set master alpha (0–100, clamped) |
comp_main.set:loop=on | OK | Enable/disable looping (on/off, true/false, 1/0) |
<path>.set:<property>=<value> | OK | Generic: set any inspector property, e.g. comp_main.set:audio.volume=80, comp_main.media3.set:warp.enabled=1, comp_main.media3.set:text.content=Now boarding (live text layer update) |
Multi-client: on a master, a
set:on a composition or anything inside it (a clip, a track, …) — and the classicset:vol/set:alpha/set:loop— is passed on to every client, like an edit in the Inspector.system.set:values are this machine's own and stay here. Sent over UDP the same lines behave the same way.
The former hyphen spellings (
set-time=,set-vol=,set-alpha=,set-loop=) are deprecated; they still work as silent aliases so cue scripts saved in existing projects keep running, but new integrations must use the unifiedset:<what>=<value>form.
Decimals land as decimals. The value is coerced to the property's own type —
on/true/1for a switch, the text for a text property — and a number keeps every digit you send, whatever the property currently holds:comp_main.media1.set:realtime.params.u_audio=0.62writes0.62, not0. (Earlier builds truncated a fractional value whenever the property's stored value happened to be a whole number — which is what made a generator parameter driven from a desk, a curve or a tracker appear not to move at all.)
The same
<path>.<property>addressing drives a property from a timeline curve: a Data Track with the Exaplay value output writes its (scaled) value to e.g.comp_main.media3.realtime.params.u_speedon every change while the composition plays — see Data Tracks.
Getter verbs
| Dotted command | Response format |
|---|---|
comp_main.get:status | STATUS,TIME,FRAME,CUEINDEX,TOTAL (same as comma get:status) |
comp_main.get:type | timeline, cuelist, or composition |
comp_main.get:vol | <0–100> (integer, truncated) |
comp_main.get:duration | Timeline: DURATION · Playlist: COUNT,TOTAL |
comp_main.get:itemcount | <count> |
comp_main.get:playingitem | Playlist: INDEX,NAME,FILE · Timeline: INDEX,CUENAME,TIME |
comp_main.get:cuename | <name> of the active cue/item |
comp_main.get:cuelist | One line per cue/item, then END |
comp_main.get:cueinfo=5 | Timeline: INDEX,NAME,OFFSET,ACTIVE · Playlist: INDEX,NAME,FILE,DURATION (index or name) |
<path>.get:<property> | Generic: read any inspector property, e.g. comp_main.get:audio.volume, comp_main.get:alpha, comp_main.get:loop, comp_main.get:name. Numbers come back as stored — get:alpha may answer 37.5 where the classic get:alpha answers 37 |
get:duration, get:playingitem, get:cuename and get:cuelist need a Timeline or a Playlist; on a plain composition they answer ERR,command_failed.
System verbs (top-level system node)
The project tree has a top-level system node — everything engine-wide that is not a composition lives there: Show Mode, engine restart, machine shutdown/reboot, waking and powering other PCs, PJLink projector control, and (via the generic set:/get:) the output configuration.
| Dotted command | Response | Action |
|---|---|---|
system.showmode=on / =off / =toggle | OK | Set global Show Mode (1/0, true/false work too) |
system.get:showmode | 1 or 0 | Query Show Mode |
system.blank=on / =off / =toggle | OK | Blank every Output to black (and back) over the blank fade time. Add a fade in seconds to use another one, which also becomes the blank fade time: system.blank=on,3 (0–60) |
system.get:blank | 1 or 0 | Query Blank |
system.blankfade=<seconds> | OK | Set the blank fade time without blanking (0–60; 0 = hard cut) |
system.get:blankfade | e.g. 1.00 | Query the blank fade time |
system.audiomute=on / =off / =toggle | OK | Mute the show's audio (on a multi-client master: every machine) |
system.get:audiomute | 1 or 0 | Query audio mute |
system.identify=on / =off / =toggle | OK | Label every Output on the wall (Identify) |
system.get:identify | 1 or 0 | Query Identify |
system.playall | OK | Start all compositions |
system.stopall | OK | Stop all compositions |
system.restart | OK | Restart the engine and reload the current project (same as =project, =reload, =default) |
system.restart=clean | OK | Restart the engine with no project loaded (same as =empty, =blank); any other argument answers ERR,command_failed |
system.shutdown | OK | Shut down the machine (same as the SHUTDOWN cue action) |
system.reboot | OK | Reboot the machine (same as the RESTART cue action) |
system.pjlink=IP,CMD[,PARAM[,PASSWORD[,PORT]]] | OK or query value | Send a PJLink command to a projector. Over TCP/UDP only a projector configured under Control → TCP Devices at that IP (and PORT, default 4352) is reached, with its stored password — a PASSWORD in the line is ignored, and any other address answers ERR,command_failed. ERR,command_failed also if the projector cannot be reached or refuses |
system.pjlink-all=on / =off | OK,<succeeded>/<total> | Power all configured PJLink projectors (device list = project value pjlink_devices, synced by the Devices tab). 1/0, true/false work too. Answers OK,0/3 when every projector failed and ERR,no_devices when none is configured |
system.pc=<name>,wake / ,restart / ,shutdown | OK,sent | Wake (Wake-on-LAN), restart or shut down a PC from Control → System → PCs, named by its name (case-insensitive) or id. on/reboot/off work too. The line answers as soon as the command is on its way; the outcome is logged and shown in the Monitor. In a multi-client fleet only the master sends, so a cue line does not power a PC once per machine. An unknown action answers ERR,command_failed |
system.get:ver | Exaplay,<version> | Engine version |
system.get:projname | <project name> | Loaded project name |
system.get:<path> / system.set:<path>=<value> | value / OK | Generic: the system node's own values (outputs, target, …) |
PJLink examples (port defaults to 4352; the projector at that address must be configured under Control → TCP Devices, whose password is used):
→ system.pjlink=192.168.1.50,POWR,1 (projector power ON)
← OK
→ system.pjlink=192.168.1.50,POWR,? (query power state)
← 1
→ system.pjlink=192.168.1.50,AVMT,31 (shutter/mute ON)
← OK
→ system.pjlink=192.168.1.50,POWR,0,,4352
← OK
→ system.pjlink=10.0.0.99,POWR,1 (not a configured projector)
← ERR,command_failed
→ system.pjlink-all=on (all configured projectors)
← OK,3/3
→ system.pjlink-all=off
← OK,3/3The user panel's All Projectors On/Off and Show Mode ON/OFF buttons use these same code paths (system.pjlink-all= via POST /cmd {"req":"pjlink-all"}, Show Mode via the global show-mode value), so panel and external show control always behave identically.
Custom control items are not separate commands — a value-set control is exactly a generic
set:on the controlled object's path (e.g.comp_main.media3.set:image.alpha=50— a clip's opacity isimage.alpha), an action control is the composition verb (comp_main.play).
Screen verbs (a screen's calibration)
A screen is addressed by its ID — screen1, screen2, … (shown in the screen's Inspector; an ID is never re-used, so a rename or a deleted screen never redirects a command). Three switches act on the screen's VIOSO calibration, the same three as the Calibration, Blending and Black level switches in the calibration's Edit… dialog:
| Dotted command | Response | Action |
|---|---|---|
screen1.calibration=on|off|toggle | OK | Draw the screen's calibration, or not. Off draws the screen as if no calibration file were set — Exaplay's own warping, overlap & blend and black level apply. The calibration stays loaded, so on applies in the next frame |
screen1.calibration.blend=on|off|toggle | OK | The calibration's edge blending (blending works too) |
screen1.calibration.black=on|off|toggle | OK | Every black-level compensation that rides on the calibration: the calibration's own black level, a custom black level and the overall lift (blacklevel works too) |
screen1.get:calibration / screen1.get:calibration.blend / screen1.get:calibration.black | 1 or 0 | Query a switch |
screen1.calibration.reload | OK | Reapply: build the calibration again from its file. The current calibration keeps drawing until the new one is ready, then the two are swapped in one frame |
1/0, true/false and yes/no work for on/off. A switch applies in exactly one frame on every output of the screen, with no frame in between; in a multi-client rig every PC switches at the same moment (see Multi-Client). The switches are part of the show: they are saved with the project and, on a master, sent to every client. On a client the line answers ERR,command_failed — switch the master. The generic screen1.set:calibration.blend=0 is the same command.
Webcapture verbs (a web page clip)
A Webcapture clip (a web page as a live source) takes the browser actions of its Set Live Parameters dialog as tree-path verbs, so a command button, a cue line, a TCP line or an Inputs action can drive the page:
| Dotted command | Response | Action |
|---|---|---|
comp1.media3.webcapture.reload | OK | Reload the page |
comp1.media3.webcapture.back / .forward | OK | Browser history back / forward |
comp1.media3.webcapture.stop | OK | Stop loading |
comp1.media3.webcapture.click=0.25,0.6 | OK | Click the page at x,y — each 0 … 1 across the page (0,0 top left). Out of range answers ERR,command_failed |
comp1.media3.webcapture.click | OK | Click at the clip's stored click position (webcapture.click.x / .y, the dialog's Click Position). Drive the two values from an input (comp1.media3.set:webcapture.click.x=0.4, or an Inputs Set a value mapping on a touch surface's X / Y) and fire this on its press: a touch field on the page |
comp1.media3.webcapture.js=document.body.style.zoom='150%' | OK | Run JavaScript in the page (everything after js= is the script) |
The line answers OK when the clip accepted the command; the browser acts a moment later, on the clip's own thread. A clip whose page is not running (stopped, or Render page off) drops the command and logs it. A cue runs on every machine of a multi-client rig, so each PC's own browser gets the line; sent over TCP or from an Inputs action the line reaches the machine that ran it.
Example session
→ comp_main.play
← OK
→ comp_main.cue.go=Blackout
← OK
→ comp_main.cue3.go
← OK
→ system.showmode=on
← OK
→ system.pjlink=192.168.1.50,POWR,1
← OK
→ comp_main.get:status
← 1,42.1230,2527,2,0
→ comp_lobby.get:cuelist
← 1,Opening,/media/opening.mp4
← 2,Act 1,/media/act1.mp4
← END
→ comp_lobby.cue.go=Act 1
← OK
→ comp_main.get:audio.volume
← 80
→ comp_main.cue.go=NoSuchCue
← ERR,command_failedNote: arguments may contain commas (
comp_main.cue.go=Act 1, Scene 2) — a dotted first field always routes the whole line as a dotted command. A dotted line must not contain>anywhere, not even in the value: protocol-prefixed lines (OSC>,TCP>, …) are cue-script actions, not remote commands, and any line with a>answersERR,unknown_command.
The comma-delimited commands below are the classic equivalents of these verbs and remain fully supported.
Global Commands
These commands do not require a composition ID.
| Command | Dotted equivalent | Response | Description |
|---|---|---|---|
hello | — | hallo | Connection test / keep-alive |
get:ver | system.get:ver | Exaplay,<version> | Get engine version string |
get:complist | — | One line per composition, then END | List all compositions |
get:projname | system.get:projname | <project name> | Get the loaded project name |
showmode:on | system.showmode=on | OK | Enable global Show Mode |
showmode:off | system.showmode=off | OK | Disable global Show Mode |
showmode:toggle | system.showmode=toggle | OK | Toggle global Show Mode |
get:showmode | system.get:showmode | 1 or 0 | Get current Show Mode state |
exit or quit | — | OK | Close the connection gracefully |
get:complist response format
<varname>,<display name>
<varname>,<display name>
...
ENDExample session:
→ get:complist
← comp_main,Main Show
← comp_lobby,Lobby Loop
← ENDComposition Commands
All composition commands use the classic format COMMAND,COMPID[,ARGUMENT]. Each has a dotted equivalent (see Dotted command syntax) — prefer the dotted form for new integrations.
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). On a Playlist, the time within the current item (as get:status reports it). A stopped composition stays stopped |
Audio
| Command | Arguments | Response | Description |
|---|---|---|---|
get:vol,COMPID | — | <0–100> | Get volume (integer %, truncated) |
set:vol,COMPID,VALUE | VALUE — number 0–100 (out-of-range values are clamped) | OK | Set volume |
Opacity
| Command | Arguments | Response | Description |
|---|---|---|---|
get:alpha,COMPID | — | <0–100> | Get master alpha (integer %, truncated) |
set:alpha,COMPID,VALUE | VALUE — number 0–100 (clamped) | OK | Set master alpha |
Playback Options
| Command | Arguments | Response | Description |
|---|---|---|---|
get:loop,COMPID | — | 0 or 1 | Get loop state |
set:loop,COMPID,VALUE | VALUE — on/off, true/false or 1/0 | 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> | Playlist: number of items. On a Timeline, clips placed on tracks are not counted |
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 | seconds, 4 decimals | Timeline: timeline position. Playlist: position within the current item |
FRAME | integer | Current frame number (TIME × 60) |
CUEINDEX | integer | Timeline: active cue index. Playlist: 1-based current item, 0 if none |
TOTAL | seconds, shortest form (0, 184.2) | Playlist: duration of the current item. Always 0 on a Timeline |
get:duration response:
- Timeline:
DURATION— end of the last clip or cue marker, 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. On another type they answer ERR,unknown_command.
| Command | Arguments | Response | Description |
|---|---|---|---|
set:cue,COMPID,INDEX | INDEX — cue variable index | OK | Jump to cue by index and trigger it |
set:cue,COMPID,NAME | NAME — cue name (case-insensitive) | OK | Jump to cue by name and trigger it |
get:cuelist,COMPID | — | One line per cue, then END | List all cues |
get:cueinfo,COMPID,INDEX | INDEX — cue variable index (index only; use COMPID.get:cueinfo=NAME for a name) | INDEX,NAME,OFFSET,ACTIVE | Details about a specific cue |
The classic next, / prev, are Playlist-only; on a Timeline use the dotted comp_main.next / comp_main.prev, which jump to the next / previous cue marker.
A purely numeric argument is always treated as the index; anything else is matched (ASCII case-insensitively) against the cue names of this composition (so identically named cues in other compositions are never hit). Everything after the second comma counts as the name, so names containing commas work:
set:cue,comp_main,Act 1, Scene 2.
get:cuelist (Timeline) response format:
<index>,<name>,<time-offset seconds>
<index>,<name>,<time-offset seconds>
...
ENDget:cueinfo (Timeline) response fields: INDEX,NAME,OFFSET_SECONDS,IS_ACTIVE
CueList Commands
These commands are only valid when the composition is a CueList (Playlist). On another type they answer ERR,unknown_command.
| Command | Arguments | Response | Description |
|---|---|---|---|
set:cue,COMPID,INDEX | INDEX — 1-based item number | OK | Play item at the given position |
set:cue,COMPID,NAME | NAME — item name (case-insensitive) | OK | Play item by name |
next,COMPID | — | OK | Advance to the next item. Nothing playing → start the first item |
prev,COMPID | — | OK | Go back to the previous item. Nothing playing → start the last item |
get:cuelist,COMPID | — | One line per item, then END | List all items |
get:cueinfo,COMPID,INDEX | INDEX — 1-based item number (index only) | INDEX,NAME,FILE,DURATION | Details about a specific item |
next / prev step from the item that was last triggered, even if it has not started yet — two quick next commands advance two items.
get:cuelist (CueList) response format:
<1-based index>,<name>,<file path>
<1-based index>,<name>,<file path>
...
ENDNote: Timeline cue indices match the cue's variable index (arbitrary integer). CueList item indices are always 1-based sequential numbers.
Inputs (tracking channels)
A controller can feed an Inputs channel over the same connection:
| Command | Meaning |
|---|---|
track,CHANNEL,X[,Y] | Position (Y optional) |
track,CHANNEL,x,VALUE | X only |
track,CHANNEL,y,VALUE | Y only |
track,CHANNEL,click[,VALUE] | Click (value defaults to 1) |
Answers OK, or ERR,track_expected_channel / ERR,track_expected_value / ERR,track_bad_args.
Error Responses
All error responses begin with ERR,:
| Error | Meaning |
|---|---|
ERR,unknown_command | Command string not recognised — also a composition command sent without its composition (play), a type-specific command on the wrong composition type, or a dotted line containing > |
ERR,composition_not_found | No composition matches the given ID or name (also: a global command with extra fields, e.g. get:ver,x). Without a loaded project, classic commands answer this too |
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 | get:playingitem, get:cuename or get:duration on a plain composition (neither Timeline nor Playlist) |
ERR,no_project | Classic get:projname with no project loaded (a dotted command answers ERR,command_failed then) |
ERR,no_devices | system.pjlink-all with no PJLink projector configured |
ERR,command_failed | A dotted command could not be resolved or failed — also system.pjlink= to an address that is not a configured projector |
ERR,track_expected_channel, ERR,track_expected_value, ERR,track_bad_args | A malformed track,… line (see Inputs) |
ERR,exception | An internal error occurred |
Example Sessions
Connection test
→ hello
← halloDiscover and play a composition
→ get:complist
← comp_main,Main Show
← comp_lobby,Lobby Loop
← END
→ comp_main.play (classic: play,comp_main)
← OK
→ comp_main.get:status (classic: get:status,comp_main)
← 1,12.3456,740,0,0Work with a CueList
→ comp_lobby.get:type
← cuelist
→ comp_lobby.get:cuelist
← 1,Opening,/media/opening.mp4
← 2,Act 1,/media/act1.mp4
← 3,Closing,/media/closing.mp4
← END
→ comp_lobby.cue.go=2 (classic: set:cue,comp_lobby,2)
← OK
→ comp_lobby.get:playingitem
← 2,Act 1,/media/act1.mp4
→ comp_lobby.next (classic: next,comp_lobby)
← OKWork with a Timeline
→ comp_main.get:type
← timeline
→ comp_main.get:cuelist
← 1,Intro,0.0000
← 2,Scene A,30.0000
← 3,Scene B,90.0000
← END
→ comp_main.cue.go=2 (classic: set:cue,comp_main,2)
← OK
→ comp_main.cue.go=Scene B (classic: set:cue,comp_main,Scene B)
← OK
→ comp_main.cue.go=Scene C
← ERR,command_failed (classic set:cue answers ERR,cue_not_found)Adjust volume and alpha
→ comp_main.set:vol=75 (classic: set:vol,comp_main,75)
← OK
→ comp_main.get:vol
← 75
→ comp_main.set:alpha=50 (classic: set:alpha,comp_main,50)
← OKShow Mode
→ system.showmode=on (classic: showmode:on)
← OK
→ system.get:showmode (classic: get:showmode)
← 1
→ system.showmode=toggle
← OK
→ system.get:showmode
← 0Close the connection
→ exit
← OKConnecting from Common Show Control Systems
Production-oriented, independently versioned examples are installed with Exaplay in its integrations folder (by default C:\Program Files\Exaplay 3\integrations, also reachable from the Start menu as Show-control integrations) — or download them in any browser that can open Exaplay under Control → Integrations. It includes a Q-SYS plugin (.qplug), Crestron SIMPL+ modules with their SIMPL# library (.clz), Q-SYS Lua, AMX NetLinx, QLab/CueLab, ETC Eos, a Bitfocus Companion module, an Extron Global Scripter driver, Medialon and Alcorn McBride recipes, a Node-RED flow, TouchDesigner and Home Assistant templates, and generic TCP/OSC clients, with compatibility metadata, validation labels, reconnect behavior, framing, response parsing, and complete transport/cue actions. Each carries a validation label saying what was actually tested — most are illustrative source reviewed against this API rather than code compiled in the vendor's tooling.
The snippets below are retained as a quick orientation only. They are illustrative and have not been validated in the vendors' authoring tools; use the corresponding integration README before deployment.
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. The recipes in integrations\medialon and integrations\alcorn-mcbride list every string with its reply, the line-ending rules, and the OSC bytes for the blank (over TCP the blank is system.blank=, see System verbs).
Crestron / AMX
Crestron: ready-made SIMPL+ modules ship in that folder at integrations\crestron\simpl\ — Exaplay 3 Engine (connection, Show Mode, projectors, composition list, console) and Exaplay 3 Composition (one per Timeline or Playlist: transport, cues, volume), together with the SIMPL# library they load, ExaplayCrestron.clz, for 4-Series processors and VC-4. Copy the three files into the SIMPL Windows program folder, give both symbols the same Engine_ID, and wire joins: Playing_Fb, Position$, Active_Cue$, Show_Mode_Fb and the rest come back from the engine on their own. The library is built with Crestron's SDK and its protocol logic is covered by automated tests, but the modules have not been compiled in SIMPL Windows, run on a processor, or pointed at a live engine — read integrations\crestron\VERIFICATION.md before relying on them in a show. A SIMPL# Pro program can use the same library directly (see the folder's README).
AMX, or your own Crestron code: open a TCP client to port 8100 and send ASCII lines ending in \r\n; read state back by polling comp_main.get:status.
csharp
// SIMPL# Pro, by hand
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);
Send("comp_main.play");
Send("comp_main.cue.go=3");
Send("comp_main.next");Q-SYS (QSC)
A ready-made Q-SYS plugin ships in that folder at integrations\qsys\ (exaplay.qplug): drop it into Documents\QSC\Q-SYS Designer\Plugins\ and it appears in the schematic library as Exaplay → Exaplay 3 Media Server, with transport, cues, volume, Show Mode, PJLink, status readbacks and a command console. Its Lua is covered by automated tests, but it has not been run in Q-SYS Designer, on a Core, or against a live engine — read its VERIFICATION.md for the commissioning checklist before you rely on it in a show.
A recorded walkthrough shows the Exaplay side of the setup — the address the plugin connects to, a test command before the Core is online, and the Core's traffic in the Monitor: ☰ menu → Walkthroughs → Inputs & show control → Q-SYS drives the show.
The snippet below is the minimal alternative. Q-SYS cores run a Lua 5.3 environment with a built-in TcpSocket object; this 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("comp_main.play") end
Controls["Stop"].EventHandler = function() send("comp_main.stop") end
Controls["Next"].EventHandler = function() send("comp_main.next") end
Controls["SetCue"].EventHandler = function()
send(string.format("comp_main.cue.go=%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/<comp>/cue (e.g. /exaplay/comp_main/cue) |
| Argument | cue index (integer) or cue name (string) |
Other useful OSC addresses to map to CueLab actions (replace <comp> with the composition's variable name):
/exaplay/<comp>/play → start composition
/exaplay/<comp>/start → start composition (alias for play)
/exaplay/<comp>/stop → stop composition
/exaplay/<comp>/pause → pause composition
/exaplay/<comp>/frame <n> → Timeline: jump to frame n
/exaplay/global/start → start ALL compositions
/exaplay/global/stop → stop ALL compositions
/exaplay/global/showmode <on|off|toggle>
/exaplay/global/videomute <on|off|toggle> [<fade seconds>] → blank the outputs to black
/exaplay/global/blankfade <seconds> → set the blank fade timeA string argument fires a cue by name within that composition (/exaplay/comp_main/cue "My Cue"). To search every composition for a name, use CueLab's Web Request action instead: Method POST, URL http://192.168.1.5:8123/cue/trigger, body {"name":"My Cue"}. Full address list: OSC Reference.
Python (testing / scripting)
python
import socket
HOST = "192.168.1.5"
PORT = 8100
with socket.create_connection((HOST, PORT), timeout=5) as sock:
reader = sock.makefile("r", encoding="utf-8", newline="\r\n")
def send(cmd, multiline=False):
sock.sendall((cmd + "\r\n").encode())
if not multiline:
return reader.readline().strip()
lines = [] # list replies end with END
while (line := reader.readline().strip()) != "END":
lines.append(line)
return lines
print(send("hello")) # hallo
print(send("get:complist", multiline=True)) # ['comp_main,Main Show']
print(send("comp_main.play")) # OKnetcat (quick diagnostics)
bash
nc 192.168.1.5 8100
hello
get:complist
comp_main.get:status
exitFirewall
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=8100See Also
- HTTP REST API — JSON-based HTTP API (port 8123) with equivalent seeking and transport commands
- UDP Command API — fire-and-forget datagram variant of this API
