@eluvio/elv-client-js 4.0.76 → 4.0.78

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 (37) 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 +48 -28
  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 +152 -21
  23. package/dist/src/client/LiveStream.js +1376 -1248
  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 +4 -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/ContentAccess.js +21 -0
  35. package/src/client/LiveConf.js +133 -17
  36. package/src/client/LiveStream.js +907 -947
  37. package/testScripts/Test.js +7 -0
@@ -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;
@@ -68,6 +339,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
68
339
  ]
69
340
  });
70
341
 
342
+ status.reference_url = mainMeta.live_recording_config.reference_url;
343
+
71
344
  if(mainMeta.live_recording_config == undefined || mainMeta.live_recording_config.url == undefined) {
72
345
  status.state = "unconfigured";
73
346
  return status;
@@ -96,7 +369,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
96
369
  status.url = mainMeta.live_recording.recording_config.recording_params.origin_url;
97
370
 
98
371
  let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
99
- if(edgeWriteToken == undefined) {
372
+ if(!edgeWriteToken) {
100
373
  status.state = "inactive";
101
374
  return status;
102
375
  }
@@ -114,6 +387,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
114
387
  ]
115
388
  });
116
389
 
390
+ status.edge_meta_size = JSON.stringify(edgeMeta).length;
391
+
117
392
  // If a stream has never been started return state 'inactive'
118
393
  if(edgeMeta.live_recording === undefined ||
119
394
  edgeMeta.live_recording.recordings === undefined ||
@@ -168,7 +443,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
168
443
  }
169
444
 
170
445
  if(showParams) {
171
- status.recording_paramse = edgeMeta.live_recording.recording_config.recording_params;
446
+ status.recording_params = edgeMeta.live_recording.recording_config.recording_params;
172
447
  }
173
448
 
174
449
  let state = "stopped";
@@ -207,10 +482,12 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
207
482
  await this.utils.ResponseToJson(
208
483
  await HttpClient.Fetch(lroStopUrl)
209
484
  );
485
+
210
486
  console.log("LRO Stop: ", lroStatus.body);
211
487
  } catch(error) {
212
488
  console.log("LRO Stop (failed): ", error.response.statusCode);
213
489
  }
490
+
214
491
  state = "stopped";
215
492
  status.state = state;
216
493
  }
@@ -290,36 +567,18 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
290
567
  return status;
291
568
  };
292
569
 
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
570
  /**
311
571
  * Create a new edge write token
312
572
  *
313
573
  * @methodGroup Live Stream
314
574
  * @namedParams
315
- * @param {string} name -
316
- * @param {boolean} start -
575
+ * @param {string} name - Object ID or name of the live stream object
576
+ * @param {boolean=} start - If specified, will start the stream after creation
317
577
  *
318
- * @return {Object} - The status response for the object
578
+ * @return {Promise<Object>} - The status response for the object
319
579
  *
320
580
  */
321
- exports.StreamCreate = async function({name, start = false}) {
322
-
581
+ exports.StreamCreate = async function({name, start=false}) {
323
582
  let status = await this.StreamStatus({name});
324
583
  if(status.state !== "inactive" && status.state !== "terminated" && status.state !== "stopped") {
325
584
  return {
@@ -392,6 +651,7 @@ exports.StreamCreate = async function({name, start = false}) {
392
651
  writeToken: writeToken,
393
652
  commitMessage: "Create stream edge write token " + edgeToken
394
653
  });
654
+
395
655
  const objectHash = response.hash;
396
656
  this.Log("Finalized object: ", objectHash);
397
657
 
@@ -417,20 +677,19 @@ exports.StreamCreate = async function({name, start = false}) {
417
677
  *
418
678
  * @methodGroup Live Stream
419
679
  * @namedParams
420
- * @param {string} name -
421
- * @param {string=} op - The operation to perform. Possible values:
680
+ * @param {string} name - Object ID or name of the live stream object
681
+ * @param {string} op - The operation to perform. Possible values:
422
682
  * 'start'
423
683
  * 'reset' - Stops current LRO recording and starts a new one. Does
424
684
  * not create a new edge write token (just creates a new recording
425
685
  * period in the existing edge write token)
426
686
  * - 'stop'
427
687
  *
428
- * @return {Object} - The status response for the stream
688
+ * @return {Promise<Object>} - The status response for the stream
429
689
  *
430
690
  */
431
691
  exports.StreamStartOrStopOrReset = async function({name, op}) {
432
692
  try {
433
- console.log("Stream ", op, ": ", name);
434
693
  let status = await this.StreamStatus({name});
435
694
  if(status.state != "stopped") {
436
695
  if(op === "start") {
@@ -440,7 +699,6 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
440
699
  }
441
700
 
442
701
  if(status.state == "running" || status.state == "starting" || status.state == "stalled") {
443
- console.log("STOPPING");
444
702
  try {
445
703
  await this.CallBitcodeMethod({
446
704
  libraryId: status.library_id,
@@ -458,13 +716,14 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
458
716
  let tries = 10;
459
717
  while(status.state != "stopped" && tries-- > 0) {
460
718
  console.log("Wait to terminate - ", status.state);
461
- await sleep(1000);
719
+ await Sleep(1000);
462
720
  status = await this.StreamStatus({name});
463
721
  }
464
- console.log("Status after terminate - ", status.state);
722
+
723
+ console.log("Status after stop - ", status.state);
465
724
 
466
725
  if(tries <= 0) {
467
- console.log("Failed to terminate");
726
+ console.log("Failed to stop");
468
727
  return status;
469
728
  }
470
729
  }
@@ -495,7 +754,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
495
754
  let tries = 10;
496
755
  while(status.state != "starting" && tries-- > 0) {
497
756
  console.log("Wait to start - ", status.state);
498
- await sleep(1000);
757
+ await Sleep(1000);
499
758
  status = await this.StreamStatus({name});
500
759
  }
501
760
 
@@ -507,416 +766,505 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
507
766
  }
508
767
  };
509
768
 
510
- /*
769
+ /**
511
770
  * Stop the live stream session and close the edge write token.
512
771
  * Not implemented fully
772
+ *
773
+ * @methodGroup Live Stream
774
+ * @namedParams
775
+ * @param {string} name - Object ID or name of the live stream object
776
+ *
777
+ * @return {Promise<Object>} - The finalize response for the stream object
513
778
  */
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
- // }
779
+ exports.StreamStopSession = async function({name}) {
780
+ try {
781
+ console.log("TERMINATE: ", name);
782
+
783
+ let conf = await this.LoadConf({name});
918
784
 
785
+ let {objectId} = conf;
786
+ let libraryId = await this.ContentObjectLibraryId({objectId});
787
+
788
+ let mainMeta = await this.ContentObjectMetadata({
789
+ libraryId,
790
+ objectId
791
+ });
919
792
 
793
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
794
+ // Support both hostname and URL ingress_node_api
795
+ if(!fabURI.startsWith("http")) {
796
+ // Assume https
797
+ fabURI = "https://" + fabURI;
798
+ }
799
+
800
+ this.SetNodes({fabricURIs: [fabURI]});
801
+
802
+ let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
803
+
804
+ if(edgeWriteToken === undefined || edgeWriteToken === "") {
805
+ return {
806
+ state: "inactive",
807
+ error: "no active streams - must create a stream first"
808
+ };
809
+ }
810
+ let edgeMeta = await this.ContentObjectMetadata({
811
+ libraryId,
812
+ objectId,
813
+ writeToken: edgeWriteToken
814
+ });
815
+
816
+ // Stop the LRO if running
817
+ let status = await this.StreamStatus({name});
818
+ if(status.state != "terminated") {
819
+ console.log("STOPPING");
820
+ try {
821
+ await this.CallBitcodeMethod({
822
+ libraryId: status.library_id,
823
+ objectId: status.object_id,
824
+ writeToken: status.edge_write_token,
825
+ method: "/live/stop/" + status.tlro,
826
+ constant: false
827
+ });
828
+ } catch(error) {
829
+ // The /call/lro/stop API returns empty response
830
+ // console.log("LRO Stop (failed): ", error);
831
+ }
832
+
833
+ // Wait until LRO is terminated
834
+ let tries = 10;
835
+ while(status.state != "stopped" && tries-- > 0) {
836
+ console.log("Wait to terminate - ", status.state);
837
+ await Sleep(1000);
838
+ status = await this.StreamStatus({name});
839
+ }
840
+ console.log("Status after stop - ", status.state);
841
+
842
+ if(tries <= 0) {
843
+ console.log("Failed to stop");
844
+ return status;
845
+ }
846
+ }
847
+
848
+ // Set stop time
849
+ edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
850
+ console.log("recording_start_time: ", edgeMeta.recording_start_time);
851
+ console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
852
+
853
+ edgeMeta.live_recording.status = {
854
+ state: "terminated",
855
+ recording_stop_time: edgeMeta.recording_stop_time
856
+ };
857
+
858
+ edgeMeta.live_recording.fabric_config.edge_write_token = "";
859
+
860
+ await this.ReplaceMetadata({
861
+ libraryId,
862
+ objectId,
863
+ writeToken: edgeWriteToken,
864
+ metadata: edgeMeta
865
+ });
866
+
867
+ let fin = await this.FinalizeContentObject({
868
+ libraryId,
869
+ objectId,
870
+ writeToken: edgeWriteToken,
871
+ commitMessage: "Finalize live stream - stop time " + edgeMeta.recording_stop_time,
872
+ publish: false // Don't publish this version because it is not currently useful
873
+ });
874
+
875
+ return {
876
+ fin,
877
+ name,
878
+ edge_write_token: edgeWriteToken,
879
+ state: "terminated"
880
+ };
881
+
882
+ } catch(error) {
883
+ console.error(error);
884
+ }
885
+ };
886
+
887
+ /**
888
+ * Initialize the stream object
889
+ *
890
+ * @methodGroup Live Stream
891
+ * @namedParams
892
+ * @param {string} name - Object ID or name of the live stream object
893
+ * @param {boolean=} drm - If specified, playout will be DRM protected
894
+ * @param {string=} format - Specify the list of playout formats and DRM to support,
895
+ comma-separated (hls-clear, hls-aes128, hls-sample-aes,
896
+ hls-fairplay)
897
+ *
898
+ * @return {Promise<Object>} - The name, object ID, and state of the stream
899
+ */
900
+ exports.StreamInitialize = async function({name, drm=false, format}) {
901
+ const contentTypes = await this.ContentTypes();
902
+
903
+ let typeAbrMaster;
904
+ let typeLiveStream;
905
+
906
+ for(let i = 0; i < Object.keys(contentTypes).length; i++) {
907
+ const key = Object.keys(contentTypes)[i];
908
+
909
+ if(contentTypes[key].name.includes("ABR Master") || contentTypes[key].name.includes("Title")) {
910
+ typeAbrMaster = contentTypes[key].hash;
911
+ }
912
+
913
+ if(contentTypes[key].name.includes("Live Stream")) {
914
+ typeLiveStream = contentTypes[key].hash;
915
+ }
916
+ }
917
+
918
+ if(typeAbrMaster === undefined || typeLiveStream === undefined) {
919
+ console.log("ERROR - unable to find content types", "ABR Master", typeAbrMaster, "Live Stream", typeLiveStream);
920
+ return {};
921
+ }
922
+
923
+ const res = await this.StreamSetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm, format});
924
+
925
+ return res;
926
+ };
927
+
928
+ /**
929
+ * Set the Live Stream offering
930
+ *
931
+ * @methodGroup Live Stream
932
+ * @namedParams
933
+ * @param {string} name - Object ID or name of the live stream object
934
+ * @param {string=} typeAbrMaster - Content type hash
935
+ * @param {string=} typeLiveStream - Content type hash
936
+ * @param {boolean=} drm - If specified, DRM will be applied to the stream
937
+ * @param {string=} format - A list of playout formats and DRM to support, comma-separated
938
+ * (hls-clear, hls-aes128, hls-sample-aes, hls-fairplay). If specified,
939
+ * this will take precedence over the drm value
940
+ *
941
+ * @return {Promise<Object>} - The name, object ID, and state of the stream
942
+ */
943
+ exports.StreamSetOfferingAndDRM = async function({name, typeAbrMaster, typeLiveStream, drm=false, format}) {
944
+ let status = await this.StreamStatus({name});
945
+ if(status.state != "inactive" && status.state != "stopped") {
946
+ return {
947
+ state: status.state,
948
+ error: "stream still active - must terminate first"
949
+ };
950
+ }
951
+
952
+ let objectId = status.object_id;
953
+
954
+ console.log("INIT: ", name, objectId);
955
+
956
+ const aBitRate = 128000;
957
+ const aChannels = 2;
958
+ const aSampleRate = 48000;
959
+ const aStreamIndex = 1;
960
+ const aTimeBase = "1/48000";
961
+ const aChannelLayout = "stereo";
962
+
963
+ const vBitRate = 14000000;
964
+ const vHeight = 720;
965
+ const vStreamIndex = 0;
966
+ const vWidth = 1280;
967
+ const vDisplayAspectRatio = "16/9";
968
+ const vFrameRate = "30000/1001";
969
+ const vTimeBase = "1/30000"; // "1/16000";
970
+
971
+ const abrProfile = require("../abr_profiles/abr_profile_live_drm.js");
972
+
973
+ let playoutFormats = abrProfile.playout_formats;
974
+ if(format) {
975
+ drm = true; // Override DRM parameter
976
+ playoutFormats = {};
977
+ let formats = format.split(",");
978
+ for(let i = 0; i < formats.length; i++) {
979
+ if(formats[i] === "hls-clear") {
980
+ abrProfile.drm_optional = true;
981
+ playoutFormats["hls-clear"] = {
982
+ "drm": null,
983
+ "protocol": {
984
+ "type": "ProtoHls"
985
+ }
986
+ };
987
+ continue;
988
+ }
989
+ playoutFormats[formats[i]] = abrProfile.playout_formats[formats[i]];
990
+ }
991
+ } else if(!drm) {
992
+ abrProfile.drm_optional = true;
993
+ playoutFormats = {
994
+ "hls-clear": {
995
+ "drm": null,
996
+ "protocol": {
997
+ "type": "ProtoHls"
998
+ }
999
+ }
1000
+ };
1001
+ }
1002
+
1003
+ abrProfile.playout_formats = playoutFormats;
1004
+
1005
+ let libraryId = await this.ContentObjectLibraryId({objectId});
1006
+
1007
+ try {
1008
+ let mainMeta = await this.ContentObjectMetadata({
1009
+ libraryId,
1010
+ objectId
1011
+ });
1012
+
1013
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
1014
+ // Support both hostname and URL ingress_node_api
1015
+ if(!fabURI.startsWith("http")) {
1016
+ // Assume https
1017
+ fabURI = "https://" + fabURI;
1018
+ }
1019
+
1020
+ this.SetNodes({fabricURIs: [fabURI]});
1021
+
1022
+ let streamUrl = mainMeta.live_recording.recording_config.recording_params.origin_url;
1023
+
1024
+ await StreamGenerateOffering({
1025
+ client: this,
1026
+ libraryId,
1027
+ objectId,
1028
+ typeAbrMaster,
1029
+ typeLiveStream,
1030
+ streamUrl,
1031
+ abrProfile,
1032
+ aBitRate,
1033
+ aChannels,
1034
+ aSampleRate,
1035
+ aStreamIndex,
1036
+ aTimeBase,
1037
+ aChannelLayout,
1038
+ vBitRate,
1039
+ vHeight,
1040
+ vStreamIndex,
1041
+ vWidth,
1042
+ vDisplayAspectRatio,
1043
+ vFrameRate,
1044
+ vTimeBase
1045
+ });
1046
+
1047
+ console.log("Finished generating offering");
1048
+
1049
+ return {
1050
+ name,
1051
+ object_id: objectId,
1052
+ state: "initialized"
1053
+ };
1054
+ } catch(error) {
1055
+ console.error(error);
1056
+ }
1057
+ };
1058
+
1059
+ /**
1060
+ * Add a content insertion entry
1061
+ *
1062
+ * @methodGroup Live Stream
1063
+ * @namedParams
1064
+ * @param {string} name - Object ID or name of the live stream object
1065
+ * @param {number} insertionTime - Time in seconds (float)
1066
+ * @param {boolean=} sinceStart - If specified, time specified will be elapsed seconds
1067
+ * since stream start, otherwise, time will be elapsed since epoch
1068
+ * @param {number=} duration - Time in seconds (float). Default: 20.0
1069
+ * @param {string} targetHash - The target content object hash (playable)
1070
+ * @param {boolean=} remove - If specified, will remove the inseration at the exact 'time' (instead of adding)
1071
+ *
1072
+ * @return {Promise<Object>} - Insertions, as well as any errors from bad insertions
1073
+ */
1074
+ exports.StreamInsertion = async function({name, insertionTime, sinceStart=false, duration, targetHash, remove=false}) {
1075
+ // Determine audio and video parameters of the insertion
1076
+
1077
+ // Content Type check is currently disabled due to permissions
1078
+ /*
1079
+ let ct = await this.client.ContentObject({versionHash});
1080
+ if(ct.type != undefined && ct.type != "") {
1081
+ let typeMeta = await this.client.ContentObjectMetadata({
1082
+ versionHash: ct.type
1083
+ });
1084
+ if(typeMeta.bitcode_flags != "abrmaster") {
1085
+ throw new Error("Not a playable VoD object " + versionHash);
1086
+ }
1087
+ }
1088
+ */
1089
+ let offeringMeta = await this.ContentObjectMetadata({
1090
+ versionHash: targetHash,
1091
+ metadataSubtree: "/offerings/default"
1092
+ });
1093
+
1094
+ var insertionInfo = {
1095
+ duration_sec: 0 // Minimum of video and audio duration
1096
+ };
1097
+ ["video", "audio"].forEach(mt => {
1098
+ const stream = offeringMeta.media_struct.streams[mt];
1099
+ insertionInfo[mt] = {
1100
+ seg_duration_sec: stream.optimum_seg_dur.float,
1101
+ duration_sec: stream.duration.float,
1102
+ frame_rate_rat: stream.rate,
1103
+ };
1104
+ if(insertionInfo.duration_sec === 0 || stream.duration.float < insertionInfo.duration_sec) {
1105
+ insertionInfo.duration_sec = stream.duration.float;
1106
+ }
1107
+ });
1108
+
1109
+ const audioAbrDuration = insertionInfo.audio.seg_duration_sec;
1110
+ const videoAbrDuration = insertionInfo.video.seg_duration_sec;
1111
+
1112
+ if(audioAbrDuration === 0 || videoAbrDuration === 0) {
1113
+ throw new Error("Bad segment duration hash:", targetHash);
1114
+ }
1115
+
1116
+ if(duration === undefined) {
1117
+ duration = insertionInfo.duration_sec; // Use full duration of the insertion
1118
+ } else {
1119
+ if(duration > insertionInfo.duration_sec) {
1120
+ throw new Error("Bad duration - larger than insertion object duration", insertionInfo.duration_sec);
1121
+ }
1122
+ }
1123
+
1124
+ let conf = await this.LoadConf({name});
1125
+ let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
1126
+ let objectId = conf.objectId;
1127
+
1128
+ let mainMeta = await this.ContentObjectMetadata({
1129
+ libraryId: libraryId,
1130
+ objectId: conf.objectId
1131
+ });
1132
+
1133
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
1134
+
1135
+ // Support both hostname and URL ingress_node_api
1136
+ if(!fabURI.startsWith("http")) {
1137
+ // Assume https
1138
+ fabURI = "https://" + fabURI;
1139
+ }
1140
+ this.SetNodes({fabricURIs: [fabURI]});
1141
+ let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
1142
+
1143
+ let edgeMeta = await this.ContentObjectMetadata({
1144
+ libraryId: libraryId,
1145
+ objectId: conf.objectId,
1146
+ writeToken: edgeWriteToken
1147
+ });
1148
+
1149
+ // Find stream start time (from the most recent recording section)
1150
+ let recordings = edgeMeta.live_recording.recordings;
1151
+ let sequence = 1;
1152
+ let streamStartTime = 0;
1153
+ if(recordings != undefined && recordings.recording_sequence != undefined) {
1154
+ // We have at least one recording - check if still active
1155
+ sequence = recordings.recording_sequence;
1156
+ let period = recordings.live_offering[sequence - 1];
1157
+
1158
+ if(period.end_time_epoch_sec > 0) {
1159
+ // The last period is closed - apply insertions to the next period
1160
+ sequence ++;
1161
+ } else {
1162
+ // The period is active
1163
+ streamStartTime = period.start_time_epoch_sec;
1164
+ }
1165
+ }
1166
+
1167
+ if(streamStartTime === 0) {
1168
+ // There is no active period - must use absolute time
1169
+ if(sinceStart === false) {
1170
+ throw new Error("Stream not running - must use 'time since start'");
1171
+ }
1172
+ }
1173
+
1174
+ // Find the current period playout configuration
1175
+ if(edgeMeta.live_recording.playout_config.interleaves === undefined) {
1176
+ edgeMeta.live_recording.playout_config.interleaves = {};
1177
+ }
1178
+ if(edgeMeta.live_recording.playout_config.interleaves[sequence] === undefined) {
1179
+ edgeMeta.live_recording.playout_config.interleaves[sequence] = [];
1180
+ }
1181
+
1182
+ let playoutConfig = edgeMeta.live_recording.playout_config;
1183
+ let insertions = playoutConfig.interleaves[sequence];
1184
+
1185
+ let res = {};
1186
+
1187
+ if(!sinceStart) {
1188
+ insertionTime = insertionTime - streamStartTime;
1189
+ }
1190
+
1191
+ // Assume insertions are sorted by insertion time
1192
+ let errs = [];
1193
+ let currentTime = -1;
1194
+ let insertionDone = false;
1195
+ let newInsertion = {
1196
+ insertion_time: insertionTime,
1197
+ duration: duration,
1198
+ audio_abr_duration: audioAbrDuration,
1199
+ video_abr_duration: videoAbrDuration,
1200
+ playout: "/qfab/" + targetHash + "/rep/playout" // TO FIX - should be a link
1201
+ };
1202
+
1203
+ for(let i = 0; i < insertions.length; i ++) {
1204
+ if(insertions[i].insertion_time <= currentTime) {
1205
+ // Bad insertion - must be later than current time
1206
+ append(errs, "Bad insertion - time:", insertions[i].insertion_time);
1207
+ }
1208
+ if(remove) {
1209
+ if(insertions[i].insertion_time === insertionTime) {
1210
+ insertions.splice(i, 1);
1211
+ break;
1212
+ }
1213
+ } else {
1214
+ if(insertions[i].insertion_time > insertionTime) {
1215
+ if(i > 0) {
1216
+ insertions = [
1217
+ ...insertions.splice(0, i),
1218
+ newInsertion,
1219
+ ...insertions.splice(i)
1220
+ ];
1221
+ } else {
1222
+ insertions = [
1223
+ newInsertion,
1224
+ ...insertions.splice(i)
1225
+ ];
1226
+ }
1227
+ insertionDone = true;
1228
+ break;
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ if(!remove && !insertionDone) {
1234
+ // Add to the end of the insertions list
1235
+ console.log("Add insertion at the end");
1236
+ insertions = [
1237
+ ...insertions,
1238
+ newInsertion
1239
+ ];
1240
+ }
1241
+
1242
+ playoutConfig.interleaves[sequence] = insertions;
1243
+
1244
+ // Store the new insertions in the write token
1245
+ await this.ReplaceMetadata({
1246
+ libraryId: libraryId,
1247
+ objectId: objectId,
1248
+ writeToken: edgeWriteToken,
1249
+ metadataSubtree: "/live_recording/playout_config",
1250
+ metadata: edgeMeta.live_recording.playout_config
1251
+ });
1252
+
1253
+ res.errors = errs;
1254
+ res.insertions = insertions;
1255
+
1256
+ return res;
1257
+ };
1258
+
1259
+ /**
1260
+ * Load cached stream configuration
1261
+ *
1262
+ * @methodGroup Live Stream
1263
+ * @namedParams
1264
+ * @param {string} name - Object ID or name of the live stream object
1265
+ *
1266
+ * @return {Promise<Object>} - The configuration of the stream
1267
+ */
920
1268
  exports.LoadConf = async function({name}) {
921
1269
  if(name.startsWith("iq__")) {
922
1270
  return {
@@ -945,180 +1293,22 @@ exports.LoadConf = async function({name}) {
945
1293
  return conf;
946
1294
  };
947
1295
 
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
1296
  /**
1106
1297
  * Configure the stream
1107
1298
  *
1108
1299
  * @methodGroup Live Stream
1109
1300
  * @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'
1301
+ * @param {string} name - Object ID or name of the live stream object
1302
+ * @param {Object=} customSettings - Additional options to customize configuration settings
1303
+ * - audioBitrate
1304
+ * - audioIndex
1305
+ * - partTtl
1306
+ * - channelLayout
1117
1307
  *
1118
- * @return {Object} - The status response for the stream
1308
+ * @return {Promise<Object>} - The status response for the stream
1119
1309
  *
1120
1310
  */
1121
- exports.StreamConfig = async function({name, customSettings}) {
1311
+ exports.StreamConfig = async function({name, customSettings={}}) {
1122
1312
  let conf = await this.LoadConf({name});
1123
1313
  let status = {name};
1124
1314
 
@@ -1133,9 +1323,10 @@ exports.StreamConfig = async function({name, customSettings}) {
1133
1323
 
1134
1324
  let userConfig = mainMeta.live_recording_config;
1135
1325
  status.user_config = userConfig;
1326
+ console.log("userConfig", userConfig);
1136
1327
 
1137
1328
  // Get node URI from user config
1138
- const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").split(":")[0];
1329
+ const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").replace("srt://", "").split(":")[0];
1139
1330
  const streamUrl = new URL(userConfig.url);
1140
1331
 
1141
1332
  console.log("Retrieving nodes...");
@@ -1188,7 +1379,6 @@ exports.StreamConfig = async function({name, customSettings}) {
1188
1379
  }
1189
1380
  }
1190
1381
 
1191
- console.log("PROBE", probe);
1192
1382
  probe.format.filename = streamUrl.href;
1193
1383
 
1194
1384
  // Create live recording config
@@ -1218,14 +1408,6 @@ exports.StreamConfig = async function({name, customSettings}) {
1218
1408
  metadata: liveRecordingConfig.live_recording
1219
1409
  });
1220
1410
 
1221
- await this.ReplaceMetadata({
1222
- libraryId,
1223
- objectId: conf.objectId,
1224
- writeToken,
1225
- metadataSubtree: "probe",
1226
- metadata: probe
1227
- });
1228
-
1229
1411
  status.fin = await this.FinalizeContentObject({
1230
1412
  libraryId,
1231
1413
  objectId: conf.objectId,
@@ -1236,321 +1418,99 @@ exports.StreamConfig = async function({name, customSettings}) {
1236
1418
  return status;
1237
1419
  };
1238
1420
 
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
1421
+ /**
1422
+ * Deactivate the stream
1423
+ *
1424
+ * @methodGroup Live Stream
1425
+ * @namedParams
1426
+ * @param {string} name - Object ID or name of the live stream object
1427
+ *
1428
+ * @return {Promise<Object>} - The status response for the stream
1492
1429
  */
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
- // }
1430
+ exports.StreamDeactivate = async function({name}) {
1431
+ try {
1432
+ let conf = await this.LoadConf({name});
1433
+
1434
+ let {objectId} = conf;
1435
+ let libraryId = await this.ContentObjectLibraryId({objectId});
1436
+
1437
+ let mainMeta = await this.ContentObjectMetadata({
1438
+ libraryId,
1439
+ objectId
1440
+ });
1441
+ let status = await this.StreamStatus({name});
1442
+
1443
+ if(!mainMeta.live_recording) {
1444
+ return {
1445
+ state: status.state,
1446
+ error: "Stream must be configured before deactivating"
1447
+ };
1448
+ }
1449
+
1450
+ // Return error if the LRO is running
1451
+ if(status.state !== "stopped") {
1452
+ return {
1453
+ state: status.state,
1454
+ error: "Stream must be stopped before deactivating"
1455
+ };
1456
+ }
1457
+
1458
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
1459
+ // Support both hostname and URL ingress_node_api
1460
+ if(!fabURI.startsWith("http")) {
1461
+ // Assume https
1462
+ fabURI = "https://" + fabURI;
1463
+ }
1464
+
1465
+ this.SetNodes({fabricURIs: [fabURI]});
1466
+
1467
+ let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
1468
+
1469
+ if(edgeWriteToken === undefined || edgeWriteToken === "") {
1470
+ return {
1471
+ state: "inactive",
1472
+ error: "stream is already inactive"
1473
+ };
1474
+ }
1475
+ let edgeMeta = await this.ContentObjectMetadata({
1476
+ libraryId,
1477
+ objectId,
1478
+ writeToken: edgeWriteToken
1479
+ });
1480
+
1481
+ // Set stop time
1482
+ edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
1483
+ const newState = "inactive";
1484
+
1485
+ edgeMeta.live_recording.status = {
1486
+ state: newState,
1487
+ recording_stop_time: edgeMeta.recording_stop_time
1488
+ };
1489
+
1490
+ edgeMeta.live_recording.fabric_config.edge_write_token = "";
1491
+
1492
+ await this.ReplaceMetadata({
1493
+ libraryId,
1494
+ objectId,
1495
+ writeToken: edgeWriteToken,
1496
+ metadata: edgeMeta
1497
+ });
1498
+
1499
+ let fin = await this.FinalizeContentObject({
1500
+ libraryId,
1501
+ objectId,
1502
+ writeToken: edgeWriteToken,
1503
+ commitMessage: "Deactivate stream"
1504
+ });
1505
+
1506
+ return {
1507
+ reference_url: status.reference_url,
1508
+ fin,
1509
+ name,
1510
+ edge_write_token: edgeWriteToken,
1511
+ state: newState
1512
+ };
1513
+ } catch(error) {
1514
+ console.error(error);
1515
+ }
1516
+ };