@eluvio/elv-client-js 4.0.76 → 4.0.77

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.
Files changed (35) hide show
  1. package/dist/ElvClient-min.js +18 -10
  2. package/dist/ElvClient-node-min.js +18 -10
  3. package/dist/ElvFrameClient-min.js +9 -9
  4. package/dist/ElvPermissionsClient-min.js +10 -10
  5. package/dist/ElvWalletClient-min.js +18 -10
  6. package/dist/ElvWalletClient-node-min.js +18 -10
  7. package/dist/src/AuthorizationClient.js +18 -12
  8. package/dist/src/Crypto.js +2 -2
  9. package/dist/src/ElvClient.js +111 -76
  10. package/dist/src/EthClient.js +2 -2
  11. package/dist/src/FrameClient.js +4 -4
  12. package/dist/src/PermissionsClient.js +2 -2
  13. package/dist/src/Utils.js +6 -5
  14. package/dist/src/abr_profiles/abr_profile_live_drm.js +1621 -0
  15. package/dist/src/abr_profiles/abr_profile_live_to_vod.js +1599 -0
  16. package/dist/src/client/ABRPublishing.js +2 -2
  17. package/dist/src/client/AccessGroups.js +19 -20
  18. package/dist/src/client/ContentAccess.js +2 -2
  19. package/dist/src/client/ContentManagement.js +3 -3
  20. package/dist/src/client/Contracts.js +235 -203
  21. package/dist/src/client/Files.js +2 -2
  22. package/dist/src/client/LiveConf.js +1 -1
  23. package/dist/src/client/LiveStream.js +1157 -1153
  24. package/dist/src/client/NFT.js +2 -2
  25. package/dist/src/contracts/v3b/BaseAccessControlGroup.js +1704 -0
  26. package/dist/src/walletClient/ClientMethods.js +423 -280
  27. package/dist/src/walletClient/Profile.js +2 -2
  28. package/dist/src/walletClient/Utils.js +7 -3
  29. package/dist/src/walletClient/index.js +124 -44
  30. package/package.json +2 -1
  31. package/src/FrameClient.js +3 -0
  32. package/src/abr_profiles/abr_profile_live_drm.js +1907 -0
  33. package/src/abr_profiles/abr_profile_live_to_vod.js +1885 -0
  34. package/src/client/LiveConf.js +1 -1
  35. package/src/client/LiveStream.js +809 -936
@@ -6,16 +6,9 @@
6
6
 
7
7
  const {LiveConf} = require("./LiveConf");
8
8
  const path = require("path");
9
-
10
9
  const fs = require("fs");
11
-
12
10
  const HttpClient = require("../HttpClient");
13
- //
14
- // const {
15
- // ValidateLibrary,
16
- // ValidateVersion,
17
- // ValidateParameters
18
- // } = require("../Validation");
11
+ const Fraction = require("fraction.js");
19
12
 
20
13
  const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
21
14
  const tok = await client.authClient.AuthorizationToken({libraryId, objectId,
@@ -25,18 +18,298 @@ const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
25
18
  return tok;
26
19
  };
27
20
 
28
- function sleep(ms) {
21
+ const Sleep = (ms) => {
29
22
  return new Promise(resolve => setTimeout(resolve, ms));
30
- }
23
+ };
24
+
25
+ /**
26
+ * Set the offering for the live stream
27
+ *
28
+ * @methodGroup Live Stream
29
+ * @namedParams
30
+ * @param {Object} client - The client object
31
+ * @param {string} libraryId - ID of the library for the new live stream object
32
+ * @param {string} objectId - ID of the new live stream object
33
+ * @param {string=} typeAbrMaster - Content type hash
34
+ * @param {string=} typeLiveStream - Content type hash
35
+ * @param {string} streamUrl - Live source URL
36
+ * @param {object} abrProfile - ABR Profile for the offering
37
+ * @param {number} aBitRate - Audio bitrate
38
+ * @param {number} aChannels - Audio channels
39
+ * @param {number} aSampleRate - Audio sample rate
40
+ * @param {number} aStreamIndex - Audio stream index
41
+ * @param {string} aTimeBase - Audio time base as a fraction, e.g. "1/48000" (usually equal to 1/aSampleRate)
42
+ * @param {string} aChannelLayout - Channel layout, e.g. "stereo"
43
+ * @param {number} vBitRate - Video bitrate
44
+ * @param {number} vHeight - Video height
45
+ * @param {number} vStreamIndex - Video stream index
46
+ * @param {number} vWidth - Video width
47
+ * @param {string} vDisplayAspectRatio - Display aspect ratio as a fraction, e.g. "16/9"
48
+ * @param {string} vFrameRate - Frame rate as an integer, e.g. "30"
49
+ * @param {string} vTimeBase - Time base as a fraction, e.g. "1/30000"
50
+ *
51
+ * @return {Promise<string>} - Final hash of the live stream object
52
+ */
53
+ const StreamGenerateOffering = async({
54
+ client,
55
+ libraryId,
56
+ objectId,
57
+ typeAbrMaster,
58
+ typeLiveStream,
59
+ streamUrl,
60
+ abrProfile,
61
+ aBitRate,
62
+ aChannels,
63
+ aSampleRate,
64
+ aStreamIndex,
65
+ aTimeBase,
66
+ aChannelLayout,
67
+ vBitRate,
68
+ vHeight,
69
+ vStreamIndex,
70
+ vWidth,
71
+ vDisplayAspectRatio,
72
+ vFrameRate,
73
+ vTimeBase
74
+ }) => {
75
+ // compute duration_ts
76
+ const DUMMY_DURATION = 1001; // should result in integer duration_ts values for both audio and video
77
+ const aDurationTs = Fraction(aTimeBase).inverse().mul(DUMMY_DURATION).valueOf();
78
+ const vDurationTs = Fraction(vTimeBase).inverse().mul(DUMMY_DURATION).valueOf();
79
+
80
+ // construct /production_master/sources/STREAM_URL/streams
81
+
82
+ const sourceAudioStream = {
83
+ "bit_rate": aBitRate,
84
+ "channel_layout": aChannelLayout,
85
+ "channels": aChannels,
86
+ "codec_name": "aac",
87
+ "duration": DUMMY_DURATION,
88
+ "duration_ts": aDurationTs,
89
+ "frame_count": 0,
90
+ "language": "",
91
+ "max_bit_rate": aBitRate,
92
+ "sample_rate": aSampleRate,
93
+ "start_pts": 0,
94
+ "start_time": 0,
95
+ "time_base": aTimeBase,
96
+ "type": "StreamAudio"
97
+ };
98
+
99
+ const sourceVideoStream = {
100
+ "bit_rate": vBitRate,
101
+ "codec_name": "h264",
102
+ "display_aspect_ratio": vDisplayAspectRatio,
103
+ "duration": DUMMY_DURATION,
104
+ "duration_ts": vDurationTs,
105
+ "field_order": "progressive",
106
+ "frame_count": 0,
107
+ "frame_rate": vFrameRate,
108
+ "hdr": null,
109
+ "height": vHeight,
110
+ "language": "",
111
+ "max_bit_rate": vBitRate,
112
+ "sample_aspect_ratio": "1",
113
+ "start_pts": 0,
114
+ "start_time": 0,
115
+ "time_base": vTimeBase,
116
+ "type": "StreamVideo",
117
+ "width": vWidth
118
+ };
119
+
120
+ // placeholder stream to use if [aStreamIndex, vStreamIndex].sort() is not [0,1]
121
+ const DUMMY_STREAM = {
122
+ "bit_rate": 0,
123
+ "codec_name": "",
124
+ "duration": DUMMY_DURATION,
125
+ "duration_ts": 2500 * DUMMY_DURATION,
126
+ "frame_count": 1,
127
+ "language": "",
128
+ "max_bit_rate": 0,
129
+ "start_pts": 0,
130
+ "start_time": 0,
131
+ "time_base": "1/2500",
132
+ "type": "StreamData"
133
+ };
134
+
135
+ const sourceStreams = [];
136
+ const maxStreamIndex = Math.max(aStreamIndex, vStreamIndex);
137
+
138
+ for(let i = 0; i <= maxStreamIndex; i++) {
139
+ if (i === aStreamIndex) {
140
+ sourceStreams.push(sourceAudioStream);
141
+ } else if (i === vStreamIndex) {
142
+ sourceStreams.push(sourceVideoStream);
143
+ } else {
144
+ sourceStreams.push(DUMMY_STREAM);
145
+ }
146
+ }
147
+
148
+ // construct /production_master/sources
149
+ const sources = {
150
+ [streamUrl]: {
151
+ "container_format": {
152
+ "duration": DUMMY_DURATION,
153
+ "filename": streamUrl,
154
+ "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
155
+ "start_time": 0
156
+ },
157
+ "streams": sourceStreams
158
+ }
159
+ };
160
+
161
+ // construct /production_master/variants
162
+ const variants = {
163
+ "default": {
164
+ "streams": {
165
+ "audio": {
166
+ "default_for_media_type": false,
167
+ "label": "",
168
+ "language": "",
169
+ "mapping_info": "",
170
+ "sources": [
171
+ {
172
+ "files_api_path": streamUrl,
173
+ "stream_index": aStreamIndex
174
+ }
175
+ ]
176
+ },
177
+ "video": {
178
+ "default_for_media_type": false,
179
+ "label": "",
180
+ "language": "",
181
+ "mapping_info": "",
182
+ "sources": [
183
+ {
184
+ "files_api_path": streamUrl,
185
+ "stream_index": vStreamIndex
186
+ }
187
+ ]
188
+ }
189
+ }
190
+ }
191
+ };
192
+
193
+ // construct /production_master
194
+ const production_master = {sources, variants};
195
+
196
+ // get existing metadata
197
+ console.log("Retrieving current metadata...");
198
+ let metadata = await client.ContentObjectMetadata({
199
+ libraryId,
200
+ objectId
201
+ });
202
+
203
+ // add /production_master to metadata
204
+ metadata.production_master = production_master;
205
+
206
+ // write back to object
207
+ console.log("Getting write token...");
208
+ let editResponse = await client.EditContentObject({
209
+ libraryId,
210
+ objectId,
211
+ options: {
212
+ type: typeAbrMaster
213
+ }
214
+ });
215
+ let writeToken = editResponse.write_token;
216
+ console.log(`New write token: ${writeToken}`);
217
+
218
+ console.log("Writing back metadata with /production_master added...");
219
+ await client.ReplaceMetadata({
220
+ libraryId,
221
+ metadata,
222
+ objectId,
223
+ writeToken
224
+ });
225
+
226
+ console.log("Finalizing...");
227
+ let finalizeResponse = await client.FinalizeContentObject({
228
+ libraryId,
229
+ objectId,
230
+ writeToken
231
+ });
232
+ let masterVersionHash = finalizeResponse.hash;
233
+ console.log(`Finalized, new version hash: ${masterVersionHash}`);
234
+
235
+ // Generate offering
236
+ const createResponse = await client.CreateABRMezzanine({
237
+ libraryId,
238
+ objectId,
239
+ masterVersionHash,
240
+ variant: "default",
241
+ offeringKey: "default",
242
+ abrProfile
243
+ });
244
+
245
+ if (createResponse.warnings.length > 0) {
246
+ console.log("WARNINGS:");
247
+ console.log(JSON.stringify(createResponse.warnings, null, 2));
248
+ }
249
+
250
+ if (createResponse.errors.length > 0) {
251
+ console.log("ERRORS:");
252
+ console.log(JSON.stringify(createResponse.errors, null, 2));
253
+ }
254
+
255
+ let versionHash = createResponse.hash;
256
+ console.log(`New version hash: ${versionHash}`);
257
+
258
+ // get new metadata
259
+ console.log("Retrieving revised metadata with offering...");
260
+ metadata = await client.ContentObjectMetadata({
261
+ libraryId,
262
+ versionHash
263
+ });
264
+
265
+ console.log("Moving /abr_mezzanine/offerings to /offerings and removing /abr_mezzanine...");
266
+ metadata.offerings = metadata.abr_mezzanine.offerings;
267
+ delete metadata.abr_mezzanine;
268
+
269
+ // add items to media_struct needed to use options.json handler
270
+ metadata.offerings.default.media_struct.duration_rat = `${DUMMY_DURATION}`;
271
+
272
+ // write back to object
273
+ console.log("Getting write token...");
274
+ editResponse = await client.EditContentObject({
275
+ libraryId,
276
+ objectId,
277
+ options: {
278
+ type: typeLiveStream
279
+ }
280
+ });
281
+ writeToken = editResponse.write_token;
282
+ console.log(`New write token: ${writeToken}`);
283
+
284
+ console.log("Writing back metadata with /offerings...");
285
+ await client.ReplaceMetadata({
286
+ libraryId,
287
+ metadata,
288
+ objectId,
289
+ writeToken
290
+ });
291
+
292
+ console.log("Finalizing...");
293
+ finalizeResponse = await client.FinalizeContentObject({
294
+ libraryId,
295
+ objectId,
296
+ writeToken
297
+ });
298
+
299
+ const finalHash = finalizeResponse.hash;
300
+ console.log(`Finalized, new version hash: ${finalHash}`);
301
+
302
+ return finalHash;
303
+ };
31
304
 
32
305
  /**
33
306
  * Retrieve the status of the current live stream session
34
307
  *
35
308
  * @methodGroup Live Stream
36
309
  * @namedParams
37
- * @param {string} name -
38
- * @param {boolean} stopLro -
39
- * @param {boolean} showParams -
310
+ * @param {string} name - Object ID or name of the live stream object
311
+ * @param {boolean=} stopLro - If specified, will stop LRO
312
+ * @param {boolean=} showParams - If specified, will return recording_params with status
40
313
  * States:
41
314
  * unconfigured - no live_recording_config
42
315
  * uninitialized - no live_recording config generated
@@ -46,15 +319,13 @@ function sleep(ms) {
46
319
  * running - stream is running and producing output
47
320
  * stalled - LRO running but no source data (so not producing output)
48
321
  *
49
- * @return {Object} - The status response for the object, as well as logs, warnings and errors from the master initialization
322
+ * @return {Promise<Object>} - The status response for the object, as well as logs, warnings and errors from the master initialization
50
323
  */
51
324
  exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
52
325
  let conf = await this.LoadConf({name});
53
-
54
326
  let status = {name: name};
55
327
 
56
328
  try {
57
-
58
329
  let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
59
330
  status.library_id = libraryId;
60
331
  status.object_id = conf.objectId;
@@ -114,6 +385,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
114
385
  ]
115
386
  });
