Control Video Playback from TouchDesigner over HTTP
TouchDesigner is often the creative brain of an interactive installation — processing sensors, generating visuals, and making decisions. But when it needs to put a pre-rendered video on a separate screen, it should not have to do the decoding itself. This guide shows how to use TouchDesigner as the controller and DearScenario Player as the remote player.
Why separate playback from TouchDesigner?
TouchDesigner can play video internally with Movie File In TOP, but that ties the GPU, the decoding, and the output to one machine. In exhibition setups, you often want:
- The creative application on one machine, the playback display on another
- Multiple remote screens driven from a single TouchDesigner instance
- Pre-rendered video at native quality without competing for GPU resources with real-time effects
- The ability to restart or update the TouchDesigner patch without interrupting what is playing on the screens
DearScenario Player handles video playback on the remote machine. TouchDesigner tells it what to play over HTTP.
Setup
- Install and launch DearScenario Player on the playback machine (see HTTP node setup guide)
- Ensure both machines are on the same network
- Note the DearScenario Player IP and port (default
18290)
Method 1: Web DAT
The simplest approach. Create a Web DAT in TouchDesigner and configure it to send POST requests to DearScenario Player.
Load a video
- Add a Web DAT to your network
- Set URL to
http://192.168.1.100:18290/api/command - Set Request Method to
POST - Set Custom Header to
Content-Type: application/json - Set Request Body to the JSON command
{"cmd":"play_media","params":{"source":"C:\\media\\scene_a.mp4"}}Pulse the Fetch parameter or call op('web1').par.fetch.pulse() from a Script CHOP or Execute DAT to fire the request.
Play and pause
{"cmd":"play"}
{"cmd":"pause"}Change the Request Body and pulse Fetch again. In practice, you will want to drive this from a script so you can switch commands dynamically.
Method 2: Python in TouchDesigner
For more control, use TouchDesigner’s built-in Python with the requests-style urllib or a Web DAT driven by script. Here is a reusable pattern using an Execute DAT or Script CHOP callback:
import json
import urllib.request
PLAYER = "http://192.168.1.100:18290"
def send_cmd(cmd, params=None):
data = json.dumps({"cmd": cmd, "params": params or {}}).encode()
req = urllib.request.Request(
f"{PLAYER}/api/command",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=3) as resp:
return json.loads(resp.read())
# Trigger from a CHOP value change, button press, or timeline event:
send_cmd("play_media", {"source": "C:\\media\\scene_a.mp4"})
send_cmd("play")Wrap this in an Execute DAT’s onValueChange callback to trigger playback when a sensor value crosses a threshold, or in a Timer CHOP callback to fire cues at specific timeline points.
Method 3: OSC from TouchDesigner
If you prefer OSC, first set network.enable_osc to true and restart the player. OSC then listens on the same port (18290). Use an OSC Out
DAT with unbundled, 32-bit arguments; see the OSC guide for live CHOP values:
| OSC Address | Argument | Action |
|---|---|---|
/player/play_media | "C:\\media\\scene_a.mp4", 30.0, 0 | Load and play from 30s |
/player/play | none | Start playback |
/player/pause | none | Pause |
/player/volume | 0.8 (float) | Set volume |
HTTP returns {"ok": true} after validation and queue admission, not after loading or playback completes. If a cue depends on media being ready or playing, inspect GET /api/status as shown below. OSC has no success acknowledgement; /player/error can report immediate parsing or command-validation failures, but is not playback-completion feedback.
Reading player status
To display playback state in your TouchDesigner UI or make logic decisions based on what is playing, poll the status endpoint:
import json, urllib.request
def get_status():
with urllib.request.urlopen(f"{PLAYER}/api/status", timeout=3) as resp:
return json.loads(resp.read())
status = get_status()
# GET /api/status returns the status object directly, without a result envelope.
# status["state"] → idle | loading | ready | playing | paused | ended | error
# status["source"] → current media source or null
# status["position_sec"] → playback position in seconds
# status["error"] → playback error object or NoneRun this from a Timer CHOP callback at 1–2 Hz to keep a dashboard updated without overwhelming the node.
Common patterns
- Sensor trigger: An Analyze CHOP watches a camera feed. When motion crosses a threshold, an Execute DAT sends
load+playto the node - Timeline cues: A Timer CHOP drives a show timeline. At specific segments, callbacks fire different
loadcommands to switch content on remote screens - Multi-node: Store player IPs in a Table DAT. Loop through rows and send the same command to all nodes for synchronized playback across multiple displays
Next step: If you also use QLab in the same production, see how to trigger DearScenario Player from QLab over OSC.

