@stinkycomputing/sesame-snigel-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@stinkycomputing/sesame-snigel-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Snigel replay plugin for the Sesame config editor",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "scripts": {
8
+ "build": "tsc"
9
+ },
10
+ "dependencies": {
11
+ "@stinkycomputing/sesame-editor-api": "1.0.0-alpha.5",
12
+ "@xyflow/react": "^12.10.1"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "^5.5.4"
16
+ }
17
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Snigel plugin – serializeNodes() implementation.
3
+ *
4
+ * Serializes snigel/control nodes (and their connected snigel/output and
5
+ * snigel/super-source children) into the extensions.snigel array.
6
+ */
7
+
8
+ import type { MinimalNode, MinimalEdge } from '@stinkycomputing/sesame-editor-api';
9
+ import {
10
+ getData,
11
+ nodeProps,
12
+ nodeMeta,
13
+ findSourceConnection,
14
+ getNodesByCategory,
15
+ } from '@stinkycomputing/sesame-editor-api';
16
+
17
+ export function serializeSnigelNodes<TNode extends MinimalNode, TEdge extends MinimalEdge>(
18
+ allNodes: TNode[],
19
+ allEdges: TEdge[],
20
+ ): Record<string, unknown> {
21
+ const snigels: unknown[] = [];
22
+
23
+ getNodesByCategory(allNodes, 'snigel').forEach(snigelNode => {
24
+ const serialized = serializeSnigel(snigelNode, allEdges, allNodes);
25
+ if (serialized) snigels.push({ ...serialized, _meta: nodeMeta(snigelNode) });
26
+ });
27
+
28
+ if (snigels.length === 0) return {};
29
+ return { snigel: snigels };
30
+ }
31
+
32
+ function serializeSnigel<TNode extends MinimalNode, TEdge extends MinimalEdge>(
33
+ node: TNode,
34
+ edges: TEdge[],
35
+ nodes: TNode[],
36
+ ): Record<string, unknown> | undefined {
37
+ const p = nodeProps(node);
38
+ const d = getData(node);
39
+ const inputs = d.dynamicInputs ?? d.def.inputs;
40
+ const nodeOutputs = d.dynamicOutputs ?? d.def.outputs;
41
+
42
+ // Collect sources
43
+ const snSources: unknown[] = [];
44
+ inputs.forEach((port, i) => {
45
+ if (port.type !== 'source/device') return;
46
+ const conn = findSourceConnection(edges, nodes, node.id, `in-${i}`);
47
+ if (!conn) return;
48
+ const connData = getData(conn.node);
49
+ if (connData.def.category === 'snigel-super') {
50
+ snSources.push(serializeSnigelSuperSource(conn.node, edges, nodes));
51
+ } else {
52
+ snSources.push(conn.connectionId);
53
+ }
54
+ });
55
+
56
+ // Collect outputs (snigel/output nodes)
57
+ const snOutputs: unknown[] = [];
58
+ nodeOutputs.forEach((_port, i) => {
59
+ const outEdge = edges.find(e => e.source === node.id && e.sourceHandle === `out-${i}`);
60
+ if (!outEdge) return;
61
+ const outNode = nodes.find(n => n.id === outEdge.target);
62
+ if (!outNode) return;
63
+ const op = nodeProps(outNode);
64
+ snOutputs.push({ id: op.id, type: op.type, video: {}, _meta: nodeMeta(outNode) });
65
+ });
66
+
67
+ const codecMap: Record<string, string> = {
68
+ h264: 'low_latency',
69
+ hevc: 'low_latency_hevc',
70
+ av1: 'low_latency_av1',
71
+ };
72
+
73
+ return {
74
+ id: p.id,
75
+ controllerPort: p.controllerPort,
76
+ sizeGb: p.sizeGb,
77
+ videoFilePath: p.videoFilePath,
78
+ ingestFolder: p.ingestFolder,
79
+ egressFolder: p.egressFolder,
80
+ video: { encoder: { bitrateKbs: p.bitrate, preset: codecMap[String(p.codec)] || 'low_latency_hevc' } },
81
+ sources: snSources,
82
+ outputs: snOutputs,
83
+ };
84
+ }
85
+
86
+ function serializeSnigelSuperSource<TNode extends MinimalNode, TEdge extends MinimalEdge>(
87
+ node: TNode,
88
+ edges: TEdge[],
89
+ nodes: TNode[],
90
+ ): Record<string, unknown> {
91
+ const p = nodeProps(node);
92
+ const d = getData(node);
93
+ const inputs = d.dynamicInputs ?? d.def.inputs;
94
+ const srcs: string[] = [];
95
+
96
+ inputs.forEach((port, i) => {
97
+ if (port.type !== 'source/device') return;
98
+ const conn = findSourceConnection(edges, nodes, node.id, `in-${i}`);
99
+ if (conn) srcs.push(String(getData(conn.node).properties.id));
100
+ });
101
+
102
+ return { id: p.id, type: 'super', sources: srcs, _meta: nodeMeta(node) };
103
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Snigel plugin – buildNodes() implementation.
3
+ *
4
+ * Creates snigel/control, snigel/output, and snigel/super-source nodes from
5
+ * config, wires internal edges, and returns snigel-output nodes as
6
+ * supplementalNodes so the core graph-builder can wire them to outputs/RTT.
7
+ */
8
+
9
+ import type { Node, Edge } from '@xyflow/react';
10
+ import type { NodeFactory, CoreNodeMaps, PluginBuildResult, ISesameConfig } from '@stinkycomputing/sesame-editor-api';
11
+ import type { ISnigel, ISuperSlowSnigelSource } from './types';
12
+ import { snigelControlDef } from './node-defs';
13
+
14
+ function presetToCodec(preset: string | undefined): string {
15
+ if (!preset) return 'hevc';
16
+ if (preset.includes('hevc')) return 'hevc';
17
+ if (preset.includes('av1')) return 'av1';
18
+ return 'h264';
19
+ }
20
+
21
+ export function buildSnigelNodes(
22
+ extensions: Record<string, unknown>,
23
+ config: ISesameConfig,
24
+ factory: NodeFactory<Node, Edge>,
25
+ coreNodes: CoreNodeMaps<Node>,
26
+ ): PluginBuildResult<Node, Edge> {
27
+ const nodes: Node[] = [];
28
+ const edges: Edge[] = [];
29
+ const supplementalNodes = new Map<string, Node>();
30
+
31
+ // Support both new format (extensions.snigel) and legacy (config.snigels)
32
+ const snigels = (
33
+ (extensions['snigel'] as ISnigel[] | undefined) ??
34
+ ((config as unknown as Record<string, unknown>)['snigels'] as ISnigel[] | undefined) ??
35
+ []
36
+ );
37
+
38
+ const { sourceNodes } = coreNodes;
39
+ const snigelNodes = new Map<string, Node>();
40
+ const snigelSuperNodes = new Map<string, Node>();
41
+
42
+ let x = factory.X_OFFSET * 6; // position after core columns
43
+
44
+ snigels.forEach((snigel, idx) => {
45
+ const snigelOutputTemplate = snigelControlDef.outputs[0];
46
+
47
+ const dynamicInputs = snigel.sources.map((_, i) => ({
48
+ label: `Source ${i + 1}`, type: 'source/device' as const,
49
+ }));
50
+ dynamicInputs.push({ label: `Source ${dynamicInputs.length + 1}`, type: 'source/device' });
51
+
52
+ const snigelOutputsList = snigel.outputs?.length
53
+ ? snigel.outputs
54
+ : [{ id: `${snigel.id}-out1`, video: {}, type: 'pvw' as const }];
55
+
56
+ const dynamicOutputs = snigelOutputsList.map((_, i) => ({
57
+ label: `Output ${i + 1}`,
58
+ type: 'source/snigel',
59
+ maxConnections: snigelOutputTemplate?.maxConnections,
60
+ }));
61
+ dynamicOutputs.push({
62
+ label: `Output ${dynamicOutputs.length + 1}`,
63
+ type: 'source/snigel',
64
+ maxConnections: snigelOutputTemplate?.maxConnections,
65
+ });
66
+
67
+ const snigelNode = factory.makeNode(
68
+ snigel.id,
69
+ 'snigel/control',
70
+ factory.pos(snigel, x, idx),
71
+ {
72
+ id: snigel.id,
73
+ controllerPort: snigel.controllerPort,
74
+ sizeGb: snigel.sizeGb,
75
+ bitrate: snigel.video?.encoder?.bitrateKbs ?? 50000,
76
+ codec: presetToCodec(snigel.video?.encoder?.preset),
77
+ videoFilePath: snigel.videoFilePath,
78
+ ingestFolder: snigel.ingestFolder ?? '',
79
+ egressFolder: snigel.egressFolder ?? '',
80
+ },
81
+ factory.collapsed(snigel),
82
+ dynamicInputs,
83
+ dynamicOutputs,
84
+ );
85
+ nodes.push(snigelNode);
86
+ snigelNodes.set(snigel.id, snigelNode);
87
+
88
+ // Snigel outputs
89
+ snigelOutputsList.forEach((sOut, oIdx) => {
90
+ const outNode = factory.makeNode(
91
+ sOut.id,
92
+ 'snigel/output',
93
+ factory.pos(sOut, x + factory.X_OFFSET, idx + oIdx),
94
+ { id: sOut.id, type: sOut.type },
95
+ factory.collapsed(sOut),
96
+ );
97
+ nodes.push(outNode);
98
+ supplementalNodes.set(sOut.id, outNode);
99
+
100
+ // snigel/control → snigel/output
101
+ edges.push(factory.edge(snigelNode.id, `out-${oIdx}`, outNode.id, 'in-0'));
102
+ });
103
+
104
+ // Snigel super-sources
105
+ snigel.sources
106
+ .filter((s): s is ISuperSlowSnigelSource => typeof s === 'object' && (s as ISuperSlowSnigelSource).type === 'super')
107
+ .forEach(ss => {
108
+ const dynamicSSInputs = ss.sources.map((_, i) => ({
109
+ label: `Source ${i + 1}`, type: 'source/device' as const,
110
+ }));
111
+ dynamicSSInputs.push({ label: `Source ${dynamicSSInputs.length + 1}`, type: 'source/device' });
112
+
113
+ const ssNode = factory.makeNode(
114
+ ss.id,
115
+ 'snigel/super-source',
116
+ factory.pos(ss, x - factory.X_OFFSET, idx),
117
+ { id: ss.id },
118
+ factory.collapsed(ss),
119
+ dynamicSSInputs,
120
+ );
121
+ nodes.push(ssNode);
122
+ snigelSuperNodes.set(ss.id, ssNode);
123
+ });
124
+ });
125
+
126
+ // ── Snigel ← sources edges ────────────────────────────────────────────
127
+ // Build these after all nodes exist so findOutputSlot can search them.
128
+ const allNodes = [...nodes]; // plugin nodes only; coreNodes passed via sourceNodes map
129
+
130
+ snigels.forEach(snigel => {
131
+ const snigelNode = snigelNodes.get(snigel.id);
132
+ if (!snigelNode) return;
133
+
134
+ snigel.sources.forEach((src, srcIdx) => {
135
+ if (typeof src === 'string') {
136
+ const slashIdx = src.indexOf('/');
137
+ const configId = slashIdx >= 0 ? src.slice(0, slashIdx) : src;
138
+ const outputName = slashIdx >= 0 ? src.slice(slashIdx) : undefined;
139
+ const srcNode = sourceNodes.get(configId);
140
+ if (srcNode) {
141
+ // We need to search core + plugin nodes for the slot
142
+ const combinedNodes = [...(Array.from(sourceNodes.values())), ...allNodes];
143
+ const srcSlot = factory.findOutputSlot(combinedNodes, srcNode.id, 'source/device', outputName);
144
+ if (srcSlot) edges.push(factory.edge(srcNode.id, srcSlot, snigelNode.id, `in-${srcIdx}`));
145
+ }
146
+ } else if (src.type === 'super') {
147
+ const ss = src as ISuperSlowSnigelSource;
148
+ const ssNode = snigelSuperNodes.get(ss.id);
149
+ if (ssNode) {
150
+ const combinedNodes = [...(Array.from(sourceNodes.values())), ...allNodes];
151
+ const srcSlot = factory.findOutputSlot(combinedNodes, ssNode.id, 'source/device');
152
+ if (srcSlot) edges.push(factory.edge(ssNode.id, srcSlot, snigelNode.id, `in-${srcIdx}`));
153
+
154
+ // Super-source ← core sources
155
+ ss.sources.forEach((innerSrc, innerIdx) => {
156
+ const slashIdx = innerSrc.indexOf('/');
157
+ const configId = slashIdx >= 0 ? innerSrc.slice(0, slashIdx) : innerSrc;
158
+ const outputName = slashIdx >= 0 ? innerSrc.slice(slashIdx) : undefined;
159
+ const innerNode = sourceNodes.get(configId);
160
+ if (innerNode) {
161
+ const slot = factory.findOutputSlot(combinedNodes, innerNode.id, 'source/device', outputName);
162
+ if (slot) edges.push(factory.edge(innerNode.id, slot, ssNode.id, `in-${innerIdx}`));
163
+ }
164
+ });
165
+ }
166
+ }
167
+ });
168
+ });
169
+
170
+ return { nodes, edges, supplementalNodes };
171
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Snigel config editor plugin.
3
+ *
4
+ * Register this with the Sesame config editor at startup:
5
+ *
6
+ * import { SnigelPlugin } from '@stinkycomputing/sesame-snigel-plugin';
7
+ * import { registerPlugin } from './plugins';
8
+ * registerPlugin(SnigelPlugin);
9
+ */
10
+
11
+ import type { Node, Edge } from '@xyflow/react';
12
+ import type { ISesameEditorPlugin } from '@stinkycomputing/sesame-editor-api';
13
+ import { snigelControlDef, snigelOutputDef, snigelSuperSourceDef } from './node-defs';
14
+ import { buildSnigelNodes } from './graph-builder';
15
+ import { serializeSnigelNodes } from './config-serializer';
16
+
17
+ export const SnigelPlugin: ISesameEditorPlugin<Node, Edge> = {
18
+ id: 'com.stinkycomputing.snigel',
19
+
20
+ nodeTypes: [snigelControlDef, snigelOutputDef, snigelSuperSourceDef],
21
+
22
+ buildNodes: buildSnigelNodes,
23
+
24
+ serializeNodes: serializeSnigelNodes,
25
+ };
26
+
27
+ export * from './types';
@@ -0,0 +1,94 @@
1
+ /** Node type definitions for all three Snigel node types. */
2
+
3
+ import type { NodeTypeDef, MetadataDef } from '@stinkycomputing/sesame-editor-api';
4
+
5
+ export const snigelStateMetadata: MetadataDef = {
6
+ id: 'snigel.jlc.state.v1',
7
+ label: 'Remote Control State',
8
+ direction: 'emit',
9
+ portType: 'metadata/snigel-state',
10
+ format: 'binary',
11
+ };
12
+
13
+ export const snigelCommandsMetadata: MetadataDef = {
14
+ id: 'snigel.jlc.commands.v1',
15
+ label: 'Remote Control Commands',
16
+ direction: 'receive',
17
+ portType: 'metadata/snigel-commands',
18
+ format: 'binary',
19
+ };
20
+
21
+ export const snigelColor = '#784212';
22
+
23
+ export const snigelControlDef: NodeTypeDef = {
24
+ type: 'snigel/control',
25
+ title: 'Snigel Replay',
26
+ category: 'snigel',
27
+ menuGroup: 'Snigel',
28
+ menuOrder: 7,
29
+ menuItemOrder: -1,
30
+ color: snigelColor,
31
+ inputs: [
32
+ { label: 'Source 1', type: 'source/device' },
33
+ ],
34
+ outputs: [
35
+ { label: 'Output 1', type: 'source/snigel', maxConnections: 1 },
36
+ ],
37
+ widgets: [
38
+ { type: 'number', label: 'Size (GB)', property: 'sizeGb', options: { min: 0, max: 1000, step: 1, precision: 0 } },
39
+ { type: 'text', label: 'Port', property: 'controllerPort' },
40
+ { type: 'text', label: 'Video folder', property: 'videoFilePath' },
41
+ { type: 'text', label: 'Ingest folder', property: 'ingestFolder' },
42
+ { type: 'text', label: 'Egress folder', property: 'egressFolder' },
43
+ { type: 'combo', label: 'Codec', property: 'codec', options: { values: ['h264', 'hevc', 'av1'] } },
44
+ { type: 'number', label: 'Bitrate', property: 'bitrate', options: { min: 0, max: 10000000, step: 1000, precision: 0 } },
45
+ ],
46
+ defaultProperties: { id: '', controllerPort: 'COM1', videoFilePath: '', ingestFolder: '', egressFolder: '', sizeGb: 1, bitrate: 50000, codec: 'hevc' },
47
+ dynamicPorts: true,
48
+ metadata: [snigelStateMetadata, snigelCommandsMetadata],
49
+ };
50
+
51
+ export const snigelOutputDef: NodeTypeDef = {
52
+ type: 'snigel/output',
53
+ title: 'Snigel Output',
54
+ category: 'snigel-output',
55
+ menuGroup: 'Snigel',
56
+ menuOrder: 7,
57
+ color: snigelColor,
58
+ inputs: [
59
+ { label: 'Source', type: 'source/snigel' },
60
+ ],
61
+ outputs: [
62
+ { label: 'Video', type: 'video/composition', name: '/compositor' },
63
+ { label: 'Audio 1+2', type: 'audio/stereo', name: '/audio_mixer_0' },
64
+ { label: 'Audio 3+4', type: 'audio/stereo', name: '/audio_mixer_1' },
65
+ { label: 'Audio 5+6', type: 'audio/stereo', name: '/audio_mixer_2' },
66
+ { label: 'Audio 7+8', type: 'audio/stereo', name: '/audio_mixer_3' },
67
+ { label: 'Audio Bus 1+2', type: 'audio/raw', name: '/audio_mixer_0' },
68
+ { label: 'Audio Bus 3+4', type: 'audio/raw', name: '/audio_mixer_1' },
69
+ { label: 'Audio Bus 5+6', type: 'audio/raw', name: '/audio_mixer_2' },
70
+ { label: 'Audio Bus 7+8', type: 'audio/raw', name: '/audio_mixer_3' },
71
+ ],
72
+ widgets: [
73
+ { type: 'combo', label: 'Type', property: 'type', options: { values: ['pvw', 'pgm'] } },
74
+ ],
75
+ defaultProperties: { id: '', type: 'pgm' },
76
+ };
77
+
78
+ export const snigelSuperSourceDef: NodeTypeDef = {
79
+ type: 'snigel/super-source',
80
+ title: 'Snigel Super Slow',
81
+ category: 'snigel-super',
82
+ menuGroup: 'Snigel',
83
+ menuOrder: 7,
84
+ color: snigelColor,
85
+ inputs: [
86
+ { label: 'Source 1', type: 'source/device' },
87
+ ],
88
+ outputs: [
89
+ { label: 'Source', type: 'source/device' },
90
+ ],
91
+ widgets: [],
92
+ defaultProperties: { id: '' },
93
+ dynamicPorts: true,
94
+ };
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ /** Snigel-specific config types (formerly in sesame-interop). */
2
+
3
+ export interface ISuperSlowSnigelSource {
4
+ id: string;
5
+ type: 'super';
6
+ sources: string[];
7
+ }
8
+
9
+ export interface ISnigel {
10
+ id: string;
11
+ sources: (string | ISuperSlowSnigelSource)[];
12
+ sizeGb: number;
13
+ videoFilePath: string;
14
+ ingestFolder: string;
15
+ egressFolder: string;
16
+ video: {
17
+ encoder?: {
18
+ preset?: string;
19
+ bitrateKbs?: number;
20
+ };
21
+ };
22
+ controllerPort: string;
23
+ outputs: ISnigelOutput[];
24
+ }
25
+
26
+ export type SnigelOutputType = 'pgm' | 'pvw';
27
+
28
+ export interface ISnigelOutput {
29
+ id: string;
30
+ type: SnigelOutputType;
31
+ video: Record<string, unknown>;
32
+ }