@eluvio/elv-client-js 4.2.25 → 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.25",
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 => {
@@ -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,
@@ -0,0 +1,424 @@
1
+ // Create or update a playable media object from specified file(s)
2
+ const R = require("ramda");
3
+ const fs = require("fs");
4
+
5
+ const {ModOpt, NewOpt} = require("./lib/options");
6
+ const Utility = require("./lib/Utility");
7
+
8
+ const ABR = require("@eluvio/elv-abr-profile");
9
+
10
+ const Client = require("./lib/concerns/Client");
11
+ const CloudFile = require("./lib/concerns/CloudFile");
12
+ const Finalize = require("./lib/concerns/Finalize");
13
+ const LocalFile = require("./lib/concerns/LocalFile");
14
+ const LRO = require("./lib/concerns/LRO");
15
+ const ArgLibraryId = require("./lib/concerns/ArgLibraryId");
16
+ const ArgTenant = require("./lib/concerns/ArgTenant");
17
+ const {seconds} = require("./lib/helpers");
18
+
19
+ const AbrProfile = require("./lib/abr_profiles/abr_profile_4k_all.json");
20
+ const STATUS_FILE = "./ingest-status.json";
21
+
22
+ // Status object - used for reporting and resume.
23
+ const status = {
24
+ state: "",
25
+ object_id: "",
26
+ write_token: "",
27
+ master_created: false
28
+ };
29
+
30
+ const SetStatus = statusUpdate => {
31
+ Object.assign(status, statusUpdate);
32
+ fs.writeFileSync(STATUS_FILE, `${JSON.stringify(status, null, 2)}\n`);
33
+ };
34
+
35
+ class MediaIngest extends Utility {
36
+ blueprint() {
37
+ return {
38
+ concerns: [Client, CloudFile, Finalize, LocalFile, ArgLibraryId, ArgTenant, LRO],
39
+ options: [
40
+ ModOpt("libraryId", {demand: false, forX: "new media object"}),
41
+ NewOpt("objectId", {
42
+ demand: false,
43
+ descTemplate: "Existing media object to ingest into",
44
+ type: "string"
45
+ }),
46
+ NewOpt("title", {
47
+ demand: false,
48
+ descTemplate: "Title for new media object (required unless --object-id is given)",
49
+ type: "string"
50
+ }),
51
+ NewOpt("drm", {
52
+ default: false,
53
+ descTemplate: "Use DRM for playback",
54
+ type: "boolean"
55
+ }),
56
+ NewOpt("hdrInfoFile", {
57
+ descTemplate: "Path to HDR info JSON file to enable HDR10 ingest. File format: { \"master_display\": \"G(...)B(...)R(...)WP(...)L(...)\", \"max_cll\": \"...,...\" }",
58
+ type: "string"
59
+ }),
60
+ NewOpt("finalize", {
61
+ default: true,
62
+ descTemplate: "Finalize the content object write token after ingest. Use --no-finalize to leave the draft open.",
63
+ type: "boolean"
64
+ }),
65
+ NewOpt("offeringKey", {
66
+ default: "default",
67
+ descTemplate: "Offering key (default: 'default')",
68
+ type: "string"
69
+ }),
70
+ NewOpt("variantKey", {
71
+ default: "default",
72
+ descTemplate: "Master variant key (default: 'default')",
73
+ type: "string"
74
+ }),
75
+ ModOpt("files", {forX: "for new media object"})
76
+ ]
77
+ };
78
+ }
79
+
80
+ async body() {
81
+ const logger = this.logger;
82
+
83
+ const s3Reference = Boolean(this.args.s3Reference);
84
+ const useS3 = Boolean(this.args.s3Copy || s3Reference);
85
+ const access = useS3
86
+ ? this.concerns.CloudFile.credentialSet()
87
+ : undefined;
88
+
89
+ let fileHandles = [];
90
+ const fileInfo = useS3
91
+ ? this.concerns.CloudFile.fileInfo()
92
+ : this.concerns.LocalFile.fileInfo(fileHandles);
93
+ const fileConcern = useS3
94
+ ? this.concerns.CloudFile
95
+ : this.concerns.LocalFile;
96
+
97
+ // delay getting elvClient until this point so script exits faster
98
+ // if there is a validation error above
99
+ const client = await this.concerns.Client.get();
100
+
101
+ // get type from Tenant
102
+ const tenantInfo = await this.concerns.ArgTenant.tenantInfo();
103
+
104
+ const type = tenantInfo.typeTitle;
105
+
106
+ if(R.isNil(type)) throw Error("Library does not specify content type for media ingests");
107
+
108
+ let {drm, finalize, hdrInfoFile, libraryId, objectId, offeringKey, title, variantKey} = this.args;
109
+ const noFinalize = !finalize;
110
+ const copy = !s3Reference;
111
+ const encrypt = !s3Reference;
112
+
113
+ let id;
114
+ let nodeUrl;
115
+ let writeToken;
116
+ if(status.write_token) {
117
+ id = status.object_id;
118
+ writeToken = status.write_token;
119
+ if(!libraryId) {
120
+ libraryId = await client.ContentObjectLibraryId({objectId: id});
121
+ }
122
+ // Re-pin client to the node holding the draft
123
+ nodeUrl = await client.WriteTokenNodeUrlNetwork({writeToken});
124
+ await client.SetNodes({fabricURIs: [nodeUrl]});
125
+ logger.log(`Resuming from write token ${writeToken} on ${nodeUrl}`);
126
+ } else {
127
+ if(!libraryId && !objectId) {
128
+ throw Error("One of library-id or object-id must be specified");
129
+ }
130
+
131
+ if(!objectId && !title) {
132
+ throw Error("--title is required when creating a new media object");
133
+ }
134
+
135
+ if(objectId) {
136
+ if(!libraryId) {
137
+ libraryId = await client.ContentObjectLibraryId({objectId});
138
+ }
139
+ const editResp = await client.EditContentObject({
140
+ libraryId,
141
+ objectId,
142
+ options: type ? { type } : {}
143
+ });
144
+ id = objectId;
145
+ writeToken = editResp.writeToken;
146
+ nodeUrl = editResp.nodeUrl;
147
+ } else {
148
+ const createResp = await client.CreateContentObject({
149
+ libraryId,
150
+ options: type ? { type } : {}
151
+ });
152
+ id = createResp.id;
153
+ writeToken = createResp.writeToken;
154
+ nodeUrl = createResp.nodeUrl;
155
+ }
156
+
157
+ await client.authClient.GenerateAuthorizationToken({
158
+ libraryId,
159
+ objectId: id,
160
+ update: true
161
+ });
162
+
163
+ status.object_id = id;
164
+ status.write_token = writeToken;
165
+ }
166
+
167
+ SetStatus({state: "object_created"});
168
+
169
+ if(!nodeUrl) {
170
+ nodeUrl = client.WriteTokenNodeUrlLocal({writeToken});
171
+ }
172
+ if(!nodeUrl) {
173
+ nodeUrl = await client.WriteTokenNodeUrlNetwork({writeToken});
174
+ }
175
+
176
+ logger.data("object_id", id);
177
+
178
+ if(!status.master_created) {
179
+ logger.log("Uploading files...");
180
+
181
+ const createMasterResponse = await client.CreateProductionMaster({
182
+ libraryId,
183
+ type,
184
+ name: title,
185
+ fileInfo,
186
+ encrypt,
187
+ access,
188
+ copy,
189
+ callback: fileConcern.callback,
190
+ writeToken
191
+ });
192
+
193
+ // Close file handles (if any)
194
+ this.concerns.LocalFile.closeFileHandles(fileHandles);
195
+
196
+ logger.errorsAndWarnings(createMasterResponse);
197
+
198
+ logger.logList(
199
+ "",
200
+ "Production master default variant created:",
201
+ ` Object ID: ${id}`,
202
+ ` Write token: ${writeToken}`,
203
+ ""
204
+ );
205
+
206
+ if(!R.isNil(createMasterResponse.errors) && !R.isEmpty(createMasterResponse.errors)) throw Error(`Error(s) encountered while inspecting uploaded files: ${createMasterResponse.errors.join("\n")}`);
207
+
208
+ status.master_created = true;
209
+ } else {
210
+ this.concerns.LocalFile.closeFileHandles(fileHandles);
211
+ logger.log(`Skipping upload + master creation (resume), write token ${writeToken}`);
212
+ }
213
+
214
+ SetStatus({state: "master_created", master_created: true});
215
+
216
+ // get production master metadata
217
+ const masterMetadata = (await client.ContentObjectMetadata({
218
+ libraryId,
219
+ objectId: id,
220
+ writeToken,
221
+ metadataSubtree: "/production_master"
222
+ }));
223
+
224
+ const sources = R.prop("sources", masterMetadata);
225
+ const variant = R.path(["variants", variantKey], masterMetadata);
226
+ if(R.isNil(variant)) throw Error(`Variant '${variantKey}' not found in production master metadata`);
227
+
228
+ // add info on source files and variant to data if --json selected
229
+ if(this.args.json) {
230
+ logger.data("media_files", sources);
231
+ logger.data("variant_key", variantKey);
232
+ logger.data("variant", variant);
233
+ }
234
+
235
+ // HDR: inject HDR info into first video stream's sources metadata
236
+ let hdrInfo;
237
+ if(hdrInfoFile) {
238
+ hdrInfo = JSON.parse(fs.readFileSync(hdrInfoFile, "utf8"));
239
+ const fileName = Object.keys(sources)[0];
240
+ const streams = sources[fileName].streams;
241
+ const videoStreamIdx = streams.findIndex(s => s.type === "StreamVideo");
242
+ if(videoStreamIdx < 0) throw Error(`No video stream found in /production_master/sources/${fileName}`);
243
+
244
+ streams[videoStreamIdx].hdr = hdrInfo;
245
+ logger.log(`Adding HDR info to /production_master/sources/${fileName}/streams/${videoStreamIdx}/hdr`);
246
+
247
+ await client.ReplaceMetadata({
248
+ libraryId,
249
+ objectId: id,
250
+ writeToken,
251
+ metadataSubtree: "/production_master/sources",
252
+ metadata: sources
253
+ });
254
+ }
255
+
256
+ // generate ABR profile
257
+ const genProfileRetVal = ABR.ABRProfileForVariant(sources, variant, AbrProfile);
258
+ if(!genProfileRetVal.ok) throw Error(`Error(s) encountered while generating ABR profile: ${genProfileRetVal.errors.join("\n")}`);
259
+
260
+ // filter DRM/clear as needed
261
+ const filterProfileRetVal = drm ?
262
+ ABR.ProfileExcludeClear(genProfileRetVal.result) :
263
+ ABR.ProfileExcludeDRM(genProfileRetVal.result);
264
+ if(!filterProfileRetVal.ok) throw Error(`Error(s) encountered while setting playout formats: ${filterProfileRetVal.errors.join("\n")}`);
265
+
266
+ // HDR: add HEVC/10-bit segment specs to the profile
267
+ if(hdrInfo) {
268
+ filterProfileRetVal.result.segment_specs = filterProfileRetVal.result.segment_specs || {};
269
+ filterProfileRetVal.result.segment_specs.video = {
270
+ ...(filterProfileRetVal.result.segment_specs.video || {}),
271
+ bit_depth: 10,
272
+ encoding: "h265"
273
+ };
274
+ }
275
+
276
+ // set up mezzanine offering
277
+ logger.log("Setting up media file conversion...");
278
+ const createMezResponse = await client.CreateABRMezzanine({
279
+ name: title,
280
+ libraryId,
281
+ writeToken,
282
+ type,
283
+ masterWriteToken: writeToken,
284
+ variant: variantKey,
285
+ offeringKey,
286
+ abrProfile: filterProfileRetVal.result
287
+ });
288
+
289
+ logger.errorsAndWarnings(createMezResponse);
290
+ const createMezErrors = createMezResponse.errors;
291
+ if(!R.isNil(createMezErrors) && !R.isEmpty(createMezErrors)) throw Error(`Error(s) encountered while setting up media file conversion: ${createMezErrors.join("\n")}`);
292
+
293
+ SetStatus({state: "mez_created", mez_created: true});
294
+
295
+ logger.log("Starting conversion to streaming format...");
296
+
297
+ const startJobsResponse = await client.StartABRMezzanineJobs({
298
+ libraryId,
299
+ objectId: id,
300
+ offeringKey,
301
+ writeToken
302
+ });
303
+
304
+ logger.errorsAndWarnings(startJobsResponse);
305
+ const startJobsErrors = createMezResponse.errors;
306
+ if(!R.isNil(startJobsErrors) && !R.isEmpty(startJobsErrors)) throw Error(`Error(s) encountered while starting file conversion: ${startJobsErrors.join("\n")}`);
307
+
308
+ writeToken = startJobsResponse.writeToken || writeToken;
309
+ nodeUrl = startJobsResponse.nodeUrl || nodeUrl;
310
+
311
+ const lroWriteToken = R.path(["lro_draft", "write_token"], startJobsResponse) || writeToken;
312
+ const lroNode = R.path(["lro_draft", "node"], startJobsResponse) || nodeUrl;
313
+
314
+ logger.data("library_id", libraryId);
315
+ logger.data("object_id", id);
316
+ logger.data("offering_key", offeringKey);
317
+ logger.data("write_token", lroWriteToken);
318
+ logger.data("write_node", lroNode);
319
+ logger.data("node_endpoint", lroNode);
320
+
321
+ logger.logList(
322
+ "",
323
+ `Library ID: ${libraryId}`,
324
+ `Object ID: ${id}`,
325
+ `Variant: ${variantKey}`,
326
+ `Offering: ${offeringKey}`,
327
+ `Write Token: ${lroWriteToken}`,
328
+ `Write Node: ${lroNode}`,
329
+ ""
330
+ );
331
+
332
+ SetStatus({state: "mez_started", mez_started: true, mez_job: startJobsResponse});
333
+
334
+ logger.log("Progress:");
335
+
336
+ const lro = this.concerns.LRO;
337
+ let done = false;
338
+ let lastStatus;
339
+ while(!done) {
340
+ const statusMap = await lro.status({libraryId, objectId: id, writeToken}); // TODO: check how offering key is used, if at all
341
+ const statusSummary = lro.statusSummary(statusMap);
342
+ lastStatus = statusSummary.run_state;
343
+ if(lastStatus !== LRO.STATE_RUNNING) done = true;
344
+ logger.log(`run_state: ${lastStatus}`);
345
+ const eta = statusSummary.estimated_time_left_h_m_s;
346
+ if(eta) logger.log(`estimated time left: ${eta}`);
347
+ await seconds(15);
348
+ }
349
+
350
+ const finalizeAbrResponse = await client.FinalizeABRMezzanine({
351
+ libraryId,
352
+ objectId: id,
353
+ writeToken,
354
+ offeringKey
355
+ });
356
+
357
+ logger.errorsAndWarnings(finalizeAbrResponse);
358
+ const finalizeErrors = finalizeAbrResponse.errors;
359
+ if(!R.isNil(finalizeErrors) && !R.isEmpty(finalizeErrors)) throw Error(`Error(s) encountered while finalizing object: ${finalizeErrors.join("\n")}`);
360
+
361
+ if(s3Reference) {
362
+ logger.log("Retaining S3 source file reference.");
363
+ } else {
364
+ logger.log("Deleting source files...");
365
+ await client.DeleteFiles({
366
+ libraryId,
367
+ objectId: id,
368
+ writeToken,
369
+ filePaths: fileInfo.map(f => f.path)
370
+ });
371
+ }
372
+ nodeUrl = client.WriteTokenNodeUrlLocal({writeToken}) || nodeUrl;
373
+
374
+ let latestHash = "not finalized";
375
+
376
+ if(noFinalize) {
377
+ logger.log("Skipping content object finalization (--no-finalize).");
378
+ } else {
379
+ logger.log("Finalizing content object...");
380
+ const sourceFileName = fileInfo[0].path.split("/").pop();
381
+ const finalizeObjResponse = await client.FinalizeContentObject({
382
+ libraryId,
383
+ objectId: id,
384
+ writeToken,
385
+ commitMessage: `Ingest ${sourceFileName}${hdrInfoFile ? " [HDR]" : ""}`
386
+ });
387
+
388
+ latestHash = finalizeObjResponse.hash;
389
+ }
390
+
391
+ logger.data("version_hash", latestHash);
392
+ logger.data("write_token", writeToken);
393
+ logger.data("write_node", nodeUrl);
394
+ logger.data("node_endpoint", nodeUrl);
395
+
396
+ logger.logList(
397
+ "",
398
+ noFinalize ? "Playable media object draft prepared:" : "Playable media object created:",
399
+ ` Object ID: ${id}`,
400
+ ` Version Hash: ${latestHash}`,
401
+ ` Write Token: ${writeToken}`,
402
+ ` Node Endpoint: ${nodeUrl}`,
403
+ ""
404
+ );
405
+
406
+ if(!noFinalize) {
407
+ await this.concerns.Finalize.waitForPublish({
408
+ latestHash,
409
+ libraryId,
410
+ objectId: id
411
+ });
412
+ }
413
+ }
414
+
415
+ header() {
416
+ return "Create playable media object via media ingest";
417
+ }
418
+ }
419
+
420
+ if(require.main === module) {
421
+ Utility.cmdLineInvoke(MediaIngest);
422
+ } else {
423
+ module.exports = MediaIngest;
424
+ }