@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 +5 -1
- package/src/client/ABRPublishing.js +33 -19
- package/src/client/Files.js +14 -13
- package/src/client/LiveConf.js +46 -27
- package/src/client/LiveStream.js +15 -5
- package/utilities/MediaIngest.js +424 -0
- package/utilities/README_INGEST.md +332 -0
- package/utilities/lib/abr_profiles/abr_profile_4k_all.json +181 -0
- package/utilities/lib/concerns/Client.js +71 -10
- package/utilities/lib/concerns/CloudAccess.js +2 -2
- package/utilities/lib/concerns/CloudFile.js +14 -7
- package/utilities/lib/data/network_specs.json +13 -0
- package/utilities/lib/models/CredentialSet.js +26 -7
- package/utilities/tests/unit/lib/concerns/Client.test.js +96 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eluvio/elv-client-js",
|
|
3
|
-
"version": "4.2.
|
|
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 =
|
|
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: \"" +
|
|
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
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
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;
|
package/src/client/Files.js
CHANGED
|
@@ -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 => {
|
package/src/client/LiveConf.js
CHANGED
|
@@ -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
|
|
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
|
|
233
|
-
* duration is
|
|
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
|
|
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(
|
|
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 =
|
|
344
|
-
seg.videoFrameDurationTs =
|
|
345
|
-
seg.video =
|
|
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.
|
|
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 =
|
|
356
|
-
seg.videoFrameDurationTs =
|
|
357
|
-
seg.video =
|
|
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.
|
|
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 =
|
|
368
|
-
seg.videoFrameDurationTs =
|
|
369
|
-
seg.video =
|
|
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.
|
|
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 =
|
|
380
|
-
seg.videoFrameDurationTs =
|
|
381
|
-
seg.video =
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
package/src/client/LiveStream.js
CHANGED
|
@@ -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
|
|
328
|
+
const currentLiveRecordingConfig = await this.ContentObjectMetadata({
|
|
330
329
|
libraryId,
|
|
331
330
|
objectId,
|
|
332
|
-
metadataSubtree: "live_recording_config
|
|
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,
|