@eluvio/elv-client-js 4.0.47 → 4.0.48

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.
@@ -0,0 +1,1546 @@
1
+ /**
2
+ * Methods for Live Stream creation and management
3
+ *
4
+ * @module ElvClient/LiveStream
5
+ */
6
+
7
+ const {LiveConf} = require("./LiveConf");
8
+ const path = require("path");
9
+
10
+ const fs = require("fs");
11
+
12
+ const HttpClient = require("../HttpClient");
13
+ //
14
+ // const {
15
+ // ValidateLibrary,
16
+ // ValidateVersion,
17
+ // ValidateParameters
18
+ // } = require("../Validation");
19
+
20
+ const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
21
+ const tok = await client.authClient.AuthorizationToken({libraryId, objectId,
22
+ versionHash, channelAuth: false, noCache: true,
23
+ noAuth: true});
24
+
25
+ return tok;
26
+ };
27
+
28
+ function sleep(ms) {
29
+ return new Promise(resolve => setTimeout(resolve, ms));
30
+ }
31
+
32
+ /**
33
+ * Retrieve the status of the current live stream session
34
+ *
35
+ * @methodGroup Live Stream
36
+ * @namedParams
37
+ * @param {string} name -
38
+ * @param {boolean} stopLro -
39
+ * @param {boolean} showParams -
40
+ * States:
41
+ * unconfigured - no live_recording_config
42
+ * uninitialized - no live_recording config generated
43
+ * inactive - live_recording config initialized but no 'edge write token'
44
+ * stopped - edge-write-token but not started
45
+ * starting - LRO running but no source data yet
46
+ * running - stream is running and producing output
47
+ * stalled - LRO running but no source data (so not producing output)
48
+ *
49
+ * @return {Object} - The status response for the object, as well as logs, warnings and errors from the master initialization
50
+ */
51
+ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
52
+ let conf = await this.LoadConf({name});
53
+
54
+ let status = {name: name};
55
+
56
+ try {
57
+
58
+ let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
59
+ status.library_id = libraryId;
60
+ status.object_id = conf.objectId;
61
+
62
+ let mainMeta = await this.ContentObjectMetadata({
63
+ libraryId: libraryId,
64
+ objectId: conf.objectId,
65
+ select: [
66
+ "live_recording_config",
67
+ "live_recording"
68
+ ]
69
+ });
70
+
71
+ if (mainMeta.live_recording_config == undefined || mainMeta.live_recording_config.url == undefined) {
72
+ status.state = "unconfigured";
73
+ return status;
74
+ }
75
+
76
+ if (mainMeta.live_recording == undefined || mainMeta.live_recording.fabric_config == undefined ||
77
+ mainMeta.live_recording.playout_config == undefined || mainMeta.live_recording.recording_config == undefined) {
78
+ status.state = "uninitialized";
79
+ return status;
80
+ }
81
+
82
+ let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
83
+ if(fabURI === undefined) {
84
+ console.log("bad fabric config - missing ingress node API");
85
+ status.state = "uninitialized";
86
+ return status;
87
+ }
88
+
89
+ // Support both hostname and URL ingress_node_api
90
+ if(!fabURI.startsWith("http")) {
91
+ // Assume https
92
+ fabURI = "https://" + fabURI;
93
+ }
94
+
95
+ status.fabric_api = fabURI;
96
+ status.url = mainMeta.live_recording.recording_config.recording_params.origin_url;
97
+
98
+ let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
99
+ if (edgeWriteToken == undefined) {
100
+ status.state = "inactive";
101
+ return status;
102
+ }
103
+
104
+ this.RecordWriteToken({writeToken: edgeWriteToken, fabricNodeUrl: fabURI});
105
+
106
+ status.edge_write_token = edgeWriteToken;
107
+ status.stream_id = edgeWriteToken; // By convention the stream ID is its write token
108
+ let edgeMeta = await this.ContentObjectMetadata({
109
+ libraryId: libraryId,
110
+ objectId: conf.objectId,
111
+ writeToken: edgeWriteToken,
112
+ select: [
113
+ "live_recording"
114
+ ]
115
+ });
116
+
117
+ // If a stream has never been started return state 'inactive'
118
+ if(edgeMeta.live_recording === undefined ||
119
+ edgeMeta.live_recording.recordings === undefined ||
120
+ edgeMeta.live_recording.recordings.recording_sequence === undefined) {
121
+ status.state = "stopped";
122
+ return status;
123
+ }
124
+
125
+ let recordings = edgeMeta.live_recording.recordings;
126
+ status.recording_period_sequence = recordings.recording_sequence;
127
+
128
+ let sequence = recordings.recording_sequence;
129
+ let period = recordings.live_offering[sequence - 1];
130
+
131
+ let tlro = period.live_recording_handle;
132
+ status.tlro = tlro;
133
+
134
+ let sinceLastFinalize = Math.floor(new Date().getTime() / 1000) -
135
+ period.video_finalized_parts_info.last_finalization_time /1000000;
136
+
137
+ let recording_period = {
138
+ activation_time_epoch_sec: period.recording_start_time_epoch_sec,
139
+ start_time_epoch_sec: period.start_time_epoch_sec,
140
+ start_time_text: new Date(period.start_time_epoch_sec * 1000).toLocaleString(),
141
+ end_time_epoch_sec: period.end_time_epoch_sec,
142
+ end_time_text: period.end_time_epoch_sec === 0 ? null : new Date(period.end_time_epoch_sec * 1000).toLocaleString(),
143
+ video_parts: period.video_finalized_parts_info.n_parts,
144
+ video_last_part_finalized_epoch_sec: period.video_finalized_parts_info.last_finalization_time / 1000000,
145
+ video_since_last_finalize_sec : sinceLastFinalize
146
+ };
147
+ status.recording_period = recording_period;
148
+
149
+ status.lro_status_url = await this.FabricUrl({
150
+ libraryId: libraryId,
151
+ objectId: conf.objectId,
152
+ writeToken: edgeWriteToken,
153
+ call: "live/status/" + tlro
154
+ });
155
+
156
+ status.insertions = [];
157
+ if((edgeMeta.live_recording.playout_config.interleaves != undefined) &&
158
+ (edgeMeta.live_recording.playout_config.interleaves[sequence] != undefined)) {
159
+ let insertions = edgeMeta.live_recording.playout_config.interleaves[sequence];
160
+ for (let i = 0; i < insertions.length; i ++) {
161
+ let insertionTimeSinceEpoch = recording_period.start_time_epoch_sec + insertions[i].insertion_time;
162
+ status.insertions[i] = {
163
+ insertion_time_since_start: insertions[i].insertion_time,
164
+ insertion_time: new Date(insertionTimeSinceEpoch * 1000).toISOString(),
165
+ insertion_time_local: new Date(insertionTimeSinceEpoch * 1000).toLocaleString(),
166
+ target: insertions[i].playout};
167
+ }
168
+ }
169
+
170
+ if(showParams) {
171
+ status.recording_paramse = edgeMeta.live_recording.recording_config.recording_params;
172
+ }
173
+
174
+ let state = "stopped";
175
+ let lroStatus = "";
176
+ try {
177
+ lroStatus = await this.utils.ResponseToJson(
178
+ await HttpClient.Fetch(status.lro_status_url)
179
+ );
180
+ state = lroStatus.state;
181
+ } catch(error) {
182
+ console.log("LRO Status (failed): ", error.response.statusCode);
183
+ status.state = "stopped";
184
+ status.error = error.response;
185
+ return status;
186
+ }
187
+
188
+ // Convert LRO 'state' to desired 'state'
189
+ if(state === "running" && period.video_finalized_parts_info.last_finalization_time === 0) {
190
+ state = "starting";
191
+ } else if(state === "running" && sinceLastFinalize > 32.9) {
192
+ state = "stalled";
193
+ } else if (state == "terminated") {
194
+ state = "stopped";
195
+ }
196
+ status.state = state;
197
+
198
+ if((state === "running" || state === "stalled" || state === "starting") && stopLro) {
199
+ lroStopUrl = await this.FabricUrl({
200
+ libraryId: libraryId,
201
+ objectId: conf.objectId,
202
+ writeToken: edgeWriteToken,
203
+ call: "live/stop/" + tlro
204
+ });
205
+
206
+ try {
207
+ await this.utils.ResponseToJson(
208
+ await HttpClient.Fetch(lroStopUrl)
209
+ );
210
+ console.log("LRO Stop: ", lroStatus.body);
211
+ } catch(error) {
212
+ console.log("LRO Stop (failed): ", error.response.statusCode);
213
+ }
214
+ state = "stopped";
215
+ status.state = state;
216
+ }
217
+
218
+ if(state === "running") {
219
+ let playout_urls = {};
220
+ let objectId = conf.objectId;
221
+ let playout_options = await this.PlayoutOptions({
222
+ objectId,
223
+ linkPath: "public/asset_metadata/sources/default"
224
+ });
225
+
226
+ let hls_clear_enabled = (
227
+ playout_options &&
228
+ playout_options.hls &&
229
+ playout_options.hls.playoutMethods &&
230
+ playout_options.hls.playoutMethods.clear !== undefined
231
+ );
232
+ if(hls_clear_enabled) {
233
+ playout_urls.hls_clear = await this.FabricUrl({
234
+ libraryId: libraryId,
235
+ objectId: objectId,
236
+ rep: "playout/default/hls-clear/playlist.m3u8",
237
+ });
238
+ }
239
+
240
+ let hls_aes128_enabled = (
241
+ playout_options &&
242
+ playout_options.hls &&
243
+ playout_options.hls.playoutMethods &&
244
+ playout_options.hls.playoutMethods["aes-128"] !== undefined
245
+ );
246
+ if(hls_aes128_enabled) {
247
+ playout_urls.hls_aes128 = await this.FabricUrl({
248
+ libraryId: libraryId,
249
+ objectId: objectId,
250
+ rep: "playout/default/hls-aes128/playlist.m3u8",
251
+ });
252
+ }
253
+
254
+ let hls_sample_aes_enabled = (
255
+ playout_options &&
256
+ playout_options.hls &&
257
+ playout_options.hls.playoutMethods &&
258
+ playout_options.hls.playoutMethods["sample-aes"] !== undefined
259
+ );
260
+ if(hls_sample_aes_enabled) {
261
+ playout_urls.hls_sample_aes = await this.FabricUrl({
262
+ libraryId: libraryId,
263
+ objectId: objectId,
264
+ rep: "playout/default/hls-sample-aes/playlist.m3u8",
265
+ });
266
+ }
267
+
268
+ const networkInfo = await this.NetworkInfo();
269
+ let token = await this.authClient.AuthorizationToken({
270
+ libraryId,
271
+ objectId,
272
+ channelAuth: false,
273
+ noCache: true,
274
+ noAuth: true
275
+ });
276
+
277
+ let embed_net = "main";
278
+ if(networkInfo.name.includes("demo")) {
279
+ embed_net = "demo";
280
+ }
281
+ let embed_url = `https://embed.v3.contentfabric.io/?net=${embed_net}&p&ct=h&oid=${conf.objectId}&mt=v&ath=${token}`;
282
+ playout_urls.embed_url = embed_url;
283
+
284
+ status.playout_urls = playout_urls;
285
+ }
286
+ } catch(error) {
287
+ console.error(error);
288
+ }
289
+
290
+ return status;
291
+ };
292
+
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
+ /**
311
+ * Create a new edge write token
312
+ *
313
+ * @methodGroup Live Stream
314
+ * @namedParams
315
+ * @param {string} name -
316
+ * @param {boolean} start -
317
+ *
318
+ * @return {Object} - The status response for the object
319
+ *
320
+ */
321
+ exports.StreamCreate = async function({name, start = false}) {
322
+
323
+ let status = await this.StreamStatus({name});
324
+ if(status.state !== "inactive" && status.state !== "terminated" && status.state !== "stopped") {
325
+ return {
326
+ state: status.state,
327
+ error: "stream still active - must terminate first"
328
+ };
329
+ }
330
+
331
+ let objectId = status.object_id;
332
+ console.log("START: ", name, "start", start);
333
+
334
+ let libraryId = await this.ContentObjectLibraryId({objectId: objectId});
335
+
336
+ // Read live recording parameters - determine ingest node
337
+ let liveRecording = await this.ContentObjectMetadata({
338
+ libraryId: libraryId,
339
+ objectId: objectId,
340
+ metadataSubtree: "/live_recording"
341
+ });
342
+
343
+ let fabURI = liveRecording.fabric_config.ingress_node_api;
344
+ // Support both hostname and URL ingress_node_api
345
+ if(!fabURI.startsWith("http")) {
346
+ // Assume https
347
+ fabURI = "https://" + fabURI;
348
+ }
349
+
350
+ this.SetNodes({fabricURIs: [fabURI]});
351
+
352
+ console.log("Node URI", fabURI, "ID", liveRecording.fabric_config.ingress_node_id);
353
+
354
+ let response = await this.EditContentObject({
355
+ libraryId: libraryId,
356
+ objectId: objectId
357
+ });
358
+ const edgeToken = response.write_token;
359
+ console.log("Edge token:", edgeToken);
360
+
361
+ /*
362
+ * Set the metadata, including the edge token.
363
+ */
364
+ response = await this.EditContentObject({
365
+ libraryId: libraryId,
366
+ objectId: objectId
367
+ });
368
+ let writeToken = response.write_token;
369
+
370
+ this.Log("Merging metadata: ", libraryId, objectId, writeToken);
371
+ await this.MergeMetadata({
372
+ libraryId: libraryId,
373
+ objectId: objectId,
374
+ writeToken: writeToken,
375
+ metadata: {
376
+ live_recording: {
377
+ status: {
378
+ edge_write_token: edgeToken,
379
+ state: "active" // indicates there is an active session (set to 'closed' when done)
380
+ },
381
+ fabric_config: {
382
+ edge_write_token: edgeToken
383
+ }
384
+ }
385
+ }
386
+ });
387
+
388
+ this.Log("Finalizing content draft: ", libraryId, objectId, writeToken);
389
+ response = await this.FinalizeContentObject({
390
+ libraryId: libraryId,
391
+ objectId: objectId,
392
+ writeToken: writeToken,
393
+ commitMessage: "Create stream edge write token " + edgeToken
394
+ });
395
+ const objectHash = response.hash;
396
+ this.Log("Finalized object: ", objectHash);
397
+
398
+ status = {
399
+ object_id: objectId,
400
+ hash: objectHash,
401
+ library_id: libraryId,
402
+ stream_id: edgeToken,
403
+ edge_write_token: edgeToken,
404
+ fabric_api: fabURI,
405
+ state: "stopped"
406
+ };
407
+
408
+ if(start) {
409
+ status = this.StreamStartOrStopOrReset({name, op: start});
410
+ }
411
+
412
+ return status;
413
+ };
414
+
415
+ /**
416
+ * Start, stop or reset a stream within the current session (current edge write token)
417
+ *
418
+ * @methodGroup Live Stream
419
+ * @namedParams
420
+ * @param {string} name -
421
+ * @param {string=} op - The operation to perform. Possible values:
422
+ * 'start'
423
+ * 'reset' - Stops current LRO recording and starts a new one. Does
424
+ * not create a new edge write token (just creates a new recording
425
+ * period in the existing edge write token)
426
+ * - 'stop'
427
+ *
428
+ * @return {Object} - The status response for the stream
429
+ *
430
+ */
431
+ exports.StreamStartOrStopOrReset = async function({name, op}) {
432
+ try {
433
+ console.log("Stream ", op, ": ", name);
434
+ let status = await this.StreamStatus({name});
435
+ if(status.state != "stopped") {
436
+ if(op === "start") {
437
+ status.error = "Unable to start stream - state: " + status.state;
438
+ return status;
439
+ }
440
+ }
441
+
442
+ if(status.state == "running" || status.state == "starting" || status.state == "stalled") {
443
+ console.log("STOPPING");
444
+ try {
445
+ await this.CallBitcodeMethod({
446
+ libraryId: status.library_id,
447
+ objectId: status.object_id,
448
+ writeToken: status.edge_write_token,
449
+ method: "/live/stop/" + status.tlro,
450
+ constant: false
451
+ });
452
+ } catch(error) {
453
+ // The /call/lro/stop API returns empty response
454
+ // console.log("LRO Stop (failed): ", error);
455
+ }
456
+
457
+ // Wait until LRO is terminated
458
+ let tries = 10;
459
+ while(status.state != "stopped" && tries-- > 0) {
460
+ console.log("Wait to terminate - ", status.state);
461
+ await sleep(1000);
462
+ status = await this.StreamStatus({name});
463
+ }
464
+ console.log("Status after terminate - ", status.state);
465
+
466
+ if(tries <= 0) {
467
+ console.log("Failed to terminate");
468
+ return status;
469
+ }
470
+ }
471
+
472
+ if(op === "stop") {
473
+ return status;
474
+ }
475
+
476
+ console.log("STARTING", "edge_write_token", status.edge_write_token);
477
+
478
+ try {
479
+ await this.CallBitcodeMethod({
480
+ libraryId: status.library_id,
481
+ objectId: status.object_id,
482
+ writeToken: status.edge_write_token,
483
+ method: "/live/start",
484
+ constant: false
485
+ });
486
+ } catch(error) {
487
+ console.log("LRO Start (failed): ", error);
488
+ return {
489
+ state: status.state,
490
+ error: "LRO start failed - must create a stream first"
491
+ };
492
+ }
493
+
494
+ // Wait until LRO is 'starting'
495
+ let tries = 10;
496
+ while (status.state != "starting" && tries-- > 0) {
497
+ console.log("Wait to start - ", status.state);
498
+ await sleep(1000);
499
+ status = await this.StreamStatus({name});
500
+ }
501
+
502
+ console.log("Status after restart - ", status.state);
503
+ return status;
504
+
505
+ } catch(error) {
506
+ console.error(error);
507
+ }
508
+ }
509
+
510
+ /*
511
+ * Stop the live stream session and close the edge write token.
512
+ * Not implemented fully
513
+ */
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
+ // }
918
+
919
+
920
+ exports.LoadConf = async function({name}) {
921
+ if(name.startsWith("iq__")) {
922
+ return {
923
+ name: name,
924
+ objectId: name
925
+ };
926
+ }
927
+
928
+ // If name is not a QID, load liveconf.json
929
+ let streamsBuf;
930
+ try {
931
+ streamsBuf = fs.readFileSync(
932
+ path.resolve(__dirname, "../liveconf.json")
933
+ );
934
+ } catch(error) {
935
+ console.log("Stream name must be a QID or a label in liveconf.json");
936
+ return {};
937
+ }
938
+ const streams = JSON.parse(streamsBuf);
939
+ const conf = streams[name];
940
+ if(conf === null) {
941
+ console.log("Bad name: ", name);
942
+ return {};
943
+ }
944
+
945
+ return conf;
946
+ }
947
+
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
+ /**
1106
+ * Configure the stream
1107
+ *
1108
+ * @methodGroup Live Stream
1109
+ * @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'
1117
+ *
1118
+ * @return {Object} - The status response for the stream
1119
+ *
1120
+ */
1121
+ exports.StreamConfig = async function({name}) {
1122
+ let conf = await this.LoadConf({name});
1123
+ let status = {name};
1124
+
1125
+ try {
1126
+ let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
1127
+ status.library_id = libraryId;
1128
+ status.object_id = conf.objectId;
1129
+
1130
+ let mainMeta = await this.ContentObjectMetadata({
1131
+ libraryId: libraryId,
1132
+ objectId: conf.objectId
1133
+ });
1134
+
1135
+ let userConfig = mainMeta.live_recording_config;
1136
+ status.user_config = userConfig;
1137
+
1138
+ // Get node URI from user config
1139
+ const hostName = userConfig.url.replace("udp://", "").split(":")[0];
1140
+ const streamUrl = new URL(userConfig.url);
1141
+
1142
+ console.log("Retrieving nodes...");
1143
+ let nodes = await this.SpaceNodes({matchEndpoint: hostName});
1144
+ if(nodes.length < 1) {
1145
+ status.error = "No node matching stream URL " + streamUrl.href;
1146
+ return status;
1147
+ }
1148
+ const node = nodes[0];
1149
+ status.node = node;
1150
+
1151
+ let endpoint = node.endpoints[0];
1152
+ this.SetNodes({fabricURIs: [endpoint]});
1153
+
1154
+ // Probe the stream
1155
+ let probe = {};
1156
+ const controller = new AbortController();
1157
+ const timeoutId = setTimeout(() => {
1158
+ controller.abort();
1159
+ }, 60 * 1000); // milliseconds
1160
+ try {
1161
+
1162
+ let probeUrl = await this.Rep({
1163
+ libraryId,
1164
+ objectId: conf.objectId,
1165
+ rep: "probe"
1166
+ });
1167
+
1168
+ probe = await this.utils.ResponseToJson(
1169
+ await HttpClient.Fetch(probeUrl, {
1170
+ body: JSON.stringify({
1171
+ "filename": streamUrl.href,
1172
+ "listen": true
1173
+ }),
1174
+ method: "POST",
1175
+ signal: controller.signal
1176
+ })
1177
+ );
1178
+
1179
+ if(probe) { clearTimeout(timeoutId); }
1180
+ } catch(error) {
1181
+ if(error.code === "ETIMEDOUT") {
1182
+ status.error = "Stream probe time out - make sure the stream source is available";
1183
+ } else {
1184
+ console.log("Stream probe failed", error);
1185
+ }
1186
+ }
1187
+
1188
+ probe.format.filename = streamUrl.href;
1189
+ console.log("PROBE", probe);
1190
+
1191
+ // Create live reocording config
1192
+ let lc = new LiveConf(probe, node.id, endpoint, false, false, true);
1193
+
1194
+ const liveRecordingConfigStr = lc.generateLiveConf();
1195
+ let liveRecordingConfig = JSON.parse(liveRecordingConfigStr);
1196
+ console.log("CONFIG", JSON.stringify(liveRecordingConfig.live_recording));
1197
+
1198
+ // Store live recording config into the stream object
1199
+ let e = await this.EditContentObject({
1200
+ libraryId,
1201
+ objectId: conf.objectId
1202
+ });
1203
+ let writeToken = e.write_token;
1204
+
1205
+ await this.ReplaceMetadata({
1206
+ libraryId,
1207
+ objectId: conf.objectId,
1208
+ writeToken,
1209
+ metadataSubtree: "live_recording",
1210
+ metadata: liveRecordingConfig.live_recording
1211
+ });
1212
+
1213
+ let fin = await this.FinalizeContentObject({
1214
+ libraryId,
1215
+ objectId: conf.objectId,
1216
+ writeToken,
1217
+ commitMessage: "Apply live stream configuration"
1218
+ });
1219
+
1220
+ status.fin = fin;
1221
+
1222
+ return status;
1223
+
1224
+ } catch(e) {
1225
+ console.log("ERROR", e);
1226
+ }
1227
+ };
1228
+
1229
+ // const ChannelStatus = async ({client, name}) => {
1230
+ //
1231
+ // let status = {name: name};
1232
+ //
1233
+ // const conf = channels[name];
1234
+ // if(conf === null) {
1235
+ // console.log("Bad name: ", name);
1236
+ // return;
1237
+ // }
1238
+ //
1239
+ // try {
1240
+ //
1241
+ // let meta = await client.ContentObjectMetadata({
1242
+ // libraryId: conf.libraryId,
1243
+ // objectId: conf.objectId
1244
+ // });
1245
+ //
1246
+ // status.channel_title = meta.public.asset_metadata.title;
1247
+ // let source = meta.channel.offerings.default.items[0].source["/"];
1248
+ // let hash = source.split("/")[2];
1249
+ // status.stream_hash = hash;
1250
+ // latestHash = await client.LatestVersionHash({
1251
+ // versionHash: hash
1252
+ // });
1253
+ // status.stream_latest_hash = latestHash;
1254
+ //
1255
+ // if(hash != latestHash) {
1256
+ // status.warnings = ["Stream version is not the latest"];
1257
+ // }
1258
+ //
1259
+ // let channelFormatsUrl = await client.FabricUrl({
1260
+ // libraryId: conf.libraryId,
1261
+ // objectId: conf.objectId,
1262
+ // rep: "channel/options.json"
1263
+ // });
1264
+ //
1265
+ // try {
1266
+ // let offerings = await got(channelFormatsUrl);
1267
+ // status.offerings = JSON.parse(offerings.body);
1268
+ // } catch(error) {
1269
+ // console.log(error);
1270
+ // status.offerings_error = "Failed to retrieve channel offerings";
1271
+ // }
1272
+ //
1273
+ // status.playout = await ChannelPlayout({client, libraryId: conf.libraryId, objectId: conf.objectId});
1274
+ //
1275
+ // } catch(error) {
1276
+ // console.error(error);
1277
+ // }
1278
+ //
1279
+ // return status;
1280
+ // };
1281
+
1282
+ /*
1283
+ * Performs client-side playout operations - open the channel, read offerings,
1284
+ * retrieve playlist and one video init segment.
1285
+ */
1286
+ // const ChannelPlayout = async({client, libraryId, objectId}) => {
1287
+ //
1288
+ // let playout = {};
1289
+ //
1290
+ // const offerings = await client.AvailableOfferings({
1291
+ // libraryId,
1292
+ // objectId,
1293
+ // handler: "channel",
1294
+ // linkPath: "/public/asset_metadata/offerings"
1295
+ // });
1296
+ //
1297
+ // // Choosing offering 'default'
1298
+ // let offering = offerings.default;
1299
+ //
1300
+ // const playoutOptions = await client.PlayoutOptions({
1301
+ // libraryId,
1302
+ // objectId,
1303
+ // offeringURI: offering.uri
1304
+ // });
1305
+ //
1306
+ // // Retrieve master playlist
1307
+ // let masterPlaylistUrl = playoutOptions["hls"]["playoutMethods"]["fairplay"]["playoutUrl"];
1308
+ // playout.master_playlist_url = masterPlaylistUrl;
1309
+ // try {
1310
+ // //let masterPlaylist = await got(masterPlaylistUrl);
1311
+ // playout.master_playlist = "success";
1312
+ // } catch(error) {
1313
+ // playout.master_playlist = "fail";
1314
+ // }
1315
+ //
1316
+ // let url = new URL(masterPlaylistUrl);
1317
+ // let p = url.pathname.split("/");
1318
+ //
1319
+ // // Retrieve media playlist
1320
+ // p[p.length - 1] = "video/720@14000000/live.m3u8";
1321
+ // let pathMediaPlaylist = p.join("/");
1322
+ // url.pathname = pathMediaPlaylist;
1323
+ // let mediaPlaylistUrl = url.toString();
1324
+ // playout.media_playlist_url = mediaPlaylistUrl;
1325
+ // let mediaPlaylist;
1326
+ // try {
1327
+ // mediaPlaylist = await got(mediaPlaylistUrl);
1328
+ // playout.media_playlist = "success";
1329
+ // } catch(error) {
1330
+ // playout.media_playlist = "fail";
1331
+ // }
1332
+ //
1333
+ // // Retrieve init segment
1334
+ // var regex = new RegExp("^#EXT-X-MAP:URI=\"init.m4s.(.*)\"$", "m");
1335
+ // var match = regex.exec(mediaPlaylist.body);
1336
+ // let initQueryParams;
1337
+ // if(match) {
1338
+ // initQueryParams = match[1];
1339
+ // }
1340
+ //
1341
+ // p[p.length - 1] = "video/720@14000000/init.m4s";
1342
+ // let pathInit = p.join("/");
1343
+ // url.pathname = pathInit;
1344
+ // url.search=initQueryParams;
1345
+ // let initUrl = url.toString();
1346
+ // playout.init_segment_url = initUrl;
1347
+ // /*
1348
+ // try {
1349
+ // let initSegment = await got(initUrl);
1350
+ // playout.init_segment = "success"
1351
+ // } catch(error) {
1352
+ // playout.init_segment = "fail";
1353
+ // }
1354
+ // */
1355
+ // return playout;
1356
+ // };
1357
+
1358
+
1359
+ // const Summary = async ({client}) => {
1360
+ //
1361
+ // let summary = {};
1362
+ //
1363
+ // try {
1364
+ // for (const [key] of Object.entries(streams)) {
1365
+ // conf = streams[key];
1366
+ // summary[key] = await Status({client, name: key, stopLro: false});
1367
+ // }
1368
+ //
1369
+ // } catch(error) {
1370
+ // console.error(error);
1371
+ // }
1372
+ // return summary;
1373
+ // };
1374
+
1375
+ // const ChannelSummary = async ({client}) => {
1376
+ //
1377
+ // let summary = {};
1378
+ //
1379
+ // try {
1380
+ // for (const [key] of Object.entries(channels)) {
1381
+ // conf = channels[key];
1382
+ // summary[key] = await ChannelStatus({client, name: key});
1383
+ // }
1384
+ //
1385
+ // } catch(error) {
1386
+ // console.error(error);
1387
+ // }
1388
+ // return summary;
1389
+ // };
1390
+
1391
+ // const ConfigStreamRebroadcast = async () => {
1392
+ //
1393
+ // const t = 1619850660;
1394
+ //
1395
+ // try {
1396
+ // let client;
1397
+ // if(conf.clientConf.configUrl) {
1398
+ // client = await ElvClient.FromConfigurationUrl({
1399
+ // configUrl: conf.clientConf.configUrl
1400
+ // });
1401
+ // } else {
1402
+ // client = new ElvClient(conf.clientConf);
1403
+ // }
1404
+ // const wallet = client.GenerateWallet();
1405
+ // const signer = wallet.AddAccount({ privateKey: conf.signerPrivateKey });
1406
+ // client.SetSigner({ signer });
1407
+ // const fabURI = client.fabricURIs[0];
1408
+ // console.log("Fabric URI: " + fabURI);
1409
+ // const ethURI = client.ethereumURIs[0];
1410
+ // console.log("Ethereum URI: " + ethURI);
1411
+ //
1412
+ // client.ToggleLogging(false);
1413
+ //
1414
+ // let mainMeta = await client.ContentObjectMetadata({
1415
+ // libraryId: conf.libraryId,
1416
+ // objectId: conf.objectId
1417
+ // });
1418
+ // console.log("Main meta:", mainMeta);
1419
+ //
1420
+ // edgeWriteToken = mainMeta.edge_write_token;
1421
+ // console.log("Edge: ", edgeWriteToken);
1422
+ //
1423
+ // let edgeMeta = await client.ContentObjectMetadata({
1424
+ // libraryId: conf.libraryId,
1425
+ // objectId: conf.objectId,
1426
+ // writeToken: edgeWriteToken
1427
+ // });
1428
+ // console.log("Edge meta:", edgeMeta);
1429
+ //
1430
+ // //console.log("CONFIG: ", edgeMeta.live_recording_parameters.live_playout_config);
1431
+ // console.log("recording_start_time: ", edgeMeta.recording_start_time);
1432
+ // console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
1433
+ //
1434
+ // // Set rebroadcast start
1435
+ // edgeMeta.live_recording_parameters.live_playout_config.rebroadcast_start_time_sec_epoch = t;
1436
+ //
1437
+ // if(PRINT_DEBUG) console.log("MergeMetadata", conf.libraryId, conf.objectId, writeToken);
1438
+ // await client.MergeMetadata({
1439
+ // libraryId: conf.libraryId,
1440
+ // objectId: conf.objectId,
1441
+ // writeToken: edgeWriteToken,
1442
+ // metadata: {
1443
+ // "live_recording_parameters": {
1444
+ // "live_playout_config" : edgeMeta.live_recording_parameters.live_playout_config
1445
+ // }
1446
+ // }
1447
+ // });
1448
+ //
1449
+ // } catch(error) {
1450
+ // console.error(error);
1451
+ // }
1452
+ // };
1453
+
1454
+ // async function EnsureAll() {
1455
+ // client = await StatusPrep({name: null});
1456
+ // let summary = await Summary({client});
1457
+ //
1458
+ // var res = {
1459
+ // running: 0,
1460
+ // stalled: 0,
1461
+ // terminated: 0
1462
+ // };
1463
+ //
1464
+ // try {
1465
+ // for (const [key, value] of Object.entries(summary)) {
1466
+ // if(value.state === "stalled") {
1467
+ // console.log("Stream stalled: ", key, " - restarting");
1468
+ // console.log("todo ...");
1469
+ // }
1470
+ // res[value.state] = res[value.state] + 1;
1471
+ // }
1472
+ // } catch(error) {
1473
+ // console.error(error);
1474
+ // }
1475
+ //
1476
+ // return res;
1477
+ // }
1478
+
1479
+
1480
+ /*
1481
+ * Original Run() function - kept for reference
1482
+ */
1483
+ // async function Run() {
1484
+ //
1485
+ // var client;
1486
+ //
1487
+ // switch (command) {
1488
+ //
1489
+ // case "start":
1490
+ // StartStream({name});
1491
+ // break;
1492
+ //
1493
+ // case "status":
1494
+ // client = await StatusPrep({name});
1495
+ // let status = await Status({client, name, stopLro: false});
1496
+ // console.log(JSON.stringify(status, null, 4));
1497
+ // break;
1498
+ //
1499
+ // case "stop":
1500
+ // client = await UpdatePrep({name});
1501
+ // Status({client, name, stopLro: true});
1502
+ // break;
1503
+ //
1504
+ // case "summary":
1505
+ // client = await StatusPrep({name: null});
1506
+ // let summary = await Summary({client});
1507
+ // console.log(JSON.stringify(summary, null, 4));
1508
+ // break;
1509
+ //
1510
+ // case "init": // Set up DRM
1511
+ // SetOfferingAndDRM();
1512
+ // break;
1513
+ //
1514
+ // case "reset": // Stop and start LRO recording (same edge write token)
1515
+ // client = await StatusPrep({name});
1516
+ // let reset = await Reset({client, name, stopLro: false});
1517
+ // console.log(JSON.stringify(reset, null, 4));
1518
+ // break;
1519
+ //
1520
+ // case "channel":
1521
+ // client = await StatusPrep({name});
1522
+ // let channelStatus = await ChannelStatus({client, name});
1523
+ // console.log(JSON.stringify(channelStatus, null, 4));
1524
+ // break;
1525
+ //
1526
+ // case "channel_summary":
1527
+ // client = await StatusPrep({name});
1528
+ // let channelSummary = await ChannelSummary({client, name});
1529
+ // console.log(JSON.stringify(channelSummary, null, 4));
1530
+ // break;
1531
+ //
1532
+ // case "ensure_all": // Check all and restart stalled
1533
+ // let ensureSummary = await EnsureAll();
1534
+ // console.log(JSON.stringify(ensureSummary, null, 4));
1535
+ // break;
1536
+ //
1537
+ // case "future_use_config":
1538
+ // ConfigStreamRebroadcast();
1539
+ // break;
1540
+ //
1541
+ // default:
1542
+ // console.log("Bad command: ", command);
1543
+ // break;
1544
+ //
1545
+ // }
1546
+ // }