@remavideo/sdk 0.9.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/dist/index.js ADDED
@@ -0,0 +1,1309 @@
1
+ // src/input/caption-source.ts
2
+ import { readFile } from "fs/promises";
3
+
4
+ // src/node.ts
5
+ var NODE_STATES = /* @__PURE__ */ new Set([
6
+ "READY",
7
+ "ACTIVE",
8
+ "ENDED",
9
+ "ERROR",
10
+ "CLOSED"
11
+ ]);
12
+ function dispatchNodeCallbacks(evt, callbacks) {
13
+ if (NODE_STATES.has(evt.kind)) {
14
+ callbacks.onStatusChange?.(evt.kind, evt.message);
15
+ }
16
+ if (evt.kind === "ERROR") {
17
+ callbacks.onError?.(new Error(evt.message ?? "node error"));
18
+ }
19
+ if (evt.kind === "CLOSED") {
20
+ callbacks.onClose?.();
21
+ }
22
+ }
23
+ var BaseNode = class _BaseNode {
24
+ nodeId;
25
+ workflowId;
26
+ client;
27
+ _eventCleanup = null;
28
+ _callbacks = {};
29
+ constructor(nodeId, client, workflowId) {
30
+ this.nodeId = nodeId;
31
+ this.workflowId = workflowId;
32
+ this.client = client;
33
+ }
34
+ /** Called by create functions to register the workflow event handler cleanup. */
35
+ _setEventCleanup(fn) {
36
+ this._eventCleanup = fn;
37
+ }
38
+ /**
39
+ * Called by create functions to register the node's lifecycle callbacks
40
+ * (`onError`/`onStatusChange`/`onClose`), so `.close()` can invoke `onClose`
41
+ * directly on a locally-initiated close.
42
+ */
43
+ _bindCallbacks(callbacks) {
44
+ this._callbacks = callbacks;
45
+ }
46
+ /**
47
+ * Subscribe this node to one or more source nodes.
48
+ * This tells the server to route streams from the sources into this node.
49
+ */
50
+ async subscribe(specs, options) {
51
+ await this.client.trpc.subscribeNode.mutate({
52
+ workflowId: this.workflowId,
53
+ nodeId: this.nodeId,
54
+ sources: specs.map((s) => _BaseNode._toSourceInput(s)),
55
+ gate: options?.gate
56
+ });
57
+ }
58
+ /** Map one `SubscriptionSpec` to the wire shape expected by the server. */
59
+ static _toSourceInput(spec) {
60
+ const base = {
61
+ sourceNodeId: spec.source.nodeId,
62
+ ...spec.delayMs !== void 0 ? { delayMs: spec.delayMs } : {},
63
+ ...spec.port !== void 0 ? { port: spec.port } : {},
64
+ ...spec.name !== void 0 ? { name: spec.name } : {}
65
+ };
66
+ const audio = (a) => ({
67
+ language: a.language ?? "und",
68
+ label: a.label ?? "Audio",
69
+ defaultTrack: a.defaultTrack ?? false
70
+ });
71
+ const video = (v) => v.label !== void 0 ? { video: { label: v.label } } : {};
72
+ switch (spec.sourceSelector) {
73
+ case "CAPTIONS":
74
+ return { ...base, selector: "CAPTIONS" };
75
+ case "VIDEO":
76
+ return {
77
+ ...base,
78
+ selector: "VIDEO",
79
+ ...spec.track !== void 0 ? { track: spec.track } : {},
80
+ ...spec.codec !== void 0 ? { codec: spec.codec } : {},
81
+ ...spec.video !== void 0 ? video(spec.video) : {}
82
+ };
83
+ case "AUDIO":
84
+ return {
85
+ ...base,
86
+ selector: "AUDIO",
87
+ ...spec.track !== void 0 ? { track: spec.track } : {},
88
+ ...spec.codec !== void 0 ? { codec: spec.codec } : {},
89
+ ...spec.audio !== void 0 ? { audio: audio(spec.audio) } : {}
90
+ };
91
+ case "ALL":
92
+ return {
93
+ ...base,
94
+ selector: "ALL",
95
+ ...spec.videoCodec !== void 0 ? { videoCodec: spec.videoCodec } : {},
96
+ ...spec.audioCodec !== void 0 ? { audioCodec: spec.audioCodec } : {},
97
+ ...spec.video !== void 0 ? video(spec.video) : {},
98
+ ...spec.audio !== void 0 ? { audio: audio(spec.audio) } : {}
99
+ };
100
+ }
101
+ }
102
+ /**
103
+ * Probe this node's media stream (ffprobe, server-side) and return its
104
+ * track list. Each entry's `track` is the per-type index to pass back as
105
+ * `track` in a VIDEO/AUDIO subscription:
106
+ *
107
+ * ```ts
108
+ * const tracks = await input.tracks();
109
+ * const ita = tracks.find((t) => t.type === "audio" && t.language === "ita");
110
+ * await output.subscribe([
111
+ * { source: input, sourceSelector: "VIDEO" },
112
+ * { source: input, sourceSelector: "AUDIO", track: ita!.track },
113
+ * ]);
114
+ * ```
115
+ *
116
+ * Probing consumes real-time-paced stream data, so it can take several
117
+ * seconds; `probeSizeBytes` (default 1 MB) trades discovery depth for
118
+ * latency. The server errors after ~15 s if the source produces no data
119
+ * (e.g. no publisher connected yet, or a disabled file input).
120
+ */
121
+ async tracks(options) {
122
+ return this.client.trpc.probeTracks.query({
123
+ workflowId: this.workflowId,
124
+ nodeId: this.nodeId,
125
+ ...options?.probeSizeBytes !== void 0 ? { probeSizeBytes: options.probeSizeBytes } : {}
126
+ });
127
+ }
128
+ /** Tear down this node on the server. */
129
+ async close() {
130
+ this._eventCleanup?.();
131
+ await this.client.trpc.closeNode.mutate({
132
+ workflowId: this.workflowId,
133
+ nodeId: this.nodeId
134
+ });
135
+ this._callbacks.onClose?.();
136
+ }
137
+ };
138
+
139
+ // src/input/caption-source.ts
140
+ var CaptionSourceNode = class extends BaseNode {
141
+ settings;
142
+ constructor(nodeId, workflow, settings) {
143
+ super(nodeId, workflow.client, workflow.workflowId);
144
+ this.settings = settings;
145
+ }
146
+ /**
147
+ * Queue caption text for injection into the video stream.
148
+ *
149
+ * @param options.startAt Stream-elapsed milliseconds (relative to the
150
+ * first video frame received by the server) at which
151
+ * to show the caption. The server holds the text and
152
+ * injects it at the matching PTS, so you can schedule
153
+ * captions before that point in the stream is reached.
154
+ * Omit (or 0) to inject immediately at the next frame.
155
+ * @param options.duration How long (ms) the caption stays visible before
156
+ * being automatically erased. Required.
157
+ */
158
+ async send(text, options) {
159
+ await this.client.trpc.sendCaption.mutate({
160
+ workflowId: this.workflowId,
161
+ nodeId: this.nodeId,
162
+ text,
163
+ durationMs: options.duration,
164
+ startAtMs: options.startAt ?? 0
165
+ });
166
+ }
167
+ /**
168
+ * Parse raw SRT or WebVTT content and schedule all entries on the server.
169
+ *
170
+ * Use `replayCount: Infinity` when creating this node if the pipeline may
171
+ * connect encoders after the captions are loaded.
172
+ *
173
+ * @returns The number of caption entries loaded.
174
+ */
175
+ async sendFile(content) {
176
+ return this.client.trpc.loadCaptionFile.mutate({
177
+ workflowId: this.workflowId,
178
+ nodeId: this.nodeId,
179
+ content
180
+ });
181
+ }
182
+ /**
183
+ * Read a local SRT or WebVTT file and schedule all its entries on the server.
184
+ * The file is read client-side; only the content string is sent over the wire.
185
+ *
186
+ * @param filePath Absolute or relative path to the subtitle file.
187
+ * @returns The number of caption entries loaded.
188
+ */
189
+ async sendFileByPath(filePath) {
190
+ const content = await readFile(filePath, "utf-8");
191
+ return this.sendFile(content);
192
+ }
193
+ };
194
+ async function createCaptionSourceNode(workflow, settings = {}, nodeId) {
195
+ const id = nodeId ?? `caption_source_${Date.now()}`;
196
+ const node = new CaptionSourceNode(id, workflow, settings);
197
+ node._bindCallbacks(settings);
198
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
199
+ dispatchNodeCallbacks(evt, settings);
200
+ });
201
+ node._setEventCleanup(removeHandler);
202
+ await workflow.client.trpc.createNode.mutate({
203
+ kind: "CAPTION_SOURCE",
204
+ workflowId: workflow.workflowId,
205
+ nodeId: id,
206
+ captionSource: {
207
+ replayCount: settings.replayCount ?? 0,
208
+ language: settings.language ?? "und",
209
+ label: settings.label ?? "Subtitles",
210
+ defaultTrack: settings.defaultTrack ?? true,
211
+ ...settings.maxLineLength !== void 0 && {
212
+ maxLineLength: settings.maxLineLength
213
+ }
214
+ }
215
+ }).catch((err) => {
216
+ removeHandler();
217
+ throw err;
218
+ });
219
+ return node;
220
+ }
221
+
222
+ // src/input/file.ts
223
+ import { uid } from "radash";
224
+ var FileInputNode = class extends BaseNode {
225
+ settings;
226
+ constructor(nodeId, workflow, settings) {
227
+ super(nodeId, workflow.client, workflow.workflowId);
228
+ this.settings = settings;
229
+ }
230
+ /** Enable this node — starts reading the file and streaming to downstream nodes. */
231
+ async enable() {
232
+ await this.client.trpc.enableNode.mutate({
233
+ workflowId: this.workflowId,
234
+ nodeId: this.nodeId
235
+ });
236
+ }
237
+ };
238
+ async function createFileInputNode(workflow, settings, nodeId) {
239
+ const id = nodeId ?? `file_input_${uid(12)}`;
240
+ const node = new FileInputNode(id, workflow, settings);
241
+ node._bindCallbacks(settings);
242
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
243
+ if (evt.kind === "READY") settings.onReady?.();
244
+ if (evt.kind === "ACTIVE" && evt.message !== void 0)
245
+ settings.onStreamStart?.(Number(evt.message));
246
+ if (evt.kind === "ENDED") settings.onEnded?.();
247
+ dispatchNodeCallbacks(evt, settings);
248
+ });
249
+ node._setEventCleanup(removeHandler);
250
+ await workflow.client.trpc.createNode.mutate({
251
+ kind: "FILE_INPUT",
252
+ workflowId: workflow.workflowId,
253
+ nodeId: id,
254
+ fileInput: {
255
+ fileName: settings.fileName,
256
+ loop: settings.loop ?? false,
257
+ sourceName: settings.sourceName,
258
+ disabled: settings.disabled ?? false
259
+ }
260
+ }).catch((err) => {
261
+ removeHandler();
262
+ throw err;
263
+ });
264
+ return node;
265
+ }
266
+
267
+ // src/input/node-stream.ts
268
+ import { uid as uid2 } from "radash";
269
+ var NodeStreamInputNode = class extends BaseNode {
270
+ settings;
271
+ _apiBase;
272
+ constructor(nodeId, workflow, settings) {
273
+ super(nodeId, workflow.client, workflow.workflowId);
274
+ this.settings = settings;
275
+ const { host, port } = workflow.client.options;
276
+ this._apiBase = `http://${host}:${port}`;
277
+ }
278
+ /**
279
+ * Push an audio stream to the server.
280
+ *
281
+ * Accepts any `AsyncIterable<Buffer | Uint8Array>`, which includes Node.js
282
+ * `Readable` streams (async-iterable since Node 10) and async generators.
283
+ * The call resolves when the source iterable is exhausted or the server
284
+ * closes the connection.
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * import { createReadStream } from "node:fs";
289
+ * await audioInput.stream(createReadStream("audio.mp3"));
290
+ * ```
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * // Raw PCM from a microphone via node-record-lpcm16
295
+ * const mic = recorder.start({ sampleRate: 16000, channels: 1 });
296
+ * await audioInput.stream(mic);
297
+ * ```
298
+ */
299
+ async stream(source) {
300
+ const url = `${this._apiBase}/api/node-stream/${this.workflowId}/${this.nodeId}`;
301
+ const body = new ReadableStream({
302
+ async start(controller) {
303
+ try {
304
+ for await (const chunk of source) {
305
+ controller.enqueue(
306
+ chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)
307
+ );
308
+ }
309
+ controller.close();
310
+ } catch (err) {
311
+ controller.error(err);
312
+ }
313
+ }
314
+ });
315
+ await fetch(url, {
316
+ method: "POST",
317
+ body,
318
+ // Required for streaming request bodies in Node.js 18+ native fetch.
319
+ duplex: "half"
320
+ });
321
+ }
322
+ };
323
+ async function createNodeStreamInputNode(workflow, settings, nodeId) {
324
+ const id = nodeId ?? `node_stream_${uid2(12)}`;
325
+ const node = new NodeStreamInputNode(id, workflow, settings);
326
+ node._bindCallbacks(settings);
327
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
328
+ if (evt.kind === "READY") settings.onReady?.();
329
+ if (evt.kind === "ACTIVE" && evt.message !== void 0)
330
+ settings.onStreamStart?.(Number(evt.message));
331
+ if (evt.kind === "ENDED") settings.onEnded?.();
332
+ dispatchNodeCallbacks(evt, settings);
333
+ });
334
+ node._setEventCleanup(removeHandler);
335
+ await workflow.client.trpc.createNode.mutate({
336
+ kind: "NODE_STREAM_INPUT",
337
+ workflowId: workflow.workflowId,
338
+ nodeId: id,
339
+ nodeStreamInput: {
340
+ format: settings.format,
341
+ ...settings.sampleRate !== void 0 && {
342
+ sampleRate: settings.sampleRate
343
+ },
344
+ ...settings.channels !== void 0 && {
345
+ channels: settings.channels
346
+ },
347
+ audioCodec: settings.audioCodec ?? "aac",
348
+ ...settings.label !== void 0 && { label: settings.label }
349
+ }
350
+ }).catch((err) => {
351
+ removeHandler();
352
+ throw err;
353
+ });
354
+ return node;
355
+ }
356
+
357
+ // src/input/rtmp-announcer.ts
358
+ import { uid as uid3 } from "radash";
359
+ var RtmpAnnouncerNode = class extends BaseNode {
360
+ /**
361
+ * Forcibly disconnect the connection identified by `connectionId`. Can be
362
+ * called at any time after the node is created — e.g. from an async
363
+ * moderation decision made after `onConnect` already returned.
364
+ */
365
+ async disconnectSource(connectionId) {
366
+ await this.client.trpc.disconnectSource.mutate({
367
+ workflowId: this.workflowId,
368
+ nodeId: this.nodeId,
369
+ connectionId
370
+ });
371
+ }
372
+ };
373
+ async function createRtmpAnnouncerNode(workflow, settings, nodeId) {
374
+ const id = nodeId ?? `rtmp_announcer_${uid3(12)}`;
375
+ const app = settings.app ?? "live";
376
+ const node = new RtmpAnnouncerNode(id, workflow.client, workflow.workflowId);
377
+ const operations = {
378
+ disconnectSource: (connectionId) => node.disconnectSource(connectionId)
379
+ };
380
+ node._bindCallbacks(settings);
381
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
382
+ if (evt.kind === "STREAM_CONNECTED" && evt.message !== void 0) {
383
+ const info = JSON.parse(evt.message);
384
+ settings.onConnect(info, operations);
385
+ }
386
+ if (evt.kind === "STREAM_DISCONNECTED" && evt.message !== void 0) {
387
+ const info = JSON.parse(evt.message);
388
+ settings.onDisconnect?.(info);
389
+ }
390
+ dispatchNodeCallbacks(evt, settings);
391
+ });
392
+ node._setEventCleanup(removeHandler);
393
+ await workflow.client.trpc.createNode.mutate({
394
+ kind: "RTMP_ANNOUNCER",
395
+ workflowId: workflow.workflowId,
396
+ nodeId: id,
397
+ rtmpAnnouncer: { app }
398
+ }).catch((err) => {
399
+ removeHandler();
400
+ throw err;
401
+ });
402
+ return node;
403
+ }
404
+
405
+ // src/input/rtmp-multiplexer.ts
406
+ import { uid as uid4 } from "radash";
407
+ var RtmpStreamNode = class extends BaseNode {
408
+ app;
409
+ sourceName;
410
+ constructor(nodeId, client, app, sourceName, workflowId) {
411
+ super(nodeId, client, workflowId);
412
+ this.app = app;
413
+ this.sourceName = sourceName;
414
+ }
415
+ };
416
+ var RtmpMultiplexerNode = class extends BaseNode {
417
+ };
418
+ async function createRtmpMultiplexerNode(workflow, settings, nodeId) {
419
+ const id = nodeId ?? `rtmp_mux_${uid4(12)}`;
420
+ const app = settings.app ?? "live";
421
+ const node = new RtmpMultiplexerNode(
422
+ id,
423
+ workflow.client,
424
+ workflow.workflowId
425
+ );
426
+ const activeStreams = /* @__PURE__ */ new Map();
427
+ node._bindCallbacks(settings);
428
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
429
+ dispatchNodeCallbacks(evt, settings);
430
+ if (evt.kind === "CHILD_CREATED" && evt.message !== void 0) {
431
+ const { childNodeId, sourceName } = JSON.parse(evt.message);
432
+ Promise.resolve(settings.onConnect?.({ app, sourceName })).then(async (result) => {
433
+ if (result?.accept === false) {
434
+ await workflow.client.trpc.rejectPublisher.mutate({
435
+ workflowId: workflow.workflowId,
436
+ nodeId: id,
437
+ sourceName
438
+ }).catch((err) => {
439
+ console.error("[rema] rejectPublisher failed:", err);
440
+ });
441
+ return;
442
+ }
443
+ const streamNode = new RtmpStreamNode(
444
+ childNodeId,
445
+ workflow.client,
446
+ app,
447
+ sourceName,
448
+ workflow.workflowId
449
+ );
450
+ activeStreams.set(sourceName, streamNode);
451
+ await settings.onStream(streamNode);
452
+ }).catch((err) => {
453
+ console.error("[rema] onStream error:", err);
454
+ });
455
+ return;
456
+ }
457
+ if (evt.kind === "CHILD_CLOSED" && evt.message !== void 0) {
458
+ const { sourceName } = JSON.parse(evt.message);
459
+ const streamNode = activeStreams.get(sourceName);
460
+ activeStreams.delete(sourceName);
461
+ if (streamNode) {
462
+ Promise.resolve(settings.onStreamEnd?.(streamNode)).catch(
463
+ (err) => {
464
+ console.error("[rema] onStreamEnd error:", err);
465
+ }
466
+ );
467
+ }
468
+ }
469
+ });
470
+ node._setEventCleanup(removeHandler);
471
+ await workflow.client.trpc.createNode.mutate({
472
+ kind: "RTMP_MULTIPLEXER",
473
+ workflowId: workflow.workflowId,
474
+ nodeId: id,
475
+ rtmpMultiplexer: { app, port: 1935 }
476
+ }).catch((err) => {
477
+ removeHandler();
478
+ throw err;
479
+ });
480
+ return node;
481
+ }
482
+
483
+ // src/input/rtmp-reader.ts
484
+ import { uid as uid5 } from "radash";
485
+ var RtmpReaderNode = class extends BaseNode {
486
+ settings;
487
+ constructor(nodeId, workflow, settings) {
488
+ super(nodeId, workflow.client, workflow.workflowId);
489
+ this.settings = settings;
490
+ }
491
+ };
492
+ async function createRtmpReaderNode(workflow, settings, nodeId) {
493
+ const id = nodeId ?? `rtmp_reader_${uid5(12)}`;
494
+ const node = new RtmpReaderNode(id, workflow, settings);
495
+ node._bindCallbacks(settings);
496
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
497
+ if (evt.kind === "READY") {
498
+ settings.onPublish?.({
499
+ app: settings.app,
500
+ internalUrl: evt.message ?? ""
501
+ });
502
+ }
503
+ if (evt.kind === "ACTIVE" && evt.message !== void 0) {
504
+ settings.onStreamStart?.(Number(evt.message));
505
+ }
506
+ if (evt.kind === "ENDED") settings.onStreamEnded?.();
507
+ dispatchNodeCallbacks(evt, settings);
508
+ });
509
+ node._setEventCleanup(removeHandler);
510
+ await workflow.client.trpc.createNode.mutate({
511
+ kind: "RTMP_READER",
512
+ workflowId: workflow.workflowId,
513
+ nodeId: id,
514
+ rtmpReader: { app: settings.app, sourceName: settings.sourceName }
515
+ }).catch((err) => {
516
+ removeHandler();
517
+ throw err;
518
+ });
519
+ return node;
520
+ }
521
+
522
+ // src/input/srt-announcer.ts
523
+ import { uid as uid6 } from "radash";
524
+ var SrtAnnouncerNode = class extends BaseNode {
525
+ /**
526
+ * Forcibly disconnect the connection identified by `connectionId`. Can be
527
+ * called at any time after the node is created — e.g. from an async
528
+ * moderation decision made after `onConnect` already returned.
529
+ */
530
+ async disconnectSource(connectionId) {
531
+ await this.client.trpc.disconnectSource.mutate({
532
+ workflowId: this.workflowId,
533
+ nodeId: this.nodeId,
534
+ connectionId
535
+ });
536
+ }
537
+ };
538
+ async function createSrtAnnouncerNode(workflow, settings, nodeId) {
539
+ const id = nodeId ?? `srt_announcer_${uid6(12)}`;
540
+ const app = settings.app ?? "live";
541
+ const node = new SrtAnnouncerNode(id, workflow.client, workflow.workflowId);
542
+ const operations = {
543
+ disconnectSource: (connectionId) => node.disconnectSource(connectionId)
544
+ };
545
+ node._bindCallbacks(settings);
546
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
547
+ if (evt.kind === "STREAM_CONNECTED" && evt.message !== void 0) {
548
+ const info = JSON.parse(evt.message);
549
+ settings.onConnect(info, operations);
550
+ }
551
+ if (evt.kind === "STREAM_DISCONNECTED" && evt.message !== void 0) {
552
+ const info = JSON.parse(evt.message);
553
+ settings.onDisconnect?.(info);
554
+ }
555
+ dispatchNodeCallbacks(evt, settings);
556
+ });
557
+ node._setEventCleanup(removeHandler);
558
+ await workflow.client.trpc.createNode.mutate({
559
+ kind: "SRT_ANNOUNCER",
560
+ workflowId: workflow.workflowId,
561
+ nodeId: id,
562
+ srtAnnouncer: {
563
+ app,
564
+ ...settings.passphrase !== void 0 && {
565
+ passphrase: settings.passphrase
566
+ },
567
+ ...settings.latencyMs !== void 0 && {
568
+ latencyMs: settings.latencyMs
569
+ }
570
+ }
571
+ }).catch((err) => {
572
+ removeHandler();
573
+ throw err;
574
+ });
575
+ return node;
576
+ }
577
+
578
+ // src/input/srt-reader.ts
579
+ import { uid as uid7 } from "radash";
580
+ var SrtReaderNode = class extends BaseNode {
581
+ settings;
582
+ constructor(nodeId, workflow, settings) {
583
+ super(nodeId, workflow.client, workflow.workflowId);
584
+ this.settings = settings;
585
+ }
586
+ };
587
+ async function createSrtReaderNode(workflow, settings, nodeId) {
588
+ const id = nodeId ?? `srt_reader_${uid7(12)}`;
589
+ const node = new SrtReaderNode(id, workflow, settings);
590
+ node._bindCallbacks(settings);
591
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
592
+ if (evt.kind === "READY") {
593
+ settings.onPublish?.({
594
+ app: settings.app,
595
+ internalUrl: evt.message ?? ""
596
+ });
597
+ }
598
+ if (evt.kind === "ACTIVE" && evt.message !== void 0) {
599
+ settings.onStreamStart?.(Number(evt.message));
600
+ }
601
+ if (evt.kind === "ENDED") settings.onStreamEnded?.();
602
+ dispatchNodeCallbacks(evt, settings);
603
+ });
604
+ node._setEventCleanup(removeHandler);
605
+ await workflow.client.trpc.createNode.mutate({
606
+ kind: "SRT_READER",
607
+ workflowId: workflow.workflowId,
608
+ nodeId: id,
609
+ srtReader: { app: settings.app, sourceName: settings.sourceName }
610
+ }).catch((err) => {
611
+ removeHandler();
612
+ throw err;
613
+ });
614
+ return node;
615
+ }
616
+
617
+ // src/output/file.ts
618
+ import { uid as uid8 } from "radash";
619
+ var FileOutputNode = class extends BaseNode {
620
+ settings;
621
+ constructor(nodeId, workflow, settings) {
622
+ super(nodeId, workflow.client, workflow.workflowId);
623
+ this.settings = settings;
624
+ }
625
+ };
626
+ async function createFileOutputNode(workflow, settings, nodeId) {
627
+ const id = nodeId ?? `file_output_${uid8(12)}`;
628
+ const node = new FileOutputNode(id, workflow, settings);
629
+ node._bindCallbacks(settings);
630
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
631
+ dispatchNodeCallbacks(evt, settings);
632
+ });
633
+ node._setEventCleanup(removeHandler);
634
+ await workflow.client.trpc.createNode.mutate({
635
+ kind: "FILE_OUTPUT",
636
+ workflowId: workflow.workflowId,
637
+ nodeId: id,
638
+ fileOutput: {
639
+ fileName: settings.fileName,
640
+ format: settings.format ?? "",
641
+ overwrite: settings.overwrite ?? true
642
+ }
643
+ }).catch((err) => {
644
+ removeHandler();
645
+ throw err;
646
+ });
647
+ return node;
648
+ }
649
+
650
+ // src/output/hls.ts
651
+ import { uid as uid9 } from "radash";
652
+ var HlsOutputNode = class extends BaseNode {
653
+ settings;
654
+ constructor(nodeId, workflow, settings) {
655
+ super(nodeId, workflow.client, workflow.workflowId);
656
+ this.settings = settings;
657
+ }
658
+ /**
659
+ * Return the effective base path or URL for each configured destination in
660
+ * the current session. Must be called after `subscribe()` so that the server
661
+ * has wired the destinations. For S3 destinations this includes the
662
+ * per-session subfolder appended by `makePrefixUnique`.
663
+ */
664
+ async effectivePaths() {
665
+ const result = await this.client.trpc.hlsOutputInfo.query({
666
+ workflowId: this.workflowId,
667
+ nodeId: this.nodeId
668
+ });
669
+ return result.effectivePaths;
670
+ }
671
+ };
672
+ async function createHlsOutputNode(workflow, settings, nodeId) {
673
+ const id = nodeId ?? `hls_output_${uid9(12)}`;
674
+ const node = new HlsOutputNode(id, workflow, settings);
675
+ node._bindCallbacks(settings);
676
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
677
+ dispatchNodeCallbacks(evt, settings);
678
+ });
679
+ node._setEventCleanup(removeHandler);
680
+ await workflow.client.trpc.createNode.mutate({
681
+ kind: "HLS_OUTPUT",
682
+ workflowId: workflow.workflowId,
683
+ nodeId: id,
684
+ hlsOutput: {
685
+ destinations: settings.destinations,
686
+ segmentTime: settings.segmentTime ?? 6,
687
+ listSize: settings.listSize ?? 5,
688
+ audioCodec: settings.audioCodec ?? void 0,
689
+ videoCodec: settings.videoCodec ?? void 0
690
+ }
691
+ }).catch((err) => {
692
+ removeHandler();
693
+ throw err;
694
+ });
695
+ return node;
696
+ }
697
+
698
+ // src/output/rtmp.ts
699
+ import { uid as uid10 } from "radash";
700
+ var RtmpOutputNode = class extends BaseNode {
701
+ settings;
702
+ constructor(nodeId, workflow, settings) {
703
+ super(nodeId, workflow.client, workflow.workflowId);
704
+ this.settings = settings;
705
+ }
706
+ };
707
+ async function createRtmpOutputNode(workflow, settings, nodeId) {
708
+ const id = nodeId ?? `rtmp_output_${uid10(12)}`;
709
+ const node = new RtmpOutputNode(id, workflow, settings);
710
+ node._bindCallbacks(settings);
711
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
712
+ dispatchNodeCallbacks(evt, settings);
713
+ });
714
+ node._setEventCleanup(removeHandler);
715
+ await workflow.client.trpc.createNode.mutate({
716
+ kind: "RTMP_OUTPUT",
717
+ workflowId: workflow.workflowId,
718
+ nodeId: id,
719
+ rtmpOutput: { url: settings.url }
720
+ }).catch((err) => {
721
+ removeHandler();
722
+ throw err;
723
+ });
724
+ return node;
725
+ }
726
+
727
+ // src/output/srt.ts
728
+ import { uid as uid11 } from "radash";
729
+ var SrtOutputNode = class extends BaseNode {
730
+ settings;
731
+ constructor(nodeId, workflow, settings) {
732
+ super(nodeId, workflow.client, workflow.workflowId);
733
+ this.settings = settings;
734
+ }
735
+ };
736
+ async function createSrtOutputNode(workflow, settings, nodeId) {
737
+ const id = nodeId ?? `srt_output_${uid11(12)}`;
738
+ const node = new SrtOutputNode(id, workflow, settings);
739
+ node._bindCallbacks(settings);
740
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
741
+ dispatchNodeCallbacks(evt, settings);
742
+ });
743
+ node._setEventCleanup(removeHandler);
744
+ await workflow.client.trpc.createNode.mutate({
745
+ kind: "SRT_OUTPUT",
746
+ workflowId: workflow.workflowId,
747
+ nodeId: id,
748
+ srtOutput: { url: settings.url }
749
+ }).catch((err) => {
750
+ removeHandler();
751
+ throw err;
752
+ });
753
+ return node;
754
+ }
755
+
756
+ // src/processor/buffer.ts
757
+ import { uid as uid12 } from "radash";
758
+ var BufferNode = class extends BaseNode {
759
+ settings;
760
+ constructor(nodeId, workflow, settings) {
761
+ super(nodeId, workflow.client, workflow.workflowId);
762
+ this.settings = settings;
763
+ }
764
+ };
765
+ async function createBufferNode(workflow, settings, nodeId) {
766
+ const id = nodeId ?? `buffer_${uid12(12)}`;
767
+ const node = new BufferNode(id, workflow, settings);
768
+ node._bindCallbacks(settings);
769
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
770
+ dispatchNodeCallbacks(evt, settings);
771
+ });
772
+ node._setEventCleanup(removeHandler);
773
+ await workflow.client.trpc.createNode.mutate({
774
+ kind: "BUFFER",
775
+ workflowId: workflow.workflowId,
776
+ nodeId: id,
777
+ buffer: { bufferMs: settings.bufferMs }
778
+ }).catch((err) => {
779
+ removeHandler();
780
+ throw err;
781
+ });
782
+ return node;
783
+ }
784
+
785
+ // src/processor/camb-ai-translate.ts
786
+ import { uid as uid13 } from "radash";
787
+ var CambAiTranslateNode = class extends BaseNode {
788
+ settings;
789
+ constructor(nodeId, workflow, settings) {
790
+ super(nodeId, workflow.client, workflow.workflowId);
791
+ this.settings = settings;
792
+ }
793
+ };
794
+ async function createCambAiTranslateNode(workflow, settings, nodeId) {
795
+ const id = nodeId ?? `camb_translate_${uid13(12)}`;
796
+ const node = new CambAiTranslateNode(id, workflow, settings);
797
+ node._bindCallbacks(settings);
798
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
799
+ dispatchNodeCallbacks(evt, settings);
800
+ });
801
+ node._setEventCleanup(removeHandler);
802
+ await workflow.client.trpc.createNode.mutate({
803
+ kind: "CAMB_AI_TRANSLATE",
804
+ workflowId: workflow.workflowId,
805
+ nodeId: id,
806
+ cambAiTranslate: {
807
+ apiKey: settings.apiKey,
808
+ sourceLanguageCode: settings.sourceLanguageCode,
809
+ targetLanguageCode: settings.targetLanguageCode,
810
+ model: settings.model,
811
+ voiceId: settings.voiceId,
812
+ label: settings.label
813
+ }
814
+ }).catch((err) => {
815
+ removeHandler();
816
+ throw err;
817
+ });
818
+ return node;
819
+ }
820
+
821
+ // src/processor/cea608-encoder.ts
822
+ import { uid as uid14 } from "radash";
823
+ var Cea608EncoderNode = class extends BaseNode {
824
+ settings;
825
+ constructor(nodeId, workflow, settings) {
826
+ super(nodeId, workflow.client, workflow.workflowId);
827
+ this.settings = settings;
828
+ }
829
+ };
830
+ async function createCea608EncoderNode(workflow, settings = {}, nodeId) {
831
+ const id = nodeId ?? `cea608_${uid14(12)}`;
832
+ const node = new Cea608EncoderNode(id, workflow, settings);
833
+ node._bindCallbacks(settings);
834
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
835
+ dispatchNodeCallbacks(evt, settings);
836
+ });
837
+ node._setEventCleanup(removeHandler);
838
+ await workflow.client.trpc.createNode.mutate({
839
+ kind: "CEA608_ENCODER",
840
+ workflowId: workflow.workflowId,
841
+ nodeId: id,
842
+ cea608Encoder: {
843
+ mode: settings.mode ?? "rollUp",
844
+ rows: settings.rows ?? 2,
845
+ autoWrap: settings.autoWrap ?? false
846
+ }
847
+ }).catch((err) => {
848
+ removeHandler();
849
+ throw err;
850
+ });
851
+ return node;
852
+ }
853
+
854
+ // src/processor/ffprobe-monitor.ts
855
+ import { uid as uid15 } from "radash";
856
+ var FfprobeMonitorNode = class extends BaseNode {
857
+ settings;
858
+ constructor(nodeId, workflow, settings) {
859
+ super(nodeId, workflow.client, workflow.workflowId);
860
+ this.settings = settings;
861
+ }
862
+ /**
863
+ * Subscribe to a monitor stream on this node.
864
+ *
865
+ * - `"stream-info"` — emits a `status` event (key/value map) every time a
866
+ * probe cycle completes, containing codec names, resolution, frame rate,
867
+ * bit rates, and format.
868
+ * - `"ffprobe-log"` — emits `log` events with raw ffprobe stderr output.
869
+ *
870
+ * Returns an object with an `unsubscribe()` method; call it to stop receiving
871
+ * events and release the tRPC subscription.
872
+ */
873
+ observe(monitorName, onData, onError) {
874
+ return this.client.trpc.observeNode.subscribe(
875
+ { workflowId: this.workflowId, nodeId: this.nodeId, monitorName },
876
+ { onData, ...onError !== void 0 && { onError } }
877
+ );
878
+ }
879
+ };
880
+ async function createFfprobeMonitorNode(workflow, settings = {}, nodeId) {
881
+ const id = nodeId ?? `ffprobe_${uid15(12)}`;
882
+ const node = new FfprobeMonitorNode(id, workflow, settings);
883
+ node._bindCallbacks(settings);
884
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
885
+ dispatchNodeCallbacks(evt, settings);
886
+ });
887
+ node._setEventCleanup(removeHandler);
888
+ await workflow.client.trpc.createNode.mutate({
889
+ kind: "FFPROBE_MONITOR",
890
+ workflowId: workflow.workflowId,
891
+ nodeId: id,
892
+ ffprobeMonitor: {
893
+ intervalMs: settings.intervalMs ?? 3e4,
894
+ probeSizeBytes: settings.probeSizeBytes ?? 1048576
895
+ }
896
+ }).catch((err) => {
897
+ removeHandler();
898
+ throw err;
899
+ });
900
+ return node;
901
+ }
902
+
903
+ // src/processor/gate.ts
904
+ import { uid as uid16 } from "radash";
905
+ var GateNode = class extends BaseNode {
906
+ settings;
907
+ constructor(nodeId, workflow, settings) {
908
+ super(nodeId, workflow.client, workflow.workflowId);
909
+ this.settings = settings;
910
+ }
911
+ };
912
+ async function createGateNode(workflow, settings = {}, nodeId) {
913
+ const id = nodeId ?? `gate_${uid16(12)}`;
914
+ const node = new GateNode(id, workflow, settings);
915
+ node._bindCallbacks(settings);
916
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
917
+ dispatchNodeCallbacks(evt, settings);
918
+ });
919
+ node._setEventCleanup(removeHandler);
920
+ await workflow.client.trpc.createNode.mutate({
921
+ kind: "GATE",
922
+ workflowId: workflow.workflowId,
923
+ nodeId: id,
924
+ gate: {
925
+ ...settings.minReady !== void 0 && {
926
+ minReady: settings.minReady
927
+ },
928
+ ...settings.timeoutMs !== void 0 && {
929
+ timeoutMs: settings.timeoutMs
930
+ }
931
+ }
932
+ }).catch((err) => {
933
+ removeHandler();
934
+ throw err;
935
+ });
936
+ return node;
937
+ }
938
+
939
+ // src/processor/gemini-translate.ts
940
+ import { uid as uid17 } from "radash";
941
+ var GeminiTranslateNode = class extends BaseNode {
942
+ settings;
943
+ constructor(nodeId, workflow, settings) {
944
+ super(nodeId, workflow.client, workflow.workflowId);
945
+ this.settings = settings;
946
+ }
947
+ };
948
+ async function createGeminiTranslateNode(workflow, settings, nodeId) {
949
+ const id = nodeId ?? `gemini_translate_${uid17(12)}`;
950
+ const node = new GeminiTranslateNode(id, workflow, settings);
951
+ node._bindCallbacks(settings);
952
+ const removeHandler = workflow._onNodeEvent(id, (evt) => {
953
+ dispatchNodeCallbacks(evt, settings);
954
+ });
955
+ node._setEventCleanup(removeHandler);
956
+ await workflow.client.trpc.createNode.mutate({
957
+ kind: "GEMINI_TRANSLATE",
958
+ workflowId: workflow.workflowId,
959
+ nodeId: id,
960
+ geminiTranslate: {
961
+ apiKey: settings.apiKey,
962
+ targetLanguageCode: settings.targetLanguageCode,
963
+ label: settings.label
964
+ }
965
+ }).catch((err) => {
966
+ removeHandler();
967
+ throw err;
968
+ });
969
+ return node;
970
+ }
971
+
972
+ // src/client.ts
973
+ import {
974
+ createTRPCClient,
975
+ httpLink,
976
+ httpSubscriptionLink,
977
+ splitLink
978
+ } from "@trpc/client";
979
+ import { EventSource } from "eventsource";
980
+ import superjson from "superjson";
981
+ var RemaClient = class _RemaClient {
982
+ trpc;
983
+ options;
984
+ constructor(trpc, options) {
985
+ this.trpc = trpc;
986
+ this.options = {
987
+ host: options.host ?? "127.0.0.1",
988
+ port: options.port ?? 8080
989
+ };
990
+ }
991
+ static async connect(opts = {}) {
992
+ const host = opts.host ?? "127.0.0.1";
993
+ const port = opts.port ?? 8080;
994
+ const url = `http://${host}:${port}/trpc`;
995
+ const trpc = createTRPCClient({
996
+ links: [
997
+ splitLink({
998
+ condition: (op) => op.type === "subscription",
999
+ true: httpSubscriptionLink({
1000
+ url,
1001
+ transformer: superjson,
1002
+ EventSource
1003
+ }),
1004
+ false: httpLink({ url, transformer: superjson })
1005
+ })
1006
+ ]
1007
+ });
1008
+ try {
1009
+ await trpc.ping.query();
1010
+ opts.onReady?.();
1011
+ } catch (err) {
1012
+ opts.onFailedToConnect?.();
1013
+ throw new Error(
1014
+ `Failed to connect to rema server at ${host}:${port}: ${String(err)}`
1015
+ );
1016
+ }
1017
+ return new _RemaClient(trpc, opts);
1018
+ }
1019
+ /** No-op kept for API compatibility — SSE has no persistent connection to close. */
1020
+ close() {
1021
+ }
1022
+ /** Fetches the server's health status from the plain HTTP `/api/health` endpoint. */
1023
+ async health() {
1024
+ const url = `http://${this.options.host}:${this.options.port}/api/health`;
1025
+ const res = await fetch(url);
1026
+ if (!res.ok) {
1027
+ throw new Error(`Health check failed: ${res.status} ${res.statusText}`);
1028
+ }
1029
+ return await res.json();
1030
+ }
1031
+ };
1032
+
1033
+ // src/workflow.ts
1034
+ var Workflow = class {
1035
+ workflowId;
1036
+ /** Non-private: accessible to factory functions via WorkflowHandle. */
1037
+ client;
1038
+ _callbacks;
1039
+ _eventHandlers = /* @__PURE__ */ new Map();
1040
+ _eventSub = null;
1041
+ _stateSub = null;
1042
+ _seenInList = false;
1043
+ _erroredFired = false;
1044
+ _closedFired = false;
1045
+ /** @internal — use `rema.workflow.create()` instead */
1046
+ constructor(workflowId, client, callbacks = {}) {
1047
+ this.workflowId = workflowId;
1048
+ this.client = client;
1049
+ this._callbacks = callbacks;
1050
+ this._startEventSubscription();
1051
+ this._startStateSubscription();
1052
+ }
1053
+ /**
1054
+ * Register a handler for events emitted by a specific node in this workflow.
1055
+ * Returns an unsubscribe function — call it when the node is closed.
1056
+ * @internal used by node factory functions
1057
+ */
1058
+ _onNodeEvent(nodeId, handler) {
1059
+ let handlers = this._eventHandlers.get(nodeId);
1060
+ if (handlers === void 0) {
1061
+ handlers = /* @__PURE__ */ new Set();
1062
+ this._eventHandlers.set(nodeId, handlers);
1063
+ }
1064
+ handlers.add(handler);
1065
+ return () => {
1066
+ this._eventHandlers.get(nodeId)?.delete(handler);
1067
+ };
1068
+ }
1069
+ _startEventSubscription() {
1070
+ this._eventSub = this.client.trpc.workflow.events.subscribe(
1071
+ { workflowId: this.workflowId },
1072
+ {
1073
+ onData: (evt) => {
1074
+ this._eventHandlers.get(evt.nodeId)?.forEach((h) => {
1075
+ h(evt);
1076
+ });
1077
+ },
1078
+ onError: (err) => {
1079
+ console.error(
1080
+ `[rema] workflow ${this.workflowId} events error:`,
1081
+ err
1082
+ );
1083
+ }
1084
+ }
1085
+ );
1086
+ }
1087
+ /**
1088
+ * Watches the server's list of all workflows for this workflow's entry, so
1089
+ * `onError`/`onClose` can be derived without a dedicated per-workflow
1090
+ * subscription: a transition to `status: "crashed"` fires `onError`, and
1091
+ * the entry disappearing (graceful close) fires `onClose`.
1092
+ */
1093
+ _startStateSubscription() {
1094
+ this._stateSub = this.client.trpc.workflow.state.subscribe(void 0, {
1095
+ onData: (workflows) => {
1096
+ const entry = workflows.find((w) => w.id === this.workflowId);
1097
+ if (entry !== void 0) {
1098
+ this._seenInList = true;
1099
+ if (entry.status === "crashed" && !this._erroredFired) {
1100
+ this._erroredFired = true;
1101
+ this._callbacks.onError?.(new Error("workflow worker crashed"));
1102
+ }
1103
+ return;
1104
+ }
1105
+ if (this._seenInList) this._fireClose();
1106
+ },
1107
+ onError: (err) => {
1108
+ console.error(`[rema] workflow ${this.workflowId} state error:`, err);
1109
+ }
1110
+ });
1111
+ }
1112
+ _fireClose() {
1113
+ if (this._closedFired) return;
1114
+ this._closedFired = true;
1115
+ this._stateSub?.unsubscribe();
1116
+ this._callbacks.onClose?.();
1117
+ }
1118
+ // ─── Input nodes ─────────────────────────────────────────────────────────
1119
+ input = {
1120
+ file: (settings, nodeId) => createFileInputNode(this, settings, nodeId),
1121
+ rtmpReader: (settings, nodeId) => createRtmpReaderNode(this, settings, nodeId),
1122
+ /** Announces RTMP publisher connections on an `app` without creating nodes. */
1123
+ rtmpAnnouncer: (settings, nodeId) => createRtmpAnnouncerNode(this, settings, nodeId),
1124
+ /** Announces SRT publisher connections on an `app` without creating nodes. */
1125
+ srtAnnouncer: (settings, nodeId) => createSrtAnnouncerNode(this, settings, nodeId),
1126
+ /** Reads an SRT stream ingested by MediaMTX, pulling it back via RTSP internally. */
1127
+ srtReader: (settings, nodeId) => createSrtReaderNode(this, settings, nodeId),
1128
+ /**
1129
+ * An RTMP multiplexer: accepts any number of simultaneous publishers under
1130
+ * the given `app`. For each connection `onStream` is called with a fresh
1131
+ * `RtmpStreamNode` that can be wired into its own encoder/output pipeline.
1132
+ */
1133
+ rtmpMultiplexer: (settings, nodeId) => createRtmpMultiplexerNode(this, settings, nodeId),
1134
+ /** Format-agnostic caption schedule node. Send captions to it; wire it to a cea608() encoder. */
1135
+ captions: (settings = {}, nodeId) => createCaptionSourceNode(this, settings, nodeId),
1136
+ /**
1137
+ * Audio stream input node. Push a Node.js `Readable` (or any
1138
+ * `AsyncIterable<Buffer>`) directly into the pipeline by calling
1139
+ * `.stream(source)` on the returned node.
1140
+ */
1141
+ streamInput: (settings, nodeId) => createNodeStreamInputNode(this, settings, nodeId)
1142
+ };
1143
+ // ─── Output nodes ─────────────────────────────────────────────────────────
1144
+ output = {
1145
+ rtmp: (settings, nodeId) => createRtmpOutputNode(this, settings, nodeId),
1146
+ file: (settings, nodeId) => createFileOutputNode(this, settings, nodeId),
1147
+ /** HLS output node. Subscribe it to one or more media/subtitle sources. */
1148
+ hls: (settings, nodeId) => createHlsOutputNode(this, settings, nodeId),
1149
+ /** SRT output node. Pushes MPEG-TS over SRT to the given URL. */
1150
+ srt: (settings, nodeId) => createSrtOutputNode(this, settings, nodeId)
1151
+ };
1152
+ // ─── Processor nodes ──────────────────────────────────────────────────────
1153
+ processor = {
1154
+ /** CEA-608/708 encoder node. Subscribe it to video + a captions() node. */
1155
+ cea608: (settings = {}, nodeId) => createCea608EncoderNode(this, settings, nodeId),
1156
+ /**
1157
+ * ffprobe monitor node.
1158
+ * Subscribe it to any media source; it periodically probes the live stream
1159
+ * and exposes codec/resolution/bitrate info via the `stream-info` monitor.
1160
+ */
1161
+ ffprobeMonitor: (settings = {}, nodeId) => createFfprobeMonitorNode(this, settings, nodeId),
1162
+ /**
1163
+ * Gemini Live Translate node.
1164
+ * Subscribe it to an audio/media source; it outputs translated audio-only
1165
+ * MPEG-TS using the Gemini Live Translate API.
1166
+ */
1167
+ geminiTranslate: (settings, nodeId) => createGeminiTranslateNode(this, settings, nodeId),
1168
+ /**
1169
+ * Camb.ai realtime speech-to-speech translation node.
1170
+ * Subscribe it to an audio/media source; it outputs translated audio-only
1171
+ * MPEG-TS using the Camb.ai realtime translation API.
1172
+ */
1173
+ cambAiTranslate: (settings, nodeId) => createCambAiTranslateNode(this, settings, nodeId),
1174
+ /**
1175
+ * Time-shift node. Subscribe it to exactly one source; it re-emits that
1176
+ * source delayed by `bufferMs`, shared across every downstream
1177
+ * subscriber.
1178
+ */
1179
+ buffer: (settings, nodeId) => createBufferNode(this, settings, nodeId),
1180
+ /**
1181
+ * Synchronization barrier for fan-in from multiple, independently-timed
1182
+ * sources. Subscribe it to two or more sources; it withholds all of them
1183
+ * until enough (`minReady`, default: all) have produced their first
1184
+ * item, then forwards everything live.
1185
+ */
1186
+ gate: (settings = {}, nodeId) => createGateNode(this, settings, nodeId)
1187
+ };
1188
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
1189
+ /**
1190
+ * Close this workflow and deactivate all its nodes on the server.
1191
+ * After calling this the workflow handle is no longer usable.
1192
+ */
1193
+ async close() {
1194
+ this._eventSub?.unsubscribe();
1195
+ this._stateSub?.unsubscribe();
1196
+ await this.client.trpc.workflow.close.mutate({
1197
+ workflowId: this.workflowId
1198
+ });
1199
+ if (!this._closedFired) {
1200
+ this._closedFired = true;
1201
+ this._callbacks.onClose?.();
1202
+ }
1203
+ }
1204
+ };
1205
+
1206
+ // src/rema.ts
1207
+ var Rema = class _Rema {
1208
+ client;
1209
+ _autoCloseWorkflows = /* @__PURE__ */ new Set();
1210
+ constructor(client) {
1211
+ this.client = client;
1212
+ }
1213
+ static async connect(opts = {}) {
1214
+ const client = await RemaClient.connect(opts);
1215
+ return new _Rema(client);
1216
+ }
1217
+ // ─── Workflow management ──────────────────────────────────────────────────
1218
+ workflow = {
1219
+ /**
1220
+ * Create a new isolated workflow on the server.
1221
+ *
1222
+ * All nodes created through the returned `Workflow` handle are scoped to
1223
+ * this workflow; they cannot subscribe to nodes in other workflows.
1224
+ *
1225
+ * @param opts.name Human-readable label shown in the web UI.
1226
+ * @param opts.workflowId Optional stable ID. Server generates one if omitted.
1227
+ * @param opts.closeOnDisconnect When true, this workflow is closed automatically
1228
+ * when `rema.close()`/`wf.close()` is called, or
1229
+ * when the connection to the server is lost (e.g.
1230
+ * the process crashes) for more than a few seconds.
1231
+ * @param opts.parentId ID of an existing workflow to attach this one to as
1232
+ * a child (shown nested in the web UI).
1233
+ * @param opts.closeWithParent When true, this workflow is closed automatically
1234
+ * when its parent workflow is closed.
1235
+ * @param opts.onError Called if this workflow's worker process crashes.
1236
+ * @param opts.onClose Called when the workflow closes, whether via
1237
+ * `.close()` or a crash.
1238
+ */
1239
+ create: async (opts = {}) => {
1240
+ const info = await this.client.trpc.workflow.create.mutate({
1241
+ workflowId: opts.workflowId,
1242
+ name: opts.name,
1243
+ parentId: opts.parentId,
1244
+ closeWithParent: opts.closeWithParent,
1245
+ closeOnDisconnect: opts.closeOnDisconnect
1246
+ });
1247
+ const wf = new Workflow(info.id, this.client, {
1248
+ ...opts.onError !== void 0 && { onError: opts.onError },
1249
+ ...opts.onClose !== void 0 && { onClose: opts.onClose }
1250
+ });
1251
+ if (opts.closeOnDisconnect === true) {
1252
+ this._autoCloseWorkflows.add(wf);
1253
+ }
1254
+ return wf;
1255
+ },
1256
+ /** List all active workflows on the server. */
1257
+ list: () => this.client.trpc.workflow.list.query()
1258
+ };
1259
+ /** Fetches the server's current health status (uptime, workflow counts, MediaMTX reachability). */
1260
+ health() {
1261
+ return this.client.health();
1262
+ }
1263
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
1264
+ /**
1265
+ * Close all `closeOnDisconnect` workflows, then disconnect from the server.
1266
+ */
1267
+ async close() {
1268
+ await Promise.allSettled(
1269
+ [...this._autoCloseWorkflows].map((wf) => wf.close())
1270
+ );
1271
+ this.client.close();
1272
+ }
1273
+ };
1274
+
1275
+ // src/selectors.ts
1276
+ var selectVideo = "VIDEO";
1277
+ var selectAudio = "AUDIO";
1278
+ var selectCaptions = "CAPTIONS";
1279
+ var selectAll = "ALL";
1280
+ export {
1281
+ BaseNode,
1282
+ BufferNode,
1283
+ CambAiTranslateNode,
1284
+ CaptionSourceNode,
1285
+ Cea608EncoderNode,
1286
+ FfprobeMonitorNode,
1287
+ FileInputNode,
1288
+ FileOutputNode,
1289
+ GateNode,
1290
+ GeminiTranslateNode,
1291
+ HlsOutputNode,
1292
+ NodeStreamInputNode,
1293
+ Rema,
1294
+ RtmpAnnouncerNode,
1295
+ RtmpMultiplexerNode,
1296
+ RtmpOutputNode,
1297
+ RtmpReaderNode,
1298
+ RtmpStreamNode,
1299
+ SrtAnnouncerNode,
1300
+ SrtOutputNode,
1301
+ SrtReaderNode,
1302
+ Workflow,
1303
+ createNodeStreamInputNode,
1304
+ selectAll,
1305
+ selectAudio,
1306
+ selectCaptions,
1307
+ selectVideo
1308
+ };
1309
+ //# sourceMappingURL=index.js.map