@eluvio/elv-client-js 4.0.82 → 4.0.84

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.
@@ -9,6 +9,7 @@ const path = require("path");
9
9
  const fs = require("fs");
10
10
  const HttpClient = require("../HttpClient");
11
11
  const Fraction = require("fraction.js");
12
+ const {ValidateObject, ValidatePresence} = require("../Validation");
12
13
 
13
14
  const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
14
15
  const tok = await client.authClient.AuthorizationToken({libraryId, objectId,
@@ -22,6 +23,41 @@ const Sleep = (ms) => {
22
23
  return new Promise(resolve => setTimeout(resolve, ms));
23
24
  };
24
25
 
26
+ const CueInfo = async ({eventId, status}) => {
27
+ let cues;
28
+ try {
29
+ const lroStatusResponse = await this.utils.ResponseToJson(
30
+ await HttpClient.Fetch(status.lro_status_url)
31
+ );
32
+ console.log("lroStatusResponse", lroStatusResponse)
33
+ cues = lroStatusResponse.custom.cues;
34
+ } catch (error) {
35
+ console.log("LRO status failed", error);
36
+ return {error: "failed to retrieve status", eventId};
37
+ }
38
+
39
+ let eventStart, eventEnd;
40
+ for (const value of Object.values(cues)) {
41
+ for (const event of Object.values(value.descriptors)) {
42
+ if (event.id == eventId) {
43
+ switch (event.type_id) {
44
+ case 32:
45
+ case 16:
46
+ eventStart = value.insertion_time;
47
+ break;
48
+ case 33:
49
+ case 17:
50
+ eventEnd = value.insertion_time;
51
+ break;
52
+
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ return {eventStart, eventEnd, eventId};
59
+ }
60
+
25
61
  /**
26
62
  * Set the offering for the live stream
27
63
  *
@@ -322,17 +358,17 @@ const StreamGenerateOffering = async({
322
358
  * @return {Promise<Object>} - The status response for the object, as well as logs, warnings and errors from the master initialization
323
359
  */
324
360
  exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
325
- let conf = await this.LoadConf({name});
361
+ let objectId = name;
326
362
  let status = {name: name};
327
363
 
328
364
  try {
329
- let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
365
+ let libraryId = await this.ContentObjectLibraryId({objectId});
330
366
  status.library_id = libraryId;
331
- status.object_id = conf.objectId;
367
+ status.object_id = objectId;
332
368
 
333
369
  let mainMeta = await this.ContentObjectMetadata({
334
- libraryId: libraryId,
335
- objectId: conf.objectId,
370
+ libraryId,
371
+ objectId,
336
372
  select: [
337
373
  "live_recording_config",
338
374
  "live_recording"
@@ -380,7 +416,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
380
416
  status.stream_id = edgeWriteToken; // By convention the stream ID is its write token
381
417
  let edgeMeta = await this.ContentObjectMetadata({
382
418
  libraryId: libraryId,
383
- objectId: conf.objectId,
419
+ objectId: objectId,
384
420
  writeToken: edgeWriteToken,
385
421
  select: [
386
422
  "live_recording"
@@ -406,8 +442,14 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
406
442
  let tlro = period.live_recording_handle;
407
443
  status.tlro = tlro;
408
444
 
409
- let sinceLastFinalize = Math.floor(new Date().getTime() / 1000) -
410
- period.video_finalized_parts_info.last_finalization_time /1000000;
445
+ let videoLastFinalizationTimeEpochSec = -1;
446
+ let videoFinalizedParts = 0;
447
+ let sinceLastFinalize = -1;
448
+ if (period.finalized_parts_info && period.finalized_parts_info.video && period.finalized_parts_info.video.last_finalization_time) {
449
+ videoLastFinalizationTimeEpochSec = period.finalized_parts_info.video.last_finalization_time / 1000000;
450
+ videoFinalizedParts = period.finalized_parts_info.video.n_parts;
451
+ sinceLastFinalize = Math.floor(new Date().getTime() / 1000) - videoLastFinalizationTimeEpochSec;
452
+ }
411
453
 
412
454
  let recording_period = {
413
455
  activation_time_epoch_sec: period.recording_start_time_epoch_sec,
@@ -415,15 +457,15 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
415
457
  start_time_text: new Date(period.start_time_epoch_sec * 1000).toLocaleString(),
416
458
  end_time_epoch_sec: period.end_time_epoch_sec,
417
459
  end_time_text: period.end_time_epoch_sec === 0 ? null : new Date(period.end_time_epoch_sec * 1000).toLocaleString(),
418
- video_parts: period.video_finalized_parts_info.n_parts,
419
- video_last_part_finalized_epoch_sec: period.video_finalized_parts_info.last_finalization_time / 1000000,
460
+ video_parts: videoFinalizedParts,
461
+ video_last_part_finalized_epoch_sec: videoLastFinalizationTimeEpochSec,
420
462
  video_since_last_finalize_sec : sinceLastFinalize
421
463
  };
422
464
  status.recording_period = recording_period;
423
465
 
424
466
  status.lro_status_url = await this.FabricUrl({
425
467
  libraryId: libraryId,
426
- objectId: conf.objectId,
468
+ objectId: objectId,
427
469
  writeToken: edgeWriteToken,
428
470
  call: "live/status/" + tlro
429
471
  });
@@ -453,6 +495,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
453
495
  await HttpClient.Fetch(status.lro_status_url)
454
496
  );
455
497
  state = lroStatus.state;
498
+ status.warnings = lroStatus.custom && lroStatus.custom.warnings;
499
+ status.quality = lroStatus.custom && lroStatus.custom.quality;
456
500
  } catch(error) {
457
501
  console.log("LRO Status (failed): ", error.response.statusCode);
458
502
  status.state = "stopped";
@@ -461,7 +505,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
461
505
  }
462
506
 
463
507
  // Convert LRO 'state' to desired 'state'
464
- if(state === "running" && period.video_finalized_parts_info.last_finalization_time === 0) {
508
+ if(state === "running" && videoLastFinalizationTimeEpochSec <= 0) {
465
509
  state = "starting";
466
510
  } else if(state === "running" && sinceLastFinalize > 32.9) {
467
511
  state = "stalled";
@@ -472,8 +516,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
472
516
 
473
517
  if((state === "running" || state === "stalled" || state === "starting") && stopLro) {
474
518
  lroStopUrl = await this.FabricUrl({
475
- libraryId: libraryId,
476
- objectId: conf.objectId,
519
+ libraryId,
520
+ objectId,
477
521
  writeToken: edgeWriteToken,
478
522
  call: "live/stop/" + tlro
479
523
  });
@@ -494,7 +538,6 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
494
538
 
495
539
  if(state === "running") {
496
540
  let playout_urls = {};
497
- let objectId = conf.objectId;
498
541
  let playout_options = await this.PlayoutOptions({
499
542
  objectId,
500
543
  linkPath: "public/asset_metadata/sources/default"
@@ -555,7 +598,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
555
598
  if(networkInfo.name.includes("demo")) {
556
599
  embed_net = "demo";
557
600
  }
558
- let embed_url = `https://embed.v3.contentfabric.io/?net=${embed_net}&p&ct=h&oid=${conf.objectId}&mt=lv&ath=${token}`;
601
+ let embed_url = `https://embed.v3.contentfabric.io/?net=${embed_net}&p&ct=h&oid=${objectId}&mt=lv&ath=${token}`;
559
602
  playout_urls.embed_url = embed_url;
560
603
 
561
604
  status.playout_urls = playout_urls;
@@ -580,7 +623,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
580
623
  */
581
624
  exports.StreamCreate = async function({name, start=false}) {
582
625
  let status = await this.StreamStatus({name});
583
- if(status.state !== "inactive" && status.state !== "terminated" && status.state !== "stopped") {
626
+ if(status.state != "uninitialized" && status.state !== "inactive" && status.state !== "terminated" && status.state !== "stopped") {
584
627
  return {
585
628
  state: status.state,
586
629
  error: "stream still active - must terminate first"
@@ -690,7 +733,7 @@ exports.StreamCreate = async function({name, start=false}) {
690
733
  */
691
734
  exports.StreamStartOrStopOrReset = async function({name, op}) {
692
735
  try {
693
- let status = await this.StreamStatus({name});
736
+ let status = await this.StreamStatus({name})
694
737
  if(status.state != "stopped") {
695
738
  if(op === "start") {
696
739
  status.error = "Unable to start stream - state: " + status.state;
@@ -778,9 +821,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
778
821
  exports.StreamStopSession = async function({name}) {
779
822
  try {
780
823
  this.Log(`Terminating stream session for: ${name}`);
781
- let conf = await this.LoadConf({name});
782
-
783
- let {objectId} = conf;
824
+ let objectId = name;
784
825
  let libraryId = await this.ContentObjectLibraryId({objectId});
785
826
 
786
827
  let mainMeta = await this.ContentObjectMetadata({
@@ -827,7 +868,7 @@ exports.StreamStopSession = async function({name}) {
827
868
  writeToken: metaEdgeWriteToken
828
869
  });
829
870
  } catch(error) {
830
- this.Log(`Unable to retrieve metadata for edge write token ${edgeWriteToken}`);
871
+ this.Log("Unable to retrieve metadata for edge write token");
831
872
  }
832
873
 
833
874
  const {writeToken} = await this.EditContentObject({
@@ -849,8 +890,7 @@ exports.StreamStopSession = async function({name}) {
849
890
  fabric_config: {
850
891
  edge_write_token: ""
851
892
  }
852
- },
853
- recording_stop_time: stopTime
893
+ }
854
894
  };
855
895
 
856
896
  await this.MergeMetadata({
@@ -891,21 +931,27 @@ exports.StreamStopSession = async function({name}) {
891
931
  * @return {Promise<Object>} - The name, object ID, and state of the stream
892
932
  */
893
933
  exports.StreamInitialize = async function({name, drm=false, format}) {
894
- const contentTypes = await this.ContentTypes();
895
-
896
934
  let typeAbrMaster;
897
935
  let typeLiveStream;
898
936
 
899
- for(let i = 0; i < Object.keys(contentTypes).length; i++) {
900
- const key = Object.keys(contentTypes)[i];
937
+ // Fetch Title and Live Stream content types from tenant meta
938
+ const tenantContractId = await this.userProfileClient.TenantContractId();
939
+ const {live_stream, title} = await this.ContentObjectMetadata({
940
+ libraryId: tenantContractId.replace("iten", "ilib"),
941
+ objectId: tenantContractId.replace("iten", "iq__"),
942
+ metadataSubtree: "public/content_types",
943
+ select: [
944
+ "live_stream",
945
+ "title"
946
+ ]
947
+ });
901
948
 
902
- if(contentTypes[key].name.includes("ABR Master") || contentTypes[key].name.includes("Title")) {
903
- typeAbrMaster = contentTypes[key].hash;
904
- }
949
+ if(live_stream) {
950
+ typeLiveStream = live_stream;
951
+ }
905
952
 
906
- if(contentTypes[key].name.includes("Live Stream")) {
907
- typeLiveStream = contentTypes[key].hash;
908
- }
953
+ if(title) {
954
+ typeAbrMaster = title;
909
955
  }
910
956
 
911
957
  if(typeAbrMaster === undefined || typeLiveStream === undefined) {
@@ -919,7 +965,7 @@ exports.StreamInitialize = async function({name, drm=false, format}) {
919
965
  };
920
966
 
921
967
  /**
922
- * Set the Live Stream offering
968
+ * Create a dummy VoD offering and initialize DRM keys.
923
969
  *
924
970
  * @methodGroup Live Stream
925
971
  * @namedParams
@@ -935,7 +981,7 @@ exports.StreamInitialize = async function({name, drm=false, format}) {
935
981
  */
936
982
  exports.StreamSetOfferingAndDRM = async function({name, typeAbrMaster, typeLiveStream, drm=false, format}) {
937
983
  let status = await this.StreamStatus({name});
938
- if(status.state != "inactive" && status.state != "stopped") {
984
+ if(status.state != "uninitialized" && status.state != "inactive" && status.state != "stopped") {
939
985
  return {
940
986
  state: status.state,
941
987
  error: "stream still active - must terminate first"
@@ -961,9 +1007,10 @@ exports.StreamSetOfferingAndDRM = async function({name, typeAbrMaster, typeLiveS
961
1007
  const vFrameRate = "30000/1001";
962
1008
  const vTimeBase = "1/30000"; // "1/16000";
963
1009
 
964
- const abrProfile = require("../abr_profiles/abr_profile_live_drm.js");
1010
+ const abrProfileDefault = require("../abr_profiles/abr_profile_live_drm.js");
965
1011
 
966
- let playoutFormats = abrProfile.playout_formats;
1012
+ let playoutFormats;
1013
+ let abrProfile = JSON.parse(JSON.stringify(abrProfileDefault));
967
1014
  if(format) {
968
1015
  drm = true; // Override DRM parameter
969
1016
  playoutFormats = {};
@@ -991,6 +1038,8 @@ exports.StreamSetOfferingAndDRM = async function({name, typeAbrMaster, typeLiveS
991
1038
  }
992
1039
  }
993
1040
  };
1041
+ } else {
1042
+ playoutFormats = Object.assign({}, abrProfile.playout_formats);
994
1043
  }
995
1044
 
996
1045
  abrProfile.playout_formats = playoutFormats;
@@ -1114,13 +1163,12 @@ exports.StreamInsertion = async function({name, insertionTime, sinceStart=false,
1114
1163
  }
1115
1164
  }
1116
1165
 
1117
- let conf = await this.LoadConf({name});
1118
- let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
1119
- let objectId = conf.objectId;
1166
+ let objectId = name;
1167
+ let libraryId = await this.ContentObjectLibraryId({objectId});
1120
1168
 
1121
1169
  let mainMeta = await this.ContentObjectMetadata({
1122
- libraryId: libraryId,
1123
- objectId: conf.objectId
1170
+ libraryId,
1171
+ objectId
1124
1172
  });
1125
1173
 
1126
1174
  let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
@@ -1134,8 +1182,8 @@ exports.StreamInsertion = async function({name, insertionTime, sinceStart=false,
1134
1182
  let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
1135
1183
 
1136
1184
  let edgeMeta = await this.ContentObjectMetadata({
1137
- libraryId: libraryId,
1138
- objectId: conf.objectId,
1185
+ libraryId,
1186
+ objectId,
1139
1187
  writeToken: edgeWriteToken
1140
1188
  });
1141
1189
 
@@ -1250,73 +1298,52 @@ exports.StreamInsertion = async function({name, insertionTime, sinceStart=false,
1250
1298
  };
1251
1299
 
1252
1300
  /**
1253
- * Load cached stream configuration
1301
+ * Configure the stream based on built-in logic and optional custom settings.
1254
1302
  *
1255
- * @methodGroup Live Stream
1256
- * @namedParams
1257
- * @param {string} name - Object ID or name of the live stream object
1258
- *
1259
- * @return {Promise<Object>} - The configuration of the stream
1260
- */
1261
- exports.LoadConf = async function({name}) {
1262
- if(name.startsWith("iq__")) {
1263
- return {
1264
- name: name,
1265
- objectId: name
1266
- };
1267
- }
1268
-
1269
- // If name is not a QID, load liveconf.json
1270
- let streamsBuf;
1271
- try {
1272
- streamsBuf = fs.readFileSync(
1273
- path.resolve(__dirname, "../liveconf.json")
1274
- );
1275
- } catch(error) {
1276
- console.log("Stream name must be a QID or a label in liveconf.json");
1277
- return {};
1278
- }
1279
- const streams = JSON.parse(streamsBuf);
1280
- const conf = streams[name];
1281
- if(conf === null) {
1282
- console.log("Bad name: ", name);
1283
- return {};
1284
- }
1285
-
1286
- return conf;
1287
- };
1288
-
1289
- /**
1290
- * Configure the stream
1303
+ * Custom settings format:
1304
+ * {
1305
+ * "audio" {
1306
+ * "1" : { // This is the stream index
1307
+ * "tags" : "language: english",
1308
+ * "codec" : "aac",
1309
+ * "bitrate": 204000,
1310
+ * "record": true,
1311
+ * "recording_bitrate" : 192000,
1312
+ * "recording_channels" : 2,
1313
+ * "playout": bool
1314
+ * "playout_label": "English (Stereo)"
1315
+ * },
1316
+ * "3": {
1317
+ * ...
1318
+ * }
1319
+ * }
1320
+ * }
1291
1321
  *
1292
1322
  * @methodGroup Live Stream
1293
1323
  * @namedParams
1294
1324
  * @param {string} name - Object ID or name of the live stream object
1295
1325
  * @param {Object=} customSettings - Additional options to customize configuration settings
1296
- * - audioBitrate
1297
- * - audioIndex
1298
- * - partTtl
1299
- * - channelLayout
1300
- *
1326
+ * @param {Object=} probeMetadata - Metadata for the probe. If not specified, a new probe will be configured
1301
1327
  * @return {Promise<Object>} - The status response for the stream
1302
1328
  *
1303
1329
  */
1304
- exports.StreamConfig = async function({name, customSettings={}}) {
1305
- let conf = await this.LoadConf({name});
1330
+ exports.StreamConfig = async function({name, customSettings={}, probeMetadata}) {
1331
+ let objectId = name;
1306
1332
  let status = {name};
1307
1333
 
1308
- let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
1334
+ let libraryId = await this.ContentObjectLibraryId({objectId});
1309
1335
  status.library_id = libraryId;
1310
- status.object_id = conf.objectId;
1336
+ status.object_id = objectId;
1337
+
1338
+ let probe = probeMetadata;
1311
1339
 
1312
1340
  let mainMeta = await this.ContentObjectMetadata({
1313
1341
  libraryId: libraryId,
1314
- objectId: conf.objectId
1342
+ objectId: objectId
1315
1343
  });
1316
1344
 
1317
1345
  let userConfig = mainMeta.live_recording_config;
1318
1346
  status.user_config = userConfig;
1319
- console.log("userConfig", userConfig);
1320
1347
 
1321
1348
  // Get node URI from user config
1322
1349
  const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").replace("srt://", "").split(":")[0];
@@ -1330,80 +1357,85 @@ exports.StreamConfig = async function({name, customSettings={}}) {
1330
1357
  }
1331
1358
  const node = nodes[0];
1332
1359
  status.node = node;
1333
-
1334
1360
  let endpoint = node.endpoints[0];
1335
- this.SetNodes({fabricURIs: [endpoint]});
1336
-
1337
- // Probe the stream
1338
- let probe = {};
1339
- const controller = new AbortController();
1340
- const timeoutId = setTimeout(() => {
1341
- controller.abort();
1342
- }, 60 * 1000); // milliseconds
1343
- try {
1344
1361
 
1345
- let probeUrl = await this.Rep({
1346
- libraryId,
1347
- objectId: conf.objectId,
1348
- rep: "probe"
1349
- });
1362
+ if(!probe) {
1363
+ this.SetNodes({fabricURIs: [endpoint]});
1350
1364
 
1351
- probe = await this.utils.ResponseToJson(
1352
- await HttpClient.Fetch(probeUrl, {
1353
- body: JSON.stringify({
1354
- "filename": streamUrl.href,
1355
- "listen": true
1356
- }),
1357
- method: "POST",
1358
- signal: controller.signal
1359
- })
1360
- );
1365
+ // Probe the stream
1366
+ probe = {};
1367
+ const controller = new AbortController();
1368
+ const timeoutId = setTimeout(() => {
1369
+ controller.abort();
1370
+ }, 60 * 1000); // milliseconds
1371
+ try {
1361
1372
 
1362
- if(probe) { clearTimeout(timeoutId); }
1373
+ let probeUrl = await this.Rep({
1374
+ libraryId,
1375
+ objectId,
1376
+ rep: "probe"
1377
+ });
1363
1378
 
1364
- if(probe.errors) {
1365
- throw probe.errors[0];
1366
- }
1367
- } catch(error) {
1368
- if(error.code === "ETIMEDOUT") {
1369
- throw "Stream probe time out - make sure the stream source is available";
1370
- } else {
1371
- throw error;
1379
+ probe = await this.utils.ResponseToJson(
1380
+ await HttpClient.Fetch(probeUrl, {
1381
+ body: JSON.stringify({
1382
+ "filename": streamUrl.href,
1383
+ "listen": true
1384
+ }),
1385
+ method: "POST",
1386
+ signal: controller.signal
1387
+ })
1388
+ );
1389
+
1390
+ if(probe) { clearTimeout(timeoutId); }
1391
+
1392
+ if(probe.errors) {
1393
+ throw probe.errors[0];
1394
+ }
1395
+ } catch(error) {
1396
+ if(error.code === "ETIMEDOUT") {
1397
+ throw "Stream probe time out - make sure the stream source is available";
1398
+ } else {
1399
+ throw error;
1400
+ }
1372
1401
  }
1373
- }
1374
1402
 
1375
- probe.format.filename = streamUrl.href;
1403
+ probe.format.filename = streamUrl.href;
1404
+ }
1376
1405
 
1377
1406
  // Create live recording config
1378
1407
  let lc = new LiveConf(probe, node.id, endpoint, false, false, true);
1379
1408
 
1380
- const liveRecordingConfigStr = lc.generateLiveConf({
1381
- audioBitrate: customSettings.audioBitrate,
1382
- audioIndex: customSettings.audioIndex,
1383
- partTtl: customSettings.partTtl,
1384
- channelLayout: customSettings.channelLayout
1409
+ const liveRecordingConfig = lc.generateLiveConf({
1410
+ customSettings
1385
1411
  });
1386
- let liveRecordingConfig = JSON.parse(liveRecordingConfigStr);
1387
- console.log("CONFIG", JSON.stringify(liveRecordingConfig.live_recording));
1388
1412
 
1389
1413
  // Store live recording config into the stream object
1390
1414
  let e = await this.EditContentObject({
1391
1415
  libraryId,
1392
- objectId: conf.objectId
1416
+ objectId: objectId
1393
1417
  });
1394
1418
  let writeToken = e.write_token;
1395
1419
 
1396
1420
  await this.ReplaceMetadata({
1397
1421
  libraryId,
1398
- objectId: conf.objectId,
1422
+ objectId,
1399
1423
  writeToken,
1400
1424
  metadataSubtree: "live_recording",
1401
1425
  metadata: liveRecordingConfig.live_recording
1402
1426
  });
1403
1427
 
1428
+ await this.ReplaceMetadata({
1429
+ libraryId,
1430
+ objectId,
1431
+ writeToken,
1432
+ metadataSubtree: "live_recording_config/probe_info",
1433
+ metadata: probe
1434
+ });
1435
+
1404
1436
  status.fin = await this.FinalizeContentObject({
1405
1437
  libraryId,
1406
- objectId: conf.objectId,
1438
+ objectId,
1407
1439
  writeToken,
1408
1440
  commitMessage: "Apply live stream configuration"
1409
1441
  });
@@ -1416,7 +1448,7 @@ exports.StreamConfig = async function({name, customSettings={}}) {
1416
1448
  *
1417
1449
  * @methodGroup Live Stream
1418
1450
  * @namedParams
1419
- * @param {string=} - ID of the live stream site object
1451
+ * @param {string=} siteId - ID of the live stream site object
1420
1452
  *
1421
1453
  * @return {Promise<Object>} - The list of stream URLs
1422
1454
  */
@@ -1451,7 +1483,8 @@ exports.StreamListUrls = async function({siteId}={}) {
1451
1483
  objectId: siteId,
1452
1484
  metadataSubtree: "public/asset_metadata/live_streams",
1453
1485
  resolveLinks: true,
1454
- resolveIgnoreErrors: true
1486
+ resolveIgnoreErrors: true,
1487
+ resolveIncludeSource: true
1455
1488
  });
1456
1489
 
1457
1490
  const activeUrlMap = {};
@@ -1462,18 +1495,8 @@ exports.StreamListUrls = async function({siteId}={}) {
1462
1495
  const stream = streamMetadata[slug];
1463
1496
  let versionHash;
1464
1497
 
1465
- if(
1466
- stream &&
1467
- stream.sources &&
1468
- stream.sources.default &&
1469
- stream.sources.default["."] &&
1470
- stream.sources.default["."].container ||
1471
- ((stream["/"] || "").match(/^\/?qfab\/([\w]+)\/?.+/) || [])[1]
1472
- ) {
1473
- versionHash = (
1474
- stream.sources.default["."].container ||
1475
- ((stream["/"] || "").match(/^\/?qfab\/([\w]+)\/?.+/) || [])[1]
1476
- );
1498
+ if(stream && stream["."] && stream["."].source) {
1499
+ versionHash = stream["."].source;
1477
1500
  }
1478
1501
 
1479
1502
  if(versionHash) {
@@ -1532,3 +1555,382 @@ exports.StreamListUrls = async function({siteId}={}) {
1532
1555
  console.error(error);
1533
1556
  }
1534
1557
  };
1558
+
1559
+ /**
1560
+ * Copy a portion of a live stream recording into a standard VoD object using the zero-copy content fabric API
1561
+ *
1562
+ * Limitations:
1563
+ * - currently requires the target object to be pre-created and have content encryption keys (CAPS)
1564
+ * - for audio and video to be sync'd, the live stream needs to have the beginning of the desired recording period
1565
+ * - for an event stream, make sure the TTL is long enough to allow running the live-to-vod command before the beginning of the recording expires
1566
+ * - for 24/7 streams, make sure to reset the stream before the desired recording (as to create a new recording period) and have the TTL long enough
1567
+ * to allow running the live-to-vod command before the beginning of the recording expires.
1568
+ * - startTime and endTime are not currently implemented by this method
1569
+ *
1570
+ *
1571
+ * @methodGroup Live Stream
1572
+ * @namedParams
1573
+ * @param {string} name - Object ID or name of the live stream
1574
+ * @param {string} targetObjectId - Object ID of the target VOD object
1575
+ * @param {string=} eventId -
1576
+ * @param {boolean=} finalize - If enabled, target object will be finalized after copy to vod operations
1577
+ * @param {number=} recordingPeriod - Determines which recording period to copy, which are 0-based. -1 copies the current (or last) period
1578
+ *
1579
+ * @return {Promise<Object>} - The status response for the stream
1580
+ */
1581
+
1582
+ /*
1583
+ Example fabric API flow:
1584
+
1585
+ https://host-76-74-34-194.contentfabric.io/qlibs/ilib24CtWSJeVt9DiAzym8jB6THE9e7H/q/$QWT/call/media/live_to_vod/init -d @r1 -H "Authorization: Bearer $TOK"
1586
+
1587
+ {
1588
+ "live_qhash": "hq__5Zk1jSN8vNLUAXjQwMJV8F8J8ESXNvmVKkhaXySmGc1BXnJPG2FvvaXee4CXqvFHuGuU3fqLJc",
1589
+ "start_time": "",
1590
+ "end_time": "",
1591
+ "recording_period": -1,
1592
+ "streams": ["video", "audio"],
1593
+ "variant_key": "default"
1594
+ }
1595
+
1596
+ https://host-76-74-34-194.contentfabric.io/qlibs/ilib24CtWSJeVt9DiAzym8jB6THE9e7H/q/$QWT/call/media/abr_mezzanine/init -H "Authorization: Bearer $TOK" -d @r2
1597
+
1598
+ {
1599
+
1600
+ "abr_profile": { ... },
1601
+ "offering_key": "default",
1602
+ "prod_master_hash": "tqw__HSQHBt7vYxWfCMPH5yXwKTfhdPcQ4Lcs9WUMUbTtnMbTZPTLo4BfJWPMGpoy1Dpv1wWQVtUtAtAr429TnVs",
1603
+ "variant_key": "default",
1604
+ "keep_other_streams": false
1605
+ }
1606
+
1607
+ https://host-76-74-34-194.contentfabric.io/qlibs/ilib24CtWSJeVt9DiAzym8jB6THE9e7H/q/$QWT/call/media/live_to_vod/copy -d '{"variant_key":"","offering_key":""}' -H "Authorization: Bearer $TOK"
1608
+
1609
+
1610
+ https://host-76-74-34-194.contentfabric.io/qlibs/ilib24CtWSJeVt9DiAzym8jB6THE9e7H/q/$QWT/call/media/abr_mezzanine/offerings/default/finalize -d '{}' -H "Authorization: Bearer $TOK"
1611
+
1612
+ */
1613
+
1614
+ exports.StreamCopyToVod = async function({
1615
+ name,
1616
+ targetObjectId,
1617
+ eventId,
1618
+ streams=null,
1619
+ finalize=true,
1620
+ recordingPeriod=-1,
1621
+ startTime="",
1622
+ endTime=""
1623
+ }) {
1624
+ const objectId = name;
1625
+ const abrProfile = require("../abr_profiles/abr_profile_live_to_vod.js");
1626
+
1627
+ const status = await this.StreamStatus({name});
1628
+ const libraryId = status.library_id;
1629
+
1630
+ this.Log(`Copying stream ${name} to target ${targetObjectId}`);
1631
+
1632
+ ValidateObject(targetObjectId);
1633
+
1634
+ const targetLibraryId = await this.ContentObjectLibraryId({objectId: targetObjectId});
1635
+
1636
+ // Validation - ensure target object has content encryption keys
1637
+ const kmsAddress = await this.authClient.KMSAddress({objectId: targetObjectId});
1638
+ const kmsCapId = `eluv.caps.ikms${this.utils.AddressToHash(kmsAddress)}`;
1639
+ const kmsCap = await this.ContentObjectMetadata({
1640
+ libraryId: targetLibraryId,
1641
+ objectId: targetObjectId,
1642
+ metadataSubtree: kmsCapId
1643
+ });
1644
+
1645
+ if(!kmsCap) {
1646
+ throw Error(`No content encryption key set for object ${targetObjectId}`);
1647
+ }
1648
+
1649
+ try {
1650
+ status.live_object_id = objectId;
1651
+
1652
+ const liveHash = await this.LatestVersionHash({objectId, libraryId});
1653
+ status.live_hash = liveHash;
1654
+
1655
+ if(eventId) {
1656
+ // Retrieve start and end times for the event
1657
+ let event = await this.CueInfo({eventId, status});
1658
+ if(event.eventStart && event.eventEnd) {
1659
+ startTime = event.eventStart;
1660
+ endTime = event.eventEnd;
1661
+ }
1662
+ }
1663
+
1664
+ const {writeToken} = await this.EditContentObject({
1665
+ objectId: targetObjectId,
1666
+ libraryId: targetLibraryId
1667
+ });
1668
+
1669
+ status.target_object_id = targetObjectId;
1670
+ status.target_library_id = targetLibraryId;
1671
+ status.target_write_token = writeToken;
1672
+
1673
+ this.Log("Process live source (takes around 20 sec per hour of content)");
1674
+
1675
+ await this.CallBitcodeMethod({
1676
+ libraryId: targetLibraryId,
1677
+ objectId: targetObjectId,
1678
+ writeToken,
1679
+ method: "/media/live_to_vod/init",
1680
+ body: {
1681
+ "live_qhash": liveHash,
1682
+ "start_time": startTime, // eg. "2023-10-03T02:09:02.00Z",
1683
+ "end_time": endTime, // eg. "2023-10-03T02:15:00.00Z",
1684
+ "streams": streams,
1685
+ "recording_period": recordingPeriod,
1686
+ "variant_key": "default"
1687
+ },
1688
+ constant: false,
1689
+ format: "text"
1690
+ });
1691
+
1692
+ const abrMezInitBody = {
1693
+ abr_profile: abrProfile,
1694
+ "offering_key": "default",
1695
+ "prod_master_hash": writeToken,
1696
+ "variant_key": "default",
1697
+ "keep_other_streams": false
1698
+ };
1699
+
1700
+ await this.CallBitcodeMethod({
1701
+ libraryId: targetLibraryId,
1702
+ objectId: targetObjectId,
1703
+ writeToken,
1704
+ method: "/media/abr_mezzanine/init",
1705
+ body: abrMezInitBody,
1706
+ constant: false,
1707
+ format: "text"
1708
+ });
1709
+
1710
+ try {
1711
+ await this.CallBitcodeMethod({
1712
+ libraryId: targetLibraryId,
1713
+ objectId: targetObjectId,
1714
+ writeToken,
1715
+ method: "/media/live_to_vod/copy",
1716
+ body: {},
1717
+ constant: false,
1718
+ format: "text"
1719
+ });
1720
+ } catch(error) {
1721
+ console.error("Unable to call /media/live_to_vod/copy", error);
1722
+ throw error;
1723
+ }
1724
+
1725
+ await this.CallBitcodeMethod({
1726
+ libraryId: targetLibraryId,
1727
+ objectId: targetObjectId,
1728
+ writeToken,
1729
+ method: "/media/abr_mezzanine/offerings/default/finalize",
1730
+ body: abrMezInitBody,
1731
+ constant: false,
1732
+ format: "text"
1733
+ });
1734
+
1735
+ if(finalize) {
1736
+ const finalizeResponse = await this.FinalizeContentObject({
1737
+ libraryId: targetLibraryId,
1738
+ objectId: targetObjectId,
1739
+ writeToken,
1740
+ commitMessage: "Live Stream to VoD"
1741
+ });
1742
+
1743
+ status.target_hash = finalizeResponse.hash;
1744
+ }
1745
+
1746
+ // Clean up unnecessary status items
1747
+ delete status.playout_urls;
1748
+ delete status.lro_status_url;
1749
+ delete status.recording_period;
1750
+ delete status.recording_period_sequence;
1751
+ delete status.edge_meta_size;
1752
+ delete status.insertions;
1753
+
1754
+ return status;
1755
+ } catch(error) {
1756
+ this.Log(error, true);
1757
+ throw error;
1758
+ }
1759
+ };
1760
+
1761
+ /**
1762
+ * Remove a watermark for a live stream
1763
+ *
1764
+ * @methodGroup Live Stream
1765
+ * @namedParams
1766
+ * @param {string} objectId - Object ID of the live stream
1767
+ * @param {Array<string>} types - Specify which type of watermark to remove. Possible values:
1768
+ * - "image"
1769
+ * - "text"
1770
+ * @param {boolean=} finalize - If enabled, target object will be finalized after removing watermark
1771
+ *
1772
+ * @return {Promise<Object>} - The finalize response
1773
+ */
1774
+ exports.StreamRemoveWatermark = async function({
1775
+ objectId,
1776
+ types,
1777
+ finalize=true
1778
+ }) {
1779
+ ValidateObject(objectId);
1780
+
1781
+ const libraryId = await this.ContentObjectLibraryId({objectId});
1782
+ const {writeToken} = await this.EditContentObject({
1783
+ objectId,
1784
+ libraryId
1785
+ });
1786
+
1787
+ this.Log(`Removing watermark types: ${types.join(", ")} ${libraryId} ${objectId}`);
1788
+
1789
+ const edgeWriteToken = await this.ContentObjectMetadata({
1790
+ objectId,
1791
+ libraryId,
1792
+ metadataSubtree: "/live_recording/fabric_config/edge_write_token"
1793
+ });
1794
+
1795
+ const recordingParamsPath = "live_recording/recording_config/recording_params";
1796
+
1797
+ const recordingMetadata = await this.ContentObjectMetadata({
1798
+ libraryId,
1799
+ objectId,
1800
+ writeToken,
1801
+ metadataSubtree: recordingParamsPath,
1802
+ resolveLinks: false
1803
+ });
1804
+
1805
+ if(!recordingMetadata) {
1806
+ throw Error("Stream object must be configured");
1807
+ }
1808
+
1809
+ types.forEach(type => {
1810
+ if(type === "text") {
1811
+ delete recordingMetadata.simple_watermark;
1812
+ } else if(type === "image") {
1813
+ delete recordingMetadata.image_watermark;
1814
+ }
1815
+ });
1816
+
1817
+ await this.ReplaceMetadata({
1818
+ libraryId,
1819
+ objectId,
1820
+ writeToken,
1821
+ metadataSubtree: recordingParamsPath,
1822
+ metadata: recordingMetadata
1823
+ });
1824
+
1825
+ if(edgeWriteToken) {
1826
+ await this.ReplaceMetadata({
1827
+ libraryId,
1828
+ objectId,
1829
+ writeToken: edgeWriteToken,
1830
+ metadataSubtree: recordingParamsPath,
1831
+ metadata: recordingMetadata
1832
+ });
1833
+ }
1834
+
1835
+ if(finalize) {
1836
+ const finalizeResponse = await this.FinalizeContentObject({
1837
+ libraryId,
1838
+ objectId,
1839
+ writeToken,
1840
+ commitMessage: "Watermark removed"
1841
+ });
1842
+
1843
+ return finalizeResponse;
1844
+ }
1845
+ };
1846
+
1847
+ /**
1848
+ * Create a watermark for a live stream
1849
+ *
1850
+ * @methodGroup Live Stream
1851
+ * @namedParams
1852
+ * @param {string} objectId - Object ID of the live stream
1853
+ * @param {Object} simpleWatermark - Text watermark
1854
+ * @param {Object} imageWatermark - Image watermark
1855
+ * @param {boolean=} finalize - If enabled, target object will be finalized after adding watermark
1856
+ *
1857
+ * @return {Promise<Object>} - The finalize response
1858
+ */
1859
+ exports.StreamAddWatermark = async function({
1860
+ objectId,
1861
+ simpleWatermark,
1862
+ imageWatermark,
1863
+ finalize=true
1864
+ }) {
1865
+ ValidateObject(objectId);
1866
+
1867
+ const libraryId = await this.ContentObjectLibraryId({objectId});
1868
+ const {writeToken} = await this.EditContentObject({
1869
+ objectId,
1870
+ libraryId
1871
+ });
1872
+
1873
+ const edgeWriteToken = await this.ContentObjectMetadata({
1874
+ objectId,
1875
+ libraryId,
1876
+ metadataSubtree: "/live_recording/fabric_config/edge_write_token"
1877
+ });
1878
+
1879
+ this.Log(`Adding watermarking type: ${imageWatermark ? "image" : "text"} ${libraryId} ${objectId}`);
1880
+
1881
+ const recordingParamsPath = "live_recording/recording_config/recording_params";
1882
+
1883
+ const recordingMetadata = await this.ContentObjectMetadata({
1884
+ libraryId,
1885
+ objectId,
1886
+ writeToken,
1887
+ metadataSubtree: recordingParamsPath,
1888
+ resolveLinks: false
1889
+ });
1890
+
1891
+ if(!recordingMetadata) {
1892
+ throw Error("Stream object must be configured");
1893
+ }
1894
+
1895
+ if(simpleWatermark) {
1896
+ recordingMetadata.simple_watermark = simpleWatermark;
1897
+ } else if(imageWatermark) {
1898
+ recordingMetadata.image_watermark = imageWatermark;
1899
+ }
1900
+
1901
+ await this.ReplaceMetadata({
1902
+ libraryId,
1903
+ objectId,
1904
+ writeToken,
1905
+ metadataSubtree: recordingParamsPath,
1906
+ metadata: recordingMetadata
1907
+ });
1908
+
1909
+ if(edgeWriteToken) {
1910
+ await this.ReplaceMetadata({
1911
+ libraryId,
1912
+ objectId,
1913
+ writeToken: edgeWriteToken,
1914
+ metadataSubtree: recordingParamsPath,
1915
+ metadata: recordingMetadata
1916
+ });
1917
+ }
1918
+
1919
+ const response = {
1920
+ "imageWatermark": recordingMetadata.image_watermark,
1921
+ "textWatermark": recordingMetadata.simple_watermark
1922
+ };
1923
+
1924
+ if(finalize) {
1925
+ const finalizeResponse = await this.FinalizeContentObject({
1926
+ libraryId,
1927
+ objectId,
1928
+ writeToken,
1929
+ commitMessage: "Watermark set"
1930
+ });
1931
+
1932
+ response.hash = finalizeResponse.hash;
1933
+ }
1934
+
1935
+ return response;
1936
+ };