@eluvio/elv-client-js 4.0.46 → 4.0.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eluvio/elv-client-js",
3
- "version": "4.0.46",
3
+ "version": "4.0.48",
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
575
+ * @param {string} - matchNodeId
576
+ *
577
+ * @return {Array<Object>} - A list of nodes in the space
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
+ let nodes = [];
586
+
587
+ for (let i = 0; i < n; i ++) {
588
+
589
+ let bigi = Ethers.BigNumber.from(i);
590
+ let addr = await this.CallContractMethod({
591
+ contractAddress: this.contentSpaceAddress,
592
+ methodName: "activeNodeAddresses",
593
+ methodArgs: [bigi],
594
+ formatArguments: true
595
+ });
596
+
597
+ let locatorsHex = await this.CallContractMethod({
598
+ contractAddress: this.contentSpaceAddress,
599
+ methodName: "activeNodeLocators",
600
+ methodArgs: [bigi]
601
+ });
602
+
603
+ let nodeId = this.utils.AddressToNodeId(addr);
604
+
605
+ if (matchNodeId != undefined && nodeId != matchNodeId) {
606
+ continue; // Not a match
607
+ }
608
+
609
+ let node = {id: nodeId, endpoints: []};
610
+
611
+ // Parse locators CBOR
612
+ let buffer = locatorsHex.slice(2, locatorsHex.length); // Skip "0x"
613
+ let hex = buffer.toString("hex");
614
+ let locators = CBOR.decodeAllSync(hex);
615
+
616
+ let match = false;
617
+
618
+ if(locators.length >= 5) {
619
+ let fabArray = locators[4].fab;
620
+ if(fabArray != undefined) {
621
+ for(let i = 0; i < fabArray.length; i ++) {
622
+ let host = fabArray[i].host;
623
+
624
+ if(matchEndpoint != undefined && !host.includes(matchEndpoint)) {
625
+ continue; // Not a match
626
+ }
627
+
628
+ match = true;
629
+ let endpoint = fabArray[i].scheme + "://" + host;
630
+
631
+ if(fabArray[i].port != "") {
632
+ endpoint = endpoint + ":" + fabArray[i].port;
633
+ }
634
+
635
+ endpoint = endpoint + "/" + fabArray[i].path;
636
+ node.endpoints.push(endpoint);
637
+ }
638
+ }
639
+ }
640
+ if(match) {
641
+ nodes.push(node);
642
+ }
643
+ }
644
+
645
+ return nodes;
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
  *
package/src/Validation.js CHANGED
@@ -41,7 +41,13 @@ exports.ValidateWriteToken = (writeToken) => {
41
41
  exports.ValidatePartHash = (partHash) => {
42
42
  if(!partHash) {
43
43
  throw Error("Part hash not specified");
44
- } else if(!partHash.toString().startsWith("hqp_") && !partHash.toString().startsWith("hqpe")) {
44
+ }
45
+ else if(!partHash.toString().startsWith("hqp_") &&
46
+ !partHash.toString().startsWith("hqpe") &&
47
+ !partHash.toString().startsWith("hqt_") &&
48
+ !partHash.toString().startsWith("hqte") &&
49
+ !partHash.toString().startsWith("hql_") &&
50
+ !partHash.toString().startsWith("hqle")) {
45
51
  throw Error(`Invalid part hash: ${partHash}`);
46
52
  }
47
53
  };
@@ -2144,7 +2144,7 @@ exports.ContentObjectImageUrl = async function({libraryId, objectId, versionHash
2144
2144
  * capLevelToPlayerSize - Caps video quality to player size
2145
2145
  * clipEnd - End time for the video
2146
2146
  * clipStart - Start time for the video
2147
- * controls - Sets the player control visibility. Values: browserDefaul t | autoHide | show. Defaults to autoHide
2147
+ * controls - Sets the player control visibility. Values: browserDefault | autoHide | show | hide | hideWithVolume. Defaults to autoHide
2148
2148
  * description - Sets the page description
2149
2149
  * directLink - If enabled, sets direct link
2150
2150
  * linkPath - Video link path
@@ -2175,6 +2175,13 @@ exports.EmbedUrl = async function({
2175
2175
  // Default options
2176
2176
  options.controls = options.controls === undefined ? "autoHide" : options.controls;
2177
2177
 
2178
+ const controlsMap = {
2179
+ autoHide: "h",
2180
+ browserDefault: "d",
2181
+ show: "s",
2182
+ hideWithVolume: "hv"
2183
+ };
2184
+
2178
2185
  let embedUrl = new URL("https://embed.v3.contentfabric.io");
2179
2186
  const networkInfo = await this.NetworkInfo();
2180
2187
  const networkName = networkInfo.name === "demov3" ? "demo" : (networkInfo.name === "test" && networkInfo.id === 955205) ? "testv4" : networkInfo.name;
@@ -2207,7 +2214,9 @@ exports.EmbedUrl = async function({
2207
2214
  embedUrl.searchParams.set("start", options.clipStart);
2208
2215
  break;
2209
2216
  case "controls":
2210
- embedUrl.searchParams.set("ct", options.controls);
2217
+ if(options.controls !== "hide") {
2218
+ embedUrl.searchParams.set("ct", controlsMap[options.controls]);
2219
+ }
2211
2220
  break;
2212
2221
  case "description":
2213
2222
  data["og:description"] = options.description;
@@ -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;