116
387
 
388
+ status.edge_meta_size = JSON.stringify(edgeMeta).length;
389
+
117
390
  // If a stream has never been started return state 'inactive'
118
391
  if(edgeMeta.live_recording === undefined ||
119
392
  edgeMeta.live_recording.recordings === undefined ||
@@ -168,7 +441,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
168
441
  }
169
442
 
170
443
  if(showParams) {
171
- status.recording_paramse = edgeMeta.live_recording.recording_config.recording_params;
444
+ status.recording_params = edgeMeta.live_recording.recording_config.recording_params;
172
445
  }
173
446
 
174
447
  let state = "stopped";
@@ -207,10 +480,12 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
207
480
  await this.utils.ResponseToJson(
208
481
  await HttpClient.Fetch(lroStopUrl)
209
482
  );
483
+
210
484
  console.log("LRO Stop: ", lroStatus.body);
211
485
  } catch(error) {
212
486
  console.log("LRO Stop (failed): ", error.response.statusCode);
213
487
  }
488
+
214
489
  state = "stopped";
215
490
  status.state = state;
216
491
  }
@@ -290,36 +565,18 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
290
565
  return status;
291
566
  };
292
567
 
293
- // async StatusPrep({name}) {
294
- //
295
- // let conf = await this.LoadConf({name});
296
- //
297
- // try {
298
- //
299
- // // Set static token - avoid individual auth for separate channels/streams
300
- // let token = await MakeTxLessToken({client: this.client, libraryId: conf.libraryId});
301
- // this.client.SetStaticToken({token});
302
- //
303
- // } catch(error) {
304
- // console.log("StatusPrep failed: ", error);
305
- // return null;
306
- // }
307
- //
308
- // }
309
-
310
568
  /**
311
569
  * Create a new edge write token
312
570
  *
313
571
  * @methodGroup Live Stream
314
572
  * @namedParams
315
- * @param {string} name -
316
- * @param {boolean} start -
573
+ * @param {string} name - Object ID or name of the live stream object
574
+ * @param {boolean=} start - If specified, will start the stream after creation
317
575
  *
318
- * @return {Object} - The status response for the object
576
+ * @return {Promise<Object>} - The status response for the object
319
577
  *
320
578
  */
321
- exports.StreamCreate = async function({name, start = false}) {
322
-
579
+ exports.StreamCreate = async function({name, start=false}) {
323
580
  let status = await this.StreamStatus({name});
324
581
  if(status.state !== "inactive" && status.state !== "terminated" && status.state !== "stopped") {
325
582
  return {
@@ -392,6 +649,7 @@ exports.StreamCreate = async function({name, start = false}) {
392
649
  writeToken: writeToken,
393
650
  commitMessage: "Create stream edge write token " + edgeToken
394
651
  });
652
+
395
653
  const objectHash = response.hash;
396
654
  this.Log("Finalized object: ", objectHash);
397
655
 
@@ -417,20 +675,21 @@ exports.StreamCreate = async function({name, start = false}) {
417
675
  *
418
676
  * @methodGroup Live Stream
419
677
  * @namedParams
420
- * @param {string} name -
421
- * @param {string=} op - The operation to perform. Possible values:
678
+ * @param {string} name - Object ID or name of the live stream object
679
+ * @param {string} op - The operation to perform. Possible values:
422
680
  * 'start'
423
681
  * 'reset' - Stops current LRO recording and starts a new one. Does
424
682
  * not create a new edge write token (just creates a new recording
425
683
  * period in the existing edge write token)
426
684
  * - 'stop'
427
685
  *
428
- * @return {Object} - The status response for the stream
686
+ * @return {Promise<Object>} - The status response for the stream
429
687
  *
430
688
  */
431
689
  exports.StreamStartOrStopOrReset = async function({name, op}) {
432
690
  try {
433
691
  console.log("Stream ", op, ": ", name);
692
+
434
693
  let status = await this.StreamStatus({name});
435
694
  if(status.state != "stopped") {
436
695
  if(op === "start") {
@@ -441,6 +700,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
441
700
 
442
701
  if(status.state == "running" || status.state == "starting" || status.state == "stalled") {
443
702
  console.log("STOPPING");
703
+
444
704
  try {
445
705
  await this.CallBitcodeMethod({
446
706
  libraryId: status.library_id,
@@ -458,13 +718,14 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
458
718
  let tries = 10;
459
719
  while(status.state != "stopped" && tries-- > 0) {
460
720
  console.log("Wait to terminate - ", status.state);
461
- await sleep(1000);
721
+ await Sleep(1000);
462
722
  status = await this.StreamStatus({name});
463
723
  }
464
- console.log("Status after terminate - ", status.state);
724
+
725
+ console.log("Status after stop - ", status.state);
465
726
 
466
727
  if(tries <= 0) {
467
- console.log("Failed to terminate");
728
+ console.log("Failed to stop");
468
729
  return status;
469
730
  }
470
731
  }
@@ -495,7 +756,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
495
756
  let tries = 10;
496
757
  while(status.state != "starting" && tries-- > 0) {
497
758
  console.log("Wait to start - ", status.state);
498
- await sleep(1000);
759
+ await Sleep(1000);
499
760
  status = await this.StreamStatus({name});
500
761
  }
501
762
 
@@ -507,416 +768,505 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
507
768
  }
508
769
  };
509
770
 
510
- /*
771
+ /**
511
772
  * Stop the live stream session and close the edge write token.
512
773
  * Not implemented fully
774
+ *
775
+ * @methodGroup Live Stream
776
+ * @namedParams
777
+ * @param {string} name - Object ID or name of the live stream object
778
+ *
779
+ * @return {Promise<Object>} - The finalize response for the stream object
780
+ */
781
+ exports.StreamStopSession = async function({name}) {
782
+ try {
783
+ console.log("TERMINATE: ", name);
784
+
785
+ let conf = await this.LoadConf({name});
786
+
787
+ let {objectId} = conf;
788
+ let libraryId = await this.ContentObjectLibraryId({objectId});
789
+
790
+ let mainMeta = await this.ContentObjectMetadata({
791
+ libraryId,
792
+ objectId
793
+ });
794
+
795
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
796
+ // Support both hostname and URL ingress_node_api
797
+ if(!fabURI.startsWith("http")) {
798
+ // Assume https
799
+ fabURI = "https://" + fabURI;
800
+ }
801
+
802
+ this.SetNodes({fabricURIs: [fabURI]});
803
+
804
+ let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
805
+
806
+ if(edgeWriteToken === undefined || edgeWriteToken === "") {
807
+ return {
808
+ state: "inactive",
809
+ error: "no active streams - must create a stream first"
810
+ };
811
+ }
812
+ let edgeMeta = await this.ContentObjectMetadata({
813
+ libraryId,
814
+ objectId,
815
+ writeToken: edgeWriteToken
816
+ });
817
+
818
+ // Stop the LRO if running
819
+ let status = await this.StreamStatus({name});
820
+ if(status.state != "terminated") {
821
+ console.log("STOPPING");
822
+ try {
823
+ await this.CallBitcodeMethod({
824
+ libraryId: status.library_id,
825
+ objectId: status.object_id,
826
+ writeToken: status.edge_write_token,
827
+ method: "/live/stop/" + status.tlro,
828
+ constant: false
829
+ });
830
+ } catch(error) {
831
+ // The /call/lro/stop API returns empty response
832
+ // console.log("LRO Stop (failed): ", error);
833
+ }
834
+
835
+ // Wait until LRO is terminated
836
+ let tries = 10;
837
+ while (status.state != "stopped" && tries-- > 0) {
838
+ console.log("Wait to terminate - ", status.state);
839
+ await Sleep(1000);
840
+ status = await this.StreamStatus({name});
841
+ }
842
+ console.log("Status after stop - ", status.state);
843
+
844
+ if(tries <= 0) {
845
+ console.log("Failed to stop");
846
+ return status;
847
+ }
848
+ }
849
+
850
+ // Set stop time
851
+ edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
852
+ console.log("recording_start_time: ", edgeMeta.recording_start_time);
853
+ console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
854
+
855
+ edgeMeta.live_recording.status = {
856
+ state: "terminated",
857
+ recording_stop_time: edgeMeta.recording_stop_time
858
+ };
859
+
860
+ edgeMeta.live_recording.fabric_config.edge_write_token = "";
861
+
862
+ await this.ReplaceMetadata({
863
+ libraryId,
864
+ objectId,
865
+ writeToken: edgeWriteToken,
866
+ metadata: edgeMeta
867
+ });
868
+
869
+ let fin = await this.FinalizeContentObject({
870
+ libraryId,
871
+ objectId,
872
+ writeToken: edgeWriteToken,
873
+ commitMessage: "Finalize live stream - stop time " + edgeMeta.recording_stop_time,
874
+ publish: false // Don't publish this version because it is not currently useful
875
+ });
876
+
877
+ return {
878
+ fin,
879
+ name,
880
+ edge_write_token: edgeWriteToken,
881
+ state: "terminated"
882
+ };
883
+
884
+ } catch(error) {
885
+ console.error(error);
886
+ }
887
+ };
888
+
889
+ /**
890
+ * Initialize the stream object
891
+ *
892
+ * @methodGroup Live Stream
893
+ * @namedParams
894
+ * @param {string} name - Object ID or name of the live stream object
895
+ * @param {boolean=} drm - If specified, playout will be DRM protected
896
+ * @param {string=} format - Specify the list of playout formats and DRM to support,
897
+ comma-separated (hls-clear, hls-aes128, hls-sample-aes,
898
+ hls-fairplay)
899
+ *
900
+ * @return {Promise<Object>} - The name, object ID, and state of the stream
513
901
  */
514
- // async StopSession({name}) {
515
- //
516
- // try {
517
- //
518
- // console.log("TERMINATE: ", name);
519
- //
520
- // let conf = await this.LoadConf({name});
521
- //
522
- // let objectId = conf.objectId;
523
- // let libraryId = await this.client.ContentObjectLibraryId({objectId: objectId});
524
- //
525
- // let mainMeta = await this.client.ContentObjectMetadata({
526
- // libraryId: libraryId,
527
- // objectId: objectId
528
- // });
529
- //
530
- // let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
531
- // // Support both hostname and URL ingress_node_api
532
- // if(!fabURI.startsWith("http")) {
533
- // // Assume https
534
- // fabURI = "https://" + fabURI;
535
- // }
536
- //
537
- // this.client.SetNodes({fabricURIs: [fabURI]});
538
- //
539
- // let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
540
- //
541
- // if(edgeWriteToken === undefined || edgeWriteToken === "") {
542
- // return {
543
- // state: "inactive",
544
- // error: "no active streams - must create a stream first"
545
- // };
546
- // }
547
- // let edgeMeta = await this.client.ContentObjectMetadata({
548
- // libraryId: libraryId,
549
- // objectId: objectId,
550
- // writeToken: edgeWriteToken
551
- // });
552
- //
553
- // // Stop the LRO if running
554
- // let status = await this.Status({name});
555
- // if(status.state != "terminated") {
556
- // console.log("STOPPING");
557
- // try {
558
- // await this.client.CallBitcodeMethod({
559
- // libraryId: status.library_id,
560
- // objectId: status.object_id,
561
- // writeToken: status.edge_write_token,
562
- // method: "/live/stop/" + status.tlro,
563
- // constant: false
564
- // });
565
- // } catch(error) {
566
- // // The /call/lro/stop API returns empty response
567
- // // console.log("LRO Stop (failed): ", error);
568
- // }
569
- //
570
- // // Wait until LRO is terminated
571
- // let tries = 10;
572
- // while (status.state != "terminated" && tries-- > 0) {
573
- // console.log("Wait to terminate - ", status.state);
574
- // await sleep(1000);
575
- // status = await this.Status({name});
576
- // }
577
- // console.log("Status after terminate - ", status.state);
578
- //
579
- // if(tries <= 0) {
580
- // console.log("Failed to terminate");
581
- // return status;
582
- // }
583
- // }
584
- //
585
- // // Set stop time
586
- // edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
587
- // console.log("recording_start_time: ", edgeMeta.recording_start_time);
588
- // console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
589
- //
590
- // edgeMeta.live_recording.status = {
591
- // state: "terminated",
592
- // recording_stop_time: edgeMeta.recording_stop_time
593
- // };
594
- //
595
- // edgeMeta.live_recording.fabric_config.edge_write_token = "";
596
- //
597
- // await this.client.ReplaceMetadata({
598
- // libraryId: libraryId,
599
- // objectId: objectId,
600
- // writeToken: edgeWriteToken,
601
- // metadata: edgeMeta
602
- // });
603
- //
604
- // let fin = await this.client.FinalizeContentObject({
605
- // libraryId,
606
- // objectId,
607
- // writeToken: edgeWriteToken,
608
- // commitMessage: "Finalize live stream - stop time " + edgeMeta.recording_stop_time,
609
- // publish: false // Don't publish this version because it is not currently useful
610
- // });
611
- //
612
- // return {
613
- // fin,
614
- // name: name,
615
- // edge_write_token: edgeWriteToken,
616
- // state: "terminated"
617
- // };
618
- //
619
- // } catch(error) {
620
- // console.error(error);
621
- // }
622
- // }
623
-
624
- // async Initialize({name, drm=false, format}) {
625
- //
626
- // const contentTypes = await this.client.ContentTypes();
627
- //
628
- // let typeAbrMaster;
629
- // let typeLiveStream;
630
- // for (let i = 0; i < Object.keys(contentTypes).length; i ++) {
631
- // const key = Object.keys(contentTypes)[i];
632
- // if(contentTypes[key].name.includes("ABR Master") || contentTypes[key].name.includes("Title")) {
633
- // typeAbrMaster = contentTypes[key].hash;
634
- // }
635
- // if(contentTypes[key].name.includes("Live Stream")) {
636
- // typeLiveStream = contentTypes[key].hash;
637
- // }
638
- // }
639
- //
640
- // if(typeAbrMaster === undefined || typeLiveStream === undefined) {
641
- // console.log("ERROR - unable to find content types", "ABR Master", typeAbrMaster, "Live Stream", typeLiveStream);
642
- // return {};
643
- // }
644
- // let res = await this.SetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm, format});
645
- // return res;
646
- // }
647
-
648
- // async SetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm=false, format}) {
649
- //
650
- // let status = await this.Status({name});
651
- // if(status.state != "inactive" && status.state != "terminated") {
652
- // return {
653
- // state: status.state,
654
- // error: "stream still active - must terminate first"
655
- // };
656
- // }
657
- //
658
- // let objectId = status.object_id;
659
- //
660
- // console.log("INIT: ", name, objectId);
661
- //
662
- // const {GenerateOffering} = require("./LiveObjectSetupStepOne");
663
- //
664
- // const aBitRate = 128000;
665
- // const aChannels = 2;
666
- // const aSampleRate = 48000;
667
- // const aStreamIndex = 1;
668
- // const aTimeBase = "1/48000";
669
- // const aChannelLayout = "stereo";
670
- //
671
- // const vBitRate = 14000000;
672
- // const vHeight = 720;
673
- // const vStreamIndex = 0;
674
- // const vWidth = 1280;
675
- // const vDisplayAspectRatio = "16/9";
676
- // const vFrameRate = "30000/1001";
677
- // const vTimeBase = "1/30000"; // "1/16000";
678
- //
679
- // const abrProfile = require("./abr_profile_live_drm.json");
680
- //
681
- // let playoutFormats = abrProfile.playout_formats;
682
- // if(format) {
683
- // drm = true; // Override DRM parameter
684
- // playoutFormats = {};
685
- // let formats = format.split(",");
686
- // for (let i = 0; i < formats.length; i++) {
687
- // if(formats[i] === "hls-clear") {
688
- // abrProfile.drm_optional = true;
689
- // playoutFormats["hls-clear"] = {
690
- // "drm": null,
691
- // "protocol": {
692
- // "type": "ProtoHls"
693
- // }
694
- // };
695
- // continue;
696
- // }
697
- // playoutFormats[formats[i]] = abrProfile.playout_formats[formats[i]];
698
- // }
699
- // } else if(!drm) {
700
- // abrProfile.drm_optional = true;
701
- // playoutFormats = {
702
- // "hls-clear": {
703
- // "drm": null,
704
- // "protocol": {
705
- // "type": "ProtoHls"
706
- // }
707
- // }
708
- // };
709
- // }
710
- //
711
- // abrProfile.playout_formats = playoutFormats;
712
- //
713
- // let libraryId = await this.client.ContentObjectLibraryId({objectId});
714
- //
715
- // try {
716
- //
717
- // let mainMeta = await this.client.ContentObjectMetadata({
718
- // libraryId: libraryId,
719
- // objectId: objectId
720
- // });
721
- //
722
- // let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
723
- // // Support both hostname and URL ingress_node_api
724
- // if(!fabURI.startsWith("http")) {
725
- // // Assume https
726
- // fabURI = "https://" + fabURI;
727
- // }
728
- //
729
- // this.client.SetNodes({fabricURIs: [fabURI]});
730
- //
731
- // let streamUrl = mainMeta.live_recording.recording_config.recording_params.origin_url;
732
- //
733
- // await GenerateOffering({
734
- // client: this.client,
735
- // libraryId,
736
- // objectId,
737
- // typeAbrMaster, typeLiveStream,
738
- // streamUrl,
739
- // abrProfile,
740
- // aBitRate, aChannels, aSampleRate, aStreamIndex,
741
- // aTimeBase,
742
- // aChannelLayout,
743
- // vBitRate, vHeight, vStreamIndex, vWidth,
744
- // vDisplayAspectRatio, vFrameRate, vTimeBase
745
- // });
746
- //
747
- // console.log("GenerateOffering - DONE");
748
- //
749
- // return {
750
- // name,
751
- // object_id: objectId,
752
- // state: "initialized"
753
- // };
754
- // } catch(error) {
755
- // console.error(error);
756
- // }
757
- // }
758
-
759
- // Add a content insertion entry
760
- // Parameters:
761
- // - insertionTime - seconds (float)
762
- // - sinceStart - true if time specified since stream start, false if since epoch
763
- // - duration - seconds (float, deafault 20.0)
764
- // - targetHash - playable
765
- // - remove - flag to remove the insertion at that exact 'time' (instead of adding)
766
- // async Insertion({name, insertionTime, sinceStart, duration, targetHash, remove}) {
767
- //
768
- // // Determine audio and video parameters of the insertion
769
- // const insertionInfo = await this.getOfferingInfo({versionHash: targetHash});
770
- // const audioAbrDuration = insertionInfo.audio.seg_duration_sec;
771
- // const videoAbrDuration = insertionInfo.video.seg_duration_sec;
772
- //
773
- // if(audioAbrDuration === 0 || videoAbrDuration === 0) {
774
- // throw new Error("Bad segment duration hash:", targetHash);
775
- // }
776
- //
777
- // if(duration === undefined) {
778
- // duration = insertionInfo.duration_sec; // Use full duration of the insertion
779
- // } else {
780
- // if(duration > insertionInfo.duration_sec) {
781
- // throw new Error("Bad duration - larger than insertion object duration", insertionInfo.duration_sec);
782
- // }
783
- // }
784
- //
785
- // let conf = await this.LoadConf({name});
786
- // let libraryId = await this.client.ContentObjectLibraryId({objectId: conf.objectId});
787
- // let objectId = conf.objectId;
788
- //
789
- // let mainMeta = await this.client.ContentObjectMetadata({
790
- // libraryId: libraryId,
791
- // objectId: conf.objectId
792
- // });
793
- //
794
- // let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
795
- //
796
- // // Support both hostname and URL ingress_node_api
797
- // if(!fabURI.startsWith("http")) {
798
- // // Assume https
799
- // fabURI = "https://" + fabURI;
800
- // }
801
- // this.client.SetNodes({fabricURIs: [fabURI]});
802
- // let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
803
- //
804
- // let edgeMeta = await this.client.ContentObjectMetadata({
805
- // libraryId: libraryId,
806
- // objectId: conf.objectId,
807
- // writeToken: edgeWriteToken
808
- // });
809
- //
810
- // // Find stream start time (from the most recent recording section)
811
- // let recordings = edgeMeta.live_recording.recordings;
812
- // let sequence = 1;
813
- // let streamStartTime = 0;
814
- // if(recordings != undefined && recordings.recording_sequence != undefined) {
815
- // // We have at least one recording - check if still active
816
- // sequence = recordings.recording_sequence;
817
- // let period = recordings.live_offering[sequence - 1];
818
- //
819
- // if(period.end_time_epoch_sec > 0) {
820
- // // The last period is closed - apply insertions to the next period
821
- // sequence ++;
822
- // } else {
823
- // // The period is active
824
- // streamStartTime = period.start_time_epoch_sec;
825
- // }
826
- // }
827
- //
828
- // if(streamStartTime === 0) {
829
- // // There is no active period - must use absolute time
830
- // if(sinceStart === false) {
831
- // throw new Error("Stream not running - must use 'time since start'");
832
- // }
833
- // }
834
- //
835
- // // Find the current period playout configuration
836
- // if(edgeMeta.live_recording.playout_config.interleaves === undefined) {
837
- // edgeMeta.live_recording.playout_config.interleaves = {};
838
- // }
839
- // if(edgeMeta.live_recording.playout_config.interleaves[sequence] === undefined) {
840
- // edgeMeta.live_recording.playout_config.interleaves[sequence] = [];
841
- // }
842
- //
843
- // let playoutConfig = edgeMeta.live_recording.playout_config;
844
- // let insertions = playoutConfig.interleaves[sequence];
845
- //
846
- // let res = {};
847
- //
848
- // if(!sinceStart) {
849
- // insertionTime = insertionTime - streamStartTime;
850
- // }
851
- //
852
- // // Assume insertions are sorted by insertion time
853
- // let errs = [];
854
- // let currentTime = -1;
855
- // let insertionDone = false;
856
- // let newInsertion = {
857
- // insertion_time: insertionTime,
858
- // duration: duration,
859
- // audio_abr_duration: audioAbrDuration,
860
- // video_abr_duration: videoAbrDuration,
861
- // playout: "/qfab/" + targetHash + "/rep/playout" // TO FIX - should be a link
862
- // };
863
- //
864
- // for (let i = 0; i < insertions.length; i ++) {
865
- // if(insertions[i].insertion_time <= currentTime) {
866
- // // Bad insertion - must be later than current time
867
- // append(errs, "Bad insertion - time:", insertions[i].insertion_time);
868
- // }
869
- // if(remove) {
870
- // if(insertions[i].insertion_time === insertionTime) {
871
- // insertions.splice(i, 1);
872
- // break;
873
- // }
874
- // } else {
875
- // if(insertions[i].insertion_time > insertionTime) {
876
- // if(i > 0) {
877
- // insertions = [
878
- // ...insertions.splice(0, i),
879
- // newInsertion,
880
- // ...insertions.splice(i)
881
- // ];
882
- // } else {
883
- // insertions = [
884
- // newInsertion,
885
- // ...insertions.splice(i)
886
- // ];
887
- // }
888
- // insertionDone = true;
889
- // break;
890
- // }
891
- // }
892
- // }
893
- //
894
- // if(!remove && !insertionDone) {
895
- // // Add to the end of the insertions list
896
- // console.log("Add insertion at the end");
897
- // insertions = [
898
- // ...insertions,
899
- // newInsertion
900
- // ];
901
- // }
902
- //
903
- // playoutConfig.interleaves[sequence] = insertions;
904
- //
905
- // // Store the new insertions in the write token
906
- // await this.client.ReplaceMetadata({
907
- // libraryId: libraryId,
908
- // objectId: objectId,
909
- // writeToken: edgeWriteToken,
910
- // metadataSubtree: "/live_recording/playout_config",
911
- // metadata: edgeMeta.live_recording.playout_config
912
- // });
913
- //
914
- // res.errors = errs;
915
- // res.insertions = insertions;
916
- // return res;
917
- // }
902
+ exports.StreamInitialize = async function({name, drm=false, format}) {
903
+ const contentTypes = await this.ContentTypes();
904
+
905
+ let typeAbrMaster;
906
+ let typeLiveStream;
907
+
908
+ for(let i = 0; i < Object.keys(contentTypes).length; i++) {
909
+ const key = Object.keys(contentTypes)[i];
910
+
911
+ if(contentTypes[key].name.includes("ABR Master") || contentTypes[key].name.includes("Title")) {
912
+ typeAbrMaster = contentTypes[key].hash;
913
+ }
918
914
 
915
+ if(contentTypes[key].name.includes("Live Stream")) {
916
+ typeLiveStream = contentTypes[key].hash;
917
+ }
918
+ }
919
+
920
+ if(typeAbrMaster === undefined || typeLiveStream === undefined) {
921
+ console.log("ERROR - unable to find content types", "ABR Master", typeAbrMaster, "Live Stream", typeLiveStream);
922
+ return {};
923
+ }
919
924
 
925
+ const res = await this.StreamSetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm, format});
926
+
927
+ return res;
928
+ };
929
+
930
+ /**
931
+ * Set the Live Stream offering
932
+ *
933
+ * @methodGroup Live Stream
934
+ * @namedParams
935
+ * @param {string} name - Object ID or name of the live stream object
936
+ * @param {string=} typeAbrMaster - Content type hash
937
+ * @param {string=} typeLiveStream - Content type hash
938
+ * @param {boolean=} drm - If specified, DRM will be applied to the stream
939
+ * @param {string=} format - A list of playout formats and DRM to support, comma-separated
940
+ * (hls-clear, hls-aes128, hls-sample-aes, hls-fairplay). If specified,
941
+ * this will take precedence over the drm value
942
+ *
943
+ * @return {Promise<Object>} - The name, object ID, and state of the stream
944
+ */
945
+ exports.StreamSetOfferingAndDRM = async function({name, typeAbrMaster, typeLiveStream, drm=false, format}) {
946
+ let status = await this.StreamStatus({name});
947
+ if(status.state != "inactive" && status.state != "stopped") {
948
+ return {
949
+ state: status.state,
950
+ error: "stream still active - must terminate first"
951
+ };
952
+ }
953
+
954
+ let objectId = status.object_id;
955
+
956
+ console.log("INIT: ", name, objectId);
957
+
958
+ const aBitRate = 128000;
959
+ const aChannels = 2;
960
+ const aSampleRate = 48000;
961
+ const aStreamIndex = 1;
962
+ const aTimeBase = "1/48000";
963
+ const aChannelLayout = "stereo";
964
+
965
+ const vBitRate = 14000000;
966
+ const vHeight = 720;
967
+ const vStreamIndex = 0;
968
+ const vWidth = 1280;
969
+ const vDisplayAspectRatio = "16/9";
970
+ const vFrameRate = "30000/1001";
971
+ const vTimeBase = "1/30000"; // "1/16000";
972
+
973
+ const abrProfile = require("../abr_profiles/abr_profile_live_drm.js");
974
+
975
+ let playoutFormats = abrProfile.playout_formats;
976
+ if(format) {
977
+ drm = true; // Override DRM parameter
978
+ playoutFormats = {};
979
+ let formats = format.split(",");
980
+ for(let i = 0; i < formats.length; i++) {
981
+ if(formats[i] === "hls-clear") {
982
+ abrProfile.drm_optional = true;
983
+ playoutFormats["hls-clear"] = {
984
+ "drm": null,
985
+ "protocol": {
986
+ "type": "ProtoHls"
987
+ }
988
+ };
989
+ continue;
990
+ }
991
+ playoutFormats[formats[i]] = abrProfile.playout_formats[formats[i]];
992
+ }
993
+ } else if(!drm) {
994
+ abrProfile.drm_optional = true;
995
+ playoutFormats = {
996
+ "hls-clear": {
997
+ "drm": null,
998
+ "protocol": {
999
+ "type": "ProtoHls"
1000
+ }
1001
+ }
1002
+ };
1003
+ }
1004
+
1005
+ abrProfile.playout_formats = playoutFormats;
1006
+
1007
+ let libraryId = await this.ContentObjectLibraryId({objectId});
1008
+
1009
+ try {
1010
+ let mainMeta = await this.ContentObjectMetadata({
1011
+ libraryId,
1012
+ objectId
1013
+ });
1014
+
1015
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
1016
+ // Support both hostname and URL ingress_node_api
1017
+ if(!fabURI.startsWith("http")) {
1018
+ // Assume https
1019
+ fabURI = "https://" + fabURI;
1020
+ }
1021
+
1022
+ this.SetNodes({fabricURIs: [fabURI]});
1023
+
1024
+ let streamUrl = mainMeta.live_recording.recording_config.recording_params.origin_url;
1025
+
1026
+ await StreamGenerateOffering({
1027
+ client: this,
1028
+ libraryId,
1029
+ objectId,
1030
+ typeAbrMaster,
1031
+ typeLiveStream,
1032
+ streamUrl,
1033
+ abrProfile,
1034
+ aBitRate,
1035
+ aChannels,
1036
+ aSampleRate,
1037
+ aStreamIndex,
1038
+ aTimeBase,
1039
+ aChannelLayout,
1040
+ vBitRate,
1041
+ vHeight,
1042
+ vStreamIndex,
1043
+ vWidth,
1044
+ vDisplayAspectRatio,
1045
+ vFrameRate,
1046
+ vTimeBase
1047
+ });
1048
+
1049
+ console.log("Finished generating offering");
1050
+
1051
+ return {
1052
+ name,
1053
+ object_id: objectId,
1054
+ state: "initialized"
1055
+ };
1056
+ } catch(error) {
1057
+ console.error(error);
1058
+ }
1059
+ };
1060
+
1061
+ /**
1062
+ * Add a content insertion entry
1063
+ *
1064
+ * @methodGroup Live Stream
1065
+ * @namedParams
1066
+ * @param {string} name - Object ID or name of the live stream object
1067
+ * @param {number} insertionTime - Time in seconds (float)
1068
+ * @param {boolean=} sinceStart - If specified, time specified will be elapsed seconds
1069
+ * since stream start, otherwise, time will be elapsed since epoch
1070
+ * @param {number=} duration - Time in seconds (float). Default: 20.0
1071
+ * @param {string} targetHash - The target content object hash (playable)
1072
+ * @param {boolean=} remove - If specified, will remove the inseration at the exact 'time' (instead of adding)
1073
+ *
1074
+ * @return {Promise<Object>} - Insertions, as well as any errors from bad insertions
1075
+ */
1076
+ exports.StreamInsertion = async function({name, insertionTime, sinceStart=false, duration, targetHash, remove=false}) {
1077
+ // Determine audio and video parameters of the insertion
1078
+
1079
+ // Content Type check is currently disabled due to permissions
1080
+ /*
1081
+ let ct = await this.client.ContentObject({versionHash});
1082
+ if(ct.type != undefined && ct.type != "") {
1083
+ let typeMeta = await this.client.ContentObjectMetadata({
1084
+ versionHash: ct.type
1085
+ });
1086
+ if(typeMeta.bitcode_flags != "abrmaster") {
1087
+ throw new Error("Not a playable VoD object " + versionHash);
1088
+ }
1089
+ }
1090
+ */
1091
+ let offeringMeta = await this.ContentObjectMetadata({
1092
+ versionHash: targetHash,
1093
+ metadataSubtree: "/offerings/default"
1094
+ });
1095
+
1096
+ var insertionInfo = {
1097
+ duration_sec: 0 // Minimum of video and audio duration
1098
+ };
1099
+ ["video", "audio"].forEach(mt => {
1100
+ const stream = offeringMeta.media_struct.streams[mt];
1101
+ insertionInfo[mt] = {
1102
+ seg_duration_sec: stream.optimum_seg_dur.float,
1103
+ duration_sec: stream.duration.float,
1104
+ frame_rate_rat: stream.rate,
1105
+ };
1106
+ if(insertionInfo.duration_sec === 0 || stream.duration.float < insertionInfo.duration_sec) {
1107
+ insertionInfo.duration_sec = stream.duration.float;
1108
+ }
1109
+ });
1110
+
1111
+ const audioAbrDuration = insertionInfo.audio.seg_duration_sec;
1112
+ const videoAbrDuration = insertionInfo.video.seg_duration_sec;
1113
+
1114
+ if(audioAbrDuration === 0 || videoAbrDuration === 0) {
1115
+ throw new Error("Bad segment duration hash:", targetHash);
1116
+ }
1117
+
1118
+ if(duration === undefined) {
1119
+ duration = insertionInfo.duration_sec; // Use full duration of the insertion
1120
+ } else {
1121
+ if(duration > insertionInfo.duration_sec) {
1122
+ throw new Error("Bad duration - larger than insertion object duration", insertionInfo.duration_sec);
1123
+ }
1124
+ }
1125
+
1126
+ let conf = await this.LoadConf({name});
1127
+ let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
1128
+ let objectId = conf.objectId;
1129
+
1130
+ let mainMeta = await this.ContentObjectMetadata({
1131
+ libraryId: libraryId,
1132
+ objectId: conf.objectId
1133
+ });
1134
+
1135
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
1136
+
1137
+ // Support both hostname and URL ingress_node_api
1138
+ if(!fabURI.startsWith("http")) {
1139
+ // Assume https
1140
+ fabURI = "https://" + fabURI;
1141
+ }
1142
+ this.SetNodes({fabricURIs: [fabURI]});
1143
+ let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
1144
+
1145
+ let edgeMeta = await this.ContentObjectMetadata({
1146
+ libraryId: libraryId,
1147
+ objectId: conf.objectId,
1148
+ writeToken: edgeWriteToken
1149
+ });
1150
+
1151
+ // Find stream start time (from the most recent recording section)
1152
+ let recordings = edgeMeta.live_recording.recordings;
1153
+ let sequence = 1;
1154
+ let streamStartTime = 0;
1155
+ if(recordings != undefined && recordings.recording_sequence != undefined) {
1156
+ // We have at least one recording - check if still active
1157
+ sequence = recordings.recording_sequence;
1158
+ let period = recordings.live_offering[sequence - 1];
1159
+
1160
+ if(period.end_time_epoch_sec > 0) {
1161
+ // The last period is closed - apply insertions to the next period
1162
+ sequence ++;
1163
+ } else {
1164
+ // The period is active
1165
+ streamStartTime = period.start_time_epoch_sec;
1166
+ }
1167
+ }
1168
+
1169
+ if(streamStartTime === 0) {
1170
+ // There is no active period - must use absolute time
1171
+ if(sinceStart === false) {
1172
+ throw new Error("Stream not running - must use 'time since start'");
1173
+ }
1174
+ }
1175
+
1176
+ // Find the current period playout configuration
1177
+ if(edgeMeta.live_recording.playout_config.interleaves === undefined) {
1178
+ edgeMeta.live_recording.playout_config.interleaves = {};
1179
+ }
1180
+ if(edgeMeta.live_recording.playout_config.interleaves[sequence] === undefined) {
1181
+ edgeMeta.live_recording.playout_config.interleaves[sequence] = [];
1182
+ }
1183
+
1184
+ let playoutConfig = edgeMeta.live_recording.playout_config;
1185
+ let insertions = playoutConfig.interleaves[sequence];
1186
+
1187
+ let res = {};
1188
+
1189
+ if(!sinceStart) {
1190
+ insertionTime = insertionTime - streamStartTime;
1191
+ }
1192
+
1193
+ // Assume insertions are sorted by insertion time
1194
+ let errs = [];
1195
+ let currentTime = -1;
1196
+ let insertionDone = false;
1197
+ let newInsertion = {
1198
+ insertion_time: insertionTime,
1199
+ duration: duration,
1200
+ audio_abr_duration: audioAbrDuration,
1201
+ video_abr_duration: videoAbrDuration,
1202
+ playout: "/qfab/" + targetHash + "/rep/playout" // TO FIX - should be a link
1203
+ };
1204
+
1205
+ for (let i = 0; i < insertions.length; i ++) {
1206
+ if(insertions[i].insertion_time <= currentTime) {
1207
+ // Bad insertion - must be later than current time
1208
+ append(errs, "Bad insertion - time:", insertions[i].insertion_time);
1209
+ }
1210
+ if(remove) {
1211
+ if(insertions[i].insertion_time === insertionTime) {
1212
+ insertions.splice(i, 1);
1213
+ break;
1214
+ }
1215
+ } else {
1216
+ if(insertions[i].insertion_time > insertionTime) {
1217
+ if(i > 0) {
1218
+ insertions = [
1219
+ ...insertions.splice(0, i),
1220
+ newInsertion,
1221
+ ...insertions.splice(i)
1222
+ ];
1223
+ } else {
1224
+ insertions = [
1225
+ newInsertion,
1226
+ ...insertions.splice(i)
1227
+ ];
1228
+ }
1229
+ insertionDone = true;
1230
+ break;
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+ if(!remove && !insertionDone) {
1236
+ // Add to the end of the insertions list
1237
+ console.log("Add insertion at the end");
1238
+ insertions = [
1239
+ ...insertions,
1240
+ newInsertion
1241
+ ];
1242
+ }
1243
+
1244
+ playoutConfig.interleaves[sequence] = insertions;
1245
+
1246
+ // Store the new insertions in the write token
1247
+ await this.ReplaceMetadata({
1248
+ libraryId: libraryId,
1249
+ objectId: objectId,
1250
+ writeToken: edgeWriteToken,
1251
+ metadataSubtree: "/live_recording/playout_config",
1252
+ metadata: edgeMeta.live_recording.playout_config
1253
+ });
1254
+
1255
+ res.errors = errs;
1256
+ res.insertions = insertions;
1257
+
1258
+ return res;
1259
+ };
1260
+
1261
+ /**
1262
+ * Load cached stream configuration
1263
+ *
1264
+ * @methodGroup Live Stream
1265
+ * @namedParams
1266
+ * @param {string} name - Object ID or name of the live stream object
1267
+ *
1268
+ * @return {Promise<Object>} - The configuration of the stream
1269
+ */
920
1270
  exports.LoadConf = async function({name}) {
921
1271
  if(name.startsWith("iq__")) {
922
1272
  return {
@@ -945,180 +1295,22 @@ exports.LoadConf = async function({name}) {
945
1295
  return conf;
946
1296
  };
947
1297
 
948
- /*
949
- * Read a playable contnet object and get information about a particular offering
950
- */
951
- // async getOfferingInfo({versionHash, offering = "default"}) {
952
- //
953
- // // Content Type check is currently disabled due to permissions
954
- // /*
955
- // let ct = await this.client.ContentObject({versionHash});
956
- // if(ct.type != undefined && ct.type != "") {
957
- // let typeMeta = await this.client.ContentObjectMetadata({
958
- // versionHash: ct.type
959
- // });
960
- // if(typeMeta.bitcode_flags != "abrmaster") {
961
- // throw new Error("Not a playable VoD object " + versionHash);
962
- // }
963
- // }
964
- // */
965
- // let offeringMeta = await this.client.ContentObjectMetadata({
966
- // versionHash,
967
- // metadataSubtree: "/offerings/" + offering
968
- // });
969
- //
970
- // var info = {
971
- // duration_sec: 0 // Minimum of video and audio duration
972
- // };
973
- // ["video", "audio"].forEach(mt => {
974
- // const stream = offeringMeta.media_struct.streams[mt];
975
- // info[mt] = {
976
- // seg_duration_sec: stream.optimum_seg_dur.float,
977
- // duration_sec: stream.duration.float,
978
- // frame_rate_rat: stream.rate,
979
- // };
980
- // if(info.duration_sec === 0 || stream.duration.float < info.duration_sec) {
981
- // info.duration_sec = stream.duration.float;
982
- // }
983
- // });
984
- // return info;
985
- // }
986
-
987
-
988
- // async StreamDownload({name, period}) {
989
- //
990
- // let conf = await this.LoadConf({name});
991
- //
992
- // let status = {name};
993
- //
994
- // try {
995
- //
996
- // let libraryId = await this.client.ContentObjectLibraryId({objectId: conf.objectId});
997
- // status.library_id = libraryId;
998
- // status.object_id = conf.objectId;
999
- //
1000
- // let mainMeta = await this.client.ContentObjectMetadata({
1001
- // libraryId: libraryId,
1002
- // objectId: conf.objectId
1003
- // });
1004
- //
1005
- // let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
1006
- // if(fabURI === undefined) {
1007
- // console.log("bad fabric config - missing ingress node API");
1008
- // }
1009
- //
1010
- // // Support both hostname and URL ingress_node_api
1011
- // if(!fabURI.startsWith("http")) {
1012
- // // Assume https
1013
- // fabURI = "https://" + fabURI;
1014
- // }
1015
- // this.client.SetNodes({fabricURIs: [fabURI]});
1016
- //
1017
- // let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
1018
- // let edgeMeta = await this.client.ContentObjectMetadata({
1019
- // libraryId: libraryId,
1020
- // objectId: conf.objectId,
1021
- // writeToken: edgeWriteToken
1022
- // });
1023
- //
1024
- // // If a stream has never been started return state 'inactive'
1025
- // if(edgeMeta.live_recording === undefined ||
1026
- // edgeMeta.live_recording.recordings === undefined ||
1027
- // edgeMeta.live_recording.recordings.recording_sequence === undefined) {
1028
- // status.state = "no recordings";
1029
- // return status;
1030
- // }
1031
- //
1032
- // let recordings = edgeMeta.live_recording.recordings;
1033
- // status.recording_period_sequence = recordings.recording_sequence;
1034
- //
1035
- // let sequence = recordings.recording_sequence;
1036
- // if(period === undefined || period < 0 || period > sequence - 1) {
1037
- // period = sequence - 1;
1038
- // }
1039
- //
1040
- // console.log("Downloading stream", name, " period", period, " latest", sequence - 1);
1041
- //
1042
- // let recording = recordings.live_offering[period];
1043
- // if(recording === undefined) {
1044
- // console.log("ERROR - recording period not found: ", period);
1045
- // }
1046
- //
1047
- // let dpath = "DOWNLOAD/" + edgeWriteToken + "." + period;
1048
- // !fs.existsSync(dpath) && fs.mkdirSync(dpath, {recursive: true});
1049
- //
1050
- // let mts = ["audio", "video"];
1051
- // for (let mi = 0; mi < mts.length; mi ++) {
1052
- // let mt = mts[mi];
1053
- // console.log("Downloading ", mt);
1054
- // let mtpath = dpath + "/" + mt;
1055
- // let partsfile = dpath + "/parts_" + mt + ".txt";
1056
- // !fs.existsSync(mtpath) && fs.mkdirSync(mtpath);
1057
- // var sources = recording.sources[mt];
1058
- // for (let i = 0; i < sources.length - 1; i++) {
1059
- // console.log(sources[i].hash);
1060
- // let partHash = sources[i].hash;
1061
- // let buf = await this.client.DownloadPart({
1062
- // libraryId,
1063
- // objectId: conf.objectId,
1064
- // partHash,
1065
- // format: "buffer",
1066
- // chunked: false,
1067
- // callback: ({bytesFinished, bytesTotal}) => {
1068
- // console.log(" progress: ", bytesFinished + "/" + bytesTotal);
1069
- // }
1070
- // });
1071
- //
1072
- // let partfile = mtpath + "/" + partHash + ".mp4";
1073
- // fs.appendFile(partfile, buf, (err) => {
1074
- // if(err)
1075
- // console.log(err);
1076
- // });
1077
- // fs.appendFile(partsfile, "file '" + mt + "/" + partHash + ".mp4'\n", (err) => {
1078
- // if(err)
1079
- // console.log(err);
1080
- // });
1081
- // }
1082
- //
1083
- // // Concatenate parts into one mp4
1084
- // let cmd = "ffmpeg -f concat -safe 0 -i " + partsfile + " -c copy " + dpath + "/" + mt + ".mp4";
1085
- // console.log("Running", cmd);
1086
- // execSync(cmd);
1087
- // }
1088
- //
1089
- // // Create final mp4 file
1090
- // let f = dpath + "/download.mp4";
1091
- // let cmd = "ffmpeg -i " + dpath + "/video.mp4" + " -i " + dpath + "/audio.mp4" + " -map 0:v:0 -map 1:a:0 -c copy -shortest " + f;
1092
- // console.log("Running", cmd);
1093
- // execSync(cmd);
1094
- //
1095
- // status.file = f;
1096
- // status.state = "completed";
1097
- // } catch(e) {
1098
- // console.log("Download failed", e);
1099
- // throw e;
1100
- // }
1101
- //
1102
- // return status;
1103
- // }
1104
-
1105
1298
  /**
1106
1299
  * Configure the stream
1107
1300
  *
1108
1301
  * @methodGroup Live Stream
1109
1302
  * @namedParams
1110
- * @param {string} name -
1111
- * @param {string=} op - The operation to perform. Possible values:
1112
- * 'start'
1113
- * 'reset' - Stops current LRO recording and starts a new one. Does
1114
- * not create a new edge write token (just creates a new recording
1115
- * period in the existing edge write token)
1116
- * - 'stop'
1303
+ * @param {string} name - Object ID or name of the live stream object
1304
+ * @param {Object=} customSettings - Additional options to customize configuration settings
1305
+ * - audioBitrate
1306
+ * - audioIndex
1307
+ * - partTtl
1308
+ * - channelLayout
1117
1309
  *
1118
- * @return {Object} - The status response for the stream
1310
+ * @return {Promise<Object>} - The status response for the stream
1119
1311
  *
1120
1312
  */
1121
- exports.StreamConfig = async function({name, customSettings}) {
1313
+ exports.StreamConfig = async function({name, customSettings={}}) {
1122
1314
  let conf = await this.LoadConf({name});
1123
1315
  let status = {name};
1124
1316
 
@@ -1235,322 +1427,3 @@ exports.StreamConfig = async function({name, customSettings}) {
1235
1427
 
1236
1428
  return status;
1237
1429
  };
1238
-
1239
- // const ChannelStatus = async ({client, name}) => {
1240
- //
1241
- // let status = {name: name};
1242
- //
1243
- // const conf = channels[name];
1244
- // if(conf === null) {
1245
- // console.log("Bad name: ", name);
1246
- // return;
1247
- // }
1248
- //
1249
- // try {
1250
- //
1251
- // let meta = await client.ContentObjectMetadata({
1252
- // libraryId: conf.libraryId,
1253
- // objectId: conf.objectId
1254
- // });
1255
- //
1256
- // status.channel_title = meta.public.asset_metadata.title;
1257
- // let source = meta.channel.offerings.default.items[0].source["/"];
1258
- // let hash = source.split("/")[2];
1259
- // status.stream_hash = hash;
1260
- // latestHash = await client.LatestVersionHash({
1261
- // versionHash: hash
1262
- // });
1263
- // status.stream_latest_hash = latestHash;
1264
- //
1265
- // if(hash != latestHash) {
1266
- // status.warnings = ["Stream version is not the latest"];
1267
- // }
1268
- //
1269
- // let channelFormatsUrl = await client.FabricUrl({
1270
- // libraryId: conf.libraryId,
1271
- // objectId: conf.objectId,
1272
- // rep: "channel/options.json"
1273
- // });
1274
- //
1275
- // try {
1276
- // let offerings = await got(channelFormatsUrl);
1277
- // status.offerings = JSON.parse(offerings.body);
1278
- // } catch(error) {
1279
- // console.log(error);
1280
- // status.offerings_error = "Failed to retrieve channel offerings";
1281
- // }
1282
- //
1283
- // status.playout = await ChannelPlayout({client, libraryId: conf.libraryId, objectId: conf.objectId});
1284
- //
1285
- // } catch(error) {
1286
- // console.error(error);
1287
- // }
1288
- //
1289
- // return status;
1290
- // };
1291
-
1292
- /*
1293
- * Performs client-side playout operations - open the channel, read offerings,
1294
- * retrieve playlist and one video init segment.
1295
- */
1296
- // const ChannelPlayout = async({client, libraryId, objectId}) => {
1297
- //
1298
- // let playout = {};
1299
- //
1300
- // const offerings = await client.AvailableOfferings({
1301
- // libraryId,
1302
- // objectId,
1303
- // handler: "channel",
1304
- // linkPath: "/public/asset_metadata/offerings"
1305
- // });
1306
- //
1307
- // // Choosing offering 'default'
1308
- // let offering = offerings.default;
1309
- //
1310
- // const playoutOptions = await client.PlayoutOptions({
1311
- // libraryId,
1312
- // objectId,
1313
- // offeringURI: offering.uri
1314
- // });
1315
- //
1316
- // // Retrieve master playlist
1317
- // let masterPlaylistUrl = playoutOptions["hls"]["playoutMethods"]["fairplay"]["playoutUrl"];
1318
- // playout.master_playlist_url = masterPlaylistUrl;
1319
- // try {
1320
- // //let masterPlaylist = await got(masterPlaylistUrl);
1321
- // playout.master_playlist = "success";
1322
- // } catch(error) {
1323
- // playout.master_playlist = "fail";
1324
- // }
1325
- //
1326
- // let url = new URL(masterPlaylistUrl);
1327
- // let p = url.pathname.split("/");
1328
- //
1329
- // // Retrieve media playlist
1330
- // p[p.length - 1] = "video/720@14000000/live.m3u8";
1331
- // let pathMediaPlaylist = p.join("/");
1332
- // url.pathname = pathMediaPlaylist;
1333
- // let mediaPlaylistUrl = url.toString();
1334
- // playout.media_playlist_url = mediaPlaylistUrl;
1335
- // let mediaPlaylist;
1336
- // try {
1337
- // mediaPlaylist = await got(mediaPlaylistUrl);
1338
- // playout.media_playlist = "success";
1339
- // } catch(error) {
1340
- // playout.media_playlist = "fail";
1341
- // }
1342
- //
1343
- // // Retrieve init segment
1344
- // var regex = new RegExp("^#EXT-X-MAP:URI=\"init.m4s.(.*)\"$", "m");
1345
- // var match = regex.exec(mediaPlaylist.body);
1346
- // let initQueryParams;
1347
- // if(match) {
1348
- // initQueryParams = match[1];
1349
- // }
1350
- //
1351
- // p[p.length - 1] = "video/720@14000000/init.m4s";
1352
- // let pathInit = p.join("/");
1353
- // url.pathname = pathInit;
1354
- // url.search=initQueryParams;
1355
- // let initUrl = url.toString();
1356
- // playout.init_segment_url = initUrl;
1357
- // /*
1358
- // try {
1359
- // let initSegment = await got(initUrl);
1360
- // playout.init_segment = "success"
1361
- // } catch(error) {
1362
- // playout.init_segment = "fail";
1363
- // }
1364
- // */
1365
- // return playout;
1366
- // };
1367
-
1368
-
1369
- // const Summary = async ({client}) => {
1370
- //
1371
- // let summary = {};
1372
- //
1373
- // try {
1374
- // for (const [key] of Object.entries(streams)) {
1375
- // conf = streams[key];
1376
- // summary[key] = await Status({client, name: key, stopLro: false});
1377
- // }
1378
- //
1379
- // } catch(error) {
1380
- // console.error(error);
1381
- // }
1382
- // return summary;
1383
- // };
1384
-
1385
- // const ChannelSummary = async ({client}) => {
1386
- //
1387
- // let summary = {};
1388
- //
1389
- // try {
1390
- // for (const [key] of Object.entries(channels)) {
1391
- // conf = channels[key];
1392
- // summary[key] = await ChannelStatus({client, name: key});
1393
- // }
1394
- //
1395
- // } catch(error) {
1396
- // console.error(error);
1397
- // }
1398
- // return summary;
1399
- // };
1400
-
1401
- // const ConfigStreamRebroadcast = async () => {
1402
- //
1403
- // const t = 1619850660;
1404
- //
1405
- // try {
1406
- // let client;
1407
- // if(conf.clientConf.configUrl) {
1408
- // client = await ElvClient.FromConfigurationUrl({
1409
- // configUrl: conf.clientConf.configUrl
1410
- // });
1411
- // } else {
1412
- // client = new ElvClient(conf.clientConf);
1413
- // }
1414
- // const wallet = client.GenerateWallet();
1415
- // const signer = wallet.AddAccount({ privateKey: conf.signerPrivateKey });
1416
- // client.SetSigner({ signer });
1417
- // const fabURI = client.fabricURIs[0];
1418
- // console.log("Fabric URI: " + fabURI);
1419
- // const ethURI = client.ethereumURIs[0];
1420
- // console.log("Ethereum URI: " + ethURI);
1421
- //
1422
- // client.ToggleLogging(false);
1423
- //
1424
- // let mainMeta = await client.ContentObjectMetadata({
1425
- // libraryId: conf.libraryId,
1426
- // objectId: conf.objectId
1427
- // });
1428
- // console.log("Main meta:", mainMeta);
1429
- //
1430
- // edgeWriteToken = mainMeta.edge_write_token;
1431
- // console.log("Edge: ", edgeWriteToken);
1432
- //
1433
- // let edgeMeta = await client.ContentObjectMetadata({
1434
- // libraryId: conf.libraryId,
1435
- // objectId: conf.objectId,
1436
- // writeToken: edgeWriteToken
1437
- // });
1438
- // console.log("Edge meta:", edgeMeta);
1439
- //
1440
- // //console.log("CONFIG: ", edgeMeta.live_recording_parameters.live_playout_config);
1441
- // console.log("recording_start_time: ", edgeMeta.recording_start_time);
1442
- // console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
1443
- //
1444
- // // Set rebroadcast start
1445
- // edgeMeta.live_recording_parameters.live_playout_config.rebroadcast_start_time_sec_epoch = t;
1446
- //
1447
- // if(PRINT_DEBUG) console.log("MergeMetadata", conf.libraryId, conf.objectId, writeToken);
1448
- // await client.MergeMetadata({
1449
- // libraryId: conf.libraryId,
1450
- // objectId: conf.objectId,
1451
- // writeToken: edgeWriteToken,
1452
- // metadata: {
1453
- // "live_recording_parameters": {
1454
- // "live_playout_config" : edgeMeta.live_recording_parameters.live_playout_config
1455
- // }
1456
- // }
1457
- // });
1458
- //
1459
- // } catch(error) {
1460
- // console.error(error);
1461
- // }
1462
- // };
1463
-
1464
- // async function EnsureAll() {
1465
- // client = await StatusPrep({name: null});
1466
- // let summary = await Summary({client});
1467
- //
1468
- // var res = {
1469
- // running: 0,
1470
- // stalled: 0,
1471
- // terminated: 0
1472
- // };
1473
- //
1474
- // try {
1475
- // for (const [key, value] of Object.entries(summary)) {
1476
- // if(value.state === "stalled") {
1477
- // console.log("Stream stalled: ", key, " - restarting");
1478
- // console.log("todo ...");
1479
- // }
1480
- // res[value.state] = res[value.state] + 1;
1481
- // }
1482
- // } catch(error) {
1483
- // console.error(error);
1484
- // }
1485
- //
1486
- // return res;
1487
- // }
1488
-
1489
-
1490
- /*
1491
- * Original Run() function - kept for reference
1492
- */
1493
- // async function Run() {
1494
- //
1495
- // var client;
1496
- //
1497
- // switch (command) {
1498
- //
1499
- // case "start":
1500
- // StartStream({name});
1501
- // break;
1502
- //
1503
- // case "status":
1504
- // client = await StatusPrep({name});
1505
- // let status = await Status({client, name, stopLro: false});
1506
- // console.log(JSON.stringify(status, null, 4));
1507
- // break;
1508
- //
1509
- // case "stop":
1510
- // client = await UpdatePrep({name});
1511
- // Status({client, name, stopLro: true});
1512
- // break;
1513
- //
1514
- // case "summary":
1515
- // client = await StatusPrep({name: null});
1516
- // let summary = await Summary({client});
1517
- // console.log(JSON.stringify(summary, null, 4));
1518
- // break;
1519
- //
1520
- // case "init": // Set up DRM
1521
- // SetOfferingAndDRM();
1522
- // break;
1523
- //
1524
- // case "reset": // Stop and start LRO recording (same edge write token)
1525
- // client = await StatusPrep({name});
1526
- // let reset = await Reset({client, name, stopLro: false});
1527
- // console.log(JSON.stringify(reset, null, 4));
1528
- // break;
1529
- //
1530
- // case "channel":
1531
- // client = await StatusPrep({name});
1532
- // let channelStatus = await ChannelStatus({client, name});
1533
- // console.log(JSON.stringify(channelStatus, null, 4));
1534
- // break;
1535
- //
1536
- // case "channel_summary":
1537
- // client = await StatusPrep({name});
1538
- // let channelSummary = await ChannelSummary({client, name});
1539
- // console.log(JSON.stringify(channelSummary, null, 4));
1540
- // break;
1541
- //
1542
- // case "ensure_all": // Check all and restart stalled
1543
- // let ensureSummary = await EnsureAll();
1544
- // console.log(JSON.stringify(ensureSummary, null, 4));
1545
- // break;
1546
- //
1547
- // case "future_use_config":
1548
- // ConfigStreamRebroadcast();
1549
- // break;
1550
- //
1551
- // default:
1552
- // console.log("Bad command: ", command);
1553
- // break;
1554
- //
1555
- // }
1556
- // }