@eluvio/elv-client-js 4.2.31 → 4.2.33

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.31",
3
+ "version": "4.2.33",
4
4
  "description": "Javascript client for the Eluvio Content Fabric",
5
5
  "main": "src/index.js",
6
6
  "author": "Kevin Talmadge",
@@ -539,6 +539,7 @@ class FrameClient {
539
539
  "StreamSaveConfigProfile",
540
540
  "StreamSetOfferingAndDRM",
541
541
  "StreamSiteSettings",
542
+ "StreamRestartRecording",
542
543
  "StreamStartRecording",
543
544
  "StreamStartOrStopOrReset",
544
545
  "StreamStatus",
@@ -197,6 +197,10 @@ const CueInfo = async ({eventId, status}) => {
197
197
  * @param {boolean=} options.linkToSite - If enabled, will create a link in the live stream site
198
198
  * @param {boolean=} options.initializeDrm - If enabled, will initialize DRM for the object
199
199
  * @param {string=} options.ingressNodeId - ID of the ingress node used for stream allocation (required for non-public nodes)
200
+ * @param {Object=} options.metadata - Additional metadata to merge into the live stream object metadata
201
+ * @param {Array<string>=} options.tags - Fabric tags to associate with the live stream
202
+ * @param {Array<string>=} options.groups - Fabric content group IDs to associate with the live stream. Distinct from access groups.
203
+ * @param {Object<string, string>=} options.queryFields - Fabric query fields to associate with the live stream
200
204
  *
201
205
  * @return {Promise<Object>} - Object containing objectId, libraryId, writeToken, and hash if finalized
202
206
  */
@@ -212,6 +216,20 @@ exports.StreamCreate = async function({
212
216
  const existingObject = !!objectId;
213
217
  let contentType;
214
218
  let adminGroups = options.accessGroups ?? [];
219
+ const customMetadata = options.metadata === undefined ? {} : options.metadata;
220
+ const {tags, groups, queryFields} = options;
221
+
222
+ if(typeof customMetadata !== "object" || Array.isArray(customMetadata)) {
223
+ throw Error("Stream metadata must be an object");
224
+ }
225
+
226
+ if(customMetadata.public !== undefined && (customMetadata.public === null || typeof customMetadata.public !== "object" || Array.isArray(customMetadata.public))) {
227
+ throw Error("Stream public metadata must be an object");
228
+ }
229
+
230
+ if(customMetadata.public?.asset_metadata !== undefined && (customMetadata.public.asset_metadata === null || typeof customMetadata.public.asset_metadata !== "object" || Array.isArray(customMetadata.public.asset_metadata))) {
231
+ throw Error("Stream public asset metadata must be an object");
232
+ }
215
233
 
216
234
  // Retrieve live stream content type
217
235
  try {
@@ -265,7 +283,10 @@ exports.StreamCreate = async function({
265
283
  editResponse = await this.CreateContentObject({
266
284
  libraryId,
267
285
  options: {
268
- type: contentType
286
+ type: contentType,
287
+ tags,
288
+ groups,
289
+ queryFields
269
290
  }
270
291
  });
271
292
  objectId = editResponse.objectId;
@@ -273,14 +294,27 @@ exports.StreamCreate = async function({
273
294
 
274
295
  const {writeToken} = editResponse;
275
296
  const {
276
- accessGroup,
277
- name=defaultName,
278
297
  displayTitle,
279
298
  description,
280
299
  permission="editable",
281
300
  ingressNodeId,
282
301
  initializeDrm=true
283
302
  } = options;
303
+ const name = options.name ?? customMetadata.public?.name ?? defaultName;
304
+
305
+ if(existingObject) {
306
+ if(tags !== undefined) {
307
+ await this.SetContentObjectTags({libraryId, objectId, writeToken, tags});
308
+ }
309
+
310
+ if(groups !== undefined) {
311
+ await this.SetContentObjectGroups({libraryId, objectId, writeToken, groups});
312
+ }
313
+
314
+ if(queryFields !== undefined) {
315
+ await this.SetContentObjectQueryFields({libraryId, objectId, writeToken, queryFields});
316
+ }
317
+ }
284
318
 
285
319
  if(!liveRecordingConfig) {
286
320
  liveRecordingConfig = {};
@@ -311,10 +345,9 @@ exports.StreamCreate = async function({
311
345
  })
312
346
  );
313
347
 
314
- const metadata = {
348
+ const generatedMetadata = {
315
349
  public: {
316
350
  name,
317
- description,
318
351
  asset_metadata: {
319
352
  display_title: displayTitle || name,
320
353
  title: name || displayTitle || defaultName,
@@ -324,6 +357,22 @@ exports.StreamCreate = async function({
324
357
  }
325
358
  }
326
359
  };
360
+ const metadata = R.mergeDeepRight(generatedMetadata, customMetadata);
361
+
362
+ // Explicit options take precedence over custom metadata
363
+ metadata.public.name = name;
364
+ if(options.name !== undefined) {
365
+ metadata.public.asset_metadata.title = name;
366
+ metadata.public.asset_metadata.slug = slugify(name);
367
+ }
368
+
369
+ if(displayTitle !== undefined) {
370
+ metadata.public.asset_metadata.display_title = displayTitle;
371
+ }
372
+
373
+ if(description !== undefined) {
374
+ metadata.public.description = description;
375
+ }
327
376
 
328
377
  const currentLiveRecordingConfig = await this.ContentObjectMetadata({
329
378
  libraryId,
@@ -1286,6 +1335,117 @@ exports.StreamStartRecording = async function({name, start=false}) {
1286
1335
  return status;
1287
1336
  };
1288
1337
 
1338
+ /**
1339
+ * Stop the current live recording and start a new one with a new edge write token.
1340
+ * The main object is finalized only once.
1341
+ *
1342
+ * @methodGroup Live Stream
1343
+ * @namedParams
1344
+ * @param {string} name - Object ID or name of the live stream object
1345
+ *
1346
+ * @return {Promise<Object>} - The status response for the new recording session
1347
+ */
1348
+ exports.StreamRestartRecording = async function({name}) {
1349
+ let status = await this.StreamStatus({name});
1350
+ const oldEdgeWriteToken = status?.edgeWriteToken;
1351
+
1352
+ if(!oldEdgeWriteToken) {
1353
+ throw new Error("Unable to restart stream - no active recording session");
1354
+ }
1355
+
1356
+ if(["running", "starting", "stalled"].includes(status.state)) {
1357
+ status = await this.StreamStartOrStopOrReset({name, op: "stop"});
1358
+ }
1359
+
1360
+ const IsStopped = currentStatus =>
1361
+ currentStatus?.state === "stopped" &&
1362
+ !currentStatus.error &&
1363
+ currentStatus.edgeWriteToken === oldEdgeWriteToken;
1364
+
1365
+ if(!IsStopped(status)) {
1366
+ throw new Error(`Unable to restart stream - current LRO did not stop definitively (state: ${status?.state || "unknown"})`);
1367
+ }
1368
+
1369
+ const {libraryId, objectId, fabricApi} = status;
1370
+ if(!fabricApi) {
1371
+ throw new Error("Unable to restart stream - ingress node API is unavailable");
1372
+ }
1373
+
1374
+ this.SetNodes({fabricURIs: [fabricApi]});
1375
+
1376
+ let mainWriteToken;
1377
+ let newEdgeWriteToken;
1378
+ let mainObjectFinalizeStarted = false;
1379
+
1380
+ try {
1381
+ ({writeToken: mainWriteToken} = await this.EditContentObject({
1382
+ libraryId,
1383
+ objectId
1384
+ }));
1385
+
1386
+ ({writeToken: newEdgeWriteToken} = await this.EditContentObject({
1387
+ libraryId,
1388
+ objectId
1389
+ }));
1390
+
1391
+ await this.MergeMetadata({
1392
+ libraryId,
1393
+ objectId,
1394
+ writeToken: mainWriteToken,
1395
+ metadata: {
1396
+ live_recording: {
1397
+ status: {
1398
+ edge_write_token: newEdgeWriteToken,
1399
+ state: "active"
1400
+ },
1401
+ fabric_config: {
1402
+ edge_write_token: newEdgeWriteToken
1403
+ }
1404
+ }
1405
+ }
1406
+ });
1407
+
1408
+ // Verify the old LRO is definitively stopped before publishing and deleting the old token.
1409
+ status = await this.StreamStatus({name});
1410
+ if(!IsStopped(status)) {
1411
+ throw new Error(`Unable to restart stream - current LRO stop could not be confirmed (state: ${status?.state || "unknown"})`);
1412
+ }
1413
+ mainObjectFinalizeStarted = true;
1414
+ const finalizeResponse = await this.FinalizeContentObject({
1415
+ libraryId,
1416
+ objectId,
1417
+ writeToken: mainWriteToken,
1418
+ commitMessage: `Restart stream with edge write token ${newEdgeWriteToken}`
1419
+ });
1420
+
1421
+ try {
1422
+ await this.DeleteWriteToken({writeToken: oldEdgeWriteToken});
1423
+ } catch(error) {
1424
+ this.Log(`Unable to delete old edge write token ${oldEdgeWriteToken}: ${error.message}`, true);
1425
+ }
1426
+
1427
+ const restartedStatus = await this.StreamStartOrStopOrReset({name, op: "start"});
1428
+ return {
1429
+ ...restartedStatus,
1430
+ hash: finalizeResponse.hash
1431
+ };
1432
+ } catch(error) {
1433
+ // Before main-object finalization begins, both new drafts are safe to discard and the old
1434
+ // stopped recording remains referenced by the published object.
1435
+ if(!mainObjectFinalizeStarted) {
1436
+ for(const writeToken of [newEdgeWriteToken, mainWriteToken].filter(Boolean)) {
1437
+ try {
1438
+ await this.DeleteWriteToken({writeToken});
1439
+ } catch(cleanupError) {
1440
+ this.Log(`Unable to clean up restart write token ${writeToken}: ${cleanupError.message}`, true);
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ throw error;
1446
+ }
1447
+ };
1448
+
1289
1449
  /**
1290
1450
  * Start, stop or reset a stream within the current session (current edge write token)
1291
1451
  *