@eluvio/elv-client-js 4.2.16 → 4.2.18

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.
@@ -1,7 +1,8 @@
1
1
  /* eslint no-console: 0 */
2
+ const R = require("ramda");
2
3
 
3
4
  const DefaultABRLadder = {
4
- "video" : [
5
+ "video": [
5
6
  {
6
7
  bit_rate: 14000000,
7
8
  codecs: "avc1.640028,mp4a.40.2",
@@ -33,7 +34,7 @@ const DefaultABRLadder = {
33
34
  width: 960
34
35
  }
35
36
  ],
36
- "audio" : [
37
+ "audio": [
37
38
  {
38
39
  bit_rate: 192000,
39
40
  channels: 2,
@@ -55,8 +56,9 @@ const LiveconfTemplate = {
55
56
  },
56
57
  playout_config: {
57
58
  dvr_enabled: true,
58
- dvr_max_duration: 0,
59
+ dvr_max_duration: 14400,
59
60
  rebroadcast_start_time_sec_epoch: 0,
61
+ playout_sharding_level: 2,
60
62
  vod_enabled: false
61
63
  },
62
64
  recording_config: {
@@ -64,11 +66,11 @@ const LiveconfTemplate = {
64
66
  description: "",
65
67
  ladder_specs: [],
66
68
  listen: true,
67
- live_delay_nano: 2000000000,
69
+ live_delay_nano: 6000000000,
68
70
  max_duration_sec: -1,
69
71
  name: "",
70
72
  origin_url: "",
71
- part_ttl: 3600,
73
+ part_ttl: 86400,
72
74
  playout_type: "live",
73
75
  source_timescale: null,
74
76
  reconnect_timeout: 600,
@@ -113,18 +115,49 @@ const LiveconfTemplate = {
113
115
  };
114
116
 
115
117
  class LiveConf {
116
- constructor(probeData, nodeId, nodeUrl, includeAVSegDurations, overwriteOriginUrl, syncAudioToVideo) {
118
+ constructor({
119
+ url,
120
+ probeData,
121
+ nodeId,
122
+ nodeUrl,
123
+ includeAVSegDurations,
124
+ overwriteOriginUrl,
125
+ syncAudioToVideo,
126
+ liveRecordingMeta
127
+ }) {
128
+ this.url = url;
117
129
  this.probeData = probeData;
118
130
  this.nodeId = nodeId;
119
131
  this.nodeUrl = nodeUrl;
120
132
  this.includeAVSegDurations = includeAVSegDurations;
121
133
  this.overwriteOriginUrl = overwriteOriginUrl;
122
134
  this.syncAudioToVideo = syncAudioToVideo;
135
+ this.currentLiveRecordingMeta = liveRecordingMeta;
123
136
  }
124
137
 
125
- probeKind() {
126
- let fileNameSplit = this.probeData.format.filename.split(":");
127
- return fileNameSplit[0];
138
+ getFormat() {
139
+ if (this.probeData.format.format_name) {
140
+ return this.probeData.format.format_name;
141
+ }
142
+ const fileNameSplit = this.probeData.format?.filename?.split(":");
143
+ if (fileNameSplit.length > 1) {
144
+ const protoScheme = fileNameSplit[0];
145
+ switch(protoScheme) {
146
+ case "rtmp":
147
+ return "flv";
148
+ case "udp":
149
+ case "rtp":
150
+ case "srt":
151
+ return "mpegts";
152
+ default:
153
+ return "format_unknown";
154
+ }
155
+ }
156
+ }
157
+
158
+ getProtocol() {
159
+ const protoScheme = this.url.split(":")[0];
160
+ return protoScheme;
128
161
  }
129
162
 
130
163
  getStreamDataForCodecType(codecType) {
@@ -139,16 +172,18 @@ class LiveConf {
139
172
 
140
173
  // Return all audio streams found in the probe
141
174
  // Used by generateAudioStreamsConfig()
142
- getAudioStreamsFromProbe() {
143
- let audioStreams = {};
175
+ getAudioStreamsFromProbe({ladderProfile}) {
176
+ const audioStreams = {};
144
177
  const audioStreamData = this.probeData.streams.filter((value) => value.codec_type === "audio");
145
178
 
146
179
  for(let index = 0; index < audioStreamData.length; index++) {
147
180
  const currentStreamIndex = audioStreamData[index].stream_index;
148
181
  const currentStreamData = audioStreamData[index];
149
182
 
183
+ const profileAudioForType = ladderProfile?.audio?.find(a => a.channels === currentStreamData.channels);
184
+
150
185
  audioStreams[currentStreamIndex] = {
151
- recordingBitrate: Math.max(currentStreamData.bit_rate, 128000),
186
+ recordingBitrate: profileAudioForType?.bit_rate ?? Math.max(currentStreamData.bit_rate ?? 0, 12800),
152
187
  recordingChannels: currentStreamData.channels,
153
188
  playoutLabel: `Audio ${index + 1}`
154
189
  };
@@ -205,16 +240,15 @@ class LiveConf {
205
240
  calcSegDuration({sourceTimescale, sampleRate, audioCodec}) {
206
241
  let seg = {};
207
242
 
208
- switch(this.probeKind()) {
209
- case "rtmp":
243
+ switch(this.getFormat()) {
244
+ case "flv":
210
245
  seg = this.calcSegDurationRtmp({sourceTimescale, sampleRate, audioCodec});
211
246
  break;
212
- case "udp":
213
- case "srt":
247
+ case "mpegts":
214
248
  seg = this.calcSegDurationMpegts({sourceTimescale, sampleRate, audioCodec});
215
249
  break;
216
250
  default:
217
- throw "protocol not supported - " + this.probeKind();
251
+ throw "protocol not supported - " + this.getFormat();
218
252
  }
219
253
 
220
254
  if(audioCodec == "aac") {
@@ -362,32 +396,79 @@ class LiveConf {
362
396
  syncAudioToStreamIdValue() {
363
397
  let sync_id = -1;
364
398
  let videoStream = this.getStreamDataForCodecType("video");
365
- switch(this.probeKind()) {
366
- case "udp":
367
- case "srt":
399
+ switch(this.getFormat()) {
400
+ case "mpegts":
368
401
  sync_id = videoStream.stream_id;
369
402
  break;
370
- case "rtmp":
403
+ case "flv":
371
404
  sync_id = videoStream.stream_index;
372
405
  break;
373
406
  }
374
407
  return sync_id;
375
408
  }
376
409
 
410
+ /**
411
+ * Map custom live recording profile to the expected config structure
412
+ * @param {Object} customProfile - User's custom recording profile
413
+ * @return {Object} - Mapped config in live_recording format
414
+ */
415
+ MapCustomProfileToLiveConfig({customProfile}) {
416
+ if(!customProfile) return {};
417
+
418
+ const CompactDeep = (obj) => {
419
+ if(obj === null || typeof obj !== "object" || Array.isArray(obj)) {
420
+ return obj;
421
+ }
422
+
423
+ return R.pipe(
424
+ R.reject(R.isNil),
425
+ R.map(val => typeof val === "object" ? CompactDeep(val) : val)
426
+ )(obj);
427
+ };
428
+
429
+ const {recording_config, recording_params, ...rest} = customProfile;
430
+
431
+ return CompactDeep({
432
+ live_recording: {
433
+ ...rest,
434
+ recording_config: {
435
+ recording_params: {
436
+ ...recording_params,
437
+ part_ttl: recording_config?.part_ttl,
438
+ reconnect_timeout: recording_config?.reconnect_timeout,
439
+ xc_params: {
440
+ ...(recording_params?.xc_params || {}),
441
+ connection_timeout: recording_config?.connection_timeout,
442
+ copy_mpegts: recording_config?.copy_mpegts,
443
+ input_cfg: recording_config?.input_cfg
444
+ }
445
+ }
446
+ }
447
+ }
448
+ });
449
+ }
450
+
377
451
  /*
378
452
  * Generate audio streams recording configuration based on the optional custom settings.
379
453
  * If no custom "audio" section is present, record all the acceptable audio streams found in the probe
380
454
  */
381
- generateAudioStreamsConfig({customSettings}) {
455
+ generateAudioStreamsConfig({liveRecordingConfigProfile}) {
456
+ const ladderProfile = liveRecordingConfigProfile?.playout_config?.ladder_specs || DefaultABRLadder;
457
+ const audioSettings = liveRecordingConfigProfile?.recording_stream_config?.audio;
382
458
 
383
459
  let audioStreams = {};
384
- if(customSettings && customSettings.audio && Object.keys(customSettings.audio).length > 0) {
385
- for(let i = 0; i < Object.keys(customSettings.audio).length; i ++) {
386
- let audioIdx = Object.keys(customSettings.audio)[i];
387
- let audio = customSettings.audio[audioIdx];
460
+
461
+ if(audioSettings && Object.keys(audioSettings).length > 0) {
462
+ for(let i = 0; i < Object.keys(audioSettings).length; i ++) {
463
+ const audioIdx = Object.keys(audioSettings)[i];
464
+ const audio = audioSettings[audioIdx];
465
+ const profileAudioForType = ladderProfile?.audio.find(a => a.channels === (audio.channels ?? 2));
466
+
388
467
  audioStreams[audioIdx] = {
389
- recordingBitrate: audio.recording_bitrate || 192000,
468
+ recordingBitrate: ladderProfile?.audio ? profileAudioForType.bit_rate : audio.recording_bitrate ?? 192000,
390
469
  recordingChannels: audio.recording_channels || 2,
470
+ lang: audio.lang,
471
+ isDefault: audio.default
391
472
  };
392
473
  if(audio.playout) {
393
474
  audioStreams[audioIdx].playoutLabel = audio.playout_label || `Audio ${i + 1}`;
@@ -396,21 +477,34 @@ class LiveConf {
396
477
  }
397
478
 
398
479
  // If no audio streams specified in custom config, set up all the suitable audio streams in the probe
399
- if(!customSettings.audio || Object.keys(customSettings.audio).length == 0) {
400
- audioStreams = this.getAudioStreamsFromProbe();
480
+ if(!audioSettings || Object.keys(audioSettings).length === 0) {
481
+ audioStreams = this.getAudioStreamsFromProbe({ladderProfile});
401
482
  }
402
483
 
403
484
  return audioStreams;
404
485
  }
405
486
 
406
487
  /*
407
- * Generate the live recording config as required by QFAB, based on defaults and optional custom settings.
488
+ * Generate the live recording config as required by QFAB, based on defaults, existing settings and optional custom settings.
408
489
  */
409
490
  generateLiveConf({customSettings}) {
410
- // gather required data
411
- const conf = JSON.parse(JSON.stringify(LiveconfTemplate));
412
- const fileName = this.overwriteOriginUrl || this.probeData.format.filename;
413
- const audioStreams = this.generateAudioStreamsConfig({customSettings});
491
+ // Saved config overrides defaults and is preserved on reconfiguration
492
+ let conf = R.clone(LiveconfTemplate);
493
+
494
+ if(this.currentLiveRecordingMeta) {
495
+ conf = R.mergeDeepRight(conf,
496
+ {live_recording: this.currentLiveRecordingMeta});
497
+ }
498
+
499
+ if(customSettings.liveRecordingConfigProfile) {
500
+ conf = R.mergeDeepRight(conf,
501
+ this.MapCustomProfileToLiveConfig({
502
+ customProfile: customSettings.liveRecordingConfigProfile
503
+ }));
504
+ }
505
+
506
+ const fileName = this.url;
507
+ const audioStreams = this.generateAudioStreamsConfig({liveRecordingConfigProfile: customSettings.liveRecordingConfigProfile});
414
508
 
415
509
  // Retrieve one audio stream from the probe to read the sample rate and codec name
416
510
  const audioStream = this.getStreamDataForCodecType("audio");
@@ -440,17 +534,13 @@ class LiveConf {
440
534
  }
441
535
 
442
536
  // Fill in specifics for protocol
443
- switch(this.probeKind()) {
444
- case "udp":
445
- sourceTimescale = 90000;
446
- conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
447
- break;
448
- case "srt":
537
+ switch(this.getFormat()) {
538
+ case "mpegts":
449
539
  sourceTimescale = 90000;
450
540
  conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
451
- conf.live_recording.recording_config.recording_params.live_delay_nano = 4000000000;
452
541
  break;
453
542
  case "rtmp":
543
+ case "flv":
454
544
  sourceTimescale = 16000;
455
545
  conf.live_recording.recording_config.recording_params.source_timescale = sourceTimescale;
456
546
  break;
@@ -458,7 +548,15 @@ class LiveConf {
458
548
  console.log("HLS detected. Not yet implemented");
459
549
  break;
460
550
  default:
461
- console.log("Unsupported media", this.probeKind());
551
+ console.log("Unsupported media", this.getFormat());
552
+ break;
553
+ }
554
+
555
+ switch(this.getProtocol()) {
556
+ case "srt":
557
+ if(!customSettings.liveRecordingConfigProfile?.recording_params?.live_delay_nano) {
558
+ conf.live_recording.recording_config.recording_params.live_delay_nano = 6000000000;
559
+ }
462
560
  break;
463
561
  }
464
562
 
@@ -481,11 +579,15 @@ class LiveConf {
481
579
  conf.live_recording.recording_config.recording_params.xc_params.video_frame_duration_ts = segDurations.videoFrameDurationTs;
482
580
  }
483
581
 
484
- const ladderProfile = customSettings.ladder_profile || DefaultABRLadder;
582
+ const ladder_specs = customSettings.liveRecordingConfigProfile?.playout_config?.ladder_specs;
583
+ const ladderProfile = ladder_specs?.video?.length > 0 ? ladder_specs : DefaultABRLadder;
485
584
 
486
585
  conf.live_recording.recording_config.recording_params.xc_params.enc_height = videoStream.height;
487
586
  conf.live_recording.recording_config.recording_params.xc_params.enc_width = videoStream.width;
488
587
 
588
+ // Reset ladder specs (updating existing stream will carry over old specs
589
+ conf.live_recording.recording_config.recording_params.ladder_specs = [];
590
+
489
591
  // Determine video recording bitrate and ABR ladder
490
592
  let topLadderRate = 0;
491
593
  for(let i = 0; i < ladderProfile.video.length; i ++) {
@@ -521,6 +623,7 @@ class LiveConf {
521
623
  break;
522
624
  }
523
625
  }
626
+
524
627
  if(Object.keys(audioLadderSpec).length === 0) {
525
628
  // If no channels layout match, just use the first element in the ladder
526
629
  audioLadderSpec = {...ladderProfile.audio[0]};
@@ -532,8 +635,10 @@ class LiveConf {
532
635
  audioLadderSpec.stream_name = `audio_${audioIndex}`;
533
636
  audioLadderSpec.stream_label = audio.playoutLabel ? audio.playoutLabel : null;
534
637
  audioLadderSpec.media_type = 2;
638
+ audioLadderSpec.lang = audio.lang ?? "";
535
639
 
536
- if(Object.keys(audioStreams).length === 1) {
640
+
641
+ if(Object.keys(audioStreams).length === 1 || audio.isDefault) {
537
642
  audioLadderSpec.default = true;
538
643
  }
539
644
 
@@ -541,33 +646,13 @@ class LiveConf {
541
646
  if(audio.recordingBitrate > globalAudioBitrate) {
542
647
  globalAudioBitrate = audio.recordingBitrate;
543
648
  }
544
- nAudio ++;
649
+ nAudio++;
545
650
  }
546
651
 
547
652
  // Global recording bitrate for all audio streams
548
653
  conf.live_recording.recording_config.recording_params.xc_params.audio_bitrate = globalAudioBitrate;
549
654
  conf.live_recording.recording_config.recording_params.xc_params.n_audio = nAudio;
550
655
 
551
- // Iterate through custom settings (which will override any existing setting)
552
- function SetByPath({obj, path, value}) {
553
- const keys = path.split(".");
554
- let temp = obj;
555
- for(let i = 0; i < keys.length - 1; i++) {
556
- if(!temp[keys[i]]) {
557
- temp[keys[i]] = {};
558
- }
559
- temp = temp[keys[i]];
560
- }
561
-
562
- temp[keys[keys.length - 1]] = value;
563
- }
564
-
565
- const {metaPathValues} = customSettings;
566
-
567
- for(let [path, value] of Object.entries(metaPathValues || {})) {
568
- SetByPath({obj: conf, path, value});
569
- }
570
-
571
656
  return conf;
572
657
  }
573
658
  }