@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.
- package/package.json +3 -2
- package/src/AuthorizationClient.js +1 -6
- package/src/FrameClient.js +23 -2
- package/src/abr_profiles/abr_profile_live_drm.js +0 -10
- package/src/client/ContentAccess.js +54 -64
- package/src/client/LiveConf.js +150 -65
- package/src/client/LiveStream.js +2613 -654
- package/src/client/NTP.js +71 -0
- package/src/live_recording_config_profiles/live_recording_config_default.js +54 -0
- package/src/live_recording_config_profiles/live_stream_profile_full.json +143 -0
- package/testScripts/StreamUpdateLinks.js +95 -0
- package/utilities/LiveOutputs.js +149 -0
- package/utilities/StreamCreate.js +53 -0
- package/utilities/lib/helpers.js +5 -1
- package/utilities/tests/mocks/ElvClient.mock.js +9 -1
- package/utilities/tests/unit/StreamCreate.test.js +39 -0
package/src/client/LiveConf.js
CHANGED
|
@@ -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:
|
|
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:
|
|
69
|
+
live_delay_nano: 6000000000,
|
|
68
70
|
max_duration_sec: -1,
|
|
69
71
|
name: "",
|
|
70
72
|
origin_url: "",
|
|
71
|
-
part_ttl:
|
|
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(
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
209
|
-
case "
|
|
243
|
+
switch(this.getFormat()) {
|
|
244
|
+
case "flv":
|
|
210
245
|
seg = this.calcSegDurationRtmp({sourceTimescale, sampleRate, audioCodec});
|
|
211
246
|
break;
|
|
212
|
-
case "
|
|
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.
|
|
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.
|
|
366
|
-
case "
|
|
367
|
-
case "srt":
|
|
399
|
+
switch(this.getFormat()) {
|
|
400
|
+
case "mpegts":
|
|
368
401
|
sync_id = videoStream.stream_id;
|
|
369
402
|
break;
|
|
370
|
-
case "
|
|
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({
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
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(!
|
|
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
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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.
|
|
444
|
-
case "
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|