@norskvideo/norsk-sdk 1.0.341 → 1.0.342

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.
@@ -80,8 +80,20 @@ export declare class SourceMediaNode extends MediaNodeState {
80
80
  registerForContextChange(subscriber: SinkMediaNode<string>): void;
81
81
  unregisterForContextChange(subscriber: SinkMediaNode<string>): void;
82
82
  }
83
+ /**
84
+ * @public
85
+ * Determines what to do with an incoming context
86
+ *
87
+ * - true/accept: Allow the incoming context through, and any subsequent/queued data that belongs to it
88
+ * - false/deny: Deny the incoming context, if no context has been accepted, then queue data until one is
89
+ * - accept_and_terminate: Allow the incoming context, then deny further data, flush and shut down the node
90
+ * this is useful for cleanly terminating outputs when the context is empty
91
+ * */
92
+ export declare type SubscriptionValidationResponse = true | false | "accept" | "deny" | "accept_and_terminate";
83
93
  /** @public */
84
94
  export declare class SinkMediaNode<Pins extends string> extends MediaNodeState {
95
+ permissiveSubscriptionValidation(_context: Context): SubscriptionValidationResponse;
96
+ restrictiveSubscriptionValidation(context: Context): SubscriptionValidationResponse;
85
97
  /** Subscribe to the given sources.
86
98
  *
87
99
  * This version of the function call accepts the target pins of an output
@@ -97,8 +109,9 @@ export declare class SinkMediaNode<Pins extends string> extends MediaNodeState {
97
109
  *
98
110
  * Errors are also logged to the debug log.
99
111
  */
100
- subscribeToPins(sources: ReceiveFromAddress<Pins>[], validation?: (context: Context) => boolean, done?: (error?: SubscriptionError) => void): void;
112
+ subscribeToPins(sources: ReceiveFromAddress<Pins>[], validation?: (context: Context) => SubscriptionValidationResponse, done?: (error?: SubscriptionError) => void): void;
101
113
  sourceContextChange(responseCallback: (error?: SubscriptionError) => void): Promise<boolean>;
114
+ finalise(): void;
102
115
  }
103
116
  /** @public */
104
117
  export declare class AutoSinkMediaNode<Pins extends string> extends SinkMediaNode<Pins | "auto"> {
@@ -117,6 +130,6 @@ export declare class AutoSinkMediaNode<Pins extends string> extends SinkMediaNod
117
130
  *
118
131
  * Errors are also logged to the debug log.
119
132
  */
120
- subscribe(sources: ReceiveFromAddressAuto[], validation?: (context: Context) => boolean, done?: (error?: SubscriptionError) => void): void;
133
+ subscribe(sources: ReceiveFromAddressAuto[], validation?: (context: Context) => SubscriptionValidationResponse, done?: (error?: SubscriptionError) => void): void;
121
134
  }
122
135
  //# sourceMappingURL=common.d.ts.map
@@ -69,6 +69,10 @@ class MediaNodeState {
69
69
  close() {
70
70
  console.log("Default close operator called on medianode, this is probably a missing implementation");
71
71
  }
72
+ /** @internal */
73
+ finalise() {
74
+ // We don't care here, only in Sink
75
+ }
72
76
  }
73
77
  exports.MediaNodeState = MediaNodeState;
74
78
  /** @public */
@@ -90,18 +94,20 @@ class SourceMediaNode extends MediaNodeState {
90
94
  this.subscribers.delete(subscriber);
91
95
  }
92
96
  /** @internal */
