@eluvio/elv-client-js 4.0.77 → 4.0.79

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.
@@ -107,6 +107,7 @@ const LiveconfTemplate = {
107
107
  sync_audio_to_stream_id: -1,
108
108
  video_bitrate: null,
109
109
  video_seg_duration_ts: null,
110
+ video_time_base: null,
110
111
  xc_type: 3
111
112
  }
112
113
  }
@@ -160,56 +161,156 @@ class LiveConf {
160
161
  }
161
162
  }
162
163
 
163
- calcSegDuration({sourceTimescale}) {
164
+ /*
165
+ * Calculates mez segment durations based on input stream parameters
166
+ *
167
+ * Live input formats have fixed timebase:
168
+ * - MPEG-TS/SRT input stream timebase is 90000
169
+ * - RTMP input stream timebase is 1000 and gets translated to 16000 if not otherwise specified
170
+ *
171
+ * This causes frame duration irregularities for certain frame rates.
172
+ * For example RTMP 60fps has frames of durations 16 and 17. MPEG-TS 59.94fps has frames of
173
+ * durations 1001 and 1002.
174
+ *
175
+ * Live mez segmentation requires that the segment be cut at the specific number of frames, and when
176
+ * the frame durations are irregular we adjust both the video timebase and the video frame duration
177
+ * to make the math possible. This adjustment is also required for live-to-vod conversion.
178
+ *
179
+ * For example for MPEG-TS 59.94fps, the mez segment timebase needs to be 60000
180
+ * (and resulting frame duration is 1001) and for RTMP 60fps the timebase needs to be 15360 (resulting frame
181
+ * duration is 256).
182
+ *
183
+ * @sourceTimescale - adjusted source video stream timescale (eg. MPEGTS 90000, RTMP 16000 )
184
+ * @sampleRate - audio sample rate (commonly 48000 but can be different)
185
+ * @audioCodec - audio codec as a string (eg. "aac")
186
+ * @return - segment encoding parameters
187
+ */
188
+ calcSegDuration({sourceTimescale, sampleRate, audioCodec}) {
189
+ let seg = {};
190
+
191
+ switch(this.probeKind()) {
192
+ case "rtmp":
193
+ seg = this.calcSegDurationRtmp({sourceTimescale, sampleRate, audioCodec});
194
+ break;
195
+ case "udp":
196
+ case "srt":
197
+ seg = this.calcSegDurationMpegts({sourceTimescale, sampleRate, audioCodec});
198
+ break;
199
+ default:
200
+ throw "protocol not supported - " + this.probeKind();
201
+ }
202
+
203
+ if(audioCodec == "aac") {
204
+ seg.audio = 29.76 * sampleRate;
205
+ } else {
206
+ seg.audio = 29.76 * 48000; // Other codecs are resampled @48000
207
+ }
208
+
209
+ return seg;
210
+ }
211
+
212
+ calcSegDurationMpegts({sourceTimescale}) {
164
213
  let videoStream = this.getStreamDataForCodecType("video");
165
214
  let frameRate = videoStream.frame_rate;
215
+ let seg = {};
216
+
217
+ switch(frameRate) {
218
+ case "24":
219
+ seg.video = sourceTimescale * 30;
220
+ seg.keyint = 48;
221
+ seg.duration = "30";
222
+ break;
223
+ case "25":
224
+ seg.video = sourceTimescale * 30;
225
+ seg.keyint = 50;
226
+ seg.duration = "30";
227
+ break;
228
+ case "30":
229
+ seg.video = sourceTimescale * 30;
230
+ seg.keyint = 60;
231
+ seg.duration = "30";
232
+ break;
233
+ case "30000/1001":
234
+ seg.video = sourceTimescale * 30;
235
+ seg.keyint = 60;
236
+ seg.duration = "30.03";
237
+ break;
238
+ case "48":
239
+ seg.video = sourceTimescale * 30;
240
+ seg.keyint = 96;
241
+ seg.duration = "30";
242
+ break;
243
+ case "50":
244
+ seg.video = sourceTimescale * 30;
245
+ seg.keyint = 100;
246
+ seg.duration = "30";
247
+ break;
248
+ case "60":
249
+ seg.video = sourceTimescale * 30;
250
+ seg.keyint = 120;
251
+ seg.duration = "30";
252
+ break;
253
+ case "60000/1001":
254
+ seg.videoTimeBase = 60000;
255
+ seg.video = seg.videoTimeBase * 30.03;
256
+ seg.keyint = 120;
257
+ seg.duration = "30.03";
258
+ break;
259
+ default:
260
+ throw "unsupported frame rate for MPEGTS - " + frameRate;
261
+ break;
262
+ }
263
+ return seg;
264
+ }
166
265
 
266
+ calcSegDurationRtmp({sourceTimescale}) {
267
+ let videoStream = this.getStreamDataForCodecType("video");
268
+ let frameRate = videoStream.frame_rate;
167
269
  let seg = {};
168
- seg.audio = 29.76 * 48000;
169
270
 
170
271
  switch(frameRate) {
171
272
  case "24":
172
- seg.video = 30 * sourceTimescale;
273
+ seg.video = sourceTimescale * 30;
173
274
  seg.keyint = 48;
174
275
  seg.duration = "30";
175
276
  break;
176
277
  case "25":
177
- seg.video = 30 * sourceTimescale;
278
+ seg.video = sourceTimescale * 30;
178
279
  seg.keyint = 50;
179
280
  seg.duration = "30";
180
281
  break;
181
282
  case "30":
182
- seg.video = 30 * sourceTimescale;
283
+ seg.video = sourceTimescale * 30;
183
284
  seg.keyint = 60;
184
285
  seg.duration = "30";
185
286
  break;
186
287
  case "30000/1001":
187
- seg.video = 30.03 * sourceTimescale;
288
+ seg.video = sourceTimescale * 30.03;
188
289
  seg.keyint = 60;
189
290
  seg.duration = "30.03";
190
291
  break;
191
292
  case "48":
192
- seg.video = 30 * sourceTimescale;
293
+ seg.video = sourceTimescale * 30;
193
294
  seg.keyint = 96;
194
295
  seg.duration = "30";
195
296
  break;
196
297
  case "50":
197
- seg.video = 30 * sourceTimescale;
298
+ seg.video = sourceTimescale * 30;
198
299
  seg.keyint = 100;
199
300
  seg.duration = "30";
200
301
  break;
201
302
  case "60":
202
- seg.video = 30 * sourceTimescale;
303
+ seg.video = sourceTimescale * 30;
203
304
  seg.keyint = 120;
204
305
  seg.duration = "30";
205
306
  break;
206
307
  case "60000/1001":
207
- seg.video = 30.03 * sourceTimescale;
308
+ seg.video = sourceTimescale * 30.03;
208
309
  seg.keyint = 120;
209
310
  seg.duration = "30.03";
210
311
  break;
211
312
  default:
212
- console.log("Unsupported frame rate", frameRate);
313
+ throw "unsupported frame rate for RTMP - " + frameRate;
213
314
  break;
214
315
  }
215
316
  return seg;
@@ -220,10 +321,11 @@ class LiveConf {
220
321
  let videoStream = this.getStreamDataForCodecType("video");
221
322
  switch(this.probeKind()) {
222
323
  case "udp":
324
+ case "srt":
223
325
  sync_id = videoStream.stream_id;
224
326
  break;
225
327
  case "rtmp":
226
- sync_id = -1; // Pending fabric API: videoStream.stream_index
328
+ sync_id = videoStream.stream_index;
227
329
  break;
228
330
  }
229
331
  return sync_id;
@@ -234,14 +336,14 @@ class LiveConf {
234
336
  const conf = JSON.parse(JSON.stringify(LiveconfTemplate));
235
337
  const fileName = this.overwriteOriginUrl || this.probeData.format.filename;
236
338
  const audioStream = this.getStreamDataForCodecType("audio");
339
+
237
340
  const sampleRate = parseInt(audioStream.sample_rate);
341
+ const audioCodec = audioStream.codec_name;
238
342
  const videoStream = this.getStreamDataForCodecType("video");
239
343
  let sourceTimescale;
240
344
 
241
- console.log("AUDIO", audioStream);
242
- console.log("VIDEO", videoStream);
243
-
244
345
  // Fill in liveconf all formats have in common
346
+ conf.live_recording.probe_info = this.probeData;
245
347
  conf.live_recording.fabric_config.ingress_node_api = this.nodeUrl || null;
246
348
  conf.live_recording.fabric_config.ingress_node_id = this.nodeId || null;
247
349
  conf.live_recording.recording_config.recording_params.description;
@@ -267,6 +369,11 @@ class LiveConf {
267
369
  sourceTimescale = 90000;
268
370
  conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
269
371
  break;
372
+ case "srt":
373
+ sourceTimescale = 90000;
374
+ conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
375
+ conf.live_recording.recording_config.recording_params.live_delay_nano = 4000000000;
376
+ break;
270
377
  case "rtmp":
271
378
  sourceTimescale = 16000;
272
379
  conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
@@ -279,7 +386,7 @@ class LiveConf {
279
386
  break;
280
387
  }
281
388
 
282
- const segDurations = this.calcSegDuration({sourceTimescale});
389
+ const segDurations = this.calcSegDuration({sourceTimescale, sampleRate, audioCodec});
283
390
 
284
391
  // Segment conditioning parameters
285
392
  conf.live_recording.recording_config.recording_params.xc_params.seg_duration = segDurations.duration;
@@ -287,6 +394,15 @@ class LiveConf {
287
394
  conf.live_recording.recording_config.recording_params.xc_params.video_seg_duration_ts = segDurations.video;
288
395
  conf.live_recording.recording_config.recording_params.xc_params.force_keyint = segDurations.keyint;
289
396
 
397
+ // Optional override output timebase and frame duration (ts)
398
+ if(segDurations.videoTimeBase) {
399
+ conf.live_recording.recording_config.recording_params.xc_params.video_time_base = segDurations.videoTimeBase;
400
+ conf.live_recording.recording_config.recording_params.source_timescale = segDurations.videoTimeBase;
401
+ }
402
+ if(segDurations.videoFrameDurationTs) {
403
+ conf.live_recording.recording_config.recording_params.xc_params.video_frame_duration_ts = segDurations.videoFrameDurationTs;
404
+ }
405
+
290
406
  switch(videoStream.height) {
291
407
  case 2160:
292
408
  conf.live_recording.recording_config.recording_params.ladder_specs.unshift(
@@ -136,9 +136,9 @@ const StreamGenerateOffering = async({
136
136
  const maxStreamIndex = Math.max(aStreamIndex, vStreamIndex);
137
137
 
138
138
  for(let i = 0; i <= maxStreamIndex; i++) {
139
- if (i === aStreamIndex) {
139
+ if(i === aStreamIndex) {
140
140
  sourceStreams.push(sourceAudioStream);
141
- } else if (i === vStreamIndex) {
141
+ } else if(i === vStreamIndex) {
142
142
  sourceStreams.push(sourceVideoStream);
143
143
  } else {
144
144
  sourceStreams.push(DUMMY_STREAM);
@@ -242,12 +242,12 @@ const StreamGenerateOffering = async({
242
242
  abrProfile
243
243
  });
244
244
 
245
- if (createResponse.warnings.length > 0) {
245
+ if(createResponse.warnings.length > 0) {
246
246
  console.log("WARNINGS:");
247
247
  console.log(JSON.stringify(createResponse.warnings, null, 2));
248
248
  }
249
249
 
250
- if (createResponse.errors.length > 0) {
250
+ if(createResponse.errors.length > 0) {
251
251
  console.log("ERRORS:");
252
252
  console.log(JSON.stringify(createResponse.errors, null, 2));
253
253
  }
@@ -339,6 +339,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
339
339
  ]
340
340
  });
341
341
 
342
+ status.reference_url = mainMeta.live_recording_config.reference_url;
343
+
342
344
  if(mainMeta.live_recording_config == undefined || mainMeta.live_recording_config.url == undefined) {
343
345
  status.state = "unconfigured";
344
346
  return status;
@@ -367,7 +369,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
367
369
  status.url = mainMeta.live_recording.recording_config.recording_params.origin_url;
368
370
 
369
371
  let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
370
- if(edgeWriteToken == undefined) {
372
+ if(!edgeWriteToken) {
371
373
  status.state = "inactive";
372
374
  return status;
373
375
  }
@@ -688,8 +690,6 @@ exports.StreamCreate = async function({name, start=false}) {
688
690
  */
689
691
  exports.StreamStartOrStopOrReset = async function({name, op}) {
690
692
  try {
691
- console.log("Stream ", op, ": ", name);
692
-
693
693
  let status = await this.StreamStatus({name});
694
694
  if(status.state != "stopped") {
695
695
  if(op === "start") {
@@ -699,8 +699,6 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
699
699
  }
700
700
 
701
701
  if(status.state == "running" || status.state == "starting" || status.state == "stalled") {
702
- console.log("STOPPING");
703
-
704
702
  try {
705
703
  await this.CallBitcodeMethod({
706
704
  libraryId: status.library_id,
@@ -769,8 +767,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
769
767
  };
770
768
 
771
769
  /**
772
- * Stop the live stream session and close the edge write token.
773
- * Not implemented fully
770
+ * Close the edge write token and make the stream object inactive.
774
771
  *
775
772
  * @methodGroup Live Stream
776
773
  * @namedParams
@@ -780,8 +777,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
780
777
  */
781
778
  exports.StreamStopSession = async function({name}) {
782
779
  try {
783
- console.log("TERMINATE: ", name);
784
-
780
+ this.Log(`Terminating stream session for: ${name}`);
785
781
  let conf = await this.LoadConf({name});
786
782
 
787
783
  let {objectId} = conf;
@@ -801,86 +797,81 @@ exports.StreamStopSession = async function({name}) {
801
797
 
802
798
  this.SetNodes({fabricURIs: [fabURI]});
803
799
 
804
- let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
800
+ const metaEdgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
805
801
 
806
- if(edgeWriteToken === undefined || edgeWriteToken === "") {
802
+ if(!metaEdgeWriteToken) {
807
803
  return {
808
804
  state: "inactive",
809
- error: "no active streams - must create a stream first"
805
+ error: "The stream is not active"
810
806
  };
811
807
  }
812
- let edgeMeta = await this.ContentObjectMetadata({
813
- libraryId,
814
- objectId,
815
- writeToken: edgeWriteToken
816
- });
817
808
 
818
- // Stop the LRO if running
819
- let status = await this.StreamStatus({name});
820
- if(status.state != "terminated") {
821
- console.log("STOPPING");
822
- try {
823
- await this.CallBitcodeMethod({
824
- libraryId: status.library_id,
825
- objectId: status.object_id,
826
- writeToken: status.edge_write_token,
827
- method: "/live/stop/" + status.tlro,
828
- constant: false
829
- });
830
- } catch(error) {
831
- // The /call/lro/stop API returns empty response
832
- // console.log("LRO Stop (failed): ", error);
833
- }
809
+ try {
810
+ const streamMetadata = await this.ContentObjectMetadata({
811
+ libraryId,
812
+ objectId,
813
+ writeToken: metaEdgeWriteToken
814
+ });
834
815
 
835
- // Wait until LRO is terminated
836
- let tries = 10;
837
- while (status.state != "stopped" && tries-- > 0) {
838
- console.log("Wait to terminate - ", status.state);
839
- await Sleep(1000);
840
- status = await this.StreamStatus({name});
841
- }
842
- console.log("Status after stop - ", status.state);
816
+ const status = await this.StreamStatus({name});
843
817
 
844
- if(tries <= 0) {
845
- console.log("Failed to stop");
846
- return status;
818
+ if(status.state !== "stopped") {
819
+ return {
820
+ state: status.state,
821
+ error: "The stream must be stopped before terminating"
822
+ }
847
823
  }
824
+
825
+ await this.DeleteWriteToken({
826
+ libraryId,
827
+ writeToken: metaEdgeWriteToken
828
+ });
829
+ } catch(error) {
830
+ this.Log(`Unable to retrieve metadata for edge write token ${edgeWriteToken}`);
848
831
  }
849
832
 
850
- // Set stop time
851
- edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
852
- console.log("recording_start_time: ", edgeMeta.recording_start_time);
853
- console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
833
+ const {writeToken} = await this.EditContentObject({
834
+ libraryId: libraryId,
835
+ objectId: objectId
836
+ });
854
837
 
855
- edgeMeta.live_recording.status = {
856
- state: "terminated",
857
- recording_stop_time: edgeMeta.recording_stop_time
858
- };
838
+ // Set stop time and inactive state
839
+ const newState = "inactive";
840
+ const stopTime = Math.floor(new Date().getTime() / 1000);
859
841
 
860
- edgeMeta.live_recording.fabric_config.edge_write_token = "";
842
+ const finalizeMetadata = {
843
+ live_recording: {
844
+ status: {
845
+ edge_write_token: "",
846
+ state: newState,
847
+ recording_stop_time: stopTime
848
+ },
849
+ fabric_config: {
850
+ edge_write_token: ""
851
+ }
852
+ },
853
+ recording_stop_time: stopTime
854
+ };
861
855
 
862
- await this.ReplaceMetadata({
856
+ await this.MergeMetadata({
863
857
  libraryId,
864
858
  objectId,
865
- writeToken: edgeWriteToken,
866
- metadata: edgeMeta
859
+ writeToken,
860
+ metadata: finalizeMetadata
867
861
  });
868
862
 
869
863
  let fin = await this.FinalizeContentObject({
870
864
  libraryId,
871
865
  objectId,
872
- writeToken: edgeWriteToken,
873
- commitMessage: "Finalize live stream - stop time " + edgeMeta.recording_stop_time,
874
- publish: false // Don't publish this version because it is not currently useful
866
+ writeToken,
867
+ commitMessage: `Deactivate live stream - stop time ${stopTime}`
875
868
  });
876
869
 
877
870
  return {
878
871
  fin,
879
872
  name,
880
- edge_write_token: edgeWriteToken,
881
- state: "terminated"
873
+ state: newState
882
874
  };
883
-
884
875
  } catch(error) {
885
876
  console.error(error);
886
877
  }
@@ -1202,7 +1193,7 @@ exports.StreamInsertion = async function({name, insertionTime, sinceStart=false,
1202
1193
  playout: "/qfab/" + targetHash + "/rep/playout" // TO FIX - should be a link
1203
1194
  };
1204
1195
 
1205
- for (let i = 0; i < insertions.length; i ++) {
1196
+ for(let i = 0; i < insertions.length; i ++) {
1206
1197
  if(insertions[i].insertion_time <= currentTime) {
1207
1198
  // Bad insertion - must be later than current time
1208
1199
  append(errs, "Bad insertion - time:", insertions[i].insertion_time);
@@ -1325,9 +1316,10 @@ exports.StreamConfig = async function({name, customSettings={}}) {
1325
1316
 
1326
1317
  let userConfig = mainMeta.live_recording_config;
1327
1318
  status.user_config = userConfig;
1319
+ console.log("userConfig", userConfig);
1328
1320
 
1329
1321
  // Get node URI from user config
1330
- const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").split(":")[0];
1322
+ const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").replace("srt://", "").split(":")[0];
1331
1323
  const streamUrl = new URL(userConfig.url);
1332
1324
 
1333
1325
  console.log("Retrieving nodes...");
@@ -1380,7 +1372,6 @@ exports.StreamConfig = async function({name, customSettings={}}) {
1380
1372
  }
1381
1373
  }
1382
1374
 
1383
- console.log("PROBE", probe);
1384
1375
  probe.format.filename = streamUrl.href;
1385
1376
 
1386
1377
  // Create live recording config
@@ -1410,14 +1401,6 @@ exports.StreamConfig = async function({name, customSettings={}}) {
1410
1401
  metadata: liveRecordingConfig.live_recording
1411
1402
  });
1412
1403
 
1413
- await this.ReplaceMetadata({
1414
- libraryId,
1415
- objectId: conf.objectId,
1416
- writeToken,
1417
- metadataSubtree: "probe",
1418
- metadata: probe
1419
- });
1420
-
1421
1404
  status.fin = await this.FinalizeContentObject({
1422
1405
  libraryId,
1423
1406
  objectId: conf.objectId,
@@ -1427,3 +1410,125 @@ exports.StreamConfig = async function({name, customSettings={}}) {
1427
1410
 
1428
1411
  return status;
1429
1412
  };
1413
+
1414
+ /**
1415
+ * List the pre-allocated URLs for a site
1416
+ *
1417
+ * @methodGroup Live Stream
1418
+ * @namedParams
1419
+ * @param {string=} - ID of the live stream site object
1420
+ *
1421
+ * @return {Promise<Object>} - The list of stream URLs
1422
+ */
1423
+ exports.StreamListUrls = async function({siteId}={}) {
1424
+ try {
1425
+ const STATUS_MAP = {
1426
+ UNCONFIGURED: "unconfigured",
1427
+ UNINITIALIZED: "uninitialized",
1428
+ INACTIVE: "inactive",
1429
+ STOPPED: "stopped",
1430
+ STARTING: "starting",
1431
+ RUNNING: "running",
1432
+ STALLED: "stalled",
1433
+ };
1434
+
1435
+ if(!siteId) {
1436
+ const tenantContractId = await this.userProfileClient.TenantContractId();
1437
+
1438
+ if(!tenantContractId) {
1439
+ throw Error("No tenant contract ID configured");
1440
+ }
1441
+
1442
+ siteId = await this.ContentObjectMetadata({
1443
+ libraryId: tenantContractId.replace("iten", "ilib"),
1444
+ objectId: tenantContractId.replace("iten", "iq__"),
1445
+ metadataSubtree: "public/sites/live_streams",
1446
+ });
1447
+ }
1448
+
1449
+ const streamMetadata = await this.ContentObjectMetadata({
1450
+ libraryId: await this.ContentObjectLibraryId({objectId: siteId}),
1451
+ objectId: siteId,
1452
+ metadataSubtree: "public/asset_metadata/live_streams",
1453
+ resolveLinks: true,
1454
+ resolveIgnoreErrors: true
1455
+ });
1456
+
1457
+ const activeUrlMap = {};
1458
+ await this.utils.LimitedMap(
1459
+ 10,
1460
+ Object.keys(streamMetadata || {}),
1461
+ async slug => {
1462
+ const stream = streamMetadata[slug];
1463
+ let versionHash;
1464
+
1465
+ if(
1466
+ stream &&
1467
+ stream.sources &&
1468
+ stream.sources.default &&
1469
+ stream.sources.default["."] &&
1470
+ stream.sources.default["."].container ||
1471
+ ((stream["/"] || "").match(/^\/?qfab\/([\w]+)\/?.+/) || [])[1]
1472
+ ) {
1473
+ versionHash = (
1474
+ stream.sources.default["."].container ||
1475
+ ((stream["/"] || "").match(/^\/?qfab\/([\w]+)\/?.+/) || [])[1]
1476
+ );
1477
+ }
1478
+
1479
+ if(versionHash) {
1480
+ const objectId = this.utils.DecodeVersionHash(versionHash).objectId;
1481
+ const libraryId = await this.ContentObjectLibraryId({objectId});
1482
+
1483
+ const status = await this.StreamStatus({
1484
+ name: objectId
1485
+ });
1486
+
1487
+ const streamMeta = await this.ContentObjectMetadata({
1488
+ objectId,
1489
+ libraryId,
1490
+ select: [
1491
+ "live_recording_config/reference_url",
1492
+ // live_recording_config/url is the old path
1493
+ "live_recording_config/url"
1494
+ ]
1495
+ });
1496
+
1497
+ const url = streamMeta.live_recording_config.reference_url || streamMeta.live_recording_config.url;
1498
+ const isActive = [STATUS_MAP.STARTING, STATUS_MAP.RUNNING, STATUS_MAP.STALLED, STATUS_MAP.STOPPED].includes(status.state);
1499
+
1500
+ if(url && isActive) {
1501
+ activeUrlMap[url] = true;
1502
+ }
1503
+ }
1504
+ }
1505
+ );
1506
+
1507
+ const streamUrlStatus = {};
1508
+
1509
+ const streamUrls = await this.ContentObjectMetadata({
1510
+ libraryId: await this.ContentObjectLibraryId({objectId: siteId}),
1511
+ objectId: siteId,
1512
+ metadataSubtree: "/live_stream_urls",
1513
+ resolveLinks: true,
1514
+ resolveIgnoreErrors: true
1515
+ });
1516
+
1517
+ if(!streamUrls) {
1518
+ throw Error("No pre-allocated URLs configured");
1519
+ }
1520
+
1521
+ Object.keys(streamUrls || {}).forEach(protocol => {
1522
+ streamUrlStatus[protocol] = streamUrls[protocol].map(url => {
1523
+ return {
1524
+ url,
1525
+ active: activeUrlMap[url] || false
1526
+ };
1527
+ })
1528
+ });
1529
+
1530
+ return streamUrlStatus;
1531
+ } catch(error) {
1532
+ console.error(error);
1533
+ }
1534
+ };
@@ -16,6 +16,13 @@ const Test = async () => {
16
16
  });
17
17
 
18
18
  client.SetSigner({signer});
19
+
20
+ const versionHash = "";
21
+
22
+ const url = await client.FabricUrl({
23
+ versionHash
24
+ });
25
+ console.log("url", url)
19
26
  } catch(error) {
20
27
  console.error(error);
21
28
  console.error(JSON.stringify(error, null, 2));