@eluvio/elv-client-js 4.2.15 → 4.2.17
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/dist/ElvClient-min.js +1 -1
- package/dist/ElvClient-node-min.js +1 -1
- package/dist/ElvFrameClient-min.js +1 -1
- package/dist/ElvPermissionsClient-min.js +1 -1
- package/dist/ElvWalletClient-min.js +1 -1
- package/dist/ElvWalletClient-node-min.js +1 -1
- package/dist/src/AuthorizationClient.js +2 -1
- package/dist/src/ContentObjectAudit.js +2 -1
- package/dist/src/ContentObjectVerification.js +281 -0
- package/dist/src/ElvClient.js +8 -9
- package/dist/src/FrameClient.js +1 -1
- package/dist/src/HttpClient.js +83 -47
- package/dist/src/NetworkUrls.js +8 -0
- package/dist/src/abr_profiles/abr_profile_live_drm.js +0 -10
- package/dist/src/client/ContentAccess.js +76 -85
- package/dist/src/client/LiveConf.js +170 -84
- package/dist/src/client/LiveStream.js +5205 -2118
- package/dist/src/live_recording_config_profiles/live_recording_config_default.js +45 -0
- package/package.json +3 -2
- package/src/AuthorizationClient.js +2 -1
- package/src/ContentObjectAudit.js +4 -1
- package/src/ElvClient.js +8 -15
- package/src/FrameClient.js +23 -2
- package/src/HttpClient.js +17 -1
- package/src/NetworkUrls.js +9 -0
- package/src/abr_profiles/abr_profile_live_drm.js +0 -10
- package/src/client/ContentAccess.js +8 -23
- package/src/client/LiveConf.js +149 -65
- package/src/client/LiveStream.js +2592 -654
- 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/ChannelCreate.js +1 -1
- package/utilities/LibraryDownloadMp4.js +54 -8
- package/utilities/LibraryDownloadMp4Parallel.js +544 -0
- package/utilities/LiveOutputs.js +149 -0
- package/utilities/StreamCreate.js +53 -0
- package/utilities/lib/concerns/Client.js +5 -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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const LiveRecordingConfigDefault = {
|
|
2
|
+
drm_type: "clear",
|
|
3
|
+
recording_config: {
|
|
4
|
+
part_ttl: 86400,
|
|
5
|
+
reconnect_timeout: 3600,
|
|
6
|
+
connection_timeout: 3600,
|
|
7
|
+
copy_mpegts: false,
|
|
8
|
+
},
|
|
9
|
+
profile: {
|
|
10
|
+
ladder_specs: {
|
|
11
|
+
audio: [
|
|
12
|
+
{
|
|
13
|
+
bit_rate: 192000,
|
|
14
|
+
channels: 2,
|
|
15
|
+
codecs: "mp4a.40.2"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
bit_rate: 384000,
|
|
19
|
+
channels: 6,
|
|
20
|
+
codecs: "mp4a.40.2"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
video: [
|
|
24
|
+
{
|
|
25
|
+
bit_rate: 9500000,
|
|
26
|
+
codecs: "avc1.640028,mp4a.40.2",
|
|
27
|
+
height: 1080,
|
|
28
|
+
width: 1920
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
bit_rate: 4500000,
|
|
32
|
+
codecs: "avc1.640028,mp4a.40.2",
|
|
33
|
+
height: 720,
|
|
34
|
+
width: 1280
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
bit_rate: 2000000,
|
|
38
|
+
codecs: "avc1.640028,mp4a.40.2",
|
|
39
|
+
height: 540,
|
|
40
|
+
width: 960
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
bit_rate: 900000,
|
|
44
|
+
codecs: "avc1.640028,mp4a.40.2",
|
|
45
|
+
height: 540,
|
|
46
|
+
width: 960
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
name: "Default"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = LiveRecordingConfigDefault;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Reference Full Profile",
|
|
3
|
+
"drm_type": "clear",
|
|
4
|
+
|
|
5
|
+
"recording_config": {
|
|
6
|
+
"part_ttl": 86400,
|
|
7
|
+
"reconnect_timeout": 3600,
|
|
8
|
+
"connection_timeout": 3600,
|
|
9
|
+
"copy_mpegts": false,
|
|
10
|
+
"input_cfg": {
|
|
11
|
+
"bypass_libav_reader": false,
|
|
12
|
+
"copy_mode": "",
|
|
13
|
+
"copy_packaging": "",
|
|
14
|
+
"custom_read_loop_enabled": false,
|
|
15
|
+
"input_packaging": ""
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"playout_config": {
|
|
20
|
+
"dvr_enabled": true,
|
|
21
|
+
"dvr_max_duration": 14400,
|
|
22
|
+
"rebroadcast_start_time_sec_epoch": 0,
|
|
23
|
+
"playout_sharding_level": 2,
|
|
24
|
+
"vod_enabled": false,
|
|
25
|
+
"playout_formats": [
|
|
26
|
+
"hls-clear"
|
|
27
|
+
],
|
|
28
|
+
"ladder_specs": {
|
|
29
|
+
"audio": [
|
|
30
|
+
{
|
|
31
|
+
"bit_rate": 192000,
|
|
32
|
+
"channels": 2,
|
|
33
|
+
"codecs": "mp4a.40.2"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"bit_rate": 384000,
|
|
37
|
+
"channels": 6,
|
|
38
|
+
"codecs": "mp4a.40.2"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"video": [
|
|
42
|
+
{
|
|
43
|
+
"bit_rate": 14000000,
|
|
44
|
+
"codecs": "avc1.640028,mp4a.40.2",
|
|
45
|
+
"height": 2160,
|
|
46
|
+
"width": 3840
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"bit_rate": 9500000,
|
|
50
|
+
"codecs": "avc1.640028,mp4a.40.2",
|
|
51
|
+
"height": 1080,
|
|
52
|
+
"width": 1920
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"bit_rate": 4500000,
|
|
56
|
+
"codecs": "avc1.640028,mp4a.40.2",
|
|
57
|
+
"height": 720,
|
|
58
|
+
"width": 1280
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"bit_rate": 2000000,
|
|
62
|
+
"codecs": "avc1.640028,mp4a.40.2",
|
|
63
|
+
"height": 540,
|
|
64
|
+
"width": 960
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"bit_rate": 900000,
|
|
68
|
+
"codecs": "avc1.640028,mp4a.40.2",
|
|
69
|
+
"height": 540,
|
|
70
|
+
"width": 960
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"simple_watermark": {
|
|
75
|
+
"font_color": "white@0.5",
|
|
76
|
+
"font_relative_height": 0.05,
|
|
77
|
+
"shadow": true,
|
|
78
|
+
"shadow_color": "black@0.5",
|
|
79
|
+
"template": "",
|
|
80
|
+
"x": "(w-tw)/2",
|
|
81
|
+
"y": "h-(4*lh)"
|
|
82
|
+
},
|
|
83
|
+
"image_watermark": {
|
|
84
|
+
"align_h": "right",
|
|
85
|
+
"align_v": "bottom",
|
|
86
|
+
"image": "",
|
|
87
|
+
"margin_h": "1/20",
|
|
88
|
+
"margin_v": "1/10",
|
|
89
|
+
"target_video_height": 1080,
|
|
90
|
+
"wm_enabled": false
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
"recording_stream_config": {
|
|
95
|
+
"audio": {
|
|
96
|
+
"0": {
|
|
97
|
+
"bitrate": 192000,
|
|
98
|
+
"codec": "aac",
|
|
99
|
+
"playout": true,
|
|
100
|
+
"playout_label": "Audio 1",
|
|
101
|
+
"record": true,
|
|
102
|
+
"recording_bitrate": 192000,
|
|
103
|
+
"recording_channels": 2,
|
|
104
|
+
"lang": ""
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
"recording_params": {
|
|
110
|
+
"description": "",
|
|
111
|
+
"listen": true,
|
|
112
|
+
"live_delay_nano": 6000000000,
|
|
113
|
+
"max_duration_sec": -1,
|
|
114
|
+
"playout_type": "live",
|
|
115
|
+
"source_timescale": null,
|
|
116
|
+
"xc_params": {
|
|
117
|
+
"audio_bitrate": 384000,
|
|
118
|
+
"audio_index": [0, 0, 0, 0, 0, 0, 0, 0],
|
|
119
|
+
"audio_seg_duration_ts": null,
|
|
120
|
+
"connection_timeout": 600,
|
|
121
|
+
"ecodec2": "aac",
|
|
122
|
+
"enc_height": null,
|
|
123
|
+
"enc_width": null,
|
|
124
|
+
"filter_descriptor": "",
|
|
125
|
+
"force_keyint": null,
|
|
126
|
+
"format": "fmp4-segment",
|
|
127
|
+
"listen": true,
|
|
128
|
+
"n_audio": 1,
|
|
129
|
+
"preset": "faster",
|
|
130
|
+
"sample_rate": 48000,
|
|
131
|
+
"seg_duration": null,
|
|
132
|
+
"skip_decoding": false,
|
|
133
|
+
"start_segment_str": "1",
|
|
134
|
+
"stream_id": -1,
|
|
135
|
+
"sync_audio_to_stream_id": -1,
|
|
136
|
+
"video_bitrate": null,
|
|
137
|
+
"video_seg_duration_ts": null,
|
|
138
|
+
"video_time_base": null,
|
|
139
|
+
"video_frame_duration_ts": null,
|
|
140
|
+
"xc_type": 3
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const { ElvClient } = require("../src/index");
|
|
2
|
+
|
|
3
|
+
const Test = async () => {
|
|
4
|
+
try {
|
|
5
|
+
const client = await ElvClient.FromNetworkName({
|
|
6
|
+
networkName: "demo"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const wallet = client.GenerateWallet();
|
|
10
|
+
const signer = wallet.AddAccount({
|
|
11
|
+
privateKey: process.env.PRIVATE_KEY
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
client.SetSigner({signer});
|
|
15
|
+
|
|
16
|
+
CreateLink = ({
|
|
17
|
+
targetHash,
|
|
18
|
+
linkTarget="meta/public/asset_metadata",
|
|
19
|
+
options={},
|
|
20
|
+
autoUpdate=true
|
|
21
|
+
}) => {
|
|
22
|
+
return {
|
|
23
|
+
...options,
|
|
24
|
+
".": {
|
|
25
|
+
...(options["."] || {}),
|
|
26
|
+
...autoUpdate ? {"auto_update": {"tag": "latest"}} : undefined
|
|
27
|
+
},
|
|
28
|
+
"/": `/qfab/${targetHash}/${linkTarget}`
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
UpdateStreamLink = async ({siteLibraryId, siteId, objectId, slug}) => {
|
|
33
|
+
try {
|
|
34
|
+
const originalLink = await client.ContentObjectMetadata({
|
|
35
|
+
libraryId: siteLibraryId,
|
|
36
|
+
objectId: siteId,
|
|
37
|
+
metadataSubtree: `public/asset_metadata/live_streams/${slug}`,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const link = CreateLink({
|
|
41
|
+
targetHash: await client.LatestVersionHash({objectId}),
|
|
42
|
+
options: {order: originalLink.order}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await client.ReplaceMetadata({
|
|
46
|
+
libraryId: siteLibraryId,
|
|
47
|
+
objectId: siteId,
|
|
48
|
+
writeToken,
|
|
49
|
+
metadataSubtree: `public/asset_metadata/live_streams/${slug}`,
|
|
50
|
+
metadata: link
|
|
51
|
+
});
|
|
52
|
+
} catch(error) {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.error("Unable to update stream link", error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {streamMetadata, siteObjectId, siteLibraryId} = await client.StreamGetSiteData({streamOptions: {resolveIncludeSource: false, resolveLinks: false}});
|
|
59
|
+
|
|
60
|
+
const {writeToken} = await client.EditContentObject({
|
|
61
|
+
libraryId: siteLibraryId,
|
|
62
|
+
objectId: siteObjectId
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for(let streamSlug in streamMetadata) {
|
|
66
|
+
const obj = streamMetadata[streamSlug];
|
|
67
|
+
|
|
68
|
+
const versionHash = obj["/"] ? obj["/"].split("/")[2] : obj["."].source;
|
|
69
|
+
const objId = client.utils.DecodeVersionHash(versionHash).objectId;
|
|
70
|
+
|
|
71
|
+
await UpdateStreamLink({
|
|
72
|
+
siteLibraryId,
|
|
73
|
+
objectId: objId,
|
|
74
|
+
siteId: siteObjectId,
|
|
75
|
+
slug: streamSlug
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await client.FinalizeContentObject({
|
|
80
|
+
libraryId: siteLibraryId,
|
|
81
|
+
objectId: siteObjectId,
|
|
82
|
+
writeToken,
|
|
83
|
+
commitMessage: "Update stream link",
|
|
84
|
+
awaitCommitConfirmation: true
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
} catch(error) {
|
|
88
|
+
console.error(error);
|
|
89
|
+
console.error(JSON.stringify(error, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.exit(0);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
Test();
|
|
@@ -200,7 +200,20 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
200
200
|
.substring(0, 180);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
appendFailEntry(failLogPath, entry) {
|
|
204
|
+
let entries = [];
|
|
205
|
+
if (fs.existsSync(failLogPath)) {
|
|
206
|
+
try {
|
|
207
|
+
entries = JSON.parse(fs.readFileSync(failLogPath, "utf8"));
|
|
208
|
+
} catch {
|
|
209
|
+
// If the file is malformed just start fresh
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
entries.push(entry);
|
|
213
|
+
fs.writeFileSync(failLogPath, JSON.stringify(entries, null, 2));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async processObject(e, client, libraryId, format, offering, targetDir, failedDownloads, failLogPath) {
|
|
204
217
|
const objectId = e.objectId;
|
|
205
218
|
const objectName = R.path(["metadata", "public", "name"], e) || objectId;
|
|
206
219
|
|
|
@@ -238,6 +251,8 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
238
251
|
// Poll job
|
|
239
252
|
let status;
|
|
240
253
|
let lastProgress = -1;
|
|
254
|
+
const maxPolls = 300; // 10 minutes at 2s intervals
|
|
255
|
+
let pollCount = 0;
|
|
241
256
|
|
|
242
257
|
do {
|
|
243
258
|
await this.sleep(2000);
|
|
@@ -249,6 +264,16 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
249
264
|
})
|
|
250
265
|
);
|
|
251
266
|
|
|
267
|
+
const jobStatus = status?.status;
|
|
268
|
+
if (jobStatus === "failed" || jobStatus === "error") {
|
|
269
|
+
throw new Error(`Job ${jobId} failed with status: ${jobStatus}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
pollCount++;
|
|
273
|
+
if (pollCount >= maxPolls) {
|
|
274
|
+
throw new Error(`Job ${jobId} timed out after ${maxPolls} polling attempts`);
|
|
275
|
+
}
|
|
276
|
+
|
|
252
277
|
const progress = status?.progress || 0;
|
|
253
278
|
if (progress !== lastProgress) {
|
|
254
279
|
process.stdout.write(`Progress: ${progress.toFixed(1)}%\r`);
|
|
@@ -278,6 +303,11 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
278
303
|
|
|
279
304
|
formattedObj.download_url = downloadUrl;
|
|
280
305
|
|
|
306
|
+
if (fs.existsSync(outputFile)) {
|
|
307
|
+
this.logger.log(`Skipping ${objectName}: already exists (${filename})`);
|
|
308
|
+
return formattedObj;
|
|
309
|
+
}
|
|
310
|
+
|
|
281
311
|
// NOW using HTTPS downloader
|
|
282
312
|
this.logger.log(`Downloading → ${outputFile}`);
|
|
283
313
|
await this.downloadFile(downloadUrl, outputFile);
|
|
@@ -288,12 +318,22 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
288
318
|
} catch (err) {
|
|
289
319
|
this.logger.error(`FAILED: ${objectId} - ${err.message}`);
|
|
290
320
|
|
|
291
|
-
|
|
321
|
+
const fileServiceUrl = client.FileServiceHttpClient?.uris?.[client.FileServiceHttpClient.uriIndex] || "unknown";
|
|
322
|
+
|
|
323
|
+
const failEntry = {
|
|
292
324
|
object_id: objectId,
|
|
293
325
|
name: objectName,
|
|
294
326
|
error: err.message,
|
|
327
|
+
file_service_url: fileServiceUrl,
|
|
295
328
|
timestamp: new Date().toISOString(),
|
|
296
|
-
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
failedDownloads.push(failEntry);
|
|
332
|
+
|
|
333
|
+
if (failLogPath) {
|
|
334
|
+
this.appendFailEntry(failLogPath, failEntry);
|
|
335
|
+
this.logger.warn(`Failure recorded → ${failLogPath}`);
|
|
336
|
+
}
|
|
297
337
|
|
|
298
338
|
return formattedObj;
|
|
299
339
|
}
|
|
@@ -328,6 +368,13 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
328
368
|
if (!fs.existsSync(targetDir))
|
|
329
369
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
330
370
|
|
|
371
|
+
// Initialize fail log file with empty array at the start of the run
|
|
372
|
+
const failLogPath = this.args.failLog ? path.resolve(this.args.failLog) : null;
|
|
373
|
+
if (failLogPath) {
|
|
374
|
+
fs.writeFileSync(failLogPath, JSON.stringify([], null, 2));
|
|
375
|
+
this.logger.log(`Fail log initialized: ${failLogPath}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
331
378
|
// Sequential downloads
|
|
332
379
|
const results = [];
|
|
333
380
|
for (const obj of objectList) {
|
|
@@ -338,7 +385,8 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
338
385
|
format,
|
|
339
386
|
offering,
|
|
340
387
|
targetDir,
|
|
341
|
-
failedDownloads
|
|
388
|
+
failedDownloads,
|
|
389
|
+
failLogPath
|
|
342
390
|
);
|
|
343
391
|
results.push(r);
|
|
344
392
|
}
|
|
@@ -353,10 +401,8 @@ class LibraryDownloadMp4 extends Utility {
|
|
|
353
401
|
this.logger.warn("\n=== FAILED DOWNLOADS ===");
|
|
354
402
|
this.logger.logTable({ list: failedDownloads });
|
|
355
403
|
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
fs.writeFileSync(failPath, JSON.stringify(failedDownloads, null, 2));
|
|
359
|
-
this.logger.warn(`Failures written to: ${failPath}`);
|
|
404
|
+
if (failLogPath) {
|
|
405
|
+
this.logger.warn(`Full failure log: ${failLogPath}`);
|
|
360
406
|
}
|
|
361
407
|
}
|
|
362
408
|
|