Minimal reference implementation of the SMPTE RIS-OSVP OpenTrackIO protocol.
Everything lives in three sibling folders:
python-server/ opentrackio_server.py Python reference producer
python-client/ opentrackio_client.py Python CLI consumer
opentrackio_client_ui.py Python tkinter dashboard consumer
cpp-server/ opentrackio_server.cpp Cross-platform C++17 producer
CMakeLists.txt CMake build script
cpp-client/ opentrackio_client.cpp Cross-platform C++17 consumer (TUI)
CMakeLists.txt CMake build script
The server streams OpenTrackIO samples over UDP. Each datagram is a
JSON payload wrapped in an 18-byte OpenTrackIO packet header (OTrk
identifier + sequence/segment fields + Fletcher-16 payload checksum),
which is what Unreal Engine 5.7's LiveLinkOpenTrackIO plugin and other
conformant consumers expect on the wire. Pass --no-header to either
server to emit bare JSON instead, for quick-and-dirty inspection with
nc / Wireshark. Every dynamic value — the camera rigid-body transform
(x/y/z plus pan/tilt/roll) and the lens encoders and derived lens
parameters (focus, iris, zoom, focal length, focus distance, fStop,
entrance pupil offset) — is driven by its own sine wave, so a consumer
sees the full shape of the message without needing a real tracking rig.
The client joins the multicast group, auto-detects wrapped vs. raw datagrams, parses each JSON sample, and either prints a compact summary line (CLI) or renders a live dashboard (UI).
| Thing | Value |
|---|---|
| Transport | UDPv4, one JSON document per datagram |
| Port | 55555 |
| Multicast group | 239.135.1.<source_number> where source_number ∈ 1..200 (UE5.7 plugin default: 239.135.1.1:55555) |
| TTL | 1 (link-local by default) |
These match the public OpenTrackIO documentation: the Source Number is an 8-bit value that selects the last octet of the multicast address, allowing producers and consumers to meet without prior IP-level configuration.
Each datagram is a single JSON object of the form:
{
"protocol": {"name": "OpenTrackIO", "version": [1,0,1]},
"sourceId": "urn:uuid:...",
"sourceNumber": 1,
"sampleId": "urn:uuid:...",
"timing": {"mode":"internal","sampleRate":{...},"timecode":{...}, ...},
"transforms": [
{
"id": "Camera",
"translation": {"x": 0.21, "y": 1.98, "z": 2.79},
"rotation": {"pan": 4.78, "tilt": 17.04, "roll": 6.56}
}
],
"lens": {
"encoders": {"focus": 0.71, "iris": 0.92, "zoom": 0.85},
"pinholeFocalLength": 39.70,
"focusDistance": 4.03,
"fStop": 4.34,
"entrancePupilOffset": 0.065,
"distortion": [{"model":"Brown-Conrady","radial":[0,0,0],"tangential":[0,0]}]
},
"tracker": {"notes":"...", "recording":false, "slate":"Demo", "status":"Optical Good"},
"static": {
"duration": {"num":0,"denom":1},
"camera": {"make":"...","model":"...","serialNumber":"...","label":"...", ...},
"lens": {"make":"...","model":"...","serialNumber":"...", ...},
"tracker": {"make":"...","model":"...","serialNumber":"..."}
}
}UE5.7's FLiveLinkOpenTrackIOStaticCamera::IsValid() requires
all four of make, model, serialNumber, and label to be
non-empty before the plugin will register a Live Link subject. If any
of these is missing, the OpenTrackIO source will show Status =
"Receiving" in the Live Link panel but no subject will populate.
The plugin's ConvertTypeToFName() uses Label (preferred) or
Make_Model to build the subject's display name.
fStop, pinholeFocalLength, and focusDistance are typed as
FOpenTrackIOOptionalFloat in UE, but the plugin's parser has a
custom JSON importer (TryReadStructOptional) that accepts bare
numbers and sets bIsSet = true automatically — you can send them
either as 4.34 or as {"bIsSet": true, "Value": 4.34} and both work.
Each UDP datagram is built as [16-byte header][JSON payload]. The
header layout matches Unreal Engine 5.7's
LiveLinkOpenTrackIOParser.cpp byte-for-byte (verified against the UE
plugin's own FArchive operator<<). All multi-byte integers are
big-endian (network byte order):
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 4 | identifier (uint32) |
0x4F54726B == ASCII OTrk |
| 4 | 1 | reserved (uint8) |
0 |
| 5 | 1 | encoding (uint8) |
0x01 = JSON, 0x02 = CBOR |
| 6 | 2 | sequence (uint16) |
Increments per complete sample |
| 8 | 4 | segmentOffset (uint32) |
Byte offset of this segment in the reassembled payload (0 if unfragmented) |
| 12 | 2 | lastSegFlagAndLen (uint16) |
Bit 15 = last-segment flag (0x8000); bits 0-14 = payload length in this segment |
| 14 | 2 | checksum (uint16) |
Fletcher-16 (mod 256) over header[0:14] + payload |
Fletcher-16 is computed over the concatenation of the first 14 header
bytes (everything except the checksum field itself) and the payload
bytes, with running sums reduced modulo 256 — the reference
implementation is fletcher16 in python-server/opentrackio_server.py.
Note: a common mistake is to checksum only the payload; UE5.7 will
reject those packets with "Failed to verify packet checksum.".
For an unfragmented sample the producer sets segmentOffset = 0 and
lastSegFlagAndLen = 0x8000 | payload_len (top bit on, payload length
in the low 15 bits — so per-segment payload max is 32 767 bytes;
larger samples need to be split across multiple datagrams).
Passing --no-header on either server skips the wrapper and emits bare
JSON. The Python client and dashboard auto-detect the format (presence
of the OTrk magic); UE5.7's LiveLinkOpenTrackIO plugin does
require the wrapper, so leave it on for Unreal.
# Terminal 1
python3 python-server/opentrackio_server.py --source 1 --rate 60 --verbose
# Terminal 2
python3 python-client/opentrackio_client.py --source 1Both must use the same --source number so they end up on the same
multicast group (239.135.1.<source>).
Handy for local development, containers without IGMP, or CI:
# Terminal 1
python3 python-server/opentrackio_server.py --unicast 127.0.0.1 --rate 60 --verbose
# Terminal 2
python3 python-client/opentrackio_client.py --unicast--source N OpenTrackIO Source Number (1..200). Default: 1.
--port P UDP port. Default: 55555.
--rate HZ Sample rate. Default: 60.
--ttl N Multicast TTL. Default: 1.
--unicast HOST Send unicast to HOST instead of multicast.
--no-header Emit bare JSON with no OTrk wrapper (for debugging).
--verbose Print one summary line per second.
--source N Multicast group 239.135.1.N. Default: 1.
--port P UDP port. Default: 55555.
--iface IP Local interface for the multicast join. Default: 0.0.0.0.
--unicast Listen on plain unicast on --port.
--raw Pretty-print every sample as JSON instead of a summary line.
--count N Exit after N samples (0 = unlimited).
To drive the LiveLinkOpenTrackIO Live Link source in Unreal Engine
5.7, run either server with the default settings (the OTrk wrapper is
on by default) and point UE at the same port / multicast group:
python3 python-server/opentrackio_server.py --source 1 --rate 60
# or
./cpp-server/build/opentrackio_server --source 1 --rate 60In the Unreal editor, open Window → Virtual Production → Live Link,
add an OpenTrackIO source, and select the matching source number
(1..200 — selects multicast group 239.135.1.<N>) or enter the
unicast host/port if you launched the server with --unicast.
Server (with --verbose):
[opentrackio-server] streaming unicast to 127.0.0.1:55555
[opentrackio-server] sourceId urn:uuid:f63573f6-...
[opentrackio-server] rate 60.0 Hz -- Ctrl+C to stop
[000060] t= 1.000s pos=(+0.588,+1.993,+2.349) rot=(p+14.04,t+13.42,r+5.21) ...
Client:
TC 00:00:00:10 pos=(+0.211,+1.985,+2.791) rot=(p +4.78 t+17.04 r +6.56)
enc(f0.706 i0.918 z0.847) fl=39.71mm fd=4.03m T4.34
python-client/opentrackio_client_ui.py is a tkinter dashboard variant of
the client. It runs the same network listener on a background thread and
shows:
- a numeric readout of every field (timecode, position, rotation, lens),
- horizontal bars for the focus / iris / zoom lens encoders,
- rolling 10-second history graphs for position (x/y/z) and rotation
(pan/tilt/roll), drawn directly on a
tk.Canvas(no matplotlib), - a connection / rate indicator (destination, packets/sec, live/stale).
Same flags as the CLI client:
python3 python-client/opentrackio_client_ui.py --unicast # localhost
python3 python-client/opentrackio_client_ui.py --source 1 # multicastRequires tkinter. tkinter ships with the official Python installers on Windows and macOS. On Debian/Ubuntu install it with:
sudo apt-get install python3-tkcpp-server/opentrackio_server.cpp is a C++17 port of the producer. Same
wire format, same flags, no third-party dependencies. Builds on Windows
(Winsock2), Linux, and macOS.
Build with CMake:
cd cpp-server
cmake -B build && cmake --build build --config ReleaseOr directly:
cd cpp-server
# Linux / macOS
c++ -std=c++17 -O2 -o opentrackio_server opentrackio_server.cpp
# Windows (MSVC developer prompt)
cl /std:c++17 /O2 opentrackio_server.cpp ws2_32.libUsage is identical to the Python server:
./opentrackio_server --unicast 127.0.0.1 --rate 60 --verbose
./opentrackio_server --source 1 --rate 60cpp-client/opentrackio_client.cpp is a C++17 consumer with a
self-contained VT100/ANSI dashboard — same "single .cpp file, no
third-party deps" philosophy as the C++ server. Builds on Linux,
macOS, and Windows Terminal / Win10+ conhost (it explicitly enables
virtual-terminal processing on Windows and switches the output code
page to UTF-8).
The dashboard shows live status (packet rate, sequence, bad-checksum
counter), the resolved subject identity from static.camera,
auto-scaled rolling sparklines for camera position (X/Y/Z) and
rotation (pan/tilt/roll), percentage bars for the focus / iris / zoom
encoders, derived lens parameters (focal length, focus distance, fStop,
entrance pupil), and the tracker block (recording / slate / status).
Build with CMake:
cd cpp-client
cmake -B build && cmake --build build --config ReleaseOr directly:
cd cpp-client
# Linux / macOS
c++ -std=c++17 -O2 -o opentrackio_client opentrackio_client.cpp
# Windows (MSVC developer prompt)
cl /std:c++17 /O2 opentrackio_client.cpp ws2_32.libUsage mirrors the Python client:
./opentrackio_client --unicast --port 55555 # localhost loopback
./opentrackio_client --source 1 # multicast group
./opentrackio_client --source 1 --print # CLI lines, no dashboard
./opentrackio_client --unicast --raw # dump JSON payloadsPress Ctrl+C to quit; the dashboard restores your terminal state on exit.
Sample frame:
╔════════════════════════════════════════════════════════════════════════════════════════╗
║ OpenTrackIO Client live 30.1 Hz pkts 39 bad-cksum 0 ║
╠════════════════════════════════════════════════════════════════════════════════════════╣
║ Subject : SineCam Camera : OpenTrackIO-SineCam / Demo-1 ║
║ Timecode : 00:00:01:23 Source : bc658c58-f810... ║
║ Listen : unicast 0.0.0.0:55555 Sample : c6af6798-93e9... ║
╠════════════════════════════════════════════════════════════════════════════════════════╣
║ Position (m) ║
║ X +0.897 ▁▁▁▁▂▂▂▂▃▃▃▃▄▄▄▄▄▅▅▅▅▅▆▆▆▆▆▆▇▇▇▇▇▇█ ║
║ Y +1.802 ▇▇▇▇█▇▇▇▇▇▇▇▇▇▇▇▆▆▆▆▆▅▅▅▅▄▄▄▃▃▃▂▂▂▁▁ ║
║ Z +2.570 █▇▇▇▇▇▇▇▇▆▆▆▆▆▆▅▅▅▅▅▄▄▄▄▃▃▃▃▂▂▂▂▁▁▁ ║
╠════════════════════════════════════════════════════════════════════════════════════════╣
║ Rotation (deg) ║
║ pan +23.752 ▁▁▁▁▂▂▂▂▂▃▃▃▃▃▄▄▄▄▄▅▅▅▅▆▆▆▆▆▇▇▇▇▇█ ║
║ tilt +18.113 ▁▂▃▃▄▄▅▅▆▆▆▇▇▇▇▇▇█▇▇▇▇▇▇▆▆▆▅▅▄▄▃▂▂▁ ║
║ roll +9.989 ▁▁▁▂▂▂▃▃▃▃▄▄▄▅▅▅▅▅▆▆▆▆▆▆▇▇▇▇▇▇▇▇▇▇▇█ ║
╠════════════════════════════════════════════════════════════════════════════════════════╣
║ Lens encoders ║
║ focus 0.90 █████████████████████████████░░░ 90% ║
║ iris 0.79 █████████████████████████░░░░░░░ 79% ║
║ zoom 0.93 ██████████████████████████████░░ 93% ║
╠════════════════════════════════════════════════════════════════════════════════════════╣
║ Lens parameters ║
║ focal length : 49.93 mm focus distance : 4.985 m ║
║ fStop : T 5.554 entrance pupil : 0.079 m ║
╠════════════════════════════════════════════════════════════════════════════════════════╣
║ Tracker : recording=false slate="Demo" status="Optical Good" ║
╚════════════════════════════════════════════════════════════════════════════════════════╝
| Path | Purpose |
|---|---|
python-server/opentrackio_server.py |
Python producer reference. |
python-client/opentrackio_client.py |
Python consumer (CLI). Joins the group, parses + prints samples. |
python-client/opentrackio_client_ui.py |
Python consumer (tkinter dashboard) with live readouts, encoder bars, rolling graphs. |
cpp-server/opentrackio_server.cpp |
Cross-platform C++17 producer (no deps). |
cpp-server/CMakeLists.txt |
CMake build script for the C++ server. |
cpp-client/opentrackio_client.cpp |
Cross-platform C++17 consumer with VT100 terminal dashboard (no deps). |
cpp-client/CMakeLists.txt |
CMake build script for the C++ client. |
README.md |
This document. |