Script Engine
The Script Engine is a JavaScript runtime embedded in every Composer project. A .js file referenced by the project runs alongside the rendering pipeline — OnRenderFrame is called once per rendered frame, with full access to every input, scene, layer, operator, target, and connector in the project. Anything you can do from the property panel — set a property, run a command, animate a value, switch a scene, start or stop a target — you can do from a script as well, on cue.
Because the script runs every frame and reaches the entire project tree, it's the most powerful customisation surface Composer exposes. A few concrete examples of what scripts in production projects do:
- Cue and choreograph — at frame N (or after a real-time delay), trigger a switcher cut, start a video clip, fade to black, change a chroma-key colour, fire an overlay.
- Animate properties smoothly —
AddPropertyAnimator(layer, "PositionX", 0, 400, 500ms, EaseInOut)tweens any numeric property over time with linear or eased timing, abortable mid-run. - React to the world — define
OnConnectorBreakingNews(params)and any HTTP caller (a control surface, a producer's button, an external scheduler) can invoke that script function with parameters to drive arbitrary in-project changes. - Talk to other systems —
HttpGetRequestAsyncandCallVindralApiAsyncissue async HTTP calls; responses arrive inOnComposerMessageso the script can post telemetry, fetch data, or chain into the Vindral CDN APIs without blocking the render loop. - Event-driven custom logic — "if scene X is live and audio bus 2 has been silent for 5 seconds, switch to backup", "every 30 seconds, post the current programme name and runtime to a control system", "when this connector fires, animate three layers and start a target".
- Encapsulate complex shows — wrap a show's entire run-of-show into one script: timed cues, conditional fallbacks, inter-system orchestration, all in one place that travels with the project file.
How it works at a glance
The engine runs on Composer's video worker thread, so the per-frame budget your script consumes shares the frame budget with rendering — keep OnRenderFrame lean. Long-running work (HTTP calls, file I/O) goes through async helpers; expensive periodic work goes through AddTimedCallback. The engine ships sensible per-script resource limits (memory, statement count, per-invocation timeout) so a runaway script can't take the project down.
Scripts can be authored in two styles: legacy globals (functions on the global object — every existing Composer script works this way) or ES6 modules (import / export, multi-file projects). Composer detects the mode from the file's contents — no flag, no setting. See Script structure: legacy globals vs. ES6 modules.
For iterating on a script, open the dedicated Script Debug Window — engine status, per-frame execution time, the live Logger.* stream filtered by severity, and one-click Reload / Edit / Start / Stop. For protecting a finished script before shipping it to a customer host, export it as an encrypted .cxjs blob (see Encrypted scripts and File → Encryption in Composer Desktop).
The rest of this manual is the reference: lifecycle entry points, host globals, the helper-function catalogues (Scene, Layer, Input, Target, Connector, Animation, Http, WebSocket, TimedCallback, Settings, Logging, AudioDiagnostics, ImageChecksum, AutoTest, Desktop, Ffmpeg, Misc), and a selection of FAQ snippets covering the patterns that come up most often.
General Information
- Engine: Jint (JavaScript ES2020 + ES6 modules).
- Script file: A
.js(or encrypted.cxjs) file referenced by the Composer project. The script is reloaded automatically when its file changes on disk (only when the project is running). - Execution context: The engine runs on Composer's video worker thread.
OnRenderFrameis invoked once per rendered frame and shares its budget with rendering — keep it short. - Module mode: If the main script contains a top-level
importorexport, it is loaded as an ES6 module; otherwise it runs as a classic script. See Script structure: legacy globals vs. ES6 modules. - Toggle: Controlled by
EnableScriptEnginein Composer settings. API-driven script execution (/api/scriptengine/execute) additionally requiresEnableScriptEngineApiCalls.
Resource limits
The engine is configured with the following per-script limits. These bound runaway scripts but are generous enough to support real workloads:
| Limit | Value |
|---|---|
| Max memory | 500 MB |
| Max statements per invocation | 100 000 |
| Per-invocation timeout | 3 000 ms |
A script that exceeds any of these is terminated with a logged error and ScriptEngineStatus becomes Error.
Older documentation referenced 4 MB / 10 000 / 400 ms; those values are out of date. The current limits are the ones above.
Script Lifecycle
A project script defines up to three top-level entry points. All are optional — the engine silently skips any that aren't present.
| Entry point | When it fires |
|---|---|
OnProjectInit() |
Once, when the project is loaded and the script is first parsed. Use it to seed state and configure inputs/layers. |
OnRenderFrame() |
Once per rendered frame, while playback is Running. Keep it small — heavy work here drops frames. |
OnProjectStop() |
Once, when the project unloads. Use it to release external resources you allocated in OnProjectInit. |
In addition, scripts can define any number of named callback handlers that the host invokes by name:
| Callback | Triggered by |
|---|---|
OnConnector{Name}(query) |
/api/connector/trigger?name={Name} (see Connectors and the response contract). |
OnComposerMessage(json) |
Asynchronous response from HttpGetRequestAsync, CallVindralApiAsync, etc. The argument is a JSON-stringified HttpResponse. |
| Any function name | Direct invocation via /api/scriptengine/execute?function={Name}, AddTimedCallback, button OnClick, or operator script execution. |
Lifecycle entry points and named callbacks must be defined on the global object in legacy mode and exported by the main script in module mode (see below).
Required setup boilerplate
Every project script starts with the same import/alias block. Paste it as-is:
//Import namespaces
var VindralEngine = importNamespace('VindralEngine');
var Scene = importNamespace('VindralEngine.Compositing.Scene');
var Animator = VindralEngine.Animator;
var Enums = importNamespace('VindralEngineBaseTypes.DataTypes.Enums');
//Aliases for readability
var Project = VindralEngine.Project.RunningInstance;
//Logger is provided by the host — no import needed (see Host globals).
Project is the running VindralEngine.Project instance. Every helper documented below is a method on Project.
Hello world
var VindralEngine = importNamespace('VindralEngine');
var Project = VindralEngine.Project.RunningInstance;
let frameCount = 0;
function OnProjectInit() {
Logger.Info('Project initialized');
}
function OnRenderFrame() {
frameCount++;
if (frameCount % 300 == 0)
Logger.Debug('Frames: ' + frameCount);
}
function OnProjectStop() {
Logger.Info('Project stopped');
}
Script structure: legacy globals vs. ES6 modules
Composer detects the mode from the file's contents — there is no flag or setting to toggle.
Legacy mode (default)
If the main script has no top-level import or export statements, it is evaluated as a classic script: top-level function declarations land on the global object, and host code looks them up by name. This is how every existing Composer script works, and nothing in those scripts needs to change.
// Legacy: functions on the global object
var Project = importNamespace('VindralEngine').Project.RunningInstance;
function OnProjectInit() { Logger.Info("ready"); }
function OnConnectorPing(p) { return "pong:" + p; }
The runtime helper LoadScript("file.js") is still available and inlines the named file (resolved relative to the main script directory) into the same global scope.
Module mode (ES6 imports)
If the main script contains any top-level import or export, it is loaded as an ES6 module. Module-mode scripts can split code across multiple .js files.
// main.js (module mode — has `import`/`export`)
import { greet } from "./lib.js";
export { OnConnectorPing } from "./lib.js"; // re-export from a library
export function OnProjectInit() { Logger.Info("ready"); }
export function OnConnectorGreet(p) { return greet(p); }
// lib.js
export function greet(name) { return "hello " + name; }
export function OnConnectorPing(p) { return "pong:" + p; }
Path resolution
Specifiers are resolved against the directory of the importing file:
| Specifier form | Behavior |
|---|---|
"./foo.js", "../util/bar.js" |
Relative to the importing file. Recommended. |
"/abs/path.js", "C:\\abs\\path.js" |
Absolute path. Resolved as-is. |
"some-package" |
Bare specifier. Not supported — Composer has no module resolver, so the import fails. |
The base directory passed to Jint is the main script's directory. Imports that resolve outside that directory work but are flagged by the missing-files surfaces (Desktop reallocate dialog, /api/project/assets) so you can see at a glance what the project ships with.
Public-surface rule
Entry points the host needs to invoke must be exported by the main script:
OnProjectInit,OnProjectStop,OnRenderFrame- Every
OnConnector{Name} - Every function called via
/api/scriptengine/executeorScriptOperator.MethodName
To host the implementation in a library, use a re-export:
// main.js
export { OnConnectorPing } from "./lib.js"; // OnConnectorPing now visible to host
A bare import "./lib.js"; (side-effect only) loads the library, but its exports stay invisible to the host — connectors and API calls targeting those names will silently miss.
Other rules
- Host objects stay global.
log,DebugLog,jsHelper,Logger,LoadScript, and allAllowClrtypes (e.g.importNamespace('VindralEngine')) remain on the global object — modules can use them without importing anything. LoadScript()in module mode. Still works, but logs a warning when called: anything it inlines goes to the global scope and is invisible from inside a module. Preferimportinstead.- Encrypted scripts.
.cxjsfiles are decrypted in-memory before evaluation. Module mode applies to encrypted scripts the same way it applies to plain.js.
Host globals
The following are pre-registered on the engine's global object and available without an import:
| Name | Type | Description |
|---|---|---|
log(message) |
function | Writes a message to stdout. Equivalent to Console.WriteLine. |
DebugLog(message) |
function | Writes a message to the .NET debug output (visible in Visual Studio Output → Debug). |
Logger |
object | The ScriptLogger class. Use Logger.Debug(...), Logger.Info(...), Logger.Warning(...), Logger.Error(...). Errors are surfaced in the Desktop log panel and the /api/lasterror/get endpoint. |
jsHelper |
object | A scratchpad with SetStoredValue(key, value) and GetStoredValue(key). Useful for stashing state across renders without polluting the global object. |
LoadScript(filename) |
function | Inlines filename (resolved against the main script directory) into the global scope. Throws if the file is missing. Discouraged in module mode — use import instead. |
A historical line
var Logger=VindralLogLibrary.Logger;is removed from script source on load. New code should use the globalLoggerdirectly.
Allowed CLR namespaces
importNamespace('...') works for the following assemblies, which are pre-allowed:
VindralEngineandVindralEngine.Compositing.*VindralEngineBaseTypesand its sub-namespaces (e.g.VindralEngineBaseTypes.DataTypes.Enums)VindralLogLibrary- The assemblies of every input/operator/target type, plus
Layer,LayerTransform,AnimatorEngine System(viaUri)
Enums quick reference
| Enum | Values |
|---|---|
AnimationEasing (Enums.AnimationEasing) |
None (linear), EaseInOut |
RunningAnimationBehavior (Enums.RunningAnimationBehavior) |
CancelNewAnimation, AbortRunningAnimations (default), AllowMultipleAnimations |
AnimateTransform (Animator.AnimateTransform) |
PositionX, PositionY, ScaleX, ScaleY, AnchorPointX, AnchorPointY, Rotation, Opacity |
AnimateEasing (Animator.AnimateEasing) |
Linear, EaseInEaseOut |
AnimateOptions (Animator.AnimateOptions) |
None, UseCurrentValueAsInitialValue |
ScheduledMediaActionType |
Start, Stop (used internally by ScheduleMediaPlayBack/Stop) |
ScriptEngineStatus |
Disabled, Error, Running, Updated, Stopped, NotInitialized |
Errors and debugging
When the script engine encounters an error it sets ScriptEngineStatus to Error, logs the location and message, and stops invoking entry points until the script is fixed and reloaded.
- File watcher. Composer reloads the script automatically when its file's
LastWriteTimechanges — no need to restart playback. - Where to look. Errors land in the Composer log (visible in Desktop's log panel and at
/api/lasterror/get) and inScriptLogger.LogCache(last 200 entries, exposed viaGetLogEntries). - Live debug window. While iterating on a script, open the dedicated Script Debug Window (Application menu → Script Engine → Open Script Debug Window…) for engine status, the per-frame execution time, the live
Log.*stream filtered by severity, and one-clickReload/Edit/Start/Stopcontrols. - Common error: timeout. A frame that exceeds the per-invocation timeout aborts with a
TimeoutException. Move expensive work out ofOnRenderFrame— useAddTimedCallback,HttpGetRequestAsync, or guard heavy logic with a frame counter. - Common error: missing entry point in module mode. A connector that "doesn't fire" in module mode is almost always a missing
export. Re-export the function from the main script (export { OnConnectorPing } from "./lib.js";). - Common error: ambiguous
objectNameinAddPropertyAnimator. If two named models share a name, the call fails and logs an error. Rename one.
FAQ
How do I start a video file from script?
Project.StartMediaPlayBack("LogoAnimation.mov");
// or, with the underlying command if you need the CanExecute check:
var input = Project.GetMovieInputByName("LogoAnimation.mov");
if (input != null && input.PlayCommand.CanExecute)
input.PlayCommand.Execute();
How do I trigger a connector?
Project.TriggerConnectorByName("MyTrigger");
How do I start a target (e.g. SRT output)?
Project.ExecuteTargetCommand("Scene", "SRT Output", "StartCommand");
How do I animate a property?
var Enums = importNamespace('VindralEngineBaseTypes.DataTypes.Enums');
Project.AddPropertyAnimator("MyInput", "StereoGainDb", -80, 0, 3000, Enums.AnimationEasing.None);
How do I receive HTTP responses?
Project.HttpGetRequestAsync(endpoint, messageId);
function OnComposerMessage(json) {
var msg = JSON.parse(json);
Logger.Info("HTTP " + msg.ResponseCode + " for " + msg.MessageId + ": " + msg.ResponseText);
}
Can I split my script across multiple files?
Yes — see Module mode. Add import/export to the main script and import library files relatively ("./lib.js"). Remember the public-surface rule: anything the host invokes (entry points, OnConnector{Name}, anything called via /api/scriptengine/execute) must be exported by the main script, either directly or via re-export.
My connector isn't firing.
Check, in order:
- The script engine isn't in
Errorstate —/api/lasterror/getor the Desktop log panel. - The connector exists —
/api/connector/list. - (Module mode) The
OnConnector{Name}function is exported by the main script, not just defined in a library. - The function name matches the connector name exactly (case sensitive in
OnConnectorlookup).
Related documentation
- HTTP API Reference — endpoints that drive the script engine externally (
/api/scriptengine/execute,/api/connector/trigger). - Runtime Lifecycle API — start/stop/load endpoints when running headless.