@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.
Files changed (41) hide show
  1. package/dist/ElvClient-min.js +1 -1
  2. package/dist/ElvClient-node-min.js +1 -1
  3. package/dist/ElvFrameClient-min.js +1 -1
  4. package/dist/ElvPermissionsClient-min.js +1 -1
  5. package/dist/ElvWalletClient-min.js +1 -1
  6. package/dist/ElvWalletClient-node-min.js +1 -1
  7. package/dist/src/AuthorizationClient.js +2 -1
  8. package/dist/src/ContentObjectAudit.js +2 -1
  9. package/dist/src/ContentObjectVerification.js +281 -0
  10. package/dist/src/ElvClient.js +8 -9
  11. package/dist/src/FrameClient.js +1 -1
  12. package/dist/src/HttpClient.js +83 -47
  13. package/dist/src/NetworkUrls.js +8 -0
  14. package/dist/src/abr_profiles/abr_profile_live_drm.js +0 -10
  15. package/dist/src/client/ContentAccess.js +76 -85
  16. package/dist/src/client/LiveConf.js +170 -84
  17. package/dist/src/client/LiveStream.js +5205 -2118
  18. package/dist/src/live_recording_config_profiles/live_recording_config_default.js +45 -0
  19. package/package.json +3 -2
  20. package/src/AuthorizationClient.js +2 -1
  21. package/src/ContentObjectAudit.js +4 -1
  22. package/src/ElvClient.js +8 -15
  23. package/src/FrameClient.js +23 -2
  24. package/src/HttpClient.js +17 -1
  25. package/src/NetworkUrls.js +9 -0
  26. package/src/abr_profiles/abr_profile_live_drm.js +0 -10
  27. package/src/client/ContentAccess.js +8 -23
  28. package/src/client/LiveConf.js +149 -65
  29. package/src/client/LiveStream.js +2592 -654
  30. package/src/live_recording_config_profiles/live_recording_config_default.js +54 -0
  31. package/src/live_recording_config_profiles/live_stream_profile_full.json +143 -0
  32. package/testScripts/StreamUpdateLinks.js +95 -0
  33. package/utilities/ChannelCreate.js +1 -1
  34. package/utilities/LibraryDownloadMp4.js +54 -8
  35. package/utilities/LibraryDownloadMp4Parallel.js +544 -0
  36. package/utilities/LiveOutputs.js +149 -0
  37. package/utilities/StreamCreate.js +53 -0
  38. package/utilities/lib/concerns/Client.js +5 -0
  39. package/utilities/lib/helpers.js +5 -1
  40. package/utilities/tests/mocks/ElvClient.mock.js +9 -1
  41. 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();
@@ -202,4 +202,4 @@ if(require.main === module) {
202
202
  Utility.cmdLineInvoke(ChannelCreate);
203
203
  } else {
204
204
  module.exports = ChannelCreate;
205
- }
205
+ }
@@ -200,7 +200,20 @@ class LibraryDownloadMp4 extends Utility {
200
200
  .substring(0, 180);
201
201
  }
202
202
 
203
- async processObject(e, client, libraryId, format, offering, targetDir, failedDownloads) {
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
- failedDownloads.push({
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 (this.args.failLog) {
357
- const failPath = path.resolve(this.args.failLog);
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