@norskvideo/norsk-sdk 1.0.343 → 1.0.344

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.
@@ -5,10 +5,10 @@ import { CmafAudioMessage } from '@norskvideo/norsk-api/lib/media_pb';
5
5
  import { CmafMultiVariantMessage } from '@norskvideo/norsk-api/lib/media_pb';
6
6
  import { CmafVideoMessage } from '@norskvideo/norsk-api/lib/media_pb';
7
7
  import { CmafWebVttMessage } from '@norskvideo/norsk-api/lib/media_pb';
8
- import { Context } from '@norskvideo/norsk-api/lib/media_pb';
9
8
  import { CurrentLoad } from '@norskvideo/norsk-api/lib/shared/common_pb';
10
9
  import { ExplicitChannel } from '@norskvideo/norsk-api/lib/media_pb';
11
10
  import { FileTsInputMessage } from '@norskvideo/norsk-api/lib/media_pb';
11
+ import { FrameRate as FrameRate_2 } from '@norskvideo/norsk-api/lib/media_pb';
12
12
  import { GopStructure } from '@norskvideo/norsk-api/lib/media_pb';
13
13
  import * as grpc from '@grpc/grpc-js';
14
14
  import { HlsOutputEvent } from '@norskvideo/norsk-api/lib/media_pb';
@@ -412,7 +412,7 @@ export declare interface AutoProcessorMediaNode<Pins extends string> extends Sou
412
412
  }
413
413
 
414
414
  export declare class AutoProcessorMediaNode<Pins extends string> {
415
- constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, getGrpcStream: () => (Readable | Writable), subscribeFn: (subscription: Subscription) => void, subscribeErrorFn?: (error: SubscriptionError) => void, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
415
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, getGrpcStream: () => (Readable | Writable), subscribeFn: (subscription: Subscription) => Promise<boolean>, subscribeErrorFn?: (error: SubscriptionError) => void, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
416
416
  }
417
417
 
418
418
  /** @public */
@@ -774,12 +774,12 @@ export declare interface CmafMultiVariantOutputSettings extends SinkNodeSettings
774
774
  }
775
775
 
776
776
  declare class CmafNodeBase<ClientMessage, Pins extends string, T extends MediaNodeState> extends AutoProcessorMediaNode<Pins> {
777
- constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => void, playlistPath: PlaylistPath, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
777
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => Promise<boolean>, _playlistPath: PlaylistPath, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
778
778
  close(): void;
779
779
  }
780
780
 
781
781
  declare class CmafNodeWithPlaylist<ClientMessage, Pins extends string, T extends MediaNodeState> extends CmafNodeBase<ClientMessage, Pins, T> {
782
- constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => void, playlistPath: PlaylistPath, sessionId: string);
782
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => Promise<boolean>, playlistPath: PlaylistPath, sessionId: string);
783
783
  /**
784
784
  * @public
785
785
  * Returns the URL to the HLS playlist entry. Note this can only be evaluated once the stream is active as it
@@ -984,6 +984,11 @@ export declare interface ComposePart<Pins> {
984
984
  referenceResolution?: Resolution;
985
985
  }
986
986
 
987
+ /** @public */
988
+ export declare interface Context {
989
+ streams: StreamMetadata[];
990
+ }
991
+
987
992
  /** @public */
988
993
  export declare type ContextType = "full" | "singleSource" | "singleProgram" | "singleStream" | "singleRendition";
989
994
 
@@ -1065,6 +1070,9 @@ export declare type DeckLinkVideoConnection = "sdi" | "hdmi" | "optical_sdi" | "
1065
1070
  /** @public */
1066
1071
  export declare type DeckLinkVideoIOSupport = "capture" | "playback";
1067
1072
 
1073
+ /** @public */
1074
+ export declare type DeferredVideoComposeSettings<Pins extends string> = (streams: StreamMetadata[]) => VideoComposeSettings<Pins> | undefined;
1075
+
1068
1076
  /**
1069
1077
  * @public
1070
1078
  * Drop every N frames from an incoming video stream
@@ -1939,6 +1947,11 @@ export declare interface NorskInput {
1939
1947
  * @param settings - Configuration for the RTP input
1940
1948
  */
1941
1949
  rtp(settings: RtpInputSettings): Promise<RtpInputNode>;
