@norskvideo/norsk-studio-source-switcher 1.9.0-2025-01-27-b21c8c91

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/lib/runtime.js ADDED
@@ -0,0 +1,637 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SourceSwitch = void 0;
7
+ const norsk_sdk_1 = require("@norskvideo/norsk-sdk");
8
+ const runtime_1 = require("@norskvideo/norsk-studio-built-ins/lib/input.silence/runtime");
9
+ const base_nodes_1 = require("@norskvideo/norsk-studio/lib/extension/base-nodes");
10
+ const path_1 = __importDefault(require("path"));
11
+ const promises_1 = __importDefault(require("fs/promises"));
12
+ const yaml_1 = __importDefault(require("yaml"));
13
+ const util_1 = require("@norskvideo/norsk-studio/lib/shared/util");
14
+ const logging_1 = require("@norskvideo/norsk-studio/lib/server/logging");
15
+ const config_1 = require("@norskvideo/norsk-studio/lib/shared/config");
16
+ const webrtcSettings_1 = require("@norskvideo/norsk-studio-built-ins/lib/shared/webrtcSettings");
17
+ const json_refs_1 = require("json-refs");
18
+ class SourceSwitchDefinition {
19
+ async create(norsk, cfg, cb, runtime) {
20
+ const updates = runtime.updates;
21
+ const onActiveSourceChanged = (source, overlays) => {
22
+ updates.raiseEvent({ type: 'active-source-changed', activeSource: source, overlays });
23
+ };
24
+ const onSourceOnline = (source) => {
25
+ updates.raiseEvent({ type: 'source-online', source });
26
+ };
27
+ const onPlayerOnline = ({ source, url }) => {
28
+ updates.raiseEvent({ type: 'player-online', source, url });
29
+ };
30
+ const onSourceOffline = (source) => {
31
+ updates.raiseEvent({ type: 'source-offline', source });
32
+ };
33
+ const onSourcesDiscovered = (sources) => {
34
+ updates.raiseEvent({ type: 'sources-discovered', sources });
35
+ };
36
+ const cfgWithHooks = { onActiveSourceChanged, onSourcesDiscovered, onSourceOnline, onSourceOffline, onPlayerOnline, ...cfg };
37
+ const node = new SourceSwitch(norsk, cfgWithHooks, runtime);
38
+ await node.initialised;
39
+ if (node.whepPreview?.endpointUrl)
40
+ updates.raiseEvent({ type: 'preview-player-online', url: node.whepPreview?.endpointUrl });
41
+ cb(node);
42
+ }
43
+ handleCommand(node, command) {
44
+ const commandType = command.type;
45
+ switch (commandType) {
46
+ case 'select-source':
47
+ node.setActiveSource(command.source, command.overlays);
48
+ break;
49
+ default:
50
+ (0, util_1.assertUnreachable)(commandType);
51
+ }
52
+ }
53
+ async instanceRoutes() {
54
+ const types = await promises_1.default.readFile(path_1.default.join(__dirname, 'types.yaml'));
55
+ const root = yaml_1.default.parse(types.toString());
56
+ const resolved = await (0, json_refs_1.resolveRefs)(root, {}).then((r) => r.resolved);
57
+ return [
58
+ {
59
+ url: '/status',
60
+ method: 'GET',
61
+ handler: ({ runtime: { updates } }) => ((_req, res) => {
62
+ const { availableSources, activeOverlays, activeSource } = updates.latest();
63
+ const value = {
64
+ available: availableSources.map((f) => ({
65
+ source: sourceSwitchSourceToPin(f),
66
+ resolution: f.resolution,
67
+ frameRate: f.frameRate,
68
+ age: (new Date().valueOf() - f.wentLiveAt) / 1000.0
69
+ })),
70
+ active: {
71
+ primary: sourceSwitchSourceToPin(activeSource),
72
+ overlays: activeOverlays.map((o) => ({
73
+ sourceRect: o.sourceRect,
74
+ destRect: o.destRect,
75
+ source: sourceSwitchSourceToPin(o.source)
76
+ }))
77
+ }
78
+ };
79
+ res.json(value);
80
+ }),
81
+ responses: {
82
+ '200': {
83
+ description: "Information about the presently active source and any configured overlays",
84
+ content: {
85
+ "application/json": {
86
+ schema: resolved.components.schemas['status']
87
+ },
88
+ }
89
+ }
90
+ }
91
+ },
92
+ {
93
+ url: '/active',
94
+ method: 'POST',
95
+ handler: ({ runtime: { updates } }) => ((req, res) => {
96
+ const update = req.body;
97
+ const primary = update.primary;
98
+ if (!primary) {
99
+ res.status(400).send(`Source ${primary} does not exist`);
100
+ return;
101
+ }
102
+ const latest = updates.latest();
103
+ const converted = pinToSourceSwitchSource(primary);
104
+ const exists = latest.availableSources.find((s) => s.id == converted.id && s.key == converted.key);
105
+ const overlays = update.overlays?.map((o) => {
106
+ const convertedSource = pinToSourceSwitchSource(o.source);
107
+ return {
108
+ source: convertedSource,
109
+ sourceRect: o.sourceRect,
110
+ destRect: o.destRect,
111
+ };
112
+ });
113
+ if (!exists) {
114
+ res.status(400).send("source isn't active or doesn't exist");
115
+ return;
116
+ }
117
+ if (overlays?.reduce((a, o) => {
118
+ if (a)
119
+ return a;
120
+ const exists = latest.availableSources.find((s) => s.id == o.source.id && s.key == o.source.key);
121
+ if (!exists) {
122
+ res.status(400).send(`overlay ${sourceSwitchSourceToPin(o.source)} isn't active or doesn't exist`);
123
+ }
124
+ return !exists;
125
+ }, false))
126
+ return;
127
+ if (typeof update.transition?.durationMs === 'number') {
128
+ if (update.transition.durationMs > 10000.0) {
129
+ res.status(400).send("fadeMs too large");
130
+ return;
131
+ }
132
+ }
133
+ else {
134
+ if (update.transition)
135
+ update.transition.durationMs = 0;
136
+ }
137
+ updates.sendCommand({
138
+ type: 'select-source',
139
+ source: converted,
140
+ overlays: overlays ?? []
141
+ });
142
+ res.send("ok");
143
+ }),
144
+ requestBody: {
145
+ description: "The new primary source and any overlays required on it",
146
+ content: {
147
+ "application/json": {
148
+ schema: resolved.components.schemas['status-update']
149
+ },
150
+ }
151
+ },
152
+ responses: {
153
+ '204': {
154
+ description: "The update was requested successfully, and will be applied in due course"
155
+ },
156
+ '400': {
157
+ description: "There was a problem with the request"
158
+ }
159
+ }
160
+ }
161
+ ];
162
+ }
163
+ }
164
+ exports.default = SourceSwitchDefinition;
165
+ class SourceSwitch extends base_nodes_1.CustomAutoDuplexNode {
166
+ norsk;
167
+ cfg;
168
+ audio;
169
+ video;
170
+ smooth;
171
+ initialised;
172
+ activeSource = { pin: '', primary: { id: '' }, overlays: [] };
173
+ desiredSource = this.activeSource;
174
+ pendingSource = this.activeSource;
175
+ availableSources = [];
176
+ lastSmoothSwitchContext = new Map();
177
+ streamsOnline = new Set();
178
+ whepOutputs = new Map();
179
+ encodePreview;
180
+ whepPreview;
181
+ subscriptions = [];
182
+ overlayInputs = [];
183
+ updates;
184
+ shared;
185
+ composeCount = 0;
186
+ pendingCompose;
187
+ activeCompose;
188
+ async initialise() {
189
+ const audio = await runtime_1.SilenceSource.create(this.norsk, {
190
+ id: `${this.cfg.id}-audio-silence`,
191
+ displayName: `${this.cfg.id}-audio-silence`,
192
+ channelLayout: this.cfg.channelLayout,
193
+ sampleRate: this.cfg.sampleRate,
194
+ });
195
+ const video = await this.norsk.input.videoTestCard({
196
+ id: `${this.cfg.id}-video-card`,
197
+ sourceName: 'video-card',
198
+ resolution: this.cfg.resolution,
199
+ frameRate: this.cfg.frameRate,
200
+ pattern: 'black',
201
+ });
202
+ const smooth = await this.norsk.processor.control.streamSwitchSmooth({
203
+ id: `${this.cfg.id}-switch`,
204
+ outputSource: "output",
205
+ activeSource: "fallback",
206
+ outputResolution: this.cfg.resolution,
207
+ frameRate: this.cfg.frameRate,
208
+ alignment: "aligned",
209
+ sampleRate: this.cfg.sampleRate,
210
+ channelLayout: this.cfg.channelLayout,
211
+ onInboundContextChange: this.onSwitchContext.bind(this),
212
+ onTransitionComplete: this.onTransitionComplete.bind(this),
213
+ hardwareAcceleration: (0, config_1.contractHardwareAcceleration)(this.cfg.__global.hardware, ['quadra', 'nvidia'])
214
+ });
215
+ if (this.cfg.enablePreviews) {
216
+ this.encodePreview = await this.shared.previewEncode({ source: smooth, sourceSelector: norsk_sdk_1.selectVideo }, this.cfg.__global.hardware);
217
+ this.whepPreview = await this.norsk.output.whep({
218
+ id: `${this.cfg.id}-preview`,
219
+ ...(0, webrtcSettings_1.webRtcSettings)(this.cfg.__global.iceServers),
220
+ });
221
+ this.whepPreview?.subscribe([{
222
+ source: this.encodePreview,
223
+ sourceSelector: norsk_sdk_1.selectVideo
224
+ },
225
+ {
226
+ source: smooth,
227
+ sourceSelector: norsk_sdk_1.selectAudio
228
+ }], norsk_sdk_1.requireAV);
229
+ }
230
+ {
231
+ let prev = 0.0;
232
+ let lastDiff = 0.0;
233
+ const debug = await this.norsk.debug.streamTimestampReport({
234
+ onTimestamp: (_, t) => {
235
+ const now = Number((t.n * 100n) / t.d) / 100;
236
+ const diff = now - prev;
237
+ if (Math.abs(lastDiff - diff) > 0.01) {
238
+ (0, logging_1.debuglog)('ABNORMAL AUDIO GAP DETECTED', { gap: now - prev, lastGap: lastDiff });
239
+ }
240
+ prev = now;
241
+ lastDiff = diff;
242
+ }
243
+ });
244
+ debug.subscribe([
245
+ { source: smooth, sourceSelector: norsk_sdk_1.selectAudio }
246
+ ]);
247
+ }
248
+ {
249
+ let prev = 0.0;
250
+ let lastDiff = 0.0;
251
+ const debug = await this.norsk.debug.streamTimestampReport({
252
+ onTimestamp: (_, t) => {
253
+ const now = Number((t.n * 100n) / t.d) / 100;
254
+ const diff = now - prev;
255
+ if (Math.abs(lastDiff - diff) > 0.01)
256
+ (0, logging_1.debuglog)('ABNORMAL VIDEO GAP DETECTED', { gap: now - prev, lastGap: lastDiff });
257
+ prev = now;
258
+ lastDiff = diff;
259
+ }
260
+ });
261
+ debug.subscribe([
262
+ { source: smooth, sourceSelector: norsk_sdk_1.selectVideo }
263
+ ]);
264
+ }
265
+ this.audio = audio;
266
+ this.video = video;
267
+ this.smooth = smooth;
268
+ this.setup({ input: smooth, output: [smooth] });
269
+ this.subscribe([]);
270
+ }
271
+ constructor(norsk, cfg, runtime) {
272
+ super(cfg.id);
273
+ this.norsk = norsk;
274
+ this.cfg = cfg;
275
+ this.updates = runtime.updates;
276
+ this.shared = runtime.shared;
277
+ this.initialised = this.initialise();
278
+ }
279
+ onTransitionComplete(pin) {
280
+ (0, logging_1.debuglog)("Transition complete", { id: this.id, source: this.activeSource, pin });
281
+ if (pin == this.pendingCompose?.id) {
282
+ if (this.activeCompose) {
283
+ void this.activeCompose.compose.close();
284
+ void this.activeCompose.output.close();
285
+ }
286
+ this.activeCompose = this.pendingCompose;
287
+ this.pendingCompose = undefined;
288
+ this.doSubscriptions();
289
+ }
290
+ else if (this.pendingCompose) {
291
+ void this.pendingCompose.compose.close();
292
+ void this.pendingCompose.output.close();
293
+ this.pendingCompose = undefined;
294
+ }
295
+ this.activeSource = this.desiredSource;
296
+ this.cfg.onActiveSourceChanged?.(this.activeSource.primary, this.activeSource.overlays);
297
+ }
298
+ async onSwitchContext(allStreams) {
299
+ this.lastSmoothSwitchContext = allStreams;
300
+ await this.maybeSwitchSource();
301
+ }
302
+ setActiveSource(source, overlays) {
303
+ (0, logging_1.debuglog)("Request to switch source registered", { id: this.cfg.id, source });
304
+ if (!this.sourceIsAvailable(source)) {
305
+ (0, logging_1.warninglog)("Request to switch source ignored because it doesn't exist", { id: this.cfg.id, source, available: this.availableSources });
306
+ return;
307
+ }
308
+ if (this.pendingCompose)
309
+ return;
310
+ if (this.desiredSource != this.activeSource)
311
+ return;
312
+ if (overlays.length > 0) {
313
+ void this.setupComposeSource(source, overlays);
314
+ }
315
+ else {
316
+ this.desiredSource = { pin: sourceSwitchSourceToPin(source), primary: source, overlays: [] };
317
+ void this.maybeSwitchSource();
318
+ }
319
+ }
320
+ async maybeSwitchToFallback(source) {
321
+ if (this.pendingSource.primary.id == "fallback")
322
+ return;
323
+ const allStreams = this.lastSmoothSwitchContext;
324
+ const currentUnavailable = !this.sourceIsAvailable(source.primary) || allStreams.get(sourceSwitchSourceToPin(source.primary))?.length !== 2;
325
+ const fallbackAvailable = this.sourceIsAvailable({ id: 'fallback' }) && allStreams.get(sourceSwitchSourceToPin({ id: 'fallback' }))?.length == 2;
326
+ if (currentUnavailable && fallbackAvailable) {
327
+ (0, logging_1.debuglog)("Switching to fallback source", { id: this.id });
328
+ this.desiredSource = { pin: 'fallback', primary: { id: 'fallback' }, overlays: [] };
329
+ this.pendingSource = this.desiredSource;
330
+ await this.smooth?.switchSource("fallback");
331
+ }
332
+ }
333
+ async maybeSwitchSource() {
334
+ const oldSources = this.availableSources;
335
+ const allStreams = this.lastSmoothSwitchContext;
336
+ this.availableSources = [...allStreams.keys()].map(pinToSourceSwitchSource);
337
+ if (this.pendingSource !== this.desiredSource) {
338
+ const currentAvailable = this.sourceIsAvailable(this.desiredSource.primary) && allStreams.get(this.desiredSource.pin)?.length == 2;
339
+ if (currentAvailable) {
340
+ this.pendingSource = this.desiredSource;
341
+ await this.smooth?.switchSource(this.desiredSource.pin);
342
+ }
343
+ }
344
+ else if (this.pendingSource != this.activeSource) {
345
+ await this.maybeSwitchToFallback(this.pendingSource);
346
+ }
347
+ else {
348
+ await this.maybeSwitchToFallback(this.activeSource);
349
+ }
350
+ for (const existing of oldSources) {
351
+ if (existing.id == this.pendingCompose?.id)
352
+ continue;
353
+ if (existing.id == this.activeCompose?.id)
354
+ continue;
355
+ const pin = sourceSwitchSourceToPin(existing);
356
+ if (!this.sourceIsAvailable(existing) || allStreams.get(pin)?.length == 0) {
357
+ if (this.streamsOnline.has(pin)) {
358
+ (0, logging_1.debuglog)("Source offline", { id: this.id, source: existing });
359
+ this.streamsOnline.delete(pin);
360
+ this.cfg?.onSourceOffline(existing);
361
+ }
362
+ const player = this.whepOutputs.get(pin);
363
+ if (player) {
364
+ this.whepOutputs.delete(pin);
365
+ await player.whep.close();
366
+ }
367
+ }
368
+ }
369
+ for (const current of this.availableSources) {
370
+ if (current.id == this.pendingCompose?.id)
371
+ continue;
372
+ if (current.id == this.activeCompose?.id)
373
+ continue;
374
+ const pin = sourceSwitchSourceToPin(current);
375
+ if (!this.streamsOnline.has(pin) && allStreams.get(pin)?.length == 2) {
376
+ (0, logging_1.debuglog)("Source online", { id: this.id, source: current });
377
+ const streams = allStreams.get(pin);
378
+ const video = streams?.find((f) => f.message.case == "video");
379
+ if (video && video.message.case == "video") {
380
+ this.streamsOnline.add(pin);
381
+ this.cfg?.onSourceOnline({
382
+ resolution: {
383
+ width: video.message.value.width,
384
+ height: video.message.value.height
385
+ },
386
+ frameRate: video.message.value.frameRate,
387
+ wentLiveAt: new Date().valueOf(),
388
+ ...current
389
+ });
390
+ if (this.cfg.enablePreviews && !this.whepOutputs.get(pin)) {
391
+ const whep = await this.norsk.output.whep({
392
+ id: `${this.id}-whep-${pin}`,
393
+ ...(0, webrtcSettings_1.webRtcSettings)(this.cfg.__global.iceServers),
394
+ });
395
+ this.whepOutputs.set(pin, { whep });
396
+ this.cfg?.onPlayerOnline({ source: current, url: whep.endpointUrl });
397
+ }
398
+ }
399
+ }
400
+ }
401
+ void this.setupPreviewPlayers();
402
+ }
403
+ async setupComposeSource(source, overlays) {
404
+ if (this.pendingCompose) {
405
+ await this.pendingCompose.compose.close();
406
+ await this.pendingCompose.output.close();
407
+ this.pendingCompose = undefined;
408
+ }
409
+ const id = `${this.id}-compose-${this.composeCount++}`;
410
+ this.pendingCompose = {
411
+ id,
412
+ compose: await this.norsk.processor.transform.videoCompose({
413
+ id,
414
+ outputResolution: this.cfg.resolution,
415
+ referenceStream: 'background',
416
+ missingStreamBehaviour: 'drop_part',
417
+ parts: [
418
+ {
419
+ pin: "background",
420
+ compose: norsk_sdk_1.VideoComposeDefaults.fullscreen(),
421
+ opacity: 1.0,
422
+ zIndex: 0
423
+ },
424
+ ...overlays.map((o, i) => ({
425
+ pin: `overlay-${i}`,
426
+ opacity: 1.0,
427
+ zIndex: i + 1,
428
+ compose: (metadata, settings) => ({
429
+ sourceRect: o.sourceRect ?? { x: 0, y: 0, width: metadata.width, height: metadata.height },
430
+ destRect: o.destRect ?? { x: 0, y: 0, width: settings.outputResolution.width, height: settings.outputResolution.height }
431
+ })
432
+ }))
433
+ ]
434
+ }),
435
+ output: await this.norsk.processor.transform.streamKeyOverride({
436
+ id: `${id}-video`,
437
+ streamKey: {
438
+ streamId: 256,
439
+ programNumber: 1,
440
+ sourceName: id,
441
+ renditionName: 'default'
442
+ }
443
+ }),
444
+ audio: await this.norsk.processor.transform.streamKeyOverride({
445
+ id: `${id}-audio`,
446
+ streamKey: {
447
+ streamId: 257,
448
+ programNumber: 1,
449
+ sourceName: id,
450
+ renditionName: 'default'
451
+ }
452
+ })
453
+ };
454
+ const background = this.subscriptions.find((s) => s.source.id == source.id);
455
+ if (!background) {
456
+ (0, logging_1.warninglog)("Unable to find source for stream", { source });
457
+ await this.pendingCompose.compose.close();
458
+ await this.pendingCompose.output.close();
459
+ this.pendingCompose = undefined;
460
+ return;
461
+ }
462
+ this.pendingCompose.output.subscribe([
463
+ { source: this.pendingCompose.compose, sourceSelector: norsk_sdk_1.selectVideo }
464
+ ]);
465
+ await this.ensureEnoughOverlaysExist(overlays);
466
+ overlays.forEach((o, i) => {
467
+ const source = this.subscriptions.find((s) => s.source.id == o.source.id);
468
+ if (!source) {
469
+ this.overlayInputs[i].subscribe([]);
470
+ return;
471
+ }
472
+ this.overlayInputs[i].subscribe(o.source.key ?
473
+ [source.selectVideoForKey(o.source.key)[0]] :
474
+ [source.selectVideo()[0]]);
475
+ });
476
+ this.pendingCompose.compose.subscribeToPins([
477
+ source.key ?
478
+ background.selectVideoToPinForKey("background", source.key)[0] :
479
+ background.selectVideoToPin("background")[0],
480
+ ...overlays.map((_o, i) => {
481
+ return {
482
+ source: this.overlayInputs[i],
483
+ sourceSelector: (0, norsk_sdk_1.videoToPin)(`overlay-${i}`)
484
+ };
485
+ })
486
+ ]);
487
+ this.pendingCompose.audio.subscribe([
488
+ source.key ?
489
+ background.selectAudioForKey(source.key)[0] :
490
+ background.selectAudio()[0]
491
+ ]);
492
+ this.desiredSource = {
493
+ pin: `${this.pendingCompose.id}`, overlays, primary: source
494
+ };
495
+ this.doSubscriptions();
496
+ }
497
+ async ensureEnoughOverlaysExist(overlays) {
498
+ while (this.overlayInputs.length < overlays.length) {
499
+ const i = await this.norsk.processor.transform.streamKeyOverride({
500
+ id: `${this.id}-compose-input-${this.overlayInputs.length}`,
501
+ streamKey: {
502
+ programNumber: 1,
503
+ streamId: 256,
504
+ sourceName: "compose-input",
505
+ renditionName: `input-${this.overlayInputs.length}`
506
+ }
507
+ });
508
+ this.overlayInputs.push(i);
509
+ }
510
+ }
511
+ sourceIsAvailable(source) {
512
+ return !!this.availableSources.find((s) => s.id == source.id && s.key == source.key);
513
+ }
514
+ subscribe(subs, opts) {
515
+ this.subscriptions = subs;
516
+ this.doSubscriptions(opts);
517
+ void this.setupPreviewPlayers();
518
+ }
519
+ doSubscriptions(opts) {
520
+ const knownSources = [];
521
+ const subs = this.subscriptions;
522
+ const subscriptions = subs.flatMap((s) => {
523
+ const sType = s.streams.type;
524
+ switch (sType) {
525
+ case 'take-all-streams':
526
+ return s.activeSourceKeys().flatMap((key) => {
527
+ knownSources.push({ id: s.source.id });
528
+ return s.selectAvToPinForKey(pinName(s.source.id, key), key);
529
+ });
530
+ case 'take-first-stream':
531
+ knownSources.push({ id: s.source.id });
532
+ return s.selectAvToPin(pinName(s.source.id));
533
+ case 'take-specific-stream':
534
+ knownSources.push({ id: s.source.id });
535
+ return s.selectAvToPin((pinName(s.source.id)));
536
+ case 'take-specific-streams':
537
+ return s.activeSourceKeys().flatMap((key) => {
538
+ knownSources.push({ id: s.source.id, key });
539
+ return s.selectAvToPinForKey(pinName(s.source.id, key), key);
540
+ });
541
+ default:
542
+ (0, util_1.assertUnreachable)(sType);
543
+ }
544
+ }).filter((x) => !!x) ?? [];
545
+ if (this.audio)
546
+ subscriptions.push({ source: (this.audio.relatedMediaNodes.output[0]), sourceSelector: (0, norsk_sdk_1.audioToPin)("fallback") });
547
+ if (this.video) {
548
+ subscriptions.push({ source: this.video, sourceSelector: (0, norsk_sdk_1.videoToPin)("fallback") });
549
+ knownSources.push({ id: "fallback" });
550
+ }
551
+ this.cfg.onSourcesDiscovered(knownSources);
552
+ if (this.pendingCompose) {
553
+ subscriptions.push({
554
+ source: this.pendingCompose.output,
555
+ sourceSelector: (0, norsk_sdk_1.videoToPin)(`${this.pendingCompose.id}`)
556
+ });
557
+ subscriptions.push({
558
+ source: this.pendingCompose.audio,
559
+ sourceSelector: (0, norsk_sdk_1.audioToPin)(`${this.pendingCompose.id}`)
560
+ });
561
+ }
562
+ if (this.activeCompose) {
563
+ subscriptions.push({
564
+ source: this.activeCompose.output,
565
+ sourceSelector: (0, norsk_sdk_1.videoToPin)(`${this.activeCompose.id}`)
566
+ });
567
+ subscriptions.push({
568
+ source: this.activeCompose.audio,
569
+ sourceSelector: (0, norsk_sdk_1.audioToPin)(`${this.activeCompose.id}`)
570
+ });
571
+ }
572
+ (0, logging_1.debuglog)("Subcription complete, known sources", { id: this.id, knownSources });
573
+ this.smooth?.subscribeToPins(subscriptions, opts?.requireOneOfEverything ? (ctx) => ctx.streams.length == (subs.length * 2) + 2 : undefined);
574
+ }
575
+ async setupPreviewPlayers() {
576
+ if (!this.cfg.enablePreviews) {
577
+ return;
578
+ }
579
+ for (const [active, player] of this.whepOutputs) {
580
+ const [id, key] = pinToSourceAndKey(active);
581
+ const source = this.subscriptions.find((s) => s.source.id == id);
582
+ if (!player)
583
+ continue;
584
+ if (active === "fallback") {
585
+ const subscriptions = [];
586
+ if (this.audio)
587
+ subscriptions.push({ source: (this.audio.relatedMediaNodes.output[0]), sourceSelector: norsk_sdk_1.selectAudio });
588
+ if (this.video) {
589
+ if (!player.encoder) {
590
+ player.encoder = await this.shared.previewEncode({ source: this.video, sourceSelector: norsk_sdk_1.selectVideo }, this.cfg.__global.hardware);
591
+ }
592
+ subscriptions.push({ source: player.encoder, sourceSelector: norsk_sdk_1.selectVideo });
593
+ }
594
+ player.whep.subscribe(subscriptions);
595
+ }
596
+ else {
597
+ if (!source)
598
+ continue;
599
+ if (!player.encoder) {
600
+ player.encoder = await this.shared.previewEncode(key ? source.selectVideoForKey(key)[0] : source.selectVideo()[0], this.cfg.__global.hardware);
601
+ }
602
+ player.whep.subscribe((key ? source.selectAudioForKey(key) : source.selectAudio()).concat([
603
+ { source: player.encoder, sourceSelector: norsk_sdk_1.selectVideo }
604
+ ]));
605
+ }
606
+ }
607
+ }
608
+ async close() {
609
+ await super.close();
610
+ await this.audio?.close();
611
+ await this.video?.close();
612
+ }
613
+ }
614
+ exports.SourceSwitch = SourceSwitch;
615
+ function pinName(sourceId, key) {
616
+ if (key) {
617
+ return `${sourceId}__${key}`;
618
+ }
619
+ else {
620
+ return sourceId;
621
+ }
622
+ }
623
+ function pinToSourceAndKey(pin) {
624
+ if (pin.indexOf('__') >= 0) {
625
+ const result = pin.split('__', 2);
626
+ return [result[0], result[1]];
627
+ }
628
+ return [pin, undefined];
629
+ }
630
+ function pinToSourceSwitchSource(pin) {
631
+ const [id, key] = pinToSourceAndKey(pin);
632
+ return { id, key };
633
+ }
634
+ function sourceSwitchSourceToPin(source) {
635
+ return pinName(source.id, source.key);
636
+ }
637
+ //# sourceMappingURL=runtime.js.map