@eluvio/elv-client-js 4.0.47 → 4.0.49

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.
@@ -2215,7 +2215,7 @@ exports.UpdateContentObjectGraph = /*#__PURE__*/function () {
2215
2215
  return _context30.delegateYield(_loop(), "t0", 8);
2216
2216
  case 8:
2217
2217
  _ret = _context30.t0;
2218
- if (!_ret) {
2218
+ if (!(_typeof(_ret) === "object")) {
2219
2219
  _context30.next = 11;
2220
2220
  break;
2221
2221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eluvio/elv-client-js",
3
- "version": "4.0.47",
3
+ "version": "4.0.49",
4
4
  "description": "Javascript client for the Eluvio Content Fabric",
5
5
  "main": "src/index.js",
6
6
  "author": "Kevin Talmadge",
package/src/ElvClient.js CHANGED
@@ -20,6 +20,7 @@ const Pako = require("pako");
20
20
  const {
21
21
  ValidatePresence
22
22
  } = require("./Validation");
23
+ const CBOR = require("cbor");
23
24
 
24
25
  const networks = {
25
26
  "main": "https://main.net955305.contentfabric.io",
@@ -565,6 +566,85 @@ class ElvClient {
565
566
  }
566
567
  }
567
568
 
569
+ /**
570
+ * Return a list of nodes in the content space, optionally filtered by node ID or endpoint.
571
+ *
572
+ * @methodGroup Nodes
573
+ * @namedParams
574
+ * @param {string=} matchEndpoint - Return node(s) matching the specified endpoint
575
+ * @param {string=} matchNodeId - Return node(s) matching the specified node ID
576
+ *
577
+ * @return {Array<Object>} - A list of nodes in the space matching the parameters
578
+ */
579
+ async SpaceNodes({matchEndpoint, matchNodeId}={}) {
580
+ let bign = await this.CallContractMethod({
581
+ contractAddress: this.contentSpaceAddress,
582
+ methodName: "numActiveNodes",
583
+ });
584
+ let n = bign.toNumber();
585
+
586
+ return (await Utils.LimitedMap(
587
+ 5,
588
+ [...new Array(n)],
589
+ async (_, index) => {
590
+ let bigi = Ethers.BigNumber.from(index);
591
+ let addr = await this.CallContractMethod({
592
+ contractAddress: this.contentSpaceAddress,
593
+ methodName: "activeNodeAddresses",
594
+ methodArgs: [bigi],
595
+ formatArguments: true
596
+ });
597
+
598
+ let locatorsHex = await this.CallContractMethod({
599
+ contractAddress: this.contentSpaceAddress,
600
+ methodName: "activeNodeLocators",
601
+ methodArgs: [bigi]
602
+ });
603
+
604
+ let nodeId = this.utils.AddressToNodeId(addr);
605
+
606
+ if(matchNodeId &&nodeId !== matchNodeId) {
607
+ return;
608
+ }
609
+
610
+ let node = {id: nodeId, endpoints: []};
611
+
612
+ // Parse locators CBOR
613
+ let buffer = locatorsHex.slice(2, locatorsHex.length); // Skip "0x"
614
+ let hex = buffer.toString("hex");
615
+ let locators = CBOR.decodeAllSync(hex);
616
+
617
+ let match = false;
618
+
619
+ if(locators.length >= 5) {
620
+ let fabArray = locators[4].fab;
621
+ if(fabArray) {
622
+ for(let i = 0; i < fabArray.length; i ++) {
623
+ let host = fabArray[i].host;
624
+
625
+ if(matchEndpoint && !matchEndpoint.includes(host)) {
626
+ continue; // Not a match
627
+ }
628
+
629
+ match = true;
630
+ let endpoint = fabArray[i].scheme + "://" + host;
631
+
632
+ if(fabArray[i].port) {
633
+ endpoint = endpoint + ":" + fabArray[i].port;
634
+ }
635
+
636
+ endpoint = endpoint + "/" + fabArray[i].path;
637
+ node.endpoints.push(endpoint);
638
+ }
639
+ }
640
+ }
641
+
642
+ return match ? node : undefined;
643
+ }
644
+ ))
645
+ .filter(n => n);
646
+ }
647
+
568
648
  /**
569
649
  * Return information about how the client was connected to the network
570
650
  *
@@ -1201,6 +1281,7 @@ Object.assign(ElvClient.prototype, require("./client/ContentAccess"));
1201
1281
  Object.assign(ElvClient.prototype, require("./client/Contracts"));
1202
1282
  Object.assign(ElvClient.prototype, require("./client/Files"));
1203
1283
  Object.assign(ElvClient.prototype, require("./client/ABRPublishing"));
1284
+ Object.assign(ElvClient.prototype, require("./client/LiveStream"));
1204
1285
  Object.assign(ElvClient.prototype, require("./client/ContentManagement"));
1205
1286
  Object.assign(ElvClient.prototype, require("./client/NTP"));
1206
1287
  Object.assign(ElvClient.prototype, require("./client/NFT"));
@@ -470,7 +470,12 @@ class FrameClient {
470
470
  "SetStaticToken",
471
471
  "SetVisibility",
472
472
  "SetPermission",
473
+ "SpaceNodes",
473
474
  "StartABRMezzanineJobs",
475
+ "StreamConfig",
476
+ "StreamCreate",
477
+ "StreamStatus",
478
+ "StreamStartOrStopOrReset",
474
479
  "SuspendNTPInstance",
475
480
  "UnlinkAccessGroupFromOauth",
476
481
  "UpdateContentObjectGraph",
@@ -496,6 +501,7 @@ class FrameClient {
496
501
  "MergeUserMetadata",
497
502
  "PublicUserMetadata",
498
503
  "ReplaceUserMetadata",
504
+ "TenantContractId",
499
505
  "TenantId",
500
506
  "UserMetadata",
501
507
  "UserProfileImage",
package/src/HttpClient.js CHANGED
@@ -171,8 +171,19 @@ class HttpClient {
171
171
  }
172
172
 
173
173
  URL({path, queryParams={}}) {
174
+ let baseURI = this.BaseURI();
175
+
176
+ // If URL contains a write token, it must go to the correct server and can not fail over
177
+ const writeTokenMatch = path.replace(/^\//, "").match(/(qlibs\/ilib[a-zA-Z0-9]+|q|qid)\/(tqw__[a-zA-Z0-9]+)/);
178
+ const writeToken = writeTokenMatch ? writeTokenMatch[2] : undefined;
179
+
180
+ if(writeToken && this.draftURIs[writeToken]) {
181
+ // Use saved write token URI
182
+ baseURI = this.draftURIs[writeToken];
183
+ }
184
+
174
185
  return (
175
- this.BaseURI()
186
+ baseURI
176
187
  .path(path)
177
188
  .query(queryParams)
178
189
  .hash("")
@@ -469,6 +469,19 @@ await client.userProfileClient.UserMetadata()
469
469
  this.tenantId = id;
470
470
  }
471
471
 
472
+ /**
473
+ * Return the ID of the tenant contract this user belongs to, if set.
474
+ *
475
+ * @return {Promise<string>} - Tenant Contract ID
476
+ */
477
+ async TenantContractId() {
478
+ if(!this.tenantContractId) {
479
+ this.tenantContractId = await this.UserMetadata({metadataSubtree: "tenantContractId"});
480
+ }
481
+
482
+ return this.tenantContractId;
483
+ }
484
+
472
485
  /**
473
486
  * Get the URL of the current user's profile image
474
487
  *
package/src/Utils.js CHANGED
@@ -315,6 +315,17 @@ const Utils = {
315
315
  return "ispc" + Utils.AddressToHash(address);
316
316
  },
317
317
 
318
+ /**
319
+ * Convert contract address to node ID
320
+ *
321
+ * @param {string} address - Address of contract
322
+ *
323
+ * @returns {string} - Node ID from contract address
324
+ */
325
+ AddressToNodeId: (address) => {
326
+ return "inod" + Utils.AddressToHash(address);
327
+ },
328
+
318
329
  /**
319
330
  * Convert contract address to content library ID
320
331
  *
@@ -0,0 +1,351 @@
1
+ const LadderTemplate = {
2
+ "2160": {
3
+ bit_rate: 14000000,
4
+ codecs: "avc1.640028,mp4a.40.2",
5
+ height: 2160,
6
+ media_type: 1,
7
+ representation: "videovideo_3840x2160_h264@14000000",
8
+ stream_name: "video",
9
+ width: 3840
10
+ },
11
+ "1080": {
12
+ bit_rate: 9500000,
13
+ codecs: "avc1.640028,mp4a.40.2",
14
+ height: 1080,
15
+ media_type: 1,
16
+ representation: "videovideo_1920x1080_h264@9500000",
17
+ stream_name: "video",
18
+ width: 1920
19
+ },
20
+ "720": {
21
+ bit_rate: 4500000,
22
+ codecs: "avc1.640028,mp4a.40.2",
23
+ height: 720,
24
+ media_type: 1,
25
+ representation: "videovideo_1280x720_h264@4500000",
26
+ stream_name: "video",
27
+ width: 1280
28
+ },
29
+ "540": {
30
+ bit_rate: 2000000,
31
+ codecs: "avc1.640028,mp4a.40.2",
32
+ height: 540,
33
+ media_type: 1,
34
+ representation: "videovideo_960x540_h264@2000000",
35
+ stream_name: "video",
36
+ width: 960
37
+ },
38
+ "360": {
39
+ bit_rate: 520000,
40
+ codecs: "avc1.640028,mp4a.40.2",
41
+ height: 360,
42
+ media_type: 1,
43
+ representation: "videovideo_640x360_h264@520000",
44
+ stream_name: "video",
45
+ width: 640
46
+ }
47
+ };
48
+
49
+ const LiveconfTemplate = {
50
+ live_recording: {
51
+ fabric_config: {
52
+ ingress_node_api: "",
53
+ ingress_node_id: ""
54
+ },
55
+ playout_config: {
56
+ rebroadcast_start_time_sec_epoch: 0,
57
+ vod_enabled: false
58
+ },
59
+ recording_config: {
60
+ recording_params: {
61
+ description: "",
62
+ ladder_specs: [
63
+ {
64
+ bit_rate: 384000,
65
+ channels: 2,
66
+ codecs: "mp4a.40.2",
67
+ media_type: 2,
68
+ representation: "audioaudio_aac@384000",
69
+ stream_name: "audio"
70
+ }
71
+ ],
72
+ listen: true,
73
+ live_delay_nano: 2000000000,
74
+ max_duration_sec: -1,
75
+ name: "",
76
+ origin_url: "",
77
+ part_ttl: 3600,
78
+ playout_type: "live",
79
+ source_timescale: null,
80
+ xc_params: {
81
+ audio_bitrate: 384000,
82
+ audio_index: [
83
+ 0,
84
+ 0,
85
+ 0,
86
+ 0,
87
+ 0,
88
+ 0,
89
+ 0,
90
+ 0
91
+ ],
92
+ audio_seg_duration_ts: null,
93
+ ecodec2: "aac",
94
+ enc_height: null,
95
+ enc_width: null,
96
+ filter_descriptor: "",
97
+ force_keyint: null,
98
+ format: "fmp4-segment",
99
+ listen: true,
100
+ n_audio: 1,
101
+ preset: "faster",
102
+ sample_rate: 48000,
103
+ seg_duration: null,
104
+ skip_decoding: false,
105
+ start_segment_str: "1",
106
+ stream_id: -1,
107
+ sync_audio_to_stream_id: -1,
108
+ video_bitrate: null,
109
+ video_seg_duration_ts: null,
110
+ xc_type: 3
111
+ }
112
+ }
113
+ }
114
+ }
115
+ };
116
+
117
+ class LiveConf {
118
+ constructor(probeData, nodeId, nodeUrl, includeAVSegDurations, overwriteOriginUrl, syncAudioToVideo) {
119
+ this.probeData = probeData;
120
+ this.nodeId = nodeId;
121
+ this.nodeUrl = nodeUrl;
122
+ this.includeAVSegDurations = includeAVSegDurations;
123
+ this.overwriteOriginUrl = overwriteOriginUrl;
124
+ this.syncAudioToVideo = syncAudioToVideo;
125
+ }
126
+
127
+ probeKind() {
128
+ let fileNameSplit = this.probeData.format.filename.split(":");
129
+ return fileNameSplit[0];
130
+ }
131
+
132
+ getStreamDataForCodecType(codecType) {
133
+ let stream = null;
134
+ for(let index = 0; index < this.probeData.streams.length; index++) {
135
+ if(this.probeData.streams[index].codec_type == codecType) {
136
+ stream = this.probeData.streams[index];
137
+ }
138
+ }
139
+ return stream;
140
+ }
141
+
142
+ getFrameRate() {
143
+ let videoStream = this.getStreamDataForCodecType("video");
144
+ let frameRate = videoStream.r_frame_rate || videoStream.frame_rate;
145
+ return frameRate.split("/");
146
+ }
147
+
148
+ isFrameRateWhole() {
149
+ let frameRate = this.getFrameRate();
150
+ return frameRate[1] == "1";
151
+ }
152
+
153
+ getForceKeyint() {
154
+ let frameRate = this.getFrameRate();
155
+ let roundedFrameRate = Math.round(frameRate[0] / frameRate[1]);
156
+ if(roundedFrameRate > 30) {
157
+ return roundedFrameRate;
158
+ } else {
159
+ return roundedFrameRate * 2;
160
+ }
161
+ }
162
+
163
+ calcSegDuration({sourceTimescale}) {
164
+
165
+ let videoStream = this.getStreamDataForCodecType("video");
166
+ let frameRate = videoStream.frame_rate;
167
+
168
+ let seg ={};
169
+ switch(frameRate) {
170
+ case "24":
171
+ seg.video = 30 * sourceTimescale;
172
+ seg.audio = 30 * 48000;
173
+ seg.keyint = 48;
174
+ seg.duration = "30";
175
+ break;
176
+ case "25":
177
+ seg.video = 30 * sourceTimescale;
178
+ seg.audio = 30 * 48000;
179
+ seg.keyint = 50;
180
+ seg.duration = "30";
181
+ break;
182
+ case "30":
183
+ seg.video = 30 * sourceTimescale;
184
+ seg.audio = 30 * 48000;
185
+ seg.keyint = 60;
186
+ seg.duration = "30";
187
+ break;
188
+ case "30000/1001":
189
+ seg.video = 30.03 * sourceTimescale;
190
+ seg.audio = 29.76 * 48000;
191
+ seg.keyint = 60;
192
+ seg.duration = "30.03";
193
+ break;
194
+ case "48":
195
+ seg.video = 30 * sourceTimescale;
196
+ seg.audio = 30 * 48000;
197
+ seg.keyint = 96;
198
+ seg.duration = "30";
199
+ break;
200
+ case "50":
201
+ seg.video = 30 * sourceTimescale;
202
+ seg.audio = 30 * 48000;
203
+ seg.keyint = 100;
204
+ seg.duration = "30";
205
+ break;
206
+ case "60":
207
+ seg.video = 30 * sourceTimescale;
208
+ seg.audio = 30 * 48000;
209
+ seg.keyint = 120;
210
+ seg.duration = "30";
211
+ break;
212
+ case "60000/1001":
213
+ seg.video = 30.03 * sourceTimescale;
214
+ seg.audio = 29.76 * 48000;
215
+ seg.keyint = 120;
216
+ seg.duration = "30.03";
217
+ break;
218
+ default:
219
+ console.log("Unsupported frame rate", frameRate);
220
+ break;
221
+ }
222
+ return seg;
223
+ }
224
+
225
+ syncAudioToStreamIdValue() {
226
+ let sync_id = -1;
227
+ let videoStream = this.getStreamDataForCodecType("video");
228
+ switch(this.probeKind()) {
229
+ case "udp":
230
+ sync_id = videoStream.stream_id;
231
+ break;
232
+ case "rtmp":
233
+ sync_id = -1; // Pending fabric API: videoStream.stream_index
234
+ break;
235
+ }
236
+ return sync_id;
237
+ }
238
+
239
+ generateLiveConf() {
240
+ // gather required data
241
+ const conf = LiveconfTemplate;
242
+ const fileName = this.overwriteOriginUrl || this.probeData.format.filename;
243
+ const audioStream = this.getStreamDataForCodecType("audio");
244
+ const sampleRate = parseInt(audioStream.sample_rate);
245
+ const videoStream = this.getStreamDataForCodecType("video");
246
+ let sourceTimescale;
247
+
248
+ console.log("AUDIO", audioStream);
249
+ console.log("VIDEO", videoStream);
250
+
251
+ // Fill in liveconf all formats have in common
252
+ conf.live_recording.fabric_config.ingress_node_api = this.nodeUrl || null;
253
+ conf.live_recording.fabric_config.ingress_node_id = this.nodeId || null;
254
+ conf.live_recording.recording_config.recording_params.description;
255
+ conf.live_recording.recording_config.recording_params.origin_url = fileName;
256
+ conf.live_recording.recording_config.recording_params.description = `Ingest stream ${fileName}`;
257
+ conf.live_recording.recording_config.recording_params.name = `Ingest stream ${fileName}`;
258
+ conf.live_recording.recording_config.recording_params.xc_params.audio_index[0] = audioStream.stream_index;
259
+ conf.live_recording.recording_config.recording_params.xc_params.sample_rate = sampleRate;
260
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height = videoStream.height;
261
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width = videoStream.width;
262
+
263
+ if(this.syncAudioToVideo) {
264
+ conf.live_recording.recording_config.recording_params.xc_params.sync_audio_to_stream_id = this.syncAudioToStreamIdValue();
265
+ }
266
+
267
+ // Fill in specifics for protocol
268
+ switch(this.probeKind()) {
269
+ case "udp":
270
+ sourceTimescale = 90000;
271
+ conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
272
+ break;
273
+ case "rtmp":
274
+ sourceTimescale = 16000;
275
+ conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
276
+ break;
277
+ case "hls":
278
+ console.log("HLS detected. Not yet implemented");
279
+ break;
280
+ default:
281
+ console.log("Unsuppoted media", this.probeKind());
282
+ break;
283
+ }
284
+
285
+ const segDurations = this.calcSegDuration({sourceTimescale});
286
+
287
+ // Segment conditioning parameters
288
+ conf.live_recording.recording_config.recording_params.xc_params.seg_duration = segDurations.duration;
289
+ conf.live_recording.recording_config.recording_params.xc_params.audio_seg_duration_ts = segDurations.audio;
290
+ conf.live_recording.recording_config.recording_params.xc_params.video_seg_duration_ts = segDurations.video;
291
+ conf.live_recording.recording_config.recording_params.xc_params.force_keyint = segDurations.keyint;
292
+
293
+ switch(videoStream.height) {
294
+ case 2160:
295
+ conf.live_recording.recording_config.recording_params.ladder_specs.unshift(
296
+ LadderTemplate[2160],
297
+ LadderTemplate[1080],
298
+ LadderTemplate[720],
299
+ LadderTemplate[540],
300
+ LadderTemplate[360]
301
+ );
302
+ conf.live_recording.recording_config.recording_params.xc_params.video_bitrate = LadderTemplate[2160].bit_rate;
303
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height = 2160;
304
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width = 3840;
305
+
306
+ break;
307
+ case 1080:
308
+ conf.live_recording.recording_config.recording_params.ladder_specs.unshift(
309
+ LadderTemplate[1080],
310
+ LadderTemplate[720],
311
+ LadderTemplate[540],
312
+ LadderTemplate[360]
313
+ );
314
+ conf.live_recording.recording_config.recording_params.xc_params.video_bitrate = LadderTemplate[1080].bit_rate;
315
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height = 1080;
316
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width = 1920;
317
+ break;
318
+ case 720:
319
+ conf.live_recording.recording_config.recording_params.ladder_specs.unshift(
320
+ LadderTemplate[720],
321
+ LadderTemplate[540],
322
+ LadderTemplate[360]
323
+ );
324
+ conf.live_recording.recording_config.recording_params.xc_params.video_bitrate = LadderTemplate[720].bit_rate;
325
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height = 720;
326
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width = 1280;
327
+ break;
328
+ case 540:
329
+ conf.live_recording.recording_config.recording_params.ladder_specs.unshift(
330
+ LadderTemplate[540],
331
+ LadderTemplate[360]
332
+ );
333
+ conf.live_recording.recording_config.recording_params.xc_params.video_bitrate = LadderTemplate[540].bit_rate;
334
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height = 540;
335
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width = 960;
336
+ break;
337
+ case 360:
338
+ conf.live_recording.recording_config.recording_params.ladder_specs.unshift(LadderTemplate[360]);
339
+ conf.live_recording.recording_config.recording_params.ladder_specs.unshift(LadderTemplate[360]);
340
+ conf.live_recording.recording_config.recording_params.xc_params.video_bitrate = LadderTemplate[360].bit_rate;
341
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height = 360;
342
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width = 640;
343
+ break;
344
+ default:
345
+ throw new Error("ERROR: Probed stream does not conform to one of the following built in resolution ladders [4096, 2160], [1920, 1080] [1280, 720], [960, 540], [640, 360]");
346
+ }
347
+
348
+ return JSON.stringify(conf, null, 2);
349
+ }
350
+ }
351
+ exports.LiveConf = LiveConf;