1950
+ /**
1951
+ * Generate a test video card with a configurable pattern.
1952
+ * @param settings - Configuration for the video test card
1953
+ */
1954
+ videoTestCard(settings: VideoTestcardGeneratorSettings): Promise<VideoTestcardGeneratorNode>;
1942
1955
  /**
1943
1956
  * Generate a test audio signal with a configurable waveform.
1944
1957
  * @param settings - Configuration for the audio signal
@@ -2183,7 +2196,7 @@ export declare interface NorskTransform {
2183
2196
  * Compose multiple video streams together into a single output
2184
2197
  * @param settings - Composition settings
2185
2198
  */
2186
- videoCompose<Pins extends string>(settings: VideoComposeSettings<Pins>): Promise<VideoComposeNode<Pins>>;
2199
+ videoCompose<Pins extends string>(settings: VideoComposeSettings<Pins> | DeferredVideoComposeSettings<Pins>): Promise<VideoComposeNode<Pins>>;
2187
2200
  /**
2188
2201
  * Create a Media Node performing transcription into subtitles using the
2189
2202
  * Amazon Transcribe AWS service.
@@ -2283,7 +2296,7 @@ export declare interface NorskTransform {
2283
2296
  * of the time taken to set up connections and subscriptions across various protocols, are off by a few
2284
2297
  * hundred milliseconds
2285
2298
  */
2286
- streamProgramJoin(settings: StreamProgramJoinSettings): Promise<StreamProgramJoinNode>;
2299
+ streamAlign(settings: StreamAlignSettings): Promise<StreamAlignNode>;
2287
2300
  ancillary(settings: AncillarySettings): Promise<AncillaryNode>;
2288
2301
  }
2289
2302
 
@@ -2504,6 +2517,8 @@ export declare interface PartTransition {
2504
2517
  easing?: SimpleEasing;
2505
2518
  }
2506
2519
 
2520
+ declare type Pattern = "black" | "smpte75" | "smpte100";
2521
+
2507
2522
  /** @public */
2508
2523
  export declare type PinToKey<Pins extends string> = Nullable<Partial<Record<Pins, StreamKey[]>>>;
2509
2524
 
@@ -2545,7 +2560,7 @@ export declare interface ProcessorMediaNode<Pins extends string> extends SourceM
2545
2560
  }
2546
2561
 
2547
2562
  export declare class ProcessorMediaNode<Pins extends string> {
2548
- constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, getGrpcStream: () => (Readable | Writable), subscribeFn: (subscription: Subscription) => void, subscribeErrorFn?: (error: SubscriptionError) => void, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
2563
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, getGrpcStream: () => (Readable | Writable), subscribeFn: (subscription: Subscription) => Promise<boolean>, subscribeErrorFn?: (error: SubscriptionError) => void, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
2549
2564
  }
2550
2565
 
2551
2566
  /** @public */
@@ -2963,6 +2978,9 @@ export declare function selectAudio(streams: readonly StreamMetadata[]): StreamK
2963
2978
  */
2964
2979
  export declare function selectAV(streams: readonly StreamMetadata[]): StreamKey[];
2965
2980
 
2981
+ /** @public */
2982
+ export declare function selectExactKey(key: StreamKey): (streams: readonly StreamMetadata[]) => StreamKey[];
2983
+
2966
2984
  /** @public */
2967
2985
  export declare function selectPlaylist(streams: readonly StreamMetadata[]): StreamKey[];
2968
2986
 
@@ -2988,7 +3006,7 @@ export declare interface SingleStreamStatistics extends StreamStatistics {
2988
3006
  }
2989
3007
 
2990
3008
  /** @public */
2991
- export declare class SinkMediaNode<Pins extends string> extends MediaNodeState {
3009
+ export declare class SinkMediaNode<Pins extends string> extends MediaNodeState implements SubscribeDestination {
2992
3010
  permissiveSubscriptionValidation(_context: Context): SubscriptionValidationResponse;
2993
3011
  restrictiveSubscriptionValidation(context: Context): SubscriptionValidationResponse;
2994
3012
  /** Subscribe to the given sources.
@@ -3028,8 +3046,8 @@ export declare interface Smpte2038Message {
3028
3046
  /** @public */
3029
3047
  export declare class SourceMediaNode extends MediaNodeState {
3030
3048
  outputStreams: StreamMetadata[];
3031
- registerForContextChange(subscriber: SinkMediaNode<string>): void;
3032
- unregisterForContextChange(subscriber: SinkMediaNode<string>): void;
3049
+ registerForContextChange(subscriber: SubscribeDestination): void;
3050
+ unregisterForContextChange(subscriber: SubscribeDestination): void;
3033
3051
  }
3034
3052
 
3035
3053
  /** @public */
@@ -3075,6 +3093,9 @@ export declare type SourceSubscriptionError = {
3075
3093
  reason: "unsupportedConversion";
3076
3094
  };
3077
3095
 
3096
+ /** @public */
3097
+ export declare function sourceToPin<Pins extends string>(source: string, pin: Pins): (streams: StreamMetadata[]) => PinToKey<Pins>;
3098
+
3078
3099
  /**
3079
3100
  * @public
3080
3101
  * The return value for the {@link SrtInputSettings.onConnection} callback
@@ -3212,6 +3233,26 @@ export declare interface SrtOutputSettings extends SinkNodeSettings<SrtOutputNod
3212
3233
  /** @public */
3213
3234
  export declare type StabilizationMode = "low" | "medium" | "high";
3214
3235
 
3236
+ /**
3237
+ * @public
3238
+ * see: {@link NorskTransform.streamSync}
3239
+ */
3240
+ export declare class StreamAlignNode extends AutoProcessorMediaNode<"audio" | "video"> {
3241
+ close(): void;
3242
+ }
3243
+
3244
+ /**
3245
+ * @public
3246
+ * Settings for a StreamAlign node
3247
+ * This will reset all streams to the same framerates/sample rates
3248
+ * and align their timestamps so that they completely line up for downstream operations
3249
+ * see {@link NorskTransform.streamAlign}
3250
+ * */
3251
+ export declare interface StreamAlignSettings extends ProcessorNodeSettings<StreamAlignNode> {
3252
+ sampleRate: SampleRate;
3253
+ frameRate: FrameRate;
3254
+ }
3255
+
3215
3256
  /**
3216
3257
  * @public
3217
3258
  * see: {@link NorskTransform.streamChaosMonkey}
@@ -3262,6 +3303,11 @@ export declare interface StreamKeyOverrideSettings extends ProcessorNodeSettings
3262
3303
  streamKey: StreamKey;
3263
3304
  }
3264
3305
 
3306
+ /**
3307
+ * Compares two stream keys by value, returning true if the stream keys refer to the same stream
3308
+ */
3309
+ export declare function streamKeysAreEqual(l: StreamKey, r: StreamKey): unknown;
3310
+
3265
3311
  /** @public */
3266
3312
  export declare interface StreamKeySettings {
3267
3313
  /** Source name. Default: the rtmp app */
@@ -3341,22 +3387,6 @@ export declare interface StreamMetadataOverrideSettingsUpdate {
3341
3387
  };
3342
3388
  }
3343
3389
 
3344
- /**
3345
- * @public
3346
- * see: {@link NorskTransform.streamSync}
3347
- */
3348
- export declare class StreamProgramJoinNode extends AutoProcessorMediaNode<"audio" | "video"> {
3349
- close(): void;
3350
- }
3351
-
3352
- /**
3353
- * @public
3354
- * Settings for a StreamProgramJoin node
3355
- * see {@link NorskTransform.streamSync}
3356
- * */
3357
- export declare interface StreamProgramJoinSettings extends ProcessorNodeSettings<StreamProgramJoinNode> {
3358
- }
3359
-
3360
3390
  /** @public */
3361
3391
  export declare interface StreamStatistics {
3362
3392
  /** The size of the sample window in seconds */
@@ -3463,8 +3493,20 @@ export declare interface StreamSwitchSmoothSettings<Pins extends string> extends
3463
3493
  transitionDurationMs?: number;
3464
3494
  /** The constant resolution that all output video will be scaled to */
3465
3495
  outputResolution: Resolution;
3496
+ /** The constant framerate that all output video will be sampled to */
3497
+ frameRate: FrameRate;
3466
3498
  /** The constant samplerate that all output audio will be resampled to */
3467
3499
  sampleRate: SampleRate;
3500
+ /** The constant channel layout that all output audio will be resampled to */
3501
+ channelLayout: ChannelLayout;
3502
+ /** Alignment behaviour of the component
3503
+ * whether to rebase all incoming streams to a common timeline
3504
+ * Note: This will modify the timestamps, meaning that merging streams not involved in this may result in
3505
+ * operation may result in sync issues. To avoid this, you can use {@link NorskProcessor.streamAlign} instead of relying
3506
+ * on this component for this behaviour
3507
+ * Note: This behaviour may be removed in a future release and replaced with something similar
3508
+ * */
3509
+ alignment?: "aligned" | "not_aligned";
3468
3510
  /** Callback which will be called if a switch request cannot be fulfilled */
3469
3511
  onSwitchError?: (message: string, inputPin?: Pins) => void;
3470
3512
  /** Callback which will be called a transition has succesfully completed for a requested switch, i.e. the new source
@@ -3521,6 +3563,11 @@ export declare interface StreamTimestampNudgeSettings extends ProcessorNodeSetti
3521
3563
  nudge?: number;
3522
3564
  }
3523
3565
 
3566
+ export declare interface SubscribeDestination {
3567
+ id?: string;
3568
+ sourceContextChange(responseCallback: (error?: SubscriptionError) => void): Promise<boolean>;
3569
+ }
3570
+
3524
3571
  /**
3525
3572
  * @public
3526
3573
  * Errors found while setting up subscriptions, separated out by reason:
@@ -3825,6 +3872,7 @@ export declare interface VideoStreamMetadata {
3825
3872
  codec: string;
3826
3873
  width: number;
3827
3874
  height: number;
3875
+ frameRate?: FrameRate_2;
3828
3876
  }
3829
3877
 
3830
3878
  /**
@@ -3835,6 +3883,37 @@ export declare interface VideoStreamMetadata {
3835
3883
  */
3836
3884
  export declare function videoStreams(streams: readonly StreamMetadata[]): StreamMetadata[];
3837
3885
 
3886
+ /**
3887
+ * @public
3888
+ * see: {@link NorskInput.audioSignal}
3889
+ */
3890
+ export declare class VideoTestcardGeneratorNode extends SourceMediaNode {
3891
+ close(): void;
3892
+ }
3893
+
3894
+ /**
3895
+ * @public
3896
+ * Settings for an Video Testcard Generator
3897
+ * see: {@link NorskInput.videoTestcard}
3898
+ * */
3899
+ export declare interface VideoTestcardGeneratorSettings extends SourceNodeSettings<VideoTestcardGeneratorNode> {
3900
+ /** The source name to set in the stream key of the outgoing stream */
3901
+ sourceName: string;
3902
+ /** The number of frames to send before shutting down */
3903
+ numberOfFrames?: number;
3904
+ /** Resolution of the test card stream **/
3905
+ resolution: {
3906
+ width: number;
3907
+ height: number;
3908
+ };
3909
+ /** Framerate of the produced video stream **/
3910
+ frameRate: {
3911
+ frames: number;
3912
+ seconds: number;
3913
+ };
3914
+ pattern: Pattern;
3915
+ }
3916
+
3838
3917
  /** @public */
3839
3918
  export declare function videoToPin<Pins extends string>(pin: Pins): (streams: StreamMetadata[]) => PinToKey<Pins>;
3840
3919
 
@@ -1,7 +1,7 @@
1
1
  import { MediaClient } from "@norskvideo/norsk-api/lib/media_grpc_pb";
2
- import { Context, StreamStatisticsSampling, GopStructure } from "@norskvideo/norsk-api/lib/media_pb";
2
+ import { StreamStatisticsSampling, GopStructure } from "@norskvideo/norsk-api/lib/media_pb";
3
3
  import { Nullable } from "typescript-nullable";
4
- import { StreamKey, MediaNodeId, StreamMetadata, MultiStreamStatistics, SubscriptionError } from "./types";
4
+ import { StreamKey, MediaNodeId, StreamMetadata, Context, MultiStreamStatistics, SubscriptionError } from "./types";
5
5
  export * from "../shared/utils";
6
6
  import { PlainMessage } from "@bufbuild/protobuf";
7
7
  /**
@@ -74,11 +74,15 @@ export declare type ReceiveFromAddressAuto = {
74
74
  };
75
75
  /** @public */
76
76
  export declare type PinToKey<Pins extends string> = Nullable<Partial<Record<Pins, StreamKey[]>>>;
77
+ export interface SubscribeDestination {
78
+ id?: string;
79
+ sourceContextChange(responseCallback: (error?: SubscriptionError) => void): Promise<boolean>;
80
+ }
77
81
  /** @public */
78
82
  export declare class SourceMediaNode extends MediaNodeState {
79
83
  outputStreams: StreamMetadata[];
80
- registerForContextChange(subscriber: SinkMediaNode<string>): void;
81
- unregisterForContextChange(subscriber: SinkMediaNode<string>): void;
84
+ registerForContextChange(subscriber: SubscribeDestination): void;
85
+ unregisterForContextChange(subscriber: SubscribeDestination): void;
82
86
  }
83
87
  /**
84
88
  * @public
@@ -91,7 +95,7 @@ export declare class SourceMediaNode extends MediaNodeState {
91
95
  * */
92
96
  export declare type SubscriptionValidationResponse = true | false | "accept" | "deny" | "accept_and_terminate";
93
97
  /** @public */
94
- export declare class SinkMediaNode<Pins extends string> extends MediaNodeState {
98
+ export declare class SinkMediaNode<Pins extends string> extends MediaNodeState implements SubscribeDestination {
95
99
  permissiveSubscriptionValidation(_context: Context): SubscriptionValidationResponse;
96
100
  restrictiveSubscriptionValidation(context: Context): SubscriptionValidationResponse;
97
101
  /** Subscribe to the given sources.
@@ -44,6 +44,7 @@ const types_1 = require("./types");
44
44
  __exportStar(require("../shared/utils"), exports);
45
45
  const utils_1 = require("../shared/utils");
46
46
  const crypto_1 = require("crypto");
47
+ const sdk_1 = require("../sdk");
47
48
  /** @internal */
48
49
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
50
  function applyMixins(derivedCtor, baseCtors) {
@@ -182,24 +183,24 @@ class SinkMediaNode extends MediaNodeState {
182
183
  const numStreams = context.streams.length;
183
184
  const expected = this.currentSubscription.sources.length;
184
185
  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
+ (0, utils_1.debuglog)("Node '%s' accepting inbound context because it is empty and we have previously accepted a full context %O", this.id, debugFormatSdkContext(context));
186
187
  this.subscriptionValidated = false;
187
188
  return "accept";
188
189
  }
189
190
  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
+ (0, utils_1.debuglog)("Node '%s' ignoring inbound context because not all sources are active yet %O", this.id, debugFormatSdkContext(context));
191
192
  return "deny";
192
193
  }
193
194
  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
+ (0, utils_1.debuglog)("Node '%s' ignoring inbound context because it is smaller than the already-accepted one %O", this.id, debugFormatSdkContext(context));
195
196
  return "deny";
196
197
  }
197
198
  if (numStreams == expected) {
198
- (0, utils_1.debuglog)("Node '%s' accepting inbound context because all keys and streams are accounted for %O", this.id, debugFormatContext(context));
199
+ (0, utils_1.debuglog)("Node '%s' accepting inbound context because all keys and streams are accounted for %O", this.id, debugFormatSdkContext(context));
199
200
  this.subscriptionValidated = true;
200
201
  return "accept";
201
202
  }
202
- (0, utils_1.debuglog)("Node '%s' ignoring inbound context because not all keys are accounted for yet %O", this.id, debugFormatContext(context));
203
+ (0, utils_1.debuglog)("Node '%s' ignoring inbound context because not all keys are accounted for yet %O", this.id, debugFormatSdkContext(context));
203
204
  return "deny";
204
205
  }
205
206
  /** Subscribe to the given sources.
@@ -267,7 +268,7 @@ class SinkMediaNode extends MediaNodeState {
267
268
  this.currentSourcesActive += 1;
268
269
  for (const pin in pinSubscriptions) {
269
270
  (_a = pinSubscriptions[pin]) === null || _a === void 0 ? void 0 : _a.forEach((key) => {
270
- this.subscribedStreams.push(source.outputStreams.filter((streamMetadata) => streamMetadata.streamKey == key)[0]);
271
+ this.subscribedStreams.push(source.outputStreams.filter((streamMetadata) => streamMetadata.streamKey && (0, sdk_1.streamKeysAreEqual)(streamMetadata.streamKey, key))[0]);
271
272
  subscriberSources.push((0, utils_1.provideFull)(media_pb_1.SubscribeSource, {
272
273
  stream: (0, types_1.mkStreamKey)(key),
273
274
  nodeId: (0, types_1.toMediaNodeId)(source.id),
@@ -284,39 +285,48 @@ class SinkMediaNode extends MediaNodeState {
284
285
  const id = (0, crypto_1.randomUUID)();
285
286
  this.currentSubscription = (0, utils_1.provideFull)(media_pb_1.Subscription, { sources, id });
286
287
  (0, utils_1.debuglog)("Node '%s' sending subscriptions %O", this.id, debugFormatSubscription(this.currentSubscription));
287
- this.subscribeFn(this.currentSubscription);
288
- this.subscriptionResponseCallbacks.set(id, responseCallback);
289
- return true;
288
+ let subscribed = yield this.subscribeFn(this.currentSubscription);
289
+ if (subscribed)
290
+ this.subscriptionResponseCallbacks.set(id, responseCallback);
291
+ return subscribed;
290
292
  });
291
293
  }
292
294
  /** @internal */
293
295
  subscriptionResponse(response) {
294
- const callback = this.subscriptionResponseCallbacks.get(response.id);
295
- this.subscriptionResponseCallbacks.delete(response.id);
296
- if (callback) {
297
- if (response.error === undefined) {
298
- callback();
299
- if (this.subscribedStreamsChangedFn) {
300
- this.subscribedStreamsChangedFn(this.subscribedStreams);
296
+ return __awaiter(this, void 0, void 0, function* () {
297
+ yield this.finaliseSubscription(response.id, response.error);
298
+ });
299
+ }
300
+ /** @internal */
301
+ finaliseSubscription(id, err) {
302
+ return __awaiter(this, void 0, void 0, function* () {
303
+ const callback = this.subscriptionResponseCallbacks.get(id);
304
+ this.subscriptionResponseCallbacks.delete(id);
305
+ if (callback) {
306
+ if (err === undefined) {
307
+ callback();
308
+ if (this.subscribedStreamsChangedFn) {
309
+ this.subscribedStreamsChangedFn(this.subscribedStreams);
310
+ }
311
+ }
312
+ else {
313
+ const error = (0, types_1.fromSubscriptionError)(err);
314
+ (0, utils_1.debuglog)("Node '%s' has a failed subscription response for '%s': %O", this.id, id, error);
315
+ callback(error);
316
+ if (this.subscribeErrorFn) {
317
+ this.subscribeErrorFn(error);
318
+ }
301
319
  }
302
320
  }
303
321
  else {
304
- const error = (0, types_1.fromSubscriptionError)(response.error);
305
- (0, utils_1.debuglog)("Node '%s' has a failed subscription response for '%s': %O", this.id, response.id, error);
306
- callback(error);
307
- if (this.subscribeErrorFn) {
308
- this.subscribeErrorFn(error);
309
- }
322
+ (0, utils_1.debuglog)("Missing subscription response callback for '%s'", id);
310
323
  }
311
- }
312
- else {
313
- (0, utils_1.debuglog)("Missing subscription response callback for '%s'", response.id);
314
- }
324
+ });
315
325
  }
316
326
  /** @internal */
317
327
  handleInboundContext(context) {
318
328
  return __awaiter(this, void 0, void 0, function* () {
319
- const validationResponse = this.subscriptionValidation(context);
329
+ const validationResponse = this.subscriptionValidation((0, types_1.fromContext)(context));
320
330
  const responseFn = util
321
331
  .promisify(this.client.sendValidationResponse)
322
332
  .bind(this.client);
@@ -428,6 +438,36 @@ function registerStreamHandlers(grpcStream, unregisterNode, tag, reject, setting
428
438
  exports.registerStreamHandlers = registerStreamHandlers;
429
439
  ////////////////////////////////////////////////////////////////////////////////
430
440
  // debug
441
+ function debugFormatSdkContext(context) {
442
+ return {
443
+ keys: context.streams.map((stream) => debugFormatSdkStreamMetadata(stream))
444
+ };
445
+ }
446
+ function debugFormatSdkStreamMetadata(stream) {
447
+ switch (stream.message.case) {
448
+ case "audio": {
449
+ return {
450
+ streamKey: (stream.streamKey),
451
+ audio: (stream.message.value),
452
+ };
453
+ }
454
+ case "video": {
455
+ return {
456
+ streamKey: (stream.streamKey),
457
+ video: (stream.message.value),
458
+ };
459
+ }
460
+ case "playlist": {
461
+ return {
462
+ streamKey: (stream.streamKey),
463
+ playlist: stream.message
464
+ };
465
+ }
466
+ default: {
467
+ return undefined;
468
+ }
469
+ }
470
+ }
431
471
  function debugFormatContext(context) {
432
472
  return {
433
473
  keys: context.streams.map((stream) => debugFormatStreamMetadata(stream)),
@@ -511,6 +511,36 @@ export interface AudioSignalGeneratorSettings extends SourceNodeSettings<AudioSi
511
511
  export declare class AudioSignalGeneratorNode extends SourceMediaNode {
512
512
  close(): void;
513
513
  }
514
+ /**
515
+ * @public
516
+ * Settings for an Video Testcard Generator
517
+ * see: {@link NorskInput.videoTestcard}
518
+ * */
519
+ export interface VideoTestcardGeneratorSettings extends SourceNodeSettings<VideoTestcardGeneratorNode> {
520
+ /** The source name to set in the stream key of the outgoing stream */
521
+ sourceName: string;
522
+ /** The number of frames to send before shutting down */
523
+ numberOfFrames?: number;
524
+ /** Resolution of the test card stream **/
525
+ resolution: {
526
+ width: number;
527
+ height: number;
528
+ };
529
+ /** Framerate of the produced video stream **/
530
+ frameRate: {
531
+ frames: number;
532
+ seconds: number;
533
+ };
534
+ pattern: Pattern;
535
+ }
536
+ declare type Pattern = "black" | "smpte75" | "smpte100";
537
+ /**
538
+ * @public
539
+ * see: {@link NorskInput.audioSignal}
540
+ */
541
+ export declare class VideoTestcardGeneratorNode extends SourceMediaNode {
542
+ close(): void;
543
+ }
514
544
  /**
515
545
  * @public
516
546
  * Settings for an image file source
@@ -632,6 +662,11 @@ export interface NorskInput {
632
662
  * @param settings - Configuration for the RTP input
633
663
  */
634
664
  rtp(settings: RtpInputSettings): Promise<RtpInputNode>;
665
+ /**
666
+ * Generate a test video card with a configurable pattern.
667
+ * @param settings - Configuration for the video test card
668
+ */
669
+ videoTestCard(settings: VideoTestcardGeneratorSettings): Promise<VideoTestcardGeneratorNode>;
635
670
  /**
636
671
  * Generate a test audio signal with a configurable waveform.
637
672
  * @param settings - Configuration for the audio signal
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.FileMp4InputNode = exports.FileImageInputNode = exports.AudioSignalGeneratorNode = exports.BrowserInputNode = exports.M3u8InputNode = exports.UdpTsInputNode = exports.SrtInputNode = exports.FileTsInputNode = exports.FileWebVttInputNode = exports.WhipInputNode = exports.DeckLinkInputNode = exports.RtmpServerInputNode = exports.RtpInputNode = void 0;
12
+ exports.FileMp4InputNode = exports.FileImageInputNode = exports.VideoTestcardGeneratorNode = exports.AudioSignalGeneratorNode = exports.BrowserInputNode = exports.M3u8InputNode = exports.UdpTsInputNode = exports.SrtInputNode = exports.FileTsInputNode = exports.FileWebVttInputNode = exports.WhipInputNode = exports.DeckLinkInputNode = exports.RtmpServerInputNode = exports.RtpInputNode = void 0;
13
13
  const media_pb_1 = require("@norskvideo/norsk-api/lib/media_pb");
14
14
  const types_1 = require("./types");
15
15
  const types_2 = require("../types");
@@ -1014,6 +1014,78 @@ class AudioSignalGeneratorNode extends common_1.SourceMediaNode {
1014
1014
  }
1015
1015
  }
1016
1016
  exports.AudioSignalGeneratorNode = AudioSignalGeneratorNode;
1017
+ function toPattern(p) {
1018
+ switch (p) {
1019
+ case "black":
1020
+ return media_pb_1.TestCardPattern.Black;
1021
+ case "smpte75":
1022
+ return media_pb_1.TestCardPattern.Smpte75;
1023
+ case "smpte100":
1024
+ return media_pb_1.TestCardPattern.Smpte100;
1025
+ default:
1026
+ (0, utils_1.exhaustiveCheck)(p);
1027
+ }
1028
+ }
1029
+ /**
1030
+ * @public
1031
+ * see: {@link NorskInput.audioSignal}
1032
+ */
1033
+ class VideoTestcardGeneratorNode extends common_1.SourceMediaNode {
1034
+ /** @internal */
1035
+ constructor(settings, client, unregisterNode) {
1036
+ var _a;
1037
+ super(client, settings.onOutboundContextChange);
1038
+ let id = settings.id
1039
+ ? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
1040
+ : undefined;
1041
+ const config = (0, utils_1.provideFull)(media_pb_1.TestCardVideoConfiguration, {
1042
+ numberOfFrames: (_a = settings.numberOfFrames) !== null && _a !== void 0 ? _a : 0,
1043
+ realtime: true,
1044
+ sourceName: settings.sourceName,
1045
+ frameRate: (0, utils_1.provideFull)(media_pb_1.FrameRate, settings.frameRate),
1046
+ resolution: (0, utils_1.provideFull)(media_pb_1.Resolution, settings.resolution),
1047
+ pattern: toPattern(settings.pattern)
1048
+ });
1049
+ this.grpcStream = this.client.createInputVideoTestCardGenerator();
1050
+ this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.VideoTestCardGeneratorMessage, (0, utils_1.mkMessageCase)({ setup: (0, utils_1.provideFull)(media_pb_1.TestCardVideoSetup, { id }) })));
1051
+ this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.VideoTestCardGeneratorMessage, (0, utils_1.mkMessageCase)({ configure: config })));
1052
+ this.initialised = new Promise((resolve, reject) => {
1053
+ this.grpcStream.on("data", (data) => {
1054
+ const messageCase = data.message.case;
1055
+ switch (messageCase) {
1056
+ case undefined:
1057
+ break;
1058
+ case "nodeId": {
1059
+ this.id = data.message.value.id;
1060
+ settings.onCreate && settings.onCreate(this);
1061
+ resolve();
1062
+ break;
1063
+ }
1064
+ case "outboundContext": {
1065
+ const context = data.message.value;
1066
+ this.outboundContextChange(context);
1067
+ break;
1068
+ }
1069
+ default:
1070
+ (0, utils_1.exhaustiveCheck)(messageCase);
1071
+ }
1072
+ });
1073
+ (0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "videotestcard", reject, settings);
1074
+ });
1075
+ }
1076
+ /** @internal */
1077
+ static create(settings, client, unregisterNode) {
1078
+ return __awaiter(this, void 0, void 0, function* () {
1079
+ const node = new VideoTestcardGeneratorNode(settings, client, unregisterNode);
1080
+ yield node.initialised;
1081
+ return node;
1082
+ });
1083
+ }
1084
+ close() {
1085
+ this.grpcStream.cancel();
1086
+ }
1087
+ }
1088
+ exports.VideoTestcardGeneratorNode = VideoTestcardGeneratorNode;
1017
1089
  /**
1018
1090
  * @public
1019
1091
  * see: {@link NorskInput.fileImage}
@@ -376,11 +376,11 @@ declare enum PlaylistPath {
376
376
  Ts = 1
377
377
  }
378
378
  declare class CmafNodeBase<ClientMessage, Pins extends string, T extends MediaNodeState> extends AutoProcessorMediaNode<Pins> {
379
- constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => void, playlistPath: PlaylistPath, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
379
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => Promise<boolean>, _playlistPath: PlaylistPath, subscribedStreamsChangedFn?: (streams: StreamMetadata[]) => void);
380
380
  close(): void;
381
381
  }
382
382
  declare class CmafNodeWithPlaylist<ClientMessage, Pins extends string, T extends MediaNodeState> extends CmafNodeBase<ClientMessage, Pins, T> {
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);
383
+ constructor(client: MediaClient, unregisterNode: (node: MediaNodeState) => void, settings: ProcessorNodeSettings<T> & StreamStatisticsMixin, grpcInit: () => grpc.ClientDuplexStream<ClientMessage, HlsOutputEvent>, subscribeFn: (subscription: Subscription) => Promise<boolean>, playlistPath: PlaylistPath, sessionId: string);
384
384
  /**
385
385
  * @public
386
386
  * Returns the URL to the HLS playlist entry. Note this can only be evaluated once the stream is active as it