Live Update API
The Live Update API is a websocket-based interface that enables web widgets to receive real-time updates on data held by Designer and request changes back to the main software. This documentation outlines the API’s connection lifecycle, message protocol, subscription mechanisms, and error handling. It also explains how changes are serialised using JSON and sent to clients.
Using the interface
From a high level, the live update interface provides a mechanism to look up relevant objects in a Designer project and subscribe to its properties. This is done through a mix of Designer and Python expressions. We use Designer expressions to “find” a relevant object in the project, and then Python expressions to read (and potentially write) relevant data from it.
Some examples may be constructive:
| Object | Property | Description |
|---|---|---|
track:track_1 | object.lengthInBeats | The length of “track 1” in beats (seconds). |
track:track_1 | object.layers | The layers associated with the track. |
track:track_1 | object.getLeafLayers(VariableVideoModule) | Retrieves all leaf layers of a specific type from the object. |
screen2:screen_1 | object.description | "screen 1" - the user-visible name of the object. |
Higher-level framework integrations
We recommended using higher-level framework-specific interfaces to work with the functionality provided by this API. By working with this API directly, a developer must consider details relating to data management and integrity, which is largely solved by higher-level interfaces. Badly behaved clients which leak subscriptions have the potential to cause harm to productions.
The API overview below is useful for users of higher-level interfaces as well as those interested in implementing the WebSocket API directly.
Disguise have provided a Vue composable for this reason.
API Overview
The Live Update API provides a bidirectional communication channel via websockets, allowing:
- Realtime Subscriptions: Clients can subscribe to updates on specific resources.
- Dynamic Data Changes: Clients can issue commands to modify resource properties.
- Partial Updates: The API supports partial modifications, merging new fields with existing data.
Subscriptions
Subscriptions are built from a base object. Objects are referenced using Designer expression syntax.
The object serves as the jumping off point for any number of properties. Each property accesses the object using Python syntax, e.g. object.description to get the user-visible name of the object. The object variable is provided by the object expression in order to make property access less repetitive & marginally more efficient.
This object and set of properties are combined into a single subscribe message sent to the Director. Any number of subscribe and unsubscribe messages can be made in a given session, the only limit being performance and memory. Subscription and unsubscription is fast. As a general guideline, unsubscribe from data which is not visible to the user, e.g. when scrolling on a larger page.
Assuming that both the object and property are correctly specified, a subscription list will be returned. The subscription list identifies the unique combination of object and property by a unique, never-repeating id.
The id is an integer value which is used by all subsequent messages to identify the property. This avoids repeating the potentially extremely long object & property names for every update and makes the subscription model of the API clear.
Every subscribe or unsubscribe action returns the full current status of all subscriptions for the client.
If a given client subscribes to the same property multiple times, they use the same id. Each subscription will need to be matched with a corresponding unsubscription for that property id, until the subscription is truly removed. This is achieved via a reference counting mechanism within Designer running on the Director.
This id reuse is useful when different components on a client page may subscribe to the same value, as the data is shared effectively between them, and resources are conserved. The reuse means it is not advisable to implement a client interface using the id as a primary key for anything related to subscriptions.
Property updates
When a value is newly subscribed (this does not include a resubscription to the same value), and as the underlying resource values change, updates are pushed to the client.
The id is used to identify the property. Using id as a key into a local store of values is a reasonable idea, as the id will not change over the lifetime of the WebSocket connection.
Property setting
Clients can update resource properties by sending a set command with the corresponding id and a new value.
The designer-internal live update system will distribute the changes throughout the session to all connected Designer software.
Changes made through set commands are undoable and persistent on the Director.
Not all values can be set. For example, a subscription like { object: 'track:track_1', 'properties': [ 'object.getLeafLayers(VariableVideoModule) ] } will return all video layers on the named track, but will not allow the client to change any of those layers. Attempting to do so will result in an error. There is no way to know if setting a property will work before trying it, due to the complexities of the property expressions.
If a property value is a JSON object (e.g. screen2:screen_1 / object.offset -> { x: 0.0, y: 3.0, z: 0.0 }) it is possible to only set one field of the property. e.g. {x: 4.0} - this ensures that the other values are preserved, and is sometimes useful if a subscription over a complex object was made but the interface focuses on editing individual fields one-by-one. This ensures that fields the user wasn’t actively editing don’t get reverted due to edits by other components or clients.
Error Handling & Concurrency
- JSON Parsing & Validation: Invalid JSON or missing required fields (e.g., missing
"object"or"properties"in a subscribe message) will result in an error response. - Data Type Checks: For unsubscribe messages, if non-integer IDs are provided, the system will notify the client while still processing valid IDs.
JSON value serialisation
- Basic values: Depending on the value type (e.g., integer, float, string), the appropriate JSON value is written.
- Structures: For complex types, the function recursively constructs the JSON structure using these rules.
- Resources: Resources within Designer are converted to a JSON object with keys
"uid","path", and"type".uidis serialised as a hex string ("0x0123456789abcdef") - Arrays: An array is sent, and the elements are serialised as normal.
- Tuples: A Python tuple is sent as a JSON array, and the elements are serialised as normal.
- Dictionaries: A Python dictionary is returned as a JSON object, where the values in the dictionary are serialised according to this table. This allows for more complex queries in a single property subscription.
Advanced Property Expressions
Property paths are evaluated as Python expressions in an eval context. The variable object is bound to the resolved subscription object, and all Designer types are available (via from d3 import *). This means property paths support the full range of Python expressions — including method calls, list comprehensions, dictionary comprehensions, and chained property access.
Chaining through resource references
When a subscription returns an object that contains resource references (such as a Layer or Track), those references are automatically resolved to their full Designer objects. This means you can chain property access through them.
For example, if object.layer returns a Layer reference, you can access the layer’s timing directly:
object.layer.tStart
object.layer.tEnd
object.layer.tLength
List comprehensions
When a property returns a list, you can use Python list comprehensions to transform, filter, or enrich the results in a single subscription.
Filtering: Return only items matching a condition:
[m for m in object.getMappedMediaOfType(VideoClip, 0) if m.layer.tStart < 30.0]
Enriching: Add computed fields to each item:
[{"media": m, "layerStart": m.layer.tStart, "layerEnd": m.layer.tEnd} for m in object.getMappedMediaOfType(VideoClip, 0)]
Dictionary comprehensions
You can use dictionary comprehensions to reshape results into key-value structures:
{m.mappedResource.uid: m.mappedResource.path for m in object.getMappedMediaOfType(VideoClip, 0)}
This would return a JSON object mapping each resource UID to its path.
Worked example: media schedule with layer timing
The MappedMediaDomain provides getMappedMediaOfType() which returns all sequenced media of a given type. By default, each entry includes contentSequencingStart and contentSequencingEnd — but contentSequencingEnd reports 1.7976931348623157e+308 (the maximum float value) when the clip is the last in its sequence, meaning “unbounded”. In practice, such a clip stops when its parent layer ends.
Using a list comprehension, you can build a richer subscription that includes the layer timing alongside the media details:
const subscribeMessage = { subscribe: {
object: "domain:MappedMedia",
properties: [
'[{"resource": m.mappedResource, "track": m.track, "layer": m.layer, "layerStart": m.layer.tStart, "layerEnd": m.layer.tEnd, "layerLength": m.layer.tLength, "contentStart": m.contentSequencingStart, "contentEnd": m.contentSequencingEnd} for m in object.getMappedMediaOfType(VideoClip, 0)]'
]
} };
socket.send(JSON.stringify(subscribeMessage));
Each element in the returned array will contain the mapped resource, its track and layer references, the layer’s start/end/length times (in beats), and the content sequencing boundaries. When contentEnd is the maximum float value, use layerEnd as the effective end time.
All times are in beats (track time), consistent with Designer’s timeline coordinate system.
Worked example: reading and setting keyframe values
Layers contain field sequences that hold keyframes for each animatable property. You can navigate this hierarchy using chained method calls to read — and set — individual keyframe values.
The path to a keyframe value follows the chain:
Layer → findSequence(name) → .sequence → .key(index) → .v
Where:
findSequence(name)finds aFieldSequenceby its decorated name (e.g."pos.x","pos.y","scale").sequenceaccesses the underlyingKeySequencecontaining the keyframes.key(index)returns the keyframe at the given zero-based index.vis the keyframe’s value (a float for numeric fields)
To subscribe to the first keyframe of a layer’s X position:
const subscribeMessage = { subscribe: {
object: "track:track_1.getLeafLayers()[0]",
properties: [
'object.findSequence("pos.x").sequence.key(0).v'
]
} };
socket.send(JSON.stringify(subscribeMessage));
Because .v is a writable field, you can update it using a set command with the subscription’s id:
const setMessage = { set: [
{ id: 123, value: 42.5 }
] };
socket.send(JSON.stringify(setMessage));
This sets the first X-position keyframe to 42.5. Changes made through set commands are undoable and persistent on the Director.
You can also use a comprehension to subscribe to all keyframe values at once:
[{"t": object.findSequence("pos.x").sequence.key(i).localT, "v": object.findSequence("pos.x").sequence.key(i).v} for i in range(object.findSequence("pos.x").sequence.nKeys)]
Connection Lifecycle
This section focuses on the mechanics and syntax of the JSON messages passed between the client and the Director. The client initiates a connection to ws://<director:port>/api/session/liveupdate.
Incoming messages
These messages are passed from the client to the Director.
Subscribe
A subscribe message must include the target object and a list of property paths:
{ "subscribe": {
"object": "objectPath",
"properties": ["propertyPath1", "propertyPath2"]
} }
For each property listed, a new subscription is created (using the provided object path). If the required fields are missing or the properties value is not an array an error is sent.
A subscribe message can also include the optional configuration object which supports configuring the frequency of updates received for the given properties subscribed through this request:
{ "subscribe": {
"object": "objectPath",
"properties": ["propertyPath1", "propertyPath2"],
"configuration": { "updateFrequencyMs": 1000 }
} }
Each property will send independent updates at most once every cycle determined by the duration in milliseconds specified in the configuration updateFrequencyMs property.
Unsubscribe
Unsubscribe messages support either a single subscription ID or an array of IDs:
{ "unsubscribe": { "id": 123 } }
or
{ "unsubscribe": { "ids": [123, 456] } }
Non-integer values are detected and reported as errors. No further updates are sent after this message is processed.
Set
The set command allows the client to update values for subscribed resources:
{ "set": [
{ "id": 123, "value": <value> },
{ "id": 456, "value": <value> }
] }
The server locates the subscription by ID and updates its current value using a partial merge strategy. That is, if multiple set commands modify different fields of the same object, later commands merge with previous changes.
Outgoing messages
These messages are passed from the Director to the client.
Errors
Errors are reported using a JSON-formatted error like so:
{ "error": <message> }
Subscriptions list
When the set of subscribed properties changes, a full list of all subscribed properties is sent from the server. This is to avoid the client having to do any bookkeeping to keep the current set of subscriptions straight.
{ "subscriptions": [
{ "id": <unique id integer>, "objectPath": <subscribed object>, "propertyPath", <subscribed property> }, ...
]}
Values Changed Notification
When a new subscription is made or when values subsequently change within Designer, the server notifies clients with the following JSON structure:
{ "valuesChanged": [
{ "id": 123, "value": <value>, "changeTimestamp": 257.1089659, "messageTimestamp": 265.5967602 },
{ "id": 456, "value": <value>, "changeTimestamp": 257.1089659, "messageTimestamp": 265.5967602 }
] }
- changeTimestamp: indicates at which time in seconds relative to the start of the session was this property modified.
- messageTimestamp: indicates at which time in seconds relative to the start of the session was this message sent.
Examples
Example 1: Establishing a Connection and Subscribing to a Resource
// Open a live update websocket connection
const socket = new WebSocket('ws://director:80/api/session/liveupdate');
// Once connected, subscribe to a Track resource
socket.onopen = () => {
const subscribeMessage = { subscribe: {
object: "track:track_1",
properties: ["object.lengthInBeats", "object.layers"]
} };
socket.send(JSON.stringify(subscribeMessage));
};
// Listen for incoming updates
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Update received:', data);
};
Example 2: Sending a Set Command to Update a Value
// Assume the subscription id is 123 from a previous subscribe command
const setMessage = {
set: [
{ id: 123, value: 42.0 }
]
};
socket.send(JSON.stringify(setMessage));
Example 3: Unsubscribing from a property.
// Unsubscribe from a single subscription
const unsubscribeMessage = { unsubscribe: { id: 123 } };
socket.send(JSON.stringify(unsubscribeMessage));
Examples: subscribing to graphs
Subscribing to a local graph:
const subscribeMessage = { subscribe: {
object: "subsystem:MonitoringManager.findLocalMonitor(\"fps\")",
properties: ["object.seriesAverage(\"Actual\", 1)"]
} };
socket.send(JSON.stringify(subscribeMessage));
The second argument in the seriesAverage() function is the number of samples to average. By using a value of 1, we end up obtaining the latest value in the series.
Subscribing to a remote graph:
const subscribeMessage = { subscribe: {
object: "subsystem:MonitoringManager.findRemoteMonitor(\"IYPVM:d3\", \"fps\")",
properties: ["object.seriesAverage(\"Actual\", 1)"]
} };
socket.send(JSON.stringify(subscribeMessage));
Examples: subsribing to RenderStream status data
Receive statuses for all machines subscribed to streams within a workload:
const subscribeMessage = { subscribe: {
object: "subsystem:RenderStreamSystem",
properties: ["object.getWorkloadReceiveStatuses(<workload ID>)"]
} };
socket.send(JSON.stringify(subscribeMessage));
How to obtain workload ID:
- Use a web request to
http://127.0.0.1/api/v1/renderstream/workloadsto list all workloads with their IDs - Right-click on the title bar of the ‘Cluster Workload’ widget and select ‘Copy UID’ from the popup menu.
Receive statuses for all machines subscribed to a particular render node (machine):
const subscribeMessage = { subscribe: {
object: "subsystem:RenderStreamSystem",
properties: ["object.getStreamReceiveStatuses(<workload ID>, <machine name>)"]
} };
socket.send(JSON.stringify(subscribeMessage));
How to obtain render node machine name:
- In Disguise:
- Open RenderStream layer and right-click on the Workload field.
- With the workload running, look at the Instances section of the Cluster Workload widget.
- The names of the render node machines are listed in the first column (Machine column)
- In Disguise, alternative:
- Look at the Cluster Pool used in a particular RenderStream layer: machines are listed by name.
- REST API:
- Using the
http://127.0.0.1/api/v1/renderstream/clustersendpoint exposes the machines from the Cluster Pool as well.
- Using the
All workload instances in a workload:
const subscribeMessage = { subscribe: {
object: "subsystem:RenderStreamSystem",
properties: ["object.getWorkloadInstances(<workload ID>)"]
} };
socket.send(JSON.stringify(subscribeMessage));
This gives a list of all workload instances.
Note that the health status for each workload instance is a computed property so it needs to be accessed by subscribing to the health property of a single instance like in the next example.
Health information about a single workload instance:
const subscribeMessage = { subscribe: {
object: "subsystem:RenderStreamSystem",
properties: ["object.getWorkloadInstance(<workload ID>, <instance index>).health()"]
} };
socket.send(JSON.stringify(subscribeMessage));
Failover status of a machine;
const subscribeMessage = { subscribe: {
object: "getByUID(<machine UID>)",
properties: ["object.hasTakenOverFailedMachine()"]
} };
socket.send(JSON.stringify(subscribeMessage));
OR
const subscribeMessage = { subscribe: {
object: "Machine:\"<machine name>\"",
properties: ["object.hasTakenOverFailedMachine()"]
} };
socket.send(JSON.stringify(subscribeMessage));