@stinkycomputing/sesame-interop
v1.6.0
Published
Interop types for Sesame eco-system
Readme
@stinkycomputing/sesame-interop
Shared TypeScript types, config diffing, mixer controller, and Zod schemas for the Sesame ecosystem.
npm install @stinkycomputing/sesame-interopExports
| Export | What it is |
|--------|-----------|
| SesameConfig | Server config types — sources, outputs, compositors, audio mixers |
| MixerConfig | Mixer config — compositions, transitions, mixes, states, source groups |
| MixerControllerApi | Take command types for mixer control (take, take-state, update-source-group) |
| ElmoChannelData | Channel/graphics state for Cachearoo |
| configToCommands | Diffs two ISesameConfig objects, fills a CommandList, returns ConfigDiff |
| MixerController | Stateful mixer — configure, take, takeState, dispose → returns CommandList |
| MixerControllerStateInfo | Snapshot passed to the onStateUpdate callback after every take |
| resolveState | Resolves a named state + transition table into a concrete take command |
| VideoMixer | Video compositor with A/B switching, clip-player, wipes, and transforms |
| AudioMixer | Audio channel fading and clip-player channel coordination via shared registry |
| DynamicChannelRegistry | Shared registry for clip-player audio channels |
| WipeTransition | Wipe effects (alpha matte, overlay, clip-plane) |
All config types also have auto-generated Zod schemas for runtime validation:
import { sesameConfigSchemas } from '@stinkycomputing/sesame-interop';
const result = sesameConfigSchemas.iSesameConfigSchema.safeParse(data);Config Diff
configToCommands(cl, current, next) compares two configs, fills the provided CommandList with the adds/updates/removes needed to reach next, and returns a ConfigDiff with metadata about what changed:
import { configToCommands } from '@stinkycomputing/sesame-interop';
import type { ConfigDiff } from '@stinkycomputing/sesame-interop';
import { CommandList } from '@stinkycomputing/sesame-api-client';
// initial apply — everything is an add
const cl = new CommandList();
configToCommands(cl, undefined, nextConfig);
// subsequent — only changed items emit commands
const cl2 = new CommandList();
const diff = configToCommands(cl2, currentConfig, nextConfig);
// diff.recreatedCompositorIds — compositor IDs that were removed+re-addedDiff rules:
- New item → add
- Changed item → update (sources) or remove + add (compositors, audio mixers, outputs)
- Identical item → skipped
- Missing item → remove
Commands are ordered to avoid dangling references (sources first, then compositors, audio mixers, outputs, then removals).
MixerController
Manages video and audio within a compositor. Stateful but transport-agnostic — it returns CommandList objects; the caller executes them.
import { MixerController } from '@stinkycomputing/sesame-interop';
const mc = new MixerController(id, compositorId, width, height);
// initial setup — takes default composition/mix if configured
const setup = mc.configure(mixerConfig, audioMixes, sources);
await client.execute(setup);
// direct take — switch composition and/or mix
const take = mc.take({
cmd: 'take',
composition: 'camera-2',
mix: 'scene-b',
transition: 'mix',
});
await client.execute(take);
// tear down
await client.execute(mc.dispose());configure can be called again to reconfigure live — it rebuilds transitions and the audio channel set in place.
State tracking & drift detection
MixerController tracks the last named state, drift, and accumulated composition/mix names. Subscribe to onStateUpdate to react to every take:
mc.onStateUpdate = (info) => {
console.log(info.lastStateName); // 'cam' — last take-state name
console.log(info.stateValid); // false after a raw take that drifts
console.log(info.currentComposition); // accumulated composition name
console.log(info.currentMix); // accumulated mix name
console.log(info.cmd); // the resolved take command
};takeState()setsstateValid = trueand updateslastStateName.take()(raw) setsstateValid = false— the mixer has drifted from the named state.lastStateNamepersists across raw takes so consumers can see which state drifted.dispose()resets all state fields.
Read-only getters are also available: mc.lastStateName, mc.stateValid, mc.currentComposition, mc.currentMix.
Named states
Use takeState() to resolve and execute a named state in one step:
const { cl, clipPlayers } = mc.takeState({
cmd: 'take-state',
state: 'cam',
});
await client.execute(cl);takeState resolves the state and best-matching transition rule internally, executes the take, and sets lastStateName/stateValid = true.
void is a reserved implicit state name. It does not need to be present in mixerConfig.states; takeState({ cmd: 'take-state', state: 'void' }) resolves to an empty transparent composition and an empty audio mix. The empty mix fades every channel owned by the controller to level 0, so it is the explicit "nothing" state, distinct from omitting composition or mix to leave the current selection unchanged.
The standalone resolveState function is still exported for advanced use cases where you need the resolved command without executing it:
import { resolveState } from '@stinkycomputing/sesame-interop';
const cmd = resolveState(
'cam', // target state name
mixerConfig.states ?? [], // state definitions
mixerConfig.stateTransitions ?? [], // transition table
mc.lastStateName, // current state (undefined on first take)
);Source groups
The simplest way to update a source group and take in one step is to use the sourceGroups field on take or take-state. The selections are applied before the composition is resolved:
// Switch the 'cam' group to camera 2 and cut to the cam state — one command
const { cl } = mc.takeState({
cmd: 'take-state',
state: 'cam',
sourceGroups: [{ groupName: 'cam', key: '2' }],
});
await client.execute(cl);
// Or with a raw take
await client.execute(mc.take({
cmd: 'take',
composition: 'cam-wide',
sourceGroups: [{ groupName: 'cam', key: '2' }],
}));For a state-only update (no immediate take), use updateSourceGroup instead:
mc.updateSourceGroup({ cmd: 'update-source-group', groupName: 'cam', key: '2' });
// ... take laterCurrent selections survive subsequent takes and reconfigures. A selection is pruned when the group or key no longer exists after a configure call.
Static helpers
MixerController.validateConfig(config)— validates against the Zod schema, returns an array of human-readable error strings (empty if valid)MixerController.getComposition(compositions, sources, name)— resolves a composition by name; handles the"blank"/"void"empty composition keywords and bare source-ID fallback
AudioMixer
AudioMixer is a stateless helper (no constructor side-effects) that translates an IAudioMixerConfig into a flat list of level transitions. It is used internally by MixerController but can also be used standalone.
Channel ownership
An AudioMixer instance owns a subset of the channels on the server-side audio mixer. When take() is called:
- Channels present in the target mix receive the configured level.
- Channels owned by this instance but absent from the mix are explicitly muted (level → 0).
- Channels not owned by this instance are left untouched.
Passing the reserved mix name "void" behaves like an empty mix and silences every owned channel.
Instant cuts vs. fades
| timeMs | How it is sent | Why |
|----------|----------------|-----|
| 0 | propertySet with floatValue | Hard cuts should be sent as an immediate set. The server clamps zero-duration propertyAnimate requests to at least 1 ms, so they behave like a minimal fade rather than a true instant cut. |
| > 0 | propertyAnimate with ANIM_MODE_STATE_FROM | Fades from the current live level to the target. |
Pass defaultTransitionDuration: 0 (or omit it) in the config when you want hard cuts, so the controller emits propertySet. Avoid using propertyAnimate with a duration of 0 for cuts, because the server will clamp it to a non-zero duration.
Channel IDs with /
Channel IDs may contain / characters (e.g. "snigel1-pgm1/audio_mixer_0"). Plugin parameters use :: as the delimiter between channel ID and plugin ID (e.g. address = "snigel1-pgm1/audio_mixer_0::vst_plugin_0"), so / in a channel ID is never ambiguous.
Dynamic channels
DynamicChannelRegistry tracks clip-player audio channels whose mixer channel IDs are not known at config time. When a dynamic channel is resolved the registry maps logical channel names to actual mixer channel IDs, and getChannelTransitions expands them automatically into the transition list.
Build
npm run buildRuns Zod schema generation then TypeScript compilation.
Test
npm testPublishing
- Bump version in
package.json npm run buildnpm publish --access public(or--tag alpha)
License
ISC