93
- subscriptionComplete(context) {
97
+ subscriptionComplete(context, subscriber) {
94
98
  return () => {
95
- const numPending = this.pendingContextAcks.get(context);
96
- if (numPending === undefined)
99
+ const pending = this.pendingContextAcks.get(context);
100
+ if (pending === undefined)
97
101
  return;
98
- if (numPending == 1) {
102
+ (0, utils_1.debuglog)("Checking context on %s: %O", this.id, context.blockingCallRef);
103
+ if (pending.length == 1) {
99
104
  (0, utils_1.debuglog)("Node '%s' acknowledging context %O", this.id, context.blockingCallRef);
100
105
  _ackContext(this.client, this.id, context);
101
106
  this.pendingContextAcks.delete(context);
102
107
  }
103
108
  else {
104
- this.pendingContextAcks.set(context, numPending - 1);
109
+ let index = pending.indexOf(subscriber); // should error if not null?
110
+ this.pendingContextAcks.set(context, pending.splice(index, 1));
105
111
  }
106
112
  };
107
113
  }
@@ -114,18 +120,18 @@ class SourceMediaNode extends MediaNodeState {
114
120
  if (this.onOutboundContextChange) {
115
121
  yield this.onOutboundContextChange(this.outputStreams);
116
122
  }
117
- let numPending = 0;
123
+ let pending = [];
118
124
  for (let [subscriber, _] of this.subscribers) {
119
- const pending = yield subscriber.sourceContextChange(this.subscriptionComplete(context));
120
- if (pending) {
121
- numPending++;
125
+ const alive = yield subscriber.sourceContextChange(this.subscriptionComplete(context, subscriber));
126
+ if (alive) {
127
+ pending.push(subscriber);
122
128
  }
123
129
  else {
124
130
  (0, utils_1.debuglog)("Skipping updating subscription to closed node %s", subscriber.id);
125
131
  }
126
132
  }
127
- if (numPending > 0) {
128
- this.pendingContextAcks.set(context, numPending);
133
+ if (pending.length > 0) {
134
+ this.pendingContextAcks.set(context, pending);
129
135
  }
130
136
  else {
131
137
  (0, utils_1.debuglog)("Node '%s' acknowledging context immediately %O", this.id, context.blockingCallRef);
@@ -145,6 +151,8 @@ class SinkMediaNode extends MediaNodeState {
145
151
  /** @internal */
146
152
  this.currentSubscription = null;
147
153
  /** @internal */
154
+ this.subscriptionValidated = false;
155
+ /** @internal */
148
156
  this.currentSourcesActive = 0;
149
157
  /** @internal */
150
158
  this.subscriptionResponseCallbacks = new Map();
@@ -153,30 +161,46 @@ class SinkMediaNode extends MediaNodeState {
153
161
  this.getGrpcStream = getGrpcStream;
154
162
  this.subscribeFn = subscribeFn;
155
163
  this.subscribeErrorFn = subscribeErrorFn;
156
- this.subscriptionValidation = (_) => true;
164
+ this.subscriptionValidation = this.permissiveSubscriptionValidation;
165
+ this.defaultSubscriptionValidation = this.restrictiveSubscriptionValidation;
157
166
  this.subscribedStreamsChangedFn = subscribedStreamsChangedFn;
158
167
  }
168
+ // Always allow streams and contexts through, essentially a no-op
169
+ // This is the default case for most nodes that aren't outputs or don't need to
170
+ // synchronise their streams at all
171
+ permissiveSubscriptionValidation(_context) {
172
+ return "accept";
173
+ }
159
174
  // If all sources have returned some keys
160
175
  // and if all streams are accounted for in the context
161
176
  // then we're good to go, otherwise, wait and keep queuing
162
- /** @internal */
163
- defaultSubscriptionValidation(context) {
177
+ restrictiveSubscriptionValidation(context) {
164
178
  if (!this.currentSubscription) {
165
179
  (0, utils_1.debuglog)("Node %s ignoring inbound context, no current subscription", this.id);
166
- return false;
167
- }
168
- if (this.currentSourcesActive < this.currentSources.size) {
169
- (0, utils_1.debuglog)("Node '%s' ignoring inbound context because not all sources are active yet %O", this.id, debugFormatContext(context));
170
- return false;
180
+ return "deny";
171
181
  }
172
182
  const numStreams = context.streams.length;
173
183
  const expected = this.currentSubscription.sources.length;
184
+ if (numStreams == 0 && this.subscriptionValidated) {
185
+ (0, utils_1.debuglog)("Node '%s' accepting inbound context because it is empty and we have previously accepted a full context %O", this.id, debugFormatContext(context));
186
+ this.subscriptionValidated = false;
187
+ return "accept";
188
+ }
189
+ if (this.currentSourcesActive < this.currentSources.size && !this.subscriptionValidated) {
190
+ (0, utils_1.debuglog)("Node '%s' ignoring inbound context because not all sources are active yet %O", this.id, debugFormatContext(context));
191
+ return "deny";
192
+ }
193
+ if (this.currentSourcesActive < this.currentSources.size) {
194
+ (0, utils_1.debuglog)("Node '%s' ignoring inbound context because it is smaller than the already-accepted one %O", this.id, debugFormatContext(context));
195
+ return "deny";
196
+ }
174
197
  if (numStreams == expected) {
175
198
  (0, utils_1.debuglog)("Node '%s' accepting inbound context because all keys and streams are accounted for %O", this.id, debugFormatContext(context));
176
- return true;
199
+ this.subscriptionValidated = true;
200
+ return "accept";
177
201
  }
178
202
  (0, utils_1.debuglog)("Node '%s' ignoring inbound context because not all keys are accounted for yet %O", this.id, debugFormatContext(context));
179
- return false;
203
+ return "deny";
180
204
  }
181
205
  /** Subscribe to the given sources.
182
206
  *
@@ -298,11 +322,33 @@ class SinkMediaNode extends MediaNodeState {
298
322
  .bind(this.client);
299
323
  yield responseFn(new media_pb_1.ValidationResponse({
300
324
  mediaNodeId: (0, types_1.toMediaNodeId)(this.id),
301
- result: validationResponse,
325
+ result: this.toValidationResponse(validationResponse),
302
326
  }));
303
327
  _ackContext(this.client, this.id, context);
304
328
  });
305
329
  }
330
+ /** @internal */
331
+ toValidationResponse(response) {
332
+ switch (response) {
333
+ case true:
334
+ case "accept":
335
+ return media_pb_1.ValidationResponse_ContextValidationResponse.CONTEXT_VALIDATION_ALLOW;
336
+ case false:
337
+ case "deny":
338
+ return media_pb_1.ValidationResponse_ContextValidationResponse.CONTEXT_VALIDATION_DENY;
339
+ case "accept_and_terminate":
340
+ return media_pb_1.ValidationResponse_ContextValidationResponse.CONTEXT_VALIDATION_ALLOW_AND_TERMINATE;
341
+ default:
342
+ (0, utils_1.exhaustiveCheck)(response);
343
+ }
344
+ }
345
+ finalise() {
346
+ // These will now never be invoked because grpc is gone, so do it now so we don't hang
347
+ for (const [id, callback] of this.subscriptionResponseCallbacks) {
348
+ (0, utils_1.debuglog)("Invoking subscription response for node %s because of a close %s", this.id, id);
349
+ callback();
350
+ }
351
+ }
306
352
  }
307
353
  exports.SinkMediaNode = SinkMediaNode;
308
354
  /** @public */
@@ -361,7 +407,7 @@ function registerStreamHandlers(grpcStream, unregisterNode, tag, reject, setting
361
407
  console.log("Error:", source, err.code, constants_1.Status[err.code], err.details, (_a = err.metadata) === null || _a === void 0 ? void 0 : _a.getMap());
362
408
  };
363
409
  grpcStream.on("end", () => {
364
- (0, utils_1.debuglog)(`End of ${tag} source`);
410
+ (0, utils_1.debuglog)(`End of ${tag} node`);
365
411
  grpcStream.destroy();
366
412
  });
367
413
  grpcStream.on("error", (error) => {
@@ -369,11 +415,11 @@ function registerStreamHandlers(grpcStream, unregisterNode, tag, reject, setting
369
415
  if (constants_1.Status[error.code] == "CANCELLED") {
370
416
  return;
371
417
  }
372
- logError(`${tag} source`, error);
418
+ logError(`${tag} node`, error);
373
419
  settings.onError && settings.onError(error);
374
420
  });
375
421
  grpcStream.on("close", () => {
376
- (0, utils_1.debuglog)(`${tag} source close`);
422
+ (0, utils_1.debuglog)(`${tag} node close`);
377
423
  unregisterNode();
378
424
  settings.onClose && settings.onClose();
379
425
  reject();
@@ -565,7 +565,7 @@ class WhipInputNode extends common_1.SourceMediaNode {
565
565
  }
566
566
  }
567
567
  });
568
- (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "whip", reject, settings);
568
+ (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "whipInput", reject, settings);
569
569
  });
570
570
  }
571
571
  /** @internal */
@@ -1096,7 +1096,7 @@ class FileMp4InputNode extends common_1.SourceMediaNode {
1096
1096
  this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.FileMp4InputMessage, (0, utils_1.mkMessageCase)({ initialConfig: config })));
1097
1097
  this.initialised = new Promise((resolve, reject) => {
1098
1098
  this.grpcStream.on("data", (data) => {
1099
- var _a, _b, _c;
1099
+ var _a, _b, _c, _d;
1100
1100
  const messageCase = data.message.case;
1101
1101
  switch (messageCase) {
1102
1102
  case undefined:
@@ -1132,6 +1132,9 @@ class FileMp4InputNode extends common_1.SourceMediaNode {
1132
1132
  (_c = settings.onStreamStatistics) === null || _c === void 0 ? void 0 : _c.call(settings, (0, types_1.fromStreamStatistics)(data.message.value, this.outputStreams));
1133
1133
  break;
1134
1134
  }
1135
+ case "gopStructure":
1136
+ (_d = settings.onGopStructure) === null || _d === void 0 ? void 0 : _d.call(settings, data.message.value);
1137
+ break;
1135
1138
  default:
1136
1139
  (0, utils_1.exhaustiveCheck)(messageCase);
1137
1140
  }
@@ -48,6 +48,10 @@ export interface CmafOutputSettings extends SinkNodeSettings<CmafAudioOutputNode
48
48
  * XML fragment to add to the mpd Representation element
49
49
  */
50
50
  mpdAdditions?: string;
51
+ /**
52
+ * Audio or video bitrate for the {@link NorskOutput.cmafMultiVariant} playlist
53
+ */
54
+ bitrate?: number;
51
55
  }
52
56
  /**
53
57
  * @public
@@ -84,6 +88,10 @@ export interface HlsTsVideoOutputSettings extends SinkNodeSettings<HlsTsVideoOut
84
88
  * XML fragment to add to the mpd Representation element
85
89
  */
86
90
  mpdAdditions?: string;
91
+ /**
92
+ * Video bitrate for the {@link NorskOutput.hlsTsMultiVariant} playlist
93
+ */
94
+ bitrate?: number;
87
95
  }
88
96
  /**
89
97
  * @public
@@ -120,6 +128,10 @@ export interface HlsTsAudioOutputSettings extends SinkNodeSettings<HlsTsAudioOut
120
128
  * XML fragment to add to the mpd Representation element
121
129
  */
122
130
  mpdAdditions?: string;
131
+ /**
132
+ * Audio bitrate for the {@link NorskOutput.hlsTsMultiVariant} playlist
133
+ */
134
+ bitrate?: number;
123
135
  }
124
136
  /**
125
137
  * @public
@@ -368,7 +380,7 @@ declare class CmafNodeBase<ClientMessage, Pins extends string, T extends MediaNo
368
380
  close(): void;
369
381
  }
370
382
  declare class CmafNodeWithPlaylist<ClientMessage, Pins extends string, T extends MediaNodeState> extends CmafNodeBase<ClientMessage, Pins, T> {
371
- constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => void, playlistPath: PlaylistPath);
383
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => void, playlistPath: PlaylistPath, sessionId: string);
372
384
  /**
373
385
  * @public
374
386
  * Returns the URL to the HLS playlist entry. Note this can only be evaluated once the stream is active as it
@@ -384,11 +396,11 @@ declare class CmafNodeWithPlaylist<ClientMessage, Pins extends string, T extends
384
396
  */
385
397
  export declare class CmafVideoOutputNode extends CmafNodeWithPlaylist<CmafVideoMessage, "video", CmafVideoOutputNode> {
386
398
  /**
387
- * @public
388
- * Updates the credentials for a specific destination within this output by id
389
- * see: {@link UpdateCredentials}
390
- * see: {@link CmafDestinationSettings}
391
- */
399
+ * @public
400
+ * Updates the credentials for a specific destination within this output by id
401
+ * see: {@link UpdateCredentials}
402
+ * see: {@link CmafDestinationSettings}
403
+ */
392
404
  updateCredentials(settings: UpdateCredentials): void;
393
405
  }
394
406
  /**
@@ -613,6 +625,8 @@ export interface RtmpOutputSettings extends SinkNodeSettings<RtmpOutputNode>, St
613
625
  url: string;
614
626
  /** Jitter buffer delay in milliseconds */
615
627
  bufferDelayMs?: number;
628
+ /** Called when the RTMP output succesfully connects to a server and starts publishing data */
629
+ onPublishStart?: () => void;
616
630
  }
617
631
  /**
618
632
  * @public
@@ -660,6 +674,11 @@ export interface FileMp4OutputSettings extends SinkNodeSettings<FileMp4OutputNod
660
674
  * Settings for encrypting the video track.
661
675
  */
662
676
  videoEncryption?: EncryptionSettings;
677
+ /**
678
+ * Callback that will be invoked once data stops being received by the node (determined by an empty context)
679
+ * at which point it will automatically shut down
680
+ */
681
+ onStreamEof?: () => void;
663
682
  }
664
683
  /**
665
684
  * @public
@@ -64,8 +64,9 @@ class CmafNodeBase extends processor_1.AutoProcessorMediaNode {
64
64
  }
65
65
  }
66
66
  class CmafNodeWithPlaylist extends CmafNodeBase {
67
- constructor(client, unregisterNode, settings, grpcInit, subscribeFn, playlistPath) {
67
+ constructor(client, unregisterNode, settings, grpcInit, subscribeFn, playlistPath, sessionId) {
68
68
  super(client, unregisterNode, settings, grpcInit, subscribeFn, playlistPath, (streams) => {
69
+ this.sessionId = sessionId;
69
70
  if (streams.length === 0) {
70
71
  this.subscribedStream = undefined;
71
72
  }
@@ -73,7 +74,7 @@ class CmafNodeWithPlaylist extends CmafNodeBase {
73
74
  // An HLS segmenter can only be subscribed to one stream.
74
75
  const theStream = streams[0];
75
76
  this.subscribedStream = theStream;
76
- const url = makePlaylistUrl(theStream, playlistPath);
77
+ const url = makePlaylistUrl(theStream, playlistPath, this.sessionId);
77
78
  this.pendingUrlRequests.forEach((resolve) => resolve(url));
78
79
  this.pendingUrlRequests = [];
79
80
  }
@@ -91,7 +92,7 @@ class CmafNodeWithPlaylist extends CmafNodeBase {
91
92
  return __awaiter(this, void 0, void 0, function* () {
92
93
  return (new Promise((resolve, _reject) => {
93
94
  if (this.subscribedStream) {
94
- resolve(makePlaylistUrl(this.subscribedStream, this.playlistPath));
95
+ resolve(makePlaylistUrl(this.subscribedStream, this.playlistPath, this.sessionId));
95
96
  }
96
97
  else {
97
98
  this.pendingUrlRequests.push(resolve);
@@ -124,18 +125,18 @@ class CmafVideoOutputNode extends CmafNodeWithPlaylist {
124
125
  ? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
125
126
  : undefined, encryption: settings.encryption
126
127
  ? (0, types_1.mkEncryptionSettings)(settings.encryption)
127
- : undefined, destinations: settings.destinations.map(mkCmafDestination), delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "" }));
128
+ : undefined, destinations: settings.destinations.map(mkCmafDestination), delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "", bitrate: settings.bitrate || 0 }));
128
129
  const hls = client.createOutputCmafVideo();
129
130
  hls.write((0, utils_1.provideFull)(media_pb_1.CmafVideoMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
130
131
  return hls;
131
- }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.CmafVideoMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Cmaf);
132
+ }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.CmafVideoMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Cmaf, localDestinationSessionId(settings.destinations) || "");
132
133
  }
133
134
  /**
134
- * @public
135
- * Updates the credentials for a specific destination within this output by id
136
- * see: {@link UpdateCredentials}
137
- * see: {@link CmafDestinationSettings}
138
- */
135
+ * @public
136
+ * Updates the credentials for a specific destination within this output by id
137
+ * see: {@link UpdateCredentials}
138
+ * see: {@link CmafDestinationSettings}
139
+ */
139
140
  updateCredentials(settings) {
140
141
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
142
  const conf = { destinationId: settings.destinationId, awsCredentials: (0, utils_1.provideFull)(media_pb_1.AwsCredentials, settings.awsCredentials) };
@@ -164,11 +165,11 @@ class CmafAudioOutputNode extends CmafNodeWithPlaylist {
164
165
  ? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
165
166
  : undefined, encryption: settings.encryption
166
167
  ? (0, types_1.mkEncryptionSettings)(settings.encryption)
167
- : undefined, destinations: settings.destinations.map(mkCmafDestination), delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "" }));
168
+ : undefined, destinations: settings.destinations.map(mkCmafDestination), delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "", bitrate: settings.bitrate || 0 }));
168
169
  const hls = client.createOutputCmafAudio();
169
170
  hls.write((0, utils_1.provideFull)(media_pb_1.CmafAudioMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
170
171
  return hls;
171
- }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.CmafAudioMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Cmaf);
172
+ }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.CmafAudioMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Cmaf, localDestinationSessionId(settings.destinations) || "");
172
173
  }
173
174
  /**
174
175
  * @public
@@ -184,7 +185,12 @@ class CmafAudioOutputNode extends CmafNodeWithPlaylist {
184
185
  }
185
186
  exports.CmafAudioOutputNode = CmafAudioOutputNode;
186
187
  /** @private */
187
- function makePlaylistUrl(stream, path) {
188
+ function localDestinationSessionId(destinations) {
189
+ var _a;
190
+ return (_a = destinations.filter((d) => d.type === "local")[0]) === null || _a === void 0 ? void 0 : _a.sessionId;
191
+ }
192
+ /** @private */
193
+ function makePlaylistUrl(stream, path, sessionId) {
188
194
  const theStreamKey = stream.streamKey;
189
195
  let pathPrefix;
190
196
  switch (path) {
@@ -195,7 +201,9 @@ function makePlaylistUrl(stream, path) {
195
201
  pathPrefix = "ts";
196
202
  break;
197
203
  }
198
- return `${(0, utils_1.publicUrlPrefix)()}/${pathPrefix}/stream/${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.sourceName}/${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.programNumber}/${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.streamId}/${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.renditionName}/norsk.m3u8`;
204
+ const sessionIdPath = sessionId && sessionId !== "" ? sessionId + "/" : "";
205
+ // TODO Should really come from the server
206
+ return `${(0, utils_1.publicUrlPrefix)()}/${pathPrefix}/${sessionIdPath}${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.sourceName}/${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.programNumber}/${theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.streamId}/${(theStreamKey === null || theStreamKey === void 0 ? void 0 : theStreamKey.renditionName) + "_local"}/norsk.m3u8`;
199
207
  }
200
208
  // TODO we'll need a Dash URL too?
201
209
  /**
@@ -217,11 +225,11 @@ class HlsTsVideoOutputNode extends CmafNodeWithPlaylist {
217
225
  var _a;
218
226
  const config = (0, utils_1.provideFull)(media_pb_1.HlsTsVideoConfiguration, Object.assign(Object.assign({}, settings), { id: settings.id
219
227
  ? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
220
- : undefined, delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, destinations: settings.destinations.map(mkCmafDestination), m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "" }));
228
+ : undefined, delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, destinations: settings.destinations.map(mkCmafDestination), m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "", bitrate: settings.bitrate || 0 }));
221
229
  const hls = client.createOutputHlsTsVideo();
222
230
  hls.write((0, utils_1.provideFull)(media_pb_1.HlsTsVideoMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
223
231
  return hls;
224
- }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.HlsTsVideoMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Ts);
232
+ }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.HlsTsVideoMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Ts, localDestinationSessionId(settings.destinations) || "");
225
233
  }
226
234
  /**
227
235
  * @public
@@ -255,11 +263,11 @@ class HlsTsAudioOutputNode extends CmafNodeWithPlaylist {
255
263
  var _a;
256
264
  const config = (0, utils_1.provideFull)(media_pb_1.HlsTsAudioConfiguration, Object.assign(Object.assign({}, settings), { id: settings.id
257
265
  ? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
258
- : undefined, delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, destinations: settings.destinations.map(mkCmafDestination), m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "" }));
266
+ : undefined, delayOutputMs: (_a = settings.delayOutputMs) !== null && _a !== void 0 ? _a : defaultDelayOutputMs, destinations: settings.destinations.map(mkCmafDestination), m3uAdditions: settings.m3uAdditions || "", mpdAdditions: settings.mpdAdditions || "", bitrate: settings.bitrate || 0 }));
259
267
  const hls = client.createOutputHlsTsAudio();
260
268
  hls.write((0, utils_1.provideFull)(media_pb_1.HlsTsAudioMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
261
269
  return hls;
262
- }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.HlsTsAudioMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Ts);
270
+ }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.HlsTsAudioMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Ts, localDestinationSessionId(settings.destinations) || "");
263
271
  }
264
272
  /**
265
273
  * @public
@@ -297,7 +305,7 @@ class HlsTsCombinedPushOutputNode extends CmafNodeWithPlaylist {
297
305
  const hls = client.createOutputHlsTsCombinedPush();
298
306
  hls.write((0, utils_1.provideFull)(media_pb_1.HlsTsCombinedPushMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
299
307
  return hls;
300
- }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.HlsTsCombinedPushMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Ts);
308
+ }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.HlsTsCombinedPushMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Ts, localDestinationSessionId([settings.destination]) || "");
301
309
  }
302
310
  }
303
311
  exports.HlsTsCombinedPushOutputNode = HlsTsCombinedPushOutputNode;
@@ -324,7 +332,7 @@ class CmafWebVttOutputNode extends CmafNodeWithPlaylist {
324
332
  const hls = client.createOutputCmafWebVtt();
325
333
  hls.write((0, utils_1.provideFull)(media_pb_1.CmafWebVttMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
326
334
  return hls;
327
- }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.CmafWebVttMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Cmaf);
335
+ }, (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.CmafWebVttMessage, (0, utils_1.mkMessageCase)({ subscription }))), PlaylistPath.Cmaf, localDestinationSessionId(settings.destinations) || "");
328
336
  }
329
337
  /**
330
338
  * @public
@@ -363,10 +371,13 @@ class CmafMultiVariantOutputNode extends CmafNodeBase {
363
371
  }
364
372
  /** @internal */
365
373
  static create(settings, client, unregisterNode) {
374
+ var _a;
366
375
  return __awaiter(this, void 0, void 0, function* () {
367
376
  const node = new CmafMultiVariantOutputNode(settings, client, unregisterNode);
368
377
  yield node.initialised;
369
- node.url = `${(0, utils_1.publicUrlPrefix)()}/cmaf/file/${settings.playlistName}.m3u8`;
378
+ const sessionId = (_a = settings.destinations.filter((d) => d.type === "local")[0]) === null || _a === void 0 ? void 0 : _a.sessionId;
379
+ const sessionIdPath = sessionId && sessionId !== "" ? sessionId + "/" : "";
380
+ node.url = `${(0, utils_1.publicUrlPrefix)()}/cmaf/file/${sessionIdPath}${settings.playlistName}.m3u8`;
370
381
  return node;
371
382
  });
372
383
  }
@@ -407,10 +418,13 @@ class HlsTsMultiVariantOutputNode extends CmafNodeBase {
407
418
  }
408
419
  /** @internal */
409
420
  static create(settings, client, unregisterNode) {
421
+ var _a;
410
422
  return __awaiter(this, void 0, void 0, function* () {
411
423
  const node = new HlsTsMultiVariantOutputNode(settings, client, unregisterNode);
412
424
  yield node.initialised;
413
- node.url = `${(0, utils_1.publicUrlPrefix)()}/ts/${settings.playlistName}.m3u8`;
425
+ const sessionId = (_a = settings.destinations.filter((d) => d.type === "local")[0]) === null || _a === void 0 ? void 0 : _a.sessionId;
426
+ const sessionIdPath = sessionId && sessionId != "" ? sessionId + "/" : "";
427
+ node.url = `${(0, utils_1.publicUrlPrefix)()}/ts/${sessionIdPath}${settings.playlistName}.m3u8`;
414
428
  return node;
415
429
  });
416
430
  }
@@ -612,7 +626,7 @@ class WhipOutputNode extends common_1.AutoSinkMediaNode {
612
626
  (0, utils_1.exhaustiveCheck)(messageCase);
613
627
  }
614
628
  });
615
- (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "webrtcWhep", reject, settings);
629
+ (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "whipOutput", reject, settings);
616
630
  });
617
631
  }
618
632
  /** @internal */
@@ -672,7 +686,7 @@ class WhepOutputNode extends common_1.AutoSinkMediaNode {
672
686
  (0, utils_1.exhaustiveCheck)(messageCase);
673
687
  }
674
688
  });
675
- (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "webrtcWhip", reject, settings);
689
+ (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "whepOutput", reject, settings);
676
690
  });
677
691
  }
678
692
  /** @internal */
@@ -703,6 +717,7 @@ class RtmpOutputNode extends common_1.AutoSinkMediaNode {
703
717
  : undefined, statsSampling: settings.statsSampling
704
718
  ? (0, utils_1.provideFull)(media_pb_1.StreamStatisticsSampling, settings.statsSampling)
705
719
  : undefined, bufferDelayMs: Math.round(settings.bufferDelayMs || 0) }));
720
+ this.onPublishStart = settings.onPublishStart;
706
721
  this.grpcStream = this.client.createOutputRtmp();
707
722
  this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.RtmpOutputMessage, (0, utils_1.mkMessageCase)({ configuration: rtmpOutputConfig })));
708
723
  this.initialised = new Promise((resolve, reject) => {
@@ -729,6 +744,11 @@ class RtmpOutputNode extends common_1.AutoSinkMediaNode {
729
744
  case "streamStatistics":
730
745
  (_a = settings.onStreamStatistics) === null || _a === void 0 ? void 0 : _a.call(settings, (0, types_1.fromStreamStatistics)(data.message.value, this.subscribedStreams));
731
746
  break;
747
+ case "status":
748
+ if (data.message.value == media_pb_1.RtmpOutputEvent_State.RTMP_OUTPUT_STATUS_PUBLISH_START && this.onPublishStart) {
749
+ this.onPublishStart();
750
+ }
751
+ break;
732
752
  default:
733
753
  (0, utils_1.exhaustiveCheck)(messageCase);
734
754
  }
@@ -830,7 +850,7 @@ class FileMp4OutputNode extends common_1.AutoSinkMediaNode {
830
850
  this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.FileMp4OutputMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
831
851
  this.initialised = new Promise((resolve, reject) => {
832
852
  this.grpcStream.on("data", (data) => {
833
- var _a;
853
+ var _a, _b;
834
854
  const messageCase = data.message.case;
835
855
  switch (messageCase) {
836
856
  case undefined:
@@ -852,6 +872,10 @@ class FileMp4OutputNode extends common_1.AutoSinkMediaNode {
852
872
  case "streamStatistics":
853
873
  (_a = settings.onStreamStatistics) === null || _a === void 0 ? void 0 : _a.call(settings, (0, types_1.fromStreamStatistics)(data.message.value, this.subscribedStreams));
854
874
  break;
875
+ case "status":
876
+ if (data.message.value.state = media_pb_1.FileMp4OutputStatus_State.OUTPUT_STATUS_EOF)
877
+ (_b = settings.onStreamEof) === null || _b === void 0 ? void 0 : _b.call(settings);
878
+ break;
855
879
  default:
856
880
  (0, utils_1.exhaustiveCheck)(messageCase);
857
881
  throw new Error(`Unhandled case: ${utils_1.exhaustiveCheck}`);
@@ -228,6 +228,20 @@ export interface StreamSyncSettings extends ProcessorNodeSettings<StreamSyncNode
228
228
  export declare class StreamSyncNode extends AutoProcessorMediaNode<"audio" | "video"> {
229
229
  close(): void;
230
230
  }
231
+ /**
232
+ * @public
233
+ * Settings for a StreamProgramJoin node
234
+ * see {@link NorskTransform.streamSync}
235
+ * */
236
+ export interface StreamProgramJoinSettings extends ProcessorNodeSettings<StreamProgramJoinNode> {
237
+ }
238
+ /**
239
+ * @public
240
+ * see: {@link NorskTransform.streamSync}
241
+ */
242
+ export declare class StreamProgramJoinNode extends AutoProcessorMediaNode<"audio" | "video"> {
243
+ close(): void;
244
+ }
231
245
  export interface Smpte2038Message {
232
246
  cNotYChannelFlag: boolean;
233
247
  lineNumber: number;
@@ -478,6 +492,10 @@ export interface AudioMixSettings<Pins extends string> extends ProcessorNodeSett
478
492
  sources: readonly AudioMixSource<Pins>[];
479
493
  /** The source name to use for the output stream */
480
494
  outputSource: string;
495
+ /** The channel layout that the mixer runs at
496
+ * all audio streams will be normalised to this value and therefore
497
+ * this will be the output channel layout of this node */
498
+ channelLayout: ChannelLayout;
481
499
  /** The sample rate that the mixer runs at
482
500
  * all audio streams will be normalised to this value and therefore
483
501
  * this will be the output sample rate of this node */
@@ -615,6 +633,12 @@ export interface AudioBuildMultichannelSettings extends ProcessorNodeSettings<Au
615
633
  * rate.
616
634
  */
617
635
  channelList: readonly StreamKey[];
636
+ /**
637
+ * Callback invoked when the inbound context changes
638
+ * a new channel list can be returned here that overrides the initial configuration
639
+ * and allows the channel order to be changed at runtime
640
+ */
641
+ onInputChanged?: (keys: StreamKey[]) => StreamKey[] | undefined;
618
642
  /** The stream key to use for the outoging stream*/
619
643
  outputStreamKey: StreamKey;
620
644
  }
@@ -929,6 +953,17 @@ export interface NorskTransform {
929
953
  * especially outputs.
930
954
  */
931
955
  streamSync(settings: StreamSyncSettings): Promise<StreamSyncNode>;
956
+ /**
957
+ * This processor does multiple things
958
+ * - joins together multiple streams from multiple sources
959
+ * - rebases their timestamps so that they all start at the same point
960
+ * - sets the program id to a common value
961
+ *
962
+ * It is useful for syncing multiple incoming streams that on paper are already synchronised but because
963
+ * of the time taken to set up connections and subscriptions across various protocols, are off by a few
964
+ * hundred milliseconds
965
+ */
966
+ streamProgramJoin(settings: StreamProgramJoinSettings): Promise<StreamProgramJoinNode>;
932
967
  ancillary(settings: AncillarySettings): Promise<AncillaryNode>;
933
968
  }
934
969
  /**