@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.
- package/lib/src/media_nodes/common.d.ts +15 -2
- package/lib/src/media_nodes/common.js +71 -25
- package/lib/src/media_nodes/input.js +5 -2
- package/lib/src/media_nodes/output.d.ts +25 -6
- package/lib/src/media_nodes/output.js +49 -25
- package/lib/src/media_nodes/processor.d.ts +35 -0
- package/lib/src/media_nodes/processor.js +192 -1
- package/lib/src/media_nodes/types.d.ts +6 -0
- package/lib/src/media_nodes/types.js +13 -0
- package/lib/src/sdk.d.ts +5 -10
- package/lib/src/sdk.js +19 -51
- package/lib/src/shared/utils.d.ts +2 -0
- package/lib/src/shared/utils.js +8 -1
- package/package.json +2 -2
- package/src/sdk.ts +24 -60
- package/dist/norsk-sdk.d.ts +0 -4146
- package/lib/src/tsdoc-metadata.json +0 -11
|
@@ -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) =>
|
|
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) =>
|
|
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
|
|
96
|
-
if (
|
|
99
|
+
const pending = this.pendingContextAcks.get(context);
|
|
100
|
+
if (pending === undefined)
|
|
97
101
|
return;
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
123
|
+
let pending = [];
|
|
118
124
|
for (let [subscriber, _] of this.subscribers) {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
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 (
|
|
128
|
-
this.pendingContextAcks.set(context,
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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}
|
|
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}
|
|
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}
|
|
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), "
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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), "
|
|
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), "
|
|
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
|
/**
|