@eluvio/elv-client-js 4.2.33 → 4.2.36

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.2.33",
3
+ "version": "4.2.36",
4
4
  "description": "Javascript client for the Eluvio Content Fabric",
5
5
  "main": "src/index.js",
6
6
  "author": "Kevin Talmadge",
package/src/HttpClient.js CHANGED
@@ -65,10 +65,16 @@ class HttpClient {
65
65
  allowFailover=true,
66
66
  forceFailover=false,
67
67
  allowRetry=true,
68
- uriIndex
68
+ uriIndex,
69
+ nodeUrl
69
70
  }) {
70
71
  let baseURI = this.BaseURI(uriIndex);
71
72
 
73
+ if(nodeUrl) {
74
+ baseURI = new URI(nodeUrl);
75
+ allowFailover = false;
76
+ }
77
+
72
78
  // If URL contains a write token, it must go to the correct server and can not fail over
73
79
  const writeTokenMatch = path.replace(/^\//, "").match(/(qlibs\/ilib[a-zA-Z0-9]+|q|qid)\/(tqw__[a-zA-Z0-9]+)/);
74
80
  const writeToken = writeTokenMatch ? writeTokenMatch[2] : undefined;
@@ -171,7 +177,8 @@ class HttpClient {
171
177
  headers,
172
178
  attempts: attempts + 1,
173
179
  uriIndex,
174
- forceFailover
180
+ forceFailover,
181
+ nodeUrl
175
182
  });
176
183
  }
177
184
 
@@ -1902,6 +1902,8 @@ exports.MakeFileServiceRequest = async function({
1902
1902
  * @param {boolean=} constant=true - If specified, a GET request authenticated with an AccessRequest will be made.
1903
1903
  * Otherwise, a POST with an UpdateRequest will be performed
1904
1904
  * @param {string=} format=json - The format of the response
1905
+ * @param {string=} nodeUrl - If specified, the call is made directly to this fabric node instead of
1906
+ * the client's currently configured node(s)
1905
1907
  *
1906
1908
  * @returns {Promise<format>} - The response from the call in the specified format
1907
1909
  */
@@ -1916,7 +1918,8 @@ exports.CallBitcodeMethod = async function({
1916
1918
  body={},
1917
1919
  headers={},
1918
1920
  constant=true,
1919
- format="json"
1921
+ format="json",
1922
+ nodeUrl
1920
1923
  }) {
1921
1924
  ValidateParameters({libraryId, objectId, versionHash});
1922
1925
  if(!method) { throw "Bitcode method not specified"; }
@@ -1960,7 +1963,8 @@ exports.CallBitcodeMethod = async function({
1960
1963
  method: verb,
1961
1964
  path,
1962
1965
  queryParams,
1963
- allowFailover: false
1966
+ allowFailover: false,
1967
+ nodeUrl
1964
1968
  }),
1965
1969
  this.HttpClient.debug,
1966
1970
  this.HttpClient.Log.bind(this.HttpClient)
@@ -813,10 +813,11 @@ exports.CreateNonOwnerCap = async function({objectId, libraryId, publicKey, writ
813
813
  * @param {object=} options -
814
814
  * @param {object=} options.meta - New metadata for the object - will be merged into existing metadata if specified
815
815
  * @param {string=} options.type - New type for the object - Object ID, version hash or name of type
816
+ * @param {string=} nodeUrl - Fabric node URL to create the draft on
816
817
  *
817
818
  * @returns {Promise<object>} - Response containing the object ID and write token of the draft, as well as URL of node handling the draft
818
819
  */
819
- exports.EditContentObject = async function({libraryId, objectId, options={}}) {
820
+ exports.EditContentObject = async function({libraryId, objectId, options={}, nodeUrl}) {
820
821
  ValidateParameters({libraryId, objectId});
821
822
 
822
823
  this.Log(`Opening content draft: ${libraryId} ${objectId}`);
@@ -842,11 +843,12 @@ exports.EditContentObject = async function({libraryId, objectId, options={}}) {
842
843
  headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
843
844
  method: "POST",
844
845
  path: path,
845
- body: options
846
+ body: options,
847
+ nodeUrl
846
848
  });
847
849
  // extract the url for the node that handled the request
848
850
  // TODO: remove/simplify after we start using /nodes API call to get node URLs for write tokens
849
- const nodeUrl = (new URL(rawEditResponse.url)).origin;
851
+ const responseNodeUrl = (new URL(rawEditResponse.url)).origin;
850
852
  const editResponse = await this.utils.ResponseToJson(
851
853
  rawEditResponse,
852
854
  this.HttpClient.debug,
@@ -854,11 +856,11 @@ exports.EditContentObject = async function({libraryId, objectId, options={}}) {
854
856
  );
855
857
 
856
858
  // Record the node used in creating this write token
857
- this.RecordWriteToken({writeToken: editResponse.write_token, fabricNodeUrl: nodeUrl});
859
+ this.RecordWriteToken({writeToken: editResponse.write_token, fabricNodeUrl: responseNodeUrl});
858
860
 
859
861
  editResponse.writeToken = editResponse.write_token;
860
862
  editResponse.objectId = editResponse.id;
861
- editResponse.nodeUrl = nodeUrl;
863
+ editResponse.nodeUrl = responseNodeUrl;
862
864
 
863
865
  return editResponse;
864
866
  };
@@ -538,8 +538,12 @@ class LiveConf {
538
538
  conf.live_recording.recording_config.recording_params.description = `Ingest stream ${fileName}`;
539
539
  conf.live_recording.recording_config.recording_params.name = `Ingest stream ${fileName}`;
540
540
  conf.live_recording.recording_config.recording_params.xc_params.sample_rate = sampleRate;
541
- conf.live_recording.recording_config.recording_params.xc_params.enc_height = videoStream.height;
542
- conf.live_recording.recording_config.recording_params.xc_params.enc_width = videoStream.width;
541
+
542
+ // Encoding dimensions default to the source but are overridden by profile values if present
543
+ conf.live_recording.recording_config.recording_params.xc_params.enc_height =
544
+ customXcParams?.enc_height ?? videoStream.height;
545
+ conf.live_recording.recording_config.recording_params.xc_params.enc_width =
546
+ customXcParams?.enc_width ?? videoStream.width;
543
547
 
544
548
  for(let i =0; i < Object.keys(audioStreams).length; i ++) {
545
549
  conf.live_recording.recording_config.recording_params.xc_params.audio_index[i] = parseInt(Object.keys(audioStreams)[i]);
@@ -585,25 +589,27 @@ class LiveConf {
585
589
  customXcParams?.video_seg_duration_ts ?? segDurations.video;
586
590
  conf.live_recording.recording_config.recording_params.xc_params.force_keyint = segDurations.keyint;
587
591
 
588
- // Optional override output timebase and frame duration (ts)
589
- if(segDurations.videoTimeBase && R.isNil(customXcParams?.video_time_base)) {
590
- conf.live_recording.recording_config.recording_params.xc_params.video_time_base = segDurations.videoTimeBase;
592
+ // These must be recomputed from scratch on every (re)configuration to avoid stale values
593
+ if(R.isNil(customXcParams?.video_time_base)) {
594
+ if(segDurations.videoTimeBase) {
595
+ conf.live_recording.recording_config.recording_params.xc_params.video_time_base = segDurations.videoTimeBase;
591
596
 
592
- // Note 'source_timescale' needs to be set to the output timebase and is used by playout
593
- if(R.isNil(customRecordingParams?.source_timescale)) {
594
- conf.live_recording.recording_config.recording_params.source_timescale = this.calcOutputTimebase(segDurations.videoTimeBase);
597
+ // Note 'source_timescale' needs to be set to the output timebase and is used by playout
598
+ if(R.isNil(customRecordingParams?.source_timescale)) {
599
+ conf.live_recording.recording_config.recording_params.source_timescale = this.calcOutputTimebase(segDurations.videoTimeBase);
600
+ }
601
+ } else {
602
+ conf.live_recording.recording_config.recording_params.xc_params.video_time_base = null;
595
603
  }
596
604
  }
597
- if(segDurations.videoFrameDurationTs && R.isNil(customXcParams?.video_frame_duration_ts)) {
598
- conf.live_recording.recording_config.recording_params.xc_params.video_frame_duration_ts = segDurations.videoFrameDurationTs;
605
+ if(R.isNil(customXcParams?.video_frame_duration_ts)) {
606
+ conf.live_recording.recording_config.recording_params.xc_params.video_frame_duration_ts =
607
+ segDurations.videoFrameDurationTs || null;
599
608
  }
600
609
 
601
610
  const ladder_specs = customSettings.liveRecordingConfigProfile?.playout_config?.ladder_specs;
602
611
  const ladderProfile = ladder_specs?.video?.length > 0 ? ladder_specs : DefaultABRLadder;
603
612
 
604
- conf.live_recording.recording_config.recording_params.xc_params.enc_height = videoStream.height;
605
- conf.live_recording.recording_config.recording_params.xc_params.enc_width = videoStream.width;
606
-
607
613
  // Reset ladder specs (updating existing stream will carry over old specs
608
614
  conf.live_recording.recording_config.recording_params.ladder_specs = [];
609
615
 
@@ -393,15 +393,42 @@ exports.StreamCreate = async function({
393
393
  metadata
394
394
  });
395
395
 
396
+ // If the profile is changing, only carry over name and url so stale
397
+ // settings from the old profile are cleared
398
+ const changingProfile = liveRecordingConfig?.name && liveRecordingConfig.name !== oldProfile;
399
+ const baseLiveRecordingConfig = changingProfile ?
400
+ R.pick(["url", "name"], currentLiveRecordingConfig) :
401
+ currentLiveRecordingConfig;
402
+
396
403
  // Full overwrite avoids the server-side merge mixing streams from the old and new profile
397
404
  await this.ReplaceMetadata({
398
405
  libraryId,
399
406
  objectId,
400
407
  writeToken,
401
408
  metadataSubtree: "live_recording_config",
402
- metadata: R.mergeDeepRight(currentLiveRecordingConfig, liveRecordingConfig)
409
+ metadata: R.mergeDeepRight(baseLiveRecordingConfig, liveRecordingConfig)
403
410
  });
404
411
 
412
+ if(changingProfile) {
413
+ // Clear live_recording and live_recording_overrides so stale settings from the
414
+ // previous profile don't persist, matching StreamApplyProfile
415
+ await this.ReplaceMetadata({
416
+ libraryId,
417
+ objectId,
418
+ writeToken,
419
+ metadataSubtree: "live_recording",
420
+ metadata: {}
421
+ });
422
+
423
+ await this.ReplaceMetadata({
424
+ libraryId,
425
+ objectId,
426
+ writeToken,
427
+ metadataSubtree: "live_recording_overrides",
428
+ metadata: {}
429
+ });
430
+ }
431
+
405
432
  try {
406
433
  await this.CreateLinks({
407
434
  libraryId,
@@ -1269,13 +1296,12 @@ exports.StreamStartRecording = async function({name, start=false}) {
1269
1296
  fabURI = "https://" + fabURI;
1270
1297
  }
1271
1298
 
1272
- this.SetNodes({fabricURIs: [fabURI]});
1273
-
1274
1299
  console.log("Node URI", fabURI, "ID", liveRecording.fabric_config.ingress_node_id);
1275
1300
 
1276
1301
  let response = await this.EditContentObject({
1277
1302
  libraryId: libraryId,
1278
- objectId: objectId
1303
+ objectId: objectId,
1304
+ nodeUrl: fabURI
1279
1305
  });
1280
1306
  const edgeToken = response.write_token;
1281
1307
  console.log("Edge token:", edgeToken);
@@ -1285,7 +1311,8 @@ exports.StreamStartRecording = async function({name, start=false}) {
1285
1311
  */
1286
1312
  response = await this.EditContentObject({
1287
1313
  libraryId: libraryId,
1288
- objectId: objectId
1314
+ objectId: objectId,
1315
+ nodeUrl: fabURI
1289
1316
  });
1290
1317
  let writeToken = response.write_token;
1291
1318
 
@@ -1371,8 +1398,6 @@ exports.StreamRestartRecording = async function({name}) {
1371
1398
  throw new Error("Unable to restart stream - ingress node API is unavailable");
1372
1399
  }
1373
1400
 
1374
- this.SetNodes({fabricURIs: [fabricApi]});
1375
-
1376
1401
  let mainWriteToken;
1377
1402
  let newEdgeWriteToken;
1378
1403
  let mainObjectFinalizeStarted = false;
@@ -1380,12 +1405,14 @@ exports.StreamRestartRecording = async function({name}) {
1380
1405
  try {
1381
1406
  ({writeToken: mainWriteToken} = await this.EditContentObject({
1382
1407
  libraryId,
1383
- objectId
1408
+ objectId,
1409
+ nodeUrl: fabricApi
1384
1410
  }));
1385
1411
 
1386
1412
  ({writeToken: newEdgeWriteToken} = await this.EditContentObject({
1387
1413
  libraryId,
1388
- objectId
1414
+ objectId,
1415
+ nodeUrl: fabricApi
1389
1416
  }));
1390
1417
 
1391
1418
  await this.MergeMetadata({
@@ -1518,6 +1545,34 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
1518
1545
  });
1519
1546
  } catch(error) {
1520
1547
  console.log("LRO Start (failed): ", error);
1548
+
1549
+ // Check for unsupported input conditions before returning
1550
+ // generic "LRO start failed - must create a stream first" error
1551
+ try {
1552
+ const configMeta = await this.ContentObjectMetadata({
1553
+ libraryId: status.libraryId,
1554
+ objectId: status.objectId,
1555
+ metadataSubtree: "live_recording_config",
1556
+ select: [
1557
+ "probe_info",
1558
+ "input_stream_info"
1559
+ ]
1560
+ });
1561
+
1562
+ const probeInfo = configMeta?.probe_info || configMeta?.input_stream_info;
1563
+ const unsupportedAudio = (probeInfo?.streams || [])
1564
+ .find(stream => stream.codec_type === "audio" && stream.sample_rate === 44100);
1565
+
1566
+ if(unsupportedAudio) {
1567
+ return {
1568
+ state: status.state,
1569
+ error: "Unsupported audio sample rate: 44.1 kHz. Only 48 kHz audio is supported."
1570
+ };
1571
+ }
1572
+ } catch(probeError) {
1573
+ console.error("Unable to check audio sample rate", probeError);
1574
+ }
1575
+
1521
1576
  return {
1522
1577
  state: status.state,
1523
1578
  error: "LRO start failed - must create a stream first"
@@ -1567,8 +1622,6 @@ exports.StreamStopRecording = async function({name}) {
1567
1622
  fabURI = "https://" + fabURI;
1568
1623
  }
1569
1624
 
1570
- this.SetNodes({fabricURIs: [fabURI]});
1571
-
1572
1625
  const metaEdgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
1573
1626
 
1574
1627
  if(!metaEdgeWriteToken) {
@@ -1606,7 +1659,8 @@ exports.StreamStopRecording = async function({name}) {
1606
1659
 
1607
1660
  const {writeToken} = await this.EditContentObject({
1608
1661
  libraryId: libraryId,
1609
- objectId: objectId
1662
+ objectId: objectId,
1663
+ nodeUrl: fabURI
1610
1664
  });
1611
1665
 
1612
1666
  // Set stop time and inactive state
@@ -11,7 +11,7 @@ const Test = async () => {
11
11
 
12
12
  try {
13
13
  const client = await ElvClient.FromNetworkName({
14
- networkName: "main"
14
+ networkName: "demo"
15
15
  });
16
16
 
17
17
  const wallet = client.GenerateWallet();
@@ -21,19 +21,7 @@ const Test = async () => {
21
21
 
22
22
  client.SetSigner({signer});
23
23
 
24
- const configUrl = new URL(await client.ConfigUrl());
25
- configUrl.pathname = "/config";
26
- // if(geo) { configUrl.searchParams.set("elvgeo", geo); } // optional region scoping
27
-
28
- const fabricInfo = await (await fetch(configUrl.toString())).json();
29
- // fabricInfo.network.services.* e.g. live_egress, ethereum, authority, etc.
30
- const liveEgressUrls = fabricInfo?.network?.services?.live_egress;
31
- console.log('fabricINfo', fabricInfo.network?.services?.tenant_query)
32
-
33
- // tenant_query nodes are now wired into the client and used automatically by TenantContent:
34
- console.log('client tenantQueryURIs', client.Nodes().tenantQueryURIs)
35
- // const result = await client.TenantContent({limit: 5});
36
- // console.log(JSON.stringify(result, null, 2));
24
+ console.log(await client.ContentLibraries());
37
25
  } catch(error) {
38
26
  console.error(error);
39
27
  console.error(JSON.stringify(error, null, 2));