npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-interop

Exports

| 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-added

Diff 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() sets stateValid = true and updates lastStateName.
  • take() (raw) sets stateValid = false — the mixer has drifted from the named state.
  • lastStateName persists 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 later

Current 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 build

Runs Zod schema generation then TypeScript compilation.

Test

npm test

Publishing

  1. Bump version in package.json
  2. npm run build
  3. npm publish --access public (or --tag alpha)

License

ISC