@norskvideo/norsk-sdk 1.0.384 → 1.0.386
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/package.json +2 -2
- package/lib/src/media_nodes/input.d.ts +26 -0
- package/lib/src/media_nodes/input.js +51 -2
- package/lib/src/media_nodes/mediaStore.js +8 -2
- package/lib/src/media_nodes/output.d.ts +42 -2
- package/lib/src/media_nodes/output.js +59 -4
- package/lib/src/media_nodes/processor.d.ts +53 -5
- package/lib/src/media_nodes/processor.js +101 -2
- package/lib/src/media_nodes/types.d.ts +88 -1
- package/lib/src/media_nodes/types.js +127 -2
- package/lib/src/sdk.d.ts +1 -1
- package/lib/src/sdk.js +11 -2
- package/package.json +2 -2
- package/src/sdk.ts +17 -3
- package/dist/norsk-sdk.d.ts +0 -6037
- package/lib/src/tsdoc-metadata.json +0 -11
package/lib/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "MIT",
|
|
3
3
|
"name": "@norskvideo/norsk-sdk",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.386",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@bufbuild/protobuf": "^0.3.0",
|
|
7
7
|
"@grpc/grpc-js": "^1.2.2",
|
|
8
|
-
"@norskvideo/norsk-api": "1.0.
|
|
8
|
+
"@norskvideo/norsk-api": "1.0.386",
|
|
9
9
|
"lodash": "^4.17.21",
|
|
10
10
|
"typescript-nullable": "^0.6.0"
|
|
11
11
|
},
|
|
@@ -636,6 +636,10 @@ export interface AudioSignalGeneratorSettings extends SourceNodeSettings<AudioSi
|
|
|
636
636
|
sampleFormat?: SampleFormat;
|
|
637
637
|
/** The language tag to use. Defaults to no language */
|
|
638
638
|
language?: string;
|
|
639
|
+
/** Number of audio samples per frame. Defaults to 1024 */
|
|
640
|
+
numSamplesPerFrame?: number;
|
|
641
|
+
/** Number of frames to output. Defaults to infinite */
|
|
642
|
+
numFrames?: number;
|
|
639
643
|
/**
|
|
640
644
|
* Waveform - create one with {@link mkSine}
|
|
641
645
|
* */
|
|
@@ -764,6 +768,23 @@ export declare class FileMp4InputNode extends SourceMediaNode {
|
|
|
764
768
|
/** Seek to a given point, without starting playback. When the stream is played/resumed, it will start from (about) this offset */
|
|
765
769
|
seek(offsetMs: number): void;
|
|
766
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* @public
|
|
773
|
+
* Settings for an File Based WAV Input
|
|
774
|
+
* see: {@link NorskInput.fileWav}
|
|
775
|
+
*/
|
|
776
|
+
export interface FileWavInputSettings extends SourceNodeSettings<FileWavInputNode> {
|
|
777
|
+
/** The source name to set in the stream key of the outgoing stream */
|
|
778
|
+
sourceName: string;
|
|
779
|
+
/** Path to the WAV file to read */
|
|
780
|
+
fileName: string;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* @public
|
|
784
|
+
* see: {@link NorskInput.fileWav}
|
|
785
|
+
*/
|
|
786
|
+
export declare class FileWavInputNode extends SourceMediaNode {
|
|
787
|
+
}
|
|
767
788
|
/**
|
|
768
789
|
* @public
|
|
769
790
|
* Methods that allow you to ingest media into your application
|
|
@@ -830,6 +851,11 @@ export interface NorskInput {
|
|
|
830
851
|
* @param settings - Configuration for the file input
|
|
831
852
|
*/
|
|
832
853
|
fileMp4(settings: FileMp4InputSettings): Promise<FileMp4InputNode>;
|
|
854
|
+
/**
|
|
855
|
+
* Read a WAV file with PCM audio with realtime playback.
|
|
856
|
+
* @param settings - Configuration for the file input
|
|
857
|
+
*/
|
|
858
|
+
fileWav(settings: FileWavInputSettings): Promise<FileWavInputNode>;
|
|
833
859
|
/**
|
|
834
860
|
* Stream from a remote RTP source
|
|
835
861
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FileMp4InputNode = exports.FileImageInputNode = exports.VideoTestcardGeneratorNode = exports.AudioSignalGeneratorNode = exports.BrowserInputNode = exports.M3u8InputNode = exports.UdpTsInputNode = exports.SrtInputNode = exports.FileTsInputNode = exports.StreamWebVttInputNode = exports.FileWebVttInputNode = exports.WhipInputNode = exports.DeltacastInputNode = exports.DeckLinkInputNode = exports.RtmpServerInputNode = exports.RtpInputNode = void 0;
|
|
3
|
+
exports.FileWavInputNode = exports.FileMp4InputNode = exports.FileImageInputNode = exports.VideoTestcardGeneratorNode = exports.AudioSignalGeneratorNode = exports.BrowserInputNode = exports.M3u8InputNode = exports.UdpTsInputNode = exports.SrtInputNode = exports.FileTsInputNode = exports.StreamWebVttInputNode = exports.FileWebVttInputNode = exports.WhipInputNode = exports.DeltacastInputNode = exports.DeckLinkInputNode = exports.RtmpServerInputNode = exports.RtpInputNode = void 0;
|
|
4
4
|
const media_pb_1 = require("@norskvideo/norsk-api/lib/media_pb");
|
|
5
5
|
const types_1 = require("./types");
|
|
6
6
|
const types_2 = require("../types");
|
|
@@ -1128,7 +1128,9 @@ class AudioSignalGeneratorNode extends common_1.SourceMediaNode {
|
|
|
1128
1128
|
sampleFormat: (0, types_1.toSampleFormat)(settings.sampleFormat ? settings.sampleFormat : "fltp"),
|
|
1129
1129
|
sampleRate: (0, types_1.toSampleRate)(settings.sampleRate),
|
|
1130
1130
|
channelLayout: (0, types_1.toChannelLayout)(settings.channelLayout),
|
|
1131
|
-
language: (0, types_1.toOptString)(settings.language)
|
|
1131
|
+
language: (0, types_1.toOptString)(settings.language),
|
|
1132
|
+
samplesPerFrame: (0, types_1.toOptInt)(settings.numSamplesPerFrame),
|
|
1133
|
+
outputFrameCount: (0, types_1.toOptInt)(settings.numFrames)
|
|
1132
1134
|
});
|
|
1133
1135
|
this.grpcStream = this.client.media.createInputAudioSignalGenerator(config);
|
|
1134
1136
|
this.initialised = new Promise((resolve, reject) => {
|
|
@@ -1412,4 +1414,51 @@ class FileMp4InputNode extends common_1.SourceMediaNode {
|
|
|
1412
1414
|
}
|
|
1413
1415
|
}
|
|
1414
1416
|
exports.FileMp4InputNode = FileMp4InputNode;
|
|
1417
|
+
/**
|
|
1418
|
+
* @public
|
|
1419
|
+
* see: {@link NorskInput.fileWav}
|
|
1420
|
+
*/
|
|
1421
|
+
class FileWavInputNode extends common_1.SourceMediaNode {
|
|
1422
|
+
/** @internal */
|
|
1423
|
+
constructor(settings, client, unregisterNode) {
|
|
1424
|
+
super(client, settings.onOutboundContextChange);
|
|
1425
|
+
const config = (0, utils_1.provideFull)(media_pb_1.FileWavInputConfiguration, {
|
|
1426
|
+
...settings,
|
|
1427
|
+
id: settings.id
|
|
1428
|
+
? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
|
|
1429
|
+
: undefined,
|
|
1430
|
+
});
|
|
1431
|
+
this.grpcStream = this.client.media.createInputFileWav(config);
|
|
1432
|
+
this.initialised = new Promise((resolve, reject) => {
|
|
1433
|
+
this.grpcStream.on("data", (data) => {
|
|
1434
|
+
const messageCase = data.message.case;
|
|
1435
|
+
switch (messageCase) {
|
|
1436
|
+
case undefined:
|
|
1437
|
+
break;
|
|
1438
|
+
case "nodeId": {
|
|
1439
|
+
this.id = data.message.value.id;
|
|
1440
|
+
settings.onCreate && settings.onCreate(this);
|
|
1441
|
+
resolve();
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
case "outboundContext": {
|
|
1445
|
+
const context = data.message.value;
|
|
1446
|
+
this.outboundContextChange(context);
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
default:
|
|
1450
|
+
(0, utils_1.exhaustiveCheck)(messageCase);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
(0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "mp4file", reject, settings);
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
/** @internal */
|
|
1457
|
+
static async create(settings, client, unregisterNode) {
|
|
1458
|
+
const node = new FileWavInputNode(settings, client, unregisterNode);
|
|
1459
|
+
await node.initialised;
|
|
1460
|
+
return node;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
exports.FileWavInputNode = FileWavInputNode;
|
|
1415
1464
|
//# sourceMappingURL=input.js.map
|
|
@@ -322,6 +322,7 @@ class MediaStoreAsset {
|
|
|
322
322
|
this.closed = false;
|
|
323
323
|
this.client = client;
|
|
324
324
|
this.settings = settings;
|
|
325
|
+
this._complete = false;
|
|
325
326
|
this.grpcStream = this.client.media.createMediaStoreAsset((0, common_1.provideFull)(media_pb_1.MediaStoreAssetSettings, {
|
|
326
327
|
mediaStoreName: settings.name,
|
|
327
328
|
assetSource: (0, common_1.provideFull)(media_pb_1.MediaStoreAssetSource, settings.source ?
|
|
@@ -343,8 +344,12 @@ class MediaStoreAsset {
|
|
|
343
344
|
}));
|
|
344
345
|
this._ready = new Promise((resolve, reject) => {
|
|
345
346
|
this.grpcStream.on("error", (err) => reject(err));
|
|
346
|
-
this.grpcStream.on("close", () =>
|
|
347
|
-
|
|
347
|
+
this.grpcStream.on("close", () => { if (!this._complete) {
|
|
348
|
+
reject("Close before import complete");
|
|
349
|
+
} });
|
|
350
|
+
this.grpcStream.on("end", () => { if (!this._complete) {
|
|
351
|
+
reject("End before import complete");
|
|
352
|
+
} });
|
|
348
353
|
this.grpcStream.on("data", (data) => {
|
|
349
354
|
const messageCase = data.message.case;
|
|
350
355
|
switch (messageCase) {
|
|
@@ -355,6 +360,7 @@ class MediaStoreAsset {
|
|
|
355
360
|
break;
|
|
356
361
|
}
|
|
357
362
|
case "importComplete": {
|
|
363
|
+
this._complete = true;
|
|
358
364
|
this.durationMs = data.message.value.durationMs;
|
|
359
365
|
this.size = data.message.value.size;
|
|
360
366
|
const metadataFn = util
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as grpc from "@grpc/grpc-js";
|
|
2
2
|
import { CmafAudioMessage, CmafMultiVariantMessage, CmafVideoMessage, CmafWebVttMessage, HlsOutputEvent, HlsTsAudioMessage, HlsTsCombinedPushMessage, HlsTsVideoMessage, Subscription, HlsTsMultiVariantMessage } from "@norskvideo/norsk-api/lib/media_pb";
|
|
3
|
-
import { AwsCredentials, EncryptionSettings, IceServerSettings, SrtMode, StreamMetadata, Scte35SpliceInfoSection } from "./types";
|
|
3
|
+
import { AwsCredentials, EncryptionSettings, IceServerSettings, SrtMode, StreamMetadata, Scte35SpliceInfoSection, AudioSegmentationStrategy, VideoSegmentationStrategy } from "./types";
|
|
4
4
|
import { AutoProcessorMediaNode, ProcessorNodeSettings } from "./processor";
|
|
5
5
|
import { AutoSinkMediaNode, MediaClient, MediaNodeState, SinkNodeSettings, StreamStatisticsMixin } from "./common";
|
|
6
6
|
/**
|
|
@@ -60,6 +60,11 @@ export interface HlsTsVideoOutputSettings extends SinkNodeSettings<HlsTsVideoOut
|
|
|
60
60
|
* to produce compliant segments that are less than or equal to this in duration
|
|
61
61
|
*/
|
|
62
62
|
segmentDurationSeconds: number;
|
|
63
|
+
/**
|
|
64
|
+
* Optional stategy to be used in creating segments of the correct length
|
|
65
|
+
* care should be taken to not supply values that result in segments of an invalid length
|
|
66
|
+
*/
|
|
67
|
+
segmentationStrategy?: VideoSegmentationStrategy;
|
|
63
68
|
/**
|
|
64
69
|
* A list of destinations {@link CmafDestinationSettings} for this stream to be published to
|
|
65
70
|
*/
|
|
@@ -98,6 +103,11 @@ export interface HlsTsAudioOutputSettings extends SinkNodeSettings<HlsTsAudioOut
|
|
|
98
103
|
* without going over this target using the durations of the individual audio frames
|
|
99
104
|
*/
|
|
100
105
|
segmentDurationSeconds: number;
|
|
106
|
+
/**
|
|
107
|
+
* Optional stategy to be used in creating segments of the correct length
|
|
108
|
+
* care should be taken to not supply values that result in segments of an invalid length
|
|
109
|
+
*/
|
|
110
|
+
segmentationStrategy?: AudioSegmentationStrategy;
|
|
101
111
|
/**
|
|
102
112
|
* A list of destinations {@link CmafDestinationSettings} for this stream to be published to
|
|
103
113
|
*/
|
|
@@ -171,11 +181,16 @@ export interface UpdateCredentials {
|
|
|
171
181
|
*/
|
|
172
182
|
export interface HlsTsCombinedPushOutputSettings extends SinkNodeSettings<HlsTsCombinedPushOutputNode> {
|
|
173
183
|
/**
|
|
174
|
-
* The target segment duration in seconds. Norsk will use the framerate of the video stream in order
|
|
184
|
+
* The target segment duration in seconds. By default, Norsk will use the framerate of the video stream in order
|
|
175
185
|
* to produce compliant segments that are less than or equal to this in duration, with audio packaged alongside
|
|
176
186
|
* using timestamps to line them up
|
|
177
187
|
*/
|
|
178
188
|
segmentDurationSeconds: number;
|
|
189
|
+
/**
|
|
190
|
+
* Optional stategy to be used instead of the default in creating segments of the correct length
|
|
191
|
+
* care should be taken to not supply values that result in segments of an invalid length
|
|
192
|
+
*/
|
|
193
|
+
segmentationStrategy?: VideoSegmentationStrategy;
|
|
179
194
|
/**
|
|
180
195
|
* The destination {@link CmafDestinationSettings} for this stream to be published to
|
|
181
196
|
*/
|
|
@@ -860,6 +875,23 @@ export declare class FileMp4OutputNode extends AutoSinkMediaNode<"audio" | "vide
|
|
|
860
875
|
*/
|
|
861
876
|
writeFile(nonfragmentedFileName: string): void;
|
|
862
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* @public
|
|
880
|
+
* Settings to control WAV file output
|
|
881
|
+
* see {@link NorskOutput.fileWav}
|
|
882
|
+
*/
|
|
883
|
+
export interface FileWavOutputSettings extends SinkNodeSettings<FileWavOutputNode> {
|
|
884
|
+
/**
|
|
885
|
+
* Required: stream audio to this file.
|
|
886
|
+
*/
|
|
887
|
+
fileName: string;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* @public
|
|
891
|
+
* see: {@link NorskOutput.fileWav}
|
|
892
|
+
*/
|
|
893
|
+
export declare class FileWavOutputNode extends AutoSinkMediaNode<"audio"> {
|
|
894
|
+
}
|
|
863
895
|
/**
|
|
864
896
|
* @public
|
|
865
897
|
* Settings to control MP4 file output
|
|
@@ -1031,6 +1063,14 @@ export interface NorskOutput {
|
|
|
1031
1063
|
* @param settings - Configuration for the MP4 output.
|
|
1032
1064
|
*/
|
|
1033
1065
|
fileMp4(settings: FileMp4OutputSettings): Promise<FileMp4OutputNode>;
|
|
1066
|
+
/**
|
|
1067
|
+
* Output WAV files to disk. A WAV output cannot handle
|
|
1068
|
+
* context changes (for example, a change of sample rate),
|
|
1069
|
+
* so it is important that the upstream data is normalised.
|
|
1070
|
+
* The file being written to is finalised and closed when
|
|
1071
|
+
* the inbound context becomes empty.
|
|
1072
|
+
*/
|
|
1073
|
+
fileWav(settings: FileWavOutputSettings): Promise<FileWavOutputNode>;
|
|
1034
1074
|
/**
|
|
1035
1075
|
* Output a WebVTT subtitle file to disk
|
|
1036
1076
|
* @param settings - Configuration for the WebVTT output.
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MoqOutputNode = exports.FileWebVttOutputNode = exports.FileMp4OutputNode = exports.FileTsOutputNode = exports.RtmpOutputNode = exports.RtmpConnectionFailureReason = exports.WhepOutputNode = exports.WhipOutputNode = exports.SrtOutputNode = exports.UdpTsOutputNode = exports.HlsTsMultiVariantOutputNode = exports.CmafMultiVariantOutputNode = exports.CmafWebVttOutputNode = exports.HlsTsCombinedPushOutputNode = exports.HlsTsAudioOutputNode = exports.HlsTsVideoOutputNode = exports.CmafAudioOutputNode = exports.CmafVideoOutputNode = exports.isScheduledTag = exports.isProgramDateTime = exports.isHlsTag = exports.isAdMarker = exports.isMediaSegment = void 0;
|
|
6
|
+
exports.MoqOutputNode = exports.FileWebVttOutputNode = exports.FileWavOutputNode = exports.FileMp4OutputNode = exports.FileTsOutputNode = exports.RtmpOutputNode = exports.RtmpConnectionFailureReason = exports.WhepOutputNode = exports.WhipOutputNode = exports.SrtOutputNode = exports.UdpTsOutputNode = exports.HlsTsMultiVariantOutputNode = exports.CmafMultiVariantOutputNode = exports.CmafWebVttOutputNode = exports.HlsTsCombinedPushOutputNode = exports.HlsTsAudioOutputNode = exports.HlsTsVideoOutputNode = exports.CmafAudioOutputNode = exports.CmafVideoOutputNode = exports.isScheduledTag = exports.isProgramDateTime = exports.isHlsTag = exports.isAdMarker = exports.isMediaSegment = void 0;
|
|
7
7
|
const media_pb_1 = require("@norskvideo/norsk-api/lib/media_pb");
|
|
8
8
|
const types_1 = require("./types");
|
|
9
9
|
const common_pb_1 = require("@norskvideo/norsk-api/lib/shared/common_pb");
|
|
@@ -139,6 +139,7 @@ class CmafNodeBase extends processor_1.AutoProcessorMediaNode {
|
|
|
139
139
|
break;
|
|
140
140
|
case "nodeId":
|
|
141
141
|
this.id = data.message.value.id;
|
|
142
|
+
settings.onCreate && settings.onCreate(this);
|
|
142
143
|
resolve();
|
|
143
144
|
break;
|
|
144
145
|
case "inboundContext": {
|
|
@@ -563,7 +564,8 @@ class HlsTsVideoOutputNode extends CmafNodeWithPlaylist {
|
|
|
563
564
|
bitrate: settings.bitrate || 0,
|
|
564
565
|
name: (0, types_1.toOptString)(settings.name),
|
|
565
566
|
pdtEverySegment: !!settings.pdtEverySegment,
|
|
566
|
-
metrics: (0, types_1.toApiMetrics)(settings.metrics)
|
|
567
|
+
metrics: (0, types_1.toApiMetrics)(settings.metrics),
|
|
568
|
+
segmentationStrategy: settings.segmentationStrategy ? (0, types_1.toSegmentationStrategy)(settings.segmentationStrategy) : undefined
|
|
567
569
|
});
|
|
568
570
|
const hls = client.media.createOutputHlsTsVideo();
|
|
569
571
|
hls.write((0, utils_1.provideFull)(media_pb_1.HlsTsVideoMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
|
|
@@ -618,7 +620,8 @@ class HlsTsAudioOutputNode extends CmafNodeWithPlaylist {
|
|
|
618
620
|
bitrate: settings.bitrate || 0,
|
|
619
621
|
name: (0, types_1.toOptString)(settings.name),
|
|
620
622
|
pdtEverySegment: !!settings.pdtEverySegment,
|
|
621
|
-
metrics: (0, types_1.toApiMetrics)(settings.metrics)
|
|
623
|
+
metrics: (0, types_1.toApiMetrics)(settings.metrics),
|
|
624
|
+
segmentationStrategy: settings.segmentationStrategy ? (0, types_1.toSegmentationStrategy)(settings.segmentationStrategy) : undefined
|
|
622
625
|
});
|
|
623
626
|
const hls = client.media.createOutputHlsTsAudio();
|
|
624
627
|
hls.write((0, utils_1.provideFull)(media_pb_1.HlsTsAudioMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
|
|
@@ -676,7 +679,8 @@ class HlsTsCombinedPushOutputNode extends CmafNodeWithPlaylist {
|
|
|
676
679
|
destination: mkCmafDestination(settings.destination),
|
|
677
680
|
m3uAdditions: settings.m3uAdditions || "",
|
|
678
681
|
name: (0, types_1.toOptString)(settings.name),
|
|
679
|
-
pdtEverySegment: !!settings.pdtEverySegment
|
|
682
|
+
pdtEverySegment: !!settings.pdtEverySegment,
|
|
683
|
+
segmentationStrategy: settings.segmentationStrategy ? (0, types_1.toSegmentationStrategy)(settings.segmentationStrategy) : undefined
|
|
680
684
|
});
|
|
681
685
|
const hls = client.media.createOutputHlsTsCombinedPush();
|
|
682
686
|
hls.write((0, utils_1.provideFull)(media_pb_1.HlsTsCombinedPushMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
|
|
@@ -1332,6 +1336,57 @@ class FileMp4OutputNode extends common_1.AutoSinkMediaNode {
|
|
|
1332
1336
|
}
|
|
1333
1337
|
}
|
|
1334
1338
|
exports.FileMp4OutputNode = FileMp4OutputNode;
|
|
1339
|
+
/**
|
|
1340
|
+
* @public
|
|
1341
|
+
* see: {@link NorskOutput.fileWav}
|
|
1342
|
+
*/
|
|
1343
|
+
class FileWavOutputNode extends common_1.AutoSinkMediaNode {
|
|
1344
|
+
/** @internal */
|
|
1345
|
+
constructor(settings, client, unregisterNode) {
|
|
1346
|
+
super(client, () => this.grpcStream, async (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.FileWavOutputMessage, (0, utils_1.mkMessageCase)({ subscription }))), settings.onSubscriptionError);
|
|
1347
|
+
const config = (0, utils_1.provideFull)(media_pb_1.FileWavOutputConfiguration, {
|
|
1348
|
+
...settings,
|
|
1349
|
+
id: settings.id
|
|
1350
|
+
? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
|
|
1351
|
+
: undefined,
|
|
1352
|
+
});
|
|
1353
|
+
this.grpcStream = this.client.media.createOutputFileWav();
|
|
1354
|
+
this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.FileWavOutputMessage, (0, utils_1.mkMessageCase)({ configuration: config })));
|
|
1355
|
+
this.initialised = new Promise((resolve, reject) => {
|
|
1356
|
+
this.grpcStream.on("data", (data) => {
|
|
1357
|
+
const messageCase = data.message.case;
|
|
1358
|
+
switch (messageCase) {
|
|
1359
|
+
case undefined:
|
|
1360
|
+
break;
|
|
1361
|
+
case "subscriptionResponse":
|
|
1362
|
+
super.subscriptionResponse(data.message.value);
|
|
1363
|
+
break;
|
|
1364
|
+
case "nodeId": {
|
|
1365
|
+
this.id = data.message.value.id;
|
|
1366
|
+
settings.onCreate && settings.onCreate(this);
|
|
1367
|
+
resolve();
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
case "inboundContext": {
|
|
1371
|
+
const context = data.message.value;
|
|
1372
|
+
super.handleInboundContext(context);
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
default:
|
|
1376
|
+
(0, utils_1.exhaustiveCheck)(messageCase);
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
(0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "fileMp4Output", reject, settings);
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
/** @internal */
|
|
1383
|
+
static async create(settings, client, unregisterNode) {
|
|
1384
|
+
const node = new FileWavOutputNode(settings, client, unregisterNode);
|
|
1385
|
+
await node.initialised;
|
|
1386
|
+
return node;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
exports.FileWavOutputNode = FileWavOutputNode;
|
|
1335
1390
|
/**
|
|
1336
1391
|
* @public
|
|
1337
1392
|
* see: {@link NorskOutput.fileWebVtt}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { PlainMessage } from "@bufbuild/protobuf";
|
|
3
|
-
import { StreamStatisticsSampling, Subscription, SipEvent_Status } from "@norskvideo/norsk-api/lib/media_pb";
|
|
3
|
+
import { StreamStatisticsSampling, Subscription, SipEvent_Status, AudioWatermarkLicenseInformation, AudioWatermarkSnapEvent } from "@norskvideo/norsk-api/lib/media_pb";
|
|
4
4
|
import { FrameRate, VancPayloadFormat, VancType2AncillaryId } from "../types";
|
|
5
5
|
import { AutoSinkMediaNode, MediaClient, MediaNodeState, SinkNodeSettings, SourceMediaNode, SourceMediaNodeEvents, SourceNodeSettings, StreamStatisticsMixin } from "./common";
|
|
6
|
-
import { AacProfile, AudioMeasureLevels, AwsCredentials, ChannelLayout, ComposeMissingStreamBehaviour, ComposeHardwareAcceleration, Db, IceServerSettings, LoganH264, LoganHevc, MultiStreamStatistics, NvidiaH264, NvidiaHevc, PixelFormat, QuadraH264, QuadraHevc, AmdU30H264, AmdU30Hevc, AmdMA35DH264, AmdMA35DHevc, Resolution, SampleAspectRatio, SampleRate, SentenceBuildMode, SimpleEasing, StabilizationMode, StreamKey, StreamMetadata, X264Codec, X265Codec, SubscriptionError, DeinterlaceSettings, Scte35SpliceInfoSection, Interval, SampleFormat, VideoStreamMetadata } from "./types";
|
|
6
|
+
import { AacProfile, AudioMeasureLevels, AwsCredentials, ChannelLayout, ComposeMissingStreamBehaviour, ComposeHardwareAcceleration, Db, IceServerSettings, LoganH264, LoganHevc, MultiStreamStatistics, NvidiaH264, NvidiaHevc, PixelFormat, QuadraH264, QuadraHevc, AmdU30H264, AmdU30Hevc, AmdMA35DH264, AmdMA35DHevc, Resolution, SampleAspectRatio, SampleRate, SentenceBuildMode, SimpleEasing, StabilizationMode, StreamKey, StreamMetadata, X264Codec, X265Codec, SubscriptionError, DeinterlaceSettings, Scte35SpliceInfoSection, Interval, SampleFormat, VideoStreamMetadata, QuadraAv1, AmdMA35DAv1 } from "./types";
|
|
7
7
|
import { Writable, Readable } from 'stream';
|
|
8
8
|
/** @public */
|
|
9
9
|
export interface ProcessorMediaNode<Pins extends string> extends SourceMediaNode<SourceMediaNodeEvents>, AutoSinkMediaNode<Pins, SourceMediaNodeEvents> {
|
|
@@ -91,6 +91,10 @@ export interface StreamSwitchSmoothSettings<Pins extends string> extends Process
|
|
|
91
91
|
*/
|
|
92
92
|
hardwareAcceleration?: ComposeHardwareAcceleration;
|
|
93
93
|
}
|
|
94
|
+
export interface StreamSwitchSmoothSwitchOptions {
|
|
95
|
+
/** Duration to switch for this transition only (if omitted, the configured default shall be used, to suppress a transition specify this but as 0). */
|
|
96
|
+
transitionDurationMs?: number;
|
|
97
|
+
}
|
|
94
98
|
/**
|
|
95
99
|
* @public
|
|
96
100
|
* see: {@link NorskControl.streamSwitchSmooth}
|
|
@@ -100,7 +104,7 @@ export declare class StreamSwitchSmoothNode<Pins extends string> extends Process
|
|
|
100
104
|
* @public
|
|
101
105
|
* Switches the source used for the current output of this node
|
|
102
106
|
*/
|
|
103
|
-
switchSource(newSource: Pins): void;
|
|
107
|
+
switchSource(newSource: Pins, options?: StreamSwitchSmoothSwitchOptions): void;
|
|
104
108
|
}
|
|
105
109
|
/**
|
|
106
110
|
* @public
|
|
@@ -472,6 +476,36 @@ export interface ComposePart<Pins> {
|
|
|
472
476
|
*/
|
|
473
477
|
compose: <Pins extends string>(partStream: VideoStreamMetadata, settings: VideoComposeSettings<Pins>) => ComposeOperation;
|
|
474
478
|
}
|
|
479
|
+
export interface AudioWatermarkOnlineLicense {
|
|
480
|
+
type: "online";
|
|
481
|
+
login: string;
|
|
482
|
+
password: string;
|
|
483
|
+
server?: string;
|
|
484
|
+
port?: number;
|
|
485
|
+
licenseId: bigint;
|
|
486
|
+
}
|
|
487
|
+
export interface AudioWatermarkOfflineLicense {
|
|
488
|
+
type: "offline";
|
|
489
|
+
kantarLicensePath: string;
|
|
490
|
+
audienceLicensePath: string;
|
|
491
|
+
channelName: string;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* @public
|
|
495
|
+
* Settings for an Audio Watermark node
|
|
496
|
+
* see: {@link NorskTransform.audioWatermark}
|
|
497
|
+
* */
|
|
498
|
+
export interface AudioWatermarkSettings extends ProcessorNodeSettings<AudioWatermarkNode> {
|
|
499
|
+
license: AudioWatermarkOnlineLicense | AudioWatermarkOfflineLicense;
|
|
500
|
+
onLicenseInformation?: (info: AudioWatermarkLicenseInformation) => void;
|
|
501
|
+
onSnapEvent?: (event: AudioWatermarkSnapEvent) => void;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* @public
|
|
505
|
+
* see: {@link NorskTransform.audioWatermakr}
|
|
506
|
+
*/
|
|
507
|
+
export declare class AudioWatermarkNode extends AutoProcessorMediaNode<"audio"> {
|
|
508
|
+
}
|
|
475
509
|
export declare type ComposeOperation = {
|
|
476
510
|
/**
|
|
477
511
|
* The area within the source picture to include. This may be the full picture
|
|
@@ -1025,7 +1059,7 @@ export interface VideoEncodeRung {
|
|
|
1025
1059
|
* A ladder can use several different codecs across its various rungs and the
|
|
1026
1060
|
* VideoEncode node will attempt to build a pipeline that uses the hardware efficently
|
|
1027
1061
|
*/
|
|
1028
|
-
codec: X264Codec | X265Codec | NvidiaH264 | NvidiaHevc | LoganH264 | LoganHevc | QuadraH264 | QuadraHevc | AmdU30H264 | AmdU30Hevc | AmdMA35DH264 | AmdMA35DHevc;
|
|
1062
|
+
codec: X264Codec | X265Codec | NvidiaH264 | NvidiaHevc | LoganH264 | LoganHevc | QuadraH264 | QuadraHevc | QuadraAv1 | AmdU30H264 | AmdU30Hevc | AmdMA35DH264 | AmdMA35DHevc | AmdMA35DAv1;
|
|
1029
1063
|
}
|
|
1030
1064
|
/**
|
|
1031
1065
|
* @public
|
|
@@ -1323,6 +1357,21 @@ export interface NorskTransform {
|
|
|
1323
1357
|
* bitrate.
|
|
1324
1358
|
*/
|
|
1325
1359
|
audioEncode(settings: AudioEncodeSettings): Promise<AudioEncodeNode>;
|
|
1360
|
+
/**
|
|
1361
|
+
* Embeds audio watermarks into the audio stream, using Kantar Media's SNAP
|
|
1362
|
+
* technology, see kantarmedia.com for details. Norsk supports both online
|
|
1363
|
+
* and offline licensing models, and the relevant license information is
|
|
1364
|
+
* provided to Norsk through the AudioWatermarkConfiguration message.
|
|
1365
|
+
* At runtime, notifications are returned through the stream of
|
|
1366
|
+
* AudioWatermarkEvent messages, which includes Kantar event information
|
|
1367
|
+
* and, for offline licenses, a notification of the number of days remaining.
|
|
1368
|
+
* Note that audio watermarking introduces around 50-60ms of delay to the
|
|
1369
|
+
* audio stream; Norsk will ensure that all related media streams (e.g.
|
|
1370
|
+
* video) are kept in sync.
|
|
1371
|
+
* @param settings - Settings for the watermark process, primarily
|
|
1372
|
+
* Kantar license information.
|
|
1373
|
+
*/
|
|
1374
|
+
audioWatermark(settings: AudioWatermarkSettings): Promise<AudioWatermarkNode>;
|
|
1326
1375
|
/**
|
|
1327
1376
|
* Translate subtitles using the AWS Translate service.
|
|
1328
1377
|
*
|
|
@@ -1459,7 +1508,6 @@ export declare class NorskProcessor {
|
|
|
1459
1508
|
* Implements the {@link NorskTransform} interface
|
|
1460
1509
|
*/
|
|
1461
1510
|
transform: NorskTransform;
|
|
1462
|
-
close(): Promise<void>;
|
|
1463
1511
|
constructor(client: MediaClient);
|
|
1464
1512
|
}
|
|
1465
1513
|
export {};
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.NorskProcessor = exports.SipNode = exports.WebRTCBrowserNode = exports.StreamChaosMonkeyNode = exports.VideoTransformNode = exports.VideoDecodeNode = exports.VideoEncodeNode = exports.AudioTranscribeWhisperNode = exports.AudioTranscribeAzureNode = exports.SubtitleConvertNode = exports.SubtitleTranslateAwsNode = exports.AudioTranscribeAwsNode = exports.AudioBuildMultichannelNode = exports.AudioSplitMultichannelNode = exports.AudioGainNode = exports.AudioMixMatrixNode = exports.AudioMixNode = exports.VideoComposeNode = exports.VideoComposeDefaults = exports.AudioEncodeNode = exports.MetadataCombineMode = exports.AncillaryNode = exports.StreamConditionNode = exports.StreamAlignNode = exports.StreamSyncNode = exports.JitterBufferNode = exports.StreamMetadataOverrideNode = exports.StreamKeyOverrideNode = exports.StreamTimestampNudgeNode = exports.AudioMeasureLevelsNode = exports.StreamStatisticsNode = exports.StreamSwitchSmoothNode = exports.StreamSwitchHardNode = exports.AutoProcessorMediaNode = exports.ProcessorMediaNode = void 0;
|
|
6
|
+
exports.NorskProcessor = exports.SipNode = exports.WebRTCBrowserNode = exports.StreamChaosMonkeyNode = exports.VideoTransformNode = exports.VideoDecodeNode = exports.VideoEncodeNode = exports.AudioTranscribeWhisperNode = exports.AudioTranscribeAzureNode = exports.SubtitleConvertNode = exports.SubtitleTranslateAwsNode = exports.AudioTranscribeAwsNode = exports.AudioBuildMultichannelNode = exports.AudioSplitMultichannelNode = exports.AudioGainNode = exports.AudioMixMatrixNode = exports.AudioMixNode = exports.VideoComposeNode = exports.VideoComposeDefaults = exports.AudioWatermarkNode = exports.AudioEncodeNode = exports.MetadataCombineMode = exports.AncillaryNode = exports.StreamConditionNode = exports.StreamAlignNode = exports.StreamSyncNode = exports.JitterBufferNode = exports.StreamMetadataOverrideNode = exports.StreamKeyOverrideNode = exports.StreamTimestampNudgeNode = exports.AudioMeasureLevelsNode = exports.StreamStatisticsNode = exports.StreamSwitchSmoothNode = exports.StreamSwitchHardNode = exports.AutoProcessorMediaNode = exports.ProcessorMediaNode = void 0;
|
|
7
7
|
const events_1 = __importDefault(require("events"));
|
|
8
8
|
const media_pb_1 = require("@norskvideo/norsk-api/lib/media_pb");
|
|
9
9
|
const common_pb_1 = require("@norskvideo/norsk-api/lib/shared/common_pb");
|
|
@@ -220,10 +220,12 @@ class StreamSwitchSmoothNode extends ProcessorMediaNode {
|
|
|
220
220
|
* @public
|
|
221
221
|
* Switches the source used for the current output of this node
|
|
222
222
|
*/
|
|
223
|
-
switchSource(newSource) {
|
|
223
|
+
switchSource(newSource, options) {
|
|
224
|
+
const transitionDurationMs = options?.transitionDurationMs;
|
|
224
225
|
this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.StreamSwitchSmoothMessage, (0, utils_1.mkMessageCase)({
|
|
225
226
|
switchSource: (0, utils_1.provideFull)(media_pb_1.StreamSwitchSmoothSwitch, {
|
|
226
227
|
newActiveSource: (0, utils_1.provideFull)(media_pb_1.InputPin, { inputPin: newSource }),
|
|
228
|
+
transitionDurationMs: transitionDurationMs !== undefined ? (0, utils_1.provideFull)(common_pb_1.OptionalFloat, { value: transitionDurationMs }) : undefined
|
|
227
229
|
}),
|
|
228
230
|
})));
|
|
229
231
|
}
|
|
@@ -1088,6 +1090,89 @@ class AudioEncodeNode extends AutoProcessorMediaNode {
|
|
|
1088
1090
|
}
|
|
1089
1091
|
}
|
|
1090
1092
|
exports.AudioEncodeNode = AudioEncodeNode;
|
|
1093
|
+
/**
|
|
1094
|
+
* @public
|
|
1095
|
+
* see: {@link NorskTransform.audioWatermakr}
|
|
1096
|
+
*/
|
|
1097
|
+
class AudioWatermarkNode extends AutoProcessorMediaNode {
|
|
1098
|
+
/** @internal */
|
|
1099
|
+
constructor(settings, client, unregisterNode) {
|
|
1100
|
+
super(client, unregisterNode, () => this.grpcStream, async (subscription) => this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.AudioWatermarkMessage, (0, utils_1.mkMessageCase)({ subscription }))));
|
|
1101
|
+
const license = (() => {
|
|
1102
|
+
const type = settings.license.type;
|
|
1103
|
+
switch (type) {
|
|
1104
|
+
case "online":
|
|
1105
|
+
return (0, utils_1.mkCase)({ onlineLicense: (0, utils_1.provideFull)(media_pb_1.AudioWatermarkConfiguration_OnlineLicense, { login: settings.license.login,
|
|
1106
|
+
password: settings.license.password,
|
|
1107
|
+
server: settings.license.server == undefined ? "" : settings.license.server,
|
|
1108
|
+
port: settings.license.port == undefined ? 0 : settings.license.port,
|
|
1109
|
+
licenseId: settings.license.licenseId,
|
|
1110
|
+
}) });
|
|
1111
|
+
case "offline":
|
|
1112
|
+
return (0, utils_1.mkCase)({ offlineLicense: (0, utils_1.provideFull)(media_pb_1.AudioWatermarkConfiguration_OfflineLicense, { audienceLicense: settings.license.audienceLicensePath,
|
|
1113
|
+
kantarLicense: settings.license.kantarLicensePath,
|
|
1114
|
+
channelName: settings.license.channelName
|
|
1115
|
+
}) });
|
|
1116
|
+
default:
|
|
1117
|
+
(0, utils_1.exhaustiveCheck)(type);
|
|
1118
|
+
}
|
|
1119
|
+
})();
|
|
1120
|
+
const config = new media_pb_1.AudioWatermarkConfiguration({
|
|
1121
|
+
id: settings.id
|
|
1122
|
+
? (0, utils_1.provideFull)(media_pb_1.MediaNodeId, { id: settings.id })
|
|
1123
|
+
: undefined,
|
|
1124
|
+
license: license
|
|
1125
|
+
});
|
|
1126
|
+
this.grpcStream = this.client.media.createTransformAudioWatermark();
|
|
1127
|
+
this.grpcStream.write((0, utils_1.provideFull)(media_pb_1.AudioWatermarkMessage, (0, utils_1.mkMessageCase)({ initialConfig: config })));
|
|
1128
|
+
this.initialised = new Promise((resolve, reject) => {
|
|
1129
|
+
this.grpcStream.on("data", (data) => {
|
|
1130
|
+
const messageCase = data.message.case;
|
|
1131
|
+
switch (messageCase) {
|
|
1132
|
+
case undefined:
|
|
1133
|
+
break;
|
|
1134
|
+
case "subscriptionResponse":
|
|
1135
|
+
super.subscriptionResponse(data.message.value);
|
|
1136
|
+
break;
|
|
1137
|
+
case "nodeId": {
|
|
1138
|
+
this.id = data.message.value.id;
|
|
1139
|
+
settings.onCreate && settings.onCreate(this);
|
|
1140
|
+
resolve();
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
case "outboundContext": {
|
|
1144
|
+
const context = data.message.value;
|
|
1145
|
+
this.outboundContextChange(context);
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
case "inboundContext": {
|
|
1149
|
+
const context = data.message.value;
|
|
1150
|
+
super.handleInboundContext(context);
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
case "licenseInformation": {
|
|
1154
|
+
settings.onLicenseInformation && settings.onLicenseInformation(data.message.value);
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
case "snapEvent": {
|
|
1158
|
+
settings.onSnapEvent && settings.onSnapEvent(data.message.value);
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
default:
|
|
1162
|
+
(0, utils_1.exhaustiveCheck)(messageCase);
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
(0, common_1.registerStreamHandlers)(this.grpcStream, () => unregisterNode(this), "audioWatermark", reject, settings);
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
/** @internal */
|
|
1169
|
+
static async create(settings, client, unregisterNode) {
|
|
1170
|
+
const node = new AudioWatermarkNode(settings, client, unregisterNode);
|
|
1171
|
+
await node.initialised;
|
|
1172
|
+
return node;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
exports.AudioWatermarkNode = AudioWatermarkNode;
|
|
1091
1176
|
exports.VideoComposeDefaults = {
|
|
1092
1177
|
/**
|
|
1093
1178
|
* Takes the whole input part and renders it over the whole output video, scaling it as required to fit
|
|
@@ -2104,6 +2189,12 @@ class VideoEncodeNode extends AutoProcessorMediaNode {
|
|
|
2104
2189
|
value: (0, types_2.toQuadraHevc)(stream.codec),
|
|
2105
2190
|
};
|
|
2106
2191
|
break;
|
|
2192
|
+
case "quadra-av1":
|
|
2193
|
+
codec = {
|
|
2194
|
+
case: "quadraAv1",
|
|
2195
|
+
value: (0, types_2.toQuadraAv1)(stream.codec),
|
|
2196
|
+
};
|
|
2197
|
+
break;
|
|
2107
2198
|
case "amdU30-h264":
|
|
2108
2199
|
codec = {
|
|
2109
2200
|
case: "amdU30H264",
|
|
@@ -2128,6 +2219,12 @@ class VideoEncodeNode extends AutoProcessorMediaNode {
|
|
|
2128
2219
|
value: (0, types_2.toAmdMA35DHevc)(stream.codec),
|
|
2129
2220
|
};
|
|
2130
2221
|
break;
|
|
2222
|
+
case "amdMA35D-av1":
|
|
2223
|
+
codec = {
|
|
2224
|
+
case: "amdMA35DAv1",
|
|
2225
|
+
value: (0, types_2.toAmdMA35DAv1)(stream.codec),
|
|
2226
|
+
};
|
|
2227
|
+
break;
|
|
2131
2228
|
default:
|
|
2132
2229
|
(0, utils_1.exhaustiveCheck)(codecType);
|
|
2133
2230
|
}
|
|
@@ -2604,6 +2701,7 @@ class NorskProcessor {
|
|
|
2604
2701
|
audioBuildMultichannel: async (settings) => AudioBuildMultichannelNode.create(settings, client, unregisterNode).then(registerNode),
|
|
2605
2702
|
audioSplitMultichannel: async (settings) => AudioSplitMultichannelNode.create(settings, client, unregisterNode).then(registerNode),
|
|
2606
2703
|
audioEncode: async (settings) => AudioEncodeNode.create(settings, client, unregisterNode).then(registerNode),
|
|
2704
|
+
audioWatermark: async (settings) => AudioWatermarkNode.create(settings, client, unregisterNode).then(registerNode),
|
|
2607
2705
|
subtitleTranslateAws: async (settings) => SubtitleTranslateAwsNode.create(settings, client, unregisterNode).then(registerNode),
|
|
2608
2706
|
streamTimestampNudge: async (settings) => StreamTimestampNudgeNode.create(settings, client, unregisterNode).then(registerNode),
|
|
2609
2707
|
streamKeyOverride: async (settings) => StreamKeyOverrideNode.create(settings, client, unregisterNode).then(registerNode),
|
|
@@ -2627,6 +2725,7 @@ class NorskProcessor {
|
|
|
2627
2725
|
this.nodes = this.nodes.filter(n => n != node);
|
|
2628
2726
|
node.finalise();
|
|
2629
2727
|
}
|
|
2728
|
+
/** @internal */
|
|
2630
2729
|
async close() {
|
|
2631
2730
|
for (const n of this.nodes) {
|
|
2632
2731
|
await n.close();
|