@eluvio/elv-client-js 4.2.24 → 4.2.27

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.24",
3
+ "version": "4.2.27",
4
4
  "description": "Javascript client for the Eluvio Content Fabric",
5
5
  "main": "src/index.js",
6
6
  "author": "Kevin Talmadge",
@@ -35,6 +35,10 @@
35
35
  "browser": {
36
36
  "fs": false
37
37
  },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/eluv-io/elv-client-js.git"
41
+ },
38
42
  "files": [
39
43
  "/build",
40
44
  "/dist",
@@ -78,6 +78,11 @@ const MezJobMainOfferingKey = function(abrMezOfferings) {
78
78
  * - ...
79
79
  * - ]
80
80
  * -
81
+ * - For a presigned URL, `path` and `storage_endpoint` are not required and `cloud_credentials` should be:
82
+ * - {
83
+ * - signed_url: "YOUR_PRESIGNED_S3_URL"
84
+ * - }
85
+ * -
81
86
  * - The simplest case is a one element array with .path_matchers == [".*"], in which case the same credentials will be used for all items in fileInfo
82
87
  *
83
88
  * @throws {Object} error - If the initialization of the master fails, error details can be found in error.body
@@ -127,8 +132,11 @@ exports.CreateProductionMaster = async function({
127
132
  let matched = false;
128
133
  for(let j = 0; !matched && j < access.length; j++) {
129
134
  let credentialSet = access[j];
135
+ const remoteAccess = credentialSet.remote_access;
130
136
  // strip trailing slash to get bucket name for credential set
131
- const credentialSetBucket = credentialSet.remote_access.path.replace(/\/$/, "");
137
+ const credentialSetBucket = remoteAccess.path
138
+ ? remoteAccess.path.replace(/\/$/, "")
139
+ : undefined;
132
140
  const matchers = credentialSet.path_matchers;
133
141
  for(let k = 0; !matched && k < matchers.length; k++) {
134
142
  const matcher = new RegExp(matchers[k]);
@@ -139,7 +147,7 @@ exports.CreateProductionMaster = async function({
139
147
  const s3prefixMatch = (s3prefixRegex.exec(fileSourcePath));
140
148
  if(s3prefixMatch) {
141
149
  const bucketName = s3prefixMatch[1];
142
- if(bucketName !== credentialSetBucket) {
150
+ if(credentialSetBucket && bucketName !== credentialSetBucket) {
143
151
  throw Error("Full S3 file path \"" + fileSourcePath + "\" matched to credential set with different bucket name '" + credentialSetBucket + "'");
144
152
  }
145
153
  }
@@ -154,7 +162,7 @@ exports.CreateProductionMaster = async function({
154
162
  }
155
163
  }
156
164
  if(!matched) {
157
- throw Error("no credential set found for file path: \"" + filePath + "\"");
165
+ throw Error("no credential set found for file path: \"" + oneFileInfo.source + "\"");
158
166
  }
159
167
  }
160
168
 
@@ -162,10 +170,13 @@ exports.CreateProductionMaster = async function({
162
170
  for(let i = 0; i < access.length; i++) {
163
171
  const credentialSet = access[i];
164
172
  if(credentialSet.hasOwnProperty("matched") && credentialSet.matched.length > 0) {
165
- const region = credentialSet.remote_access.storage_endpoint.region;
166
- const bucket = credentialSet.remote_access.path.replace(/\/$/, "");
167
- const accessKey = credentialSet.remote_access.cloud_credentials.access_key_id;
168
- const secret = credentialSet.remote_access.cloud_credentials.secret_access_key;
173
+ const remoteAccess = credentialSet.remote_access;
174
+ const cloudCredentials = remoteAccess.cloud_credentials;
175
+ const region = remoteAccess.storage_endpoint && remoteAccess.storage_endpoint.region;
176
+ const bucket = remoteAccess.path && remoteAccess.path.replace(/\/$/, "");
177
+ const accessKey = cloudCredentials.access_key_id;
178
+ const secret = cloudCredentials.secret_access_key;
179
+ const signedUrl = cloudCredentials.signed_url;
169
180
 
170
181
  await this.UploadFilesFromS3({
171
182
  libraryId,
@@ -176,6 +187,7 @@ exports.CreateProductionMaster = async function({
176
187
  bucket,
177
188
  accessKey,
178
189
  secret,
190
+ signedUrl,
179
191
  copy,
180
192
  callback,
181
193
  encryption: encrypt ? "cgck" : "none"
@@ -212,22 +224,24 @@ exports.CreateProductionMaster = async function({
212
224
  constant: false
213
225
  });
214
226
 
227
+ const mergeMetadata = {
228
+ ...(metadata || {}),
229
+ name: name !== undefined ? name : (metadata || {}).name,
230
+ description: description !== undefined ? description : (metadata || {}).description,
231
+ reference: access && !copy,
232
+ public: {
233
+ ...((metadata || {}).public || {}),
234
+ name: name !== undefined ? name : ((metadata || {}).public || {}).name,
235
+ description: description !== undefined ? description : ((metadata || {}).public || {}).description
236
+ },
237
+ elv_created_at: new Date().getTime(),
238
+ };
239
+
215
240
  await this.MergeMetadata({
216
241
  libraryId,
217
242
  objectId,
218
243
  writeToken,
219
- metadata: {
220
- ...(metadata || {}),
221
- name,
222
- description,
223
- reference: access && !copy,
224
- public: {
225
- ...((metadata || {}).public || {}),
226
- name: name || "",
227
- description: description || ""
228
- },
229
- elv_created_at: new Date().getTime(),
230
- }
244
+ metadata: mergeMetadata
231
245
  });
232
246
 
233
247
  let additionalReturnVals;
@@ -74,11 +74,11 @@ exports.ListFiles = async function({libraryId, objectId, path = "", versionHash,
74
74
  * @param {string} libraryId - ID of the library
75
75
  * @param {string} objectId - ID of the object
76
76
  * @param {string} writeToken - Write token of the draft
77
- * @param {string} region - AWS region to use
78
- * @param {string} bucket - AWS bucket to use
77
+ * @param {string=} region - AWS region to use
78
+ * @param {string=} bucket - AWS bucket to use
79
79
  * @param {Array<Object>} fileInfo - List of files to reference/copy
80
- * @param {string} accessKey - AWS access key
81
- * @param {string} secret - AWS secret
80
+ * @param {string=} accessKey - AWS access key
81
+ * @param {string=} secret - AWS secret
82
82
  * @param {string=} signedUrl
83
83
  * @param {string} encryption="none" - Encryption for uploaded files (copy only) - cgck | none
84
84
  * @param {boolean} copy=false - If true, will copy the data from S3 into the fabric. Otherwise, a reference to the content will be made.
@@ -152,17 +152,18 @@ exports.UploadFilesFromS3 = async function({
152
152
  };
153
153
  }
154
154
 
155
+ const remoteAccess = {
156
+ protocol: "s3",
157
+ platform: "aws",
158
+ cloud_credentials: cloudCredentials
159
+ };
160
+
161
+ if(bucket) remoteAccess.path = bucket;
162
+ if(region) remoteAccess.storage_endpoint = {region};
163
+
155
164
  const defaults = {
156
165
  encryption_key,
157
- access: {
158
- protocol: "s3",
159
- platform: "aws",
160
- path: bucket,
161
- storage_endpoint: {
162
- region
163
- },
164
- cloud_credentials: cloudCredentials
165
- }
166
+ access: remoteAccess
166
167
  };
167
168
 
168
169
  const ops = fileInfo.map(info => {
@@ -218,7 +218,8 @@ class LiveConf {
218
218
  *
219
219
  * Live input formats have fixed timebase:
220
220
  * - MPEG-TS/SRT input stream timebase is 90000
221
- * - RTMP input stream timebase is 1000 and gets translated to 16000 if not otherwise specified
221
+ * - RTMP input stream timebase is 1000 and gets translated to match the frame rate
222
+ * (eg. 24000 @24fps, 30000 @30fps/29.97fps, 60000 @60fps/59.94fps)
222
223
  *
223
224
  * This causes frame duration irregularities for certain frame rates.
224
225
  * For example RTMP 60fps has frames of durations 16 and 17. MPEG-TS 59.94fps has frames of
@@ -229,10 +230,13 @@ class LiveConf {
229
230
  * to make the math possible. This adjustment is also required for live-to-vod conversion.
230
231
  *
231
232
  * For example for MPEG-TS 59.94fps, the mez segment timebase needs to be 60000
232
- * (and resulting frame duration is 1001) and for RTMP 60fps the timebase needs to be 15360 (resulting frame
233
- * duration is 256).
233
+ * (and resulting frame duration is 1001) and for RTMP 60fps the timebase is set to 60000
234
+ * (resulting frame duration is 1000). For every supported RTMP frame rate, the video timebase is
235
+ * set equal to the source timescale, with a frame duration of 1000 (1001 for the *000/1001
236
+ * fractional rates).
234
237
  *
235
- * @sourceTimescale - adjusted source video stream timescale (eg. MPEGTS 90000, RTMP 16000 )
238
+ * @sourceTimescale - adjusted source video stream timescale (eg. MPEGTS 90000; ignored for RTMP,
239
+ * which derives its own timebase per frame rate in calcSegDurationRtmp)
236
240
  * @sampleRate - audio sample rate (commonly 48000 but can be different)
237
241
  * @audioCodec - audio codec as a string (eg. "aac")
238
242
  * @return - segment encoding parameters
@@ -333,57 +337,65 @@ class LiveConf {
333
337
  return seg;
334
338
  }
335
339
 
336
- calcSegDurationRtmp({sourceTimescale}) {
340
+ calcSegDurationRtmp() {
337
341
  let videoStream = this.getStreamDataForCodecType("video");
338
342
  let frameRate = videoStream.frame_rate;
339
343
  let seg = {};
340
344
 
341
345
  switch(frameRate) {
342
346
  case "24":
343
- seg.videoTimeBase = 768; // Note 1536 produces low output bitrate
344
- seg.videoFrameDurationTs = 512;
345
- seg.video = this.calcOutputTimebase(seg.videoTimeBase) * 30;
347
+ seg.videoTimeBase = 24000;
348
+ seg.videoFrameDurationTs = 1000;
349
+ seg.video = seg.videoTimeBase * 30;
346
350
  seg.keyint = 48;
347
351
  seg.duration = "30";
348
352
  break;
349
353
  case "25":
350
- seg.video = sourceTimescale * 30;
354
+ seg.videoTimeBase = 25000;
355
+ seg.videoFrameDurationTs = 1000;
356
+ seg.video = seg.videoTimeBase * 30;
351
357
  seg.keyint = 50;
352
358
  seg.duration = "30";
353
359
  break;
354
360
  case "30":
355
- seg.videoTimeBase = 960; // Output timebase: 15360
356
- seg.videoFrameDurationTs = 512;
357
- seg.video = this.calcOutputTimebase(seg.videoTimeBase) * 30;
361
+ seg.videoTimeBase = 30000;
362
+ seg.videoFrameDurationTs = 1000;
363
+ seg.video = seg.videoTimeBase * 30;
358
364
  seg.keyint = 60;
359
365
  seg.duration = "30";
360
366
  break;
361
367
  case "30000/1001":
362
- seg.video = sourceTimescale * 30.03;
368
+ seg.videoTimeBase = 30000;
369
+ seg.videoFrameDurationTs = 1001;
370
+ seg.video = seg.videoTimeBase * 30.03;
363
371
  seg.keyint = 60;
364
372
  seg.duration = "30.03";
365
373
  break;
366
374
  case "48":
367
- seg.videoTimeBase = 1536; // Output timebase: 12288
368
- seg.videoFrameDurationTs = 256;
369
- seg.video = this.calcOutputTimebase(seg.videoTimeBase) * 30;
375
+ seg.videoTimeBase = 48000;
376
+ seg.videoFrameDurationTs = 1000;
377
+ seg.video = seg.videoTimeBase * 30;
370
378
  seg.keyint = 96;
371
379
  seg.duration = "30";
372
380
  break;
373
381
  case "50":
374
- seg.video = sourceTimescale * 30;
382
+ seg.videoTimeBase = 50000;
383
+ seg.videoFrameDurationTs = 1000;
384
+ seg.video = seg.videoTimeBase * 30;
375
385
  seg.keyint = 100;
376
386
  seg.duration = "30";
377
387
  break;
378
388
  case "60":
379
- seg.videoTimeBase = 960; // Output timebase: 15360
380
- seg.videoFrameDurationTs = 256;
381
- seg.video = this.calcOutputTimebase(seg.videoTimeBase) * 30;
389
+ seg.videoTimeBase = 60000;
390
+ seg.videoFrameDurationTs = 1000;
391
+ seg.video = seg.videoTimeBase * 30;
382
392
  seg.keyint = 120;
383
393
  seg.duration = "30";
384
394
  break;
385
395
  case "60000/1001":
386
- seg.video = sourceTimescale * 30.03;
396
+ seg.videoTimeBase = 60000;
397
+ seg.videoFrameDurationTs = 1001;
398
+ seg.video = seg.videoTimeBase * 30.03;
387
399
  seg.keyint = 120;
388
400
  seg.duration = "30.03";
389
401
  break;
@@ -514,6 +526,10 @@ class LiveConf {
514
526
  const videoStream = this.getStreamDataForCodecType("video");
515
527
  let sourceTimescale;
516
528
 
529
+ // Custom profile values take precedence over computed defaults
530
+ const customRecordingParams = customSettings.liveRecordingConfigProfile?.recording_params;
531
+ const customXcParams = customRecordingParams?.xc_params;
532
+
517
533
  // Fill in liveconf all formats have in common
518
534
  conf.live_recording.fabric_config.ingress_node_api = this.nodeUrl || null;
519
535
  conf.live_recording.fabric_config.ingress_node_id = this.nodeId || null;
@@ -536,12 +552,12 @@ class LiveConf {
536
552
  // Fill in specifics for protocol
537
553
  switch(this.getFormat()) {
538
554
  case "mpegts":
539
- sourceTimescale = 90000;
555
+ sourceTimescale = customRecordingParams?.source_timescale ?? 90000;
540
556
  conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
541
557
  break;
542
558
  case "rtmp":
543
559
  case "flv":
544
- sourceTimescale = 16000;
560
+ sourceTimescale = customRecordingParams?.source_timescale ?? 16000;
545
561
  conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
546
562
  break;
547
563
  case "hls":
@@ -565,17 +581,20 @@ class LiveConf {
565
581
  // Segment conditioning parameters
566
582
  conf.live_recording.recording_config.recording_params.xc_params.seg_duration = segDurations.duration;
567
583
  conf.live_recording.recording_config.recording_params.xc_params.audio_seg_duration_ts = segDurations.audio;
568
- conf.live_recording.recording_config.recording_params.xc_params.video_seg_duration_ts = segDurations.video;
584
+ conf.live_recording.recording_config.recording_params.xc_params.video_seg_duration_ts =
585
+ customXcParams?.video_seg_duration_ts ?? segDurations.video;
569
586
  conf.live_recording.recording_config.recording_params.xc_params.force_keyint = segDurations.keyint;
570
587
 
571
588
  // Optional override output timebase and frame duration (ts)
572
- if(segDurations.videoTimeBase) {
589
+ if(segDurations.videoTimeBase && R.isNil(customXcParams?.video_time_base)) {
573
590
  conf.live_recording.recording_config.recording_params.xc_params.video_time_base = segDurations.videoTimeBase;
574
591
 
575
592
  // Note 'source_timescale' needs to be set to the output timebase and is used by playout
576
- conf.live_recording.recording_config.recording_params.source_timescale = this.calcOutputTimebase(segDurations.videoTimeBase);
593
+ if(R.isNil(customRecordingParams?.source_timescale)) {
594
+ conf.live_recording.recording_config.recording_params.source_timescale = this.calcOutputTimebase(segDurations.videoTimeBase);
595
+ }
577
596
  }
578
- if(segDurations.videoFrameDurationTs) {
597
+ if(segDurations.videoFrameDurationTs && R.isNil(customXcParams?.video_frame_duration_ts)) {
579
598
  conf.live_recording.recording_config.recording_params.xc_params.video_frame_duration_ts = segDurations.videoFrameDurationTs;
580
599
  }
581
600
 
@@ -322,15 +322,16 @@ exports.StreamCreate = async function({
322
322
  video_type: "live",
323
323
  slug: slugify(name)
324
324
  }
325
- },
326
- "live_recording_config": liveRecordingConfig
325
+ }
327
326
  };
328
327
 
329
- const oldProfile = await this.ContentObjectMetadata({
328
+ const currentLiveRecordingConfig = await this.ContentObjectMetadata({
330
329
  libraryId,
331
330
  objectId,
332
- metadataSubtree: "live_recording_config/name"
333
- });
331
+ metadataSubtree: "live_recording_config"
332
+ }) || {};
333
+
334
+ const oldProfile = currentLiveRecordingConfig.name;
334
335
 
335
336
  if(liveRecordingConfig?.name && liveRecordingConfig.name !== oldProfile) {
336
337
  metadata.public.asset_metadata.profile_last_updated = new Date().toISOString();
@@ -343,6 +344,15 @@ exports.StreamCreate = async function({
343
344
  metadata
344
345
  });
345
346
 
347
+ // Full overwrite avoids the server-side merge mixing streams from the old and new profile
348
+ await this.ReplaceMetadata({
349
+ libraryId,
350
+ objectId,
351
+ writeToken,
352
+ metadataSubtree: "live_recording_config",
353
+ metadata: R.mergeDeepRight(currentLiveRecordingConfig, liveRecordingConfig)
354
+ });
355
+
346
356
  try {
347
357
  await this.CreateLinks({
348
358
  libraryId,