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 smoothlyAddPropertyAnimator(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 systemsHttpGetRequestAsync and CallVindralApiAsync issue async HTTP calls; responses arrive in OnComposerMessage so 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. OnRenderFrame is invoked once per rendered frame and shares its budget with rendering — keep it short.
  • Module mode: If the main script contains a top-level import or export, 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 EnableScriptEngine in Composer settings. API-driven script execution (/api/scriptengine/execute) additionally requires EnableScriptEngineApiCalls.

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/execute or ScriptOperator.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 all AllowClr types (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. Prefer import instead.
  • Encrypted scripts. .cxjs files 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 global Logger directly.

Allowed CLR namespaces

importNamespace('...') works for the following assemblies, which are pre-allowed:

  • VindralEngine and VindralEngine.Compositing.*
  • VindralEngineBaseTypes and its sub-namespaces (e.g. VindralEngineBaseTypes.DataTypes.Enums)
  • VindralLogLibrary
  • The assemblies of every input/operator/target type, plus Layer, LayerTransform, AnimatorEngine
  • System (via Uri)

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 LastWriteTime changes — 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 in ScriptLogger.LogCache (last 200 entries, exposed via GetLogEntries).
  • 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-click Reload / Edit / Start / Stop controls.
  • Common error: timeout. A frame that exceeds the per-invocation timeout aborts with a TimeoutException. Move expensive work out of OnRenderFrame — use AddTimedCallback, 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 objectName in AddPropertyAnimator. 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:

  1. The script engine isn't in Error state — /api/lasterror/get or the Desktop log panel.
  2. The connector exists — /api/connector/list.
  3. (Module mode) The OnConnector{Name} function is exported by the main script, not just defined in a library.
  4. The function name matches the connector name exactly (case sensitive in OnConnector lookup).

  • 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.