@eluvio/elv-client-js 4.2.24 → 4.2.27
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 +5 -1
- package/src/client/ABRPublishing.js +33 -19
- package/src/client/Files.js +14 -13
- package/src/client/LiveConf.js +46 -27
- package/src/client/LiveStream.js +15 -5
- package/utilities/MediaIngest.js +424 -0
- package/utilities/README_INGEST.md +332 -0
- package/utilities/lib/abr_profiles/abr_profile_4k_all.json +181 -0
- package/utilities/lib/concerns/Client.js +71 -10
- package/utilities/lib/concerns/CloudAccess.js +2 -2
- package/utilities/lib/concerns/CloudFile.js +14 -7
- package/utilities/lib/data/network_specs.json +13 -0
- package/utilities/lib/models/CredentialSet.js +26 -7
- package/utilities/tests/unit/lib/concerns/Client.test.js +96 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Media Ingest
|
|
2
|
+
|
|
3
|
+
## CLI: `utilities/MediaIngest.js`
|
|
4
|
+
|
|
5
|
+
Ingest a video file from disk or S3 into a new or existing fabric object.
|
|
6
|
+
|
|
7
|
+
Uses a single fabric object (no separate 'master' object) and does not store the source files with the object.
|
|
8
|
+
|
|
9
|
+
Three source modes:
|
|
10
|
+
|
|
11
|
+
| Source mode | Option | Fabric behavior | Source cleanup after ABR finalization |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| Local upload | none | Uploads and encrypts the file in the object | Deletes the uploaded Fabric file |
|
|
14
|
+
| S3 copy | `--s3-copy` | Server-side copies and encrypts the S3 object into the Fabric | Deletes the copied Fabric file |
|
|
15
|
+
| S3 reference | `--s3-reference` | Adds a link to the S3 object without copying or Fabric encryption | Retains the Fabric S3 reference |
|
|
16
|
+
|
|
17
|
+
Cleanup only affects files in the Fabric draft. MediaIngest never deletes the original local file or the original S3 object.
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm ci
|
|
23
|
+
export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
|
|
24
|
+
export FABRIC_CONFIG_URL="https://main.net955305.contentfabric.io/config"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Instead of `FABRIC_CONFIG_URL` you can use `--config-url URL` or `--network main|demvo3`.
|
|
28
|
+
|
|
29
|
+
## Local upload
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
node utilities/MediaIngest.js \
|
|
33
|
+
--library-id ilib... \
|
|
34
|
+
--title "Local source" \
|
|
35
|
+
--files /absolute/path/to/source.mxf
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If media comes from multiple source files:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
node utilities/MediaIngest.js \
|
|
42
|
+
--library-id ilib... \
|
|
43
|
+
--title "Separate video and audio" \
|
|
44
|
+
--files /absolute/path/to/picture.mxf /absolute/path/to/english.wav
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## S3 reference with a presigned URL
|
|
48
|
+
|
|
49
|
+
The signed URL is specified in the credentials JSON. The value passed to `--files` is the just the filename (no path). Fabric derives the bucket and parent directory from the signed URL.
|
|
50
|
+
|
|
51
|
+
Each `path_matchers` entry is a regular expression matched against the exact relative path passed to `--files`.
|
|
52
|
+
|
|
53
|
+
`credentials-reference-signed.json`:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
"path_matchers": ["^source\\.mxf$"],
|
|
59
|
+
"remote_access": {
|
|
60
|
+
"protocol": "s3",
|
|
61
|
+
"platform": "aws",
|
|
62
|
+
"cloud_credentials": {
|
|
63
|
+
"signed_url": "https://example-ingest-bucket.s3.us-west-2.amazonaws.com/media/source.mxf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=REDACTED&X-Amz-Signature=REDACTED"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
node utilities/MediaIngest.js \
|
|
72
|
+
--library-id ilib... \
|
|
73
|
+
--title "S3 reference, signed URL" \
|
|
74
|
+
--s3-reference \
|
|
75
|
+
--credentials ./credentials-reference-signed.json \
|
|
76
|
+
--files source.mxf
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For signed URLs, `remote_access.path` (bucket) and `storage_endpoint.region` are optional. The URL must authorize `GET` access and remain valid long enough for source inspection and transcoding.
|
|
80
|
+
|
|
81
|
+
## S3 reference with an access key and secret key
|
|
82
|
+
|
|
83
|
+
`credentials-reference-aksk.json`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
"path_matchers": ["^s3://example-ingest-bucket/media/source\\.mxf$"],
|
|
89
|
+
"remote_access": {
|
|
90
|
+
"protocol": "s3",
|
|
91
|
+
"platform": "aws",
|
|
92
|
+
"path": "example-ingest-bucket/",
|
|
93
|
+
"storage_endpoint": {
|
|
94
|
+
"region": "us-west-2"
|
|
95
|
+
},
|
|
96
|
+
"cloud_credentials": {
|
|
97
|
+
"access_key_id": "YOUR_AWS_ACCESS_KEY_ID",
|
|
98
|
+
"secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
node utilities/MediaIngest.js \
|
|
107
|
+
--library-id ilib... \
|
|
108
|
+
--title "S3 reference, access key" \
|
|
109
|
+
--s3-reference \
|
|
110
|
+
--credentials ./credentials-reference-aksk.json \
|
|
111
|
+
--files s3://example-ingest-bucket/media/source.mxf
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The bucket in a full `s3://` source must match `remote_access.path`. A bare key such as `media/source.mxf` also works when the credential file supplies the bucket.
|
|
115
|
+
|
|
116
|
+
## S3 copy with a presigned URL
|
|
117
|
+
|
|
118
|
+
Credentials and file arguments are the same as signed-URL reference. For the example URL below, that file is `source.mxf`.
|
|
119
|
+
|
|
120
|
+
`credentials-copy-signed.json`:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
"path_matchers": ["^source\\.mxf$"],
|
|
126
|
+
"remote_access": {
|
|
127
|
+
"protocol": "s3",
|
|
128
|
+
"platform": "aws",
|
|
129
|
+
"cloud_credentials": {
|
|
130
|
+
"signed_url": "https://example-ingest-bucket.s3.us-west-2.amazonaws.com/media/source.mxf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=REDACTED&X-Amz-Signature=REDACTED"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
node utilities/MediaIngest.js \
|
|
139
|
+
--library-id ilib... \
|
|
140
|
+
--title "S3 copy, signed URL" \
|
|
141
|
+
--s3-copy \
|
|
142
|
+
--credentials ./credentials-copy-signed.json \
|
|
143
|
+
--files source.mxf
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The server copies the object from S3 into the Fabric and then deletes the Fabric copy after the ABR mezzanine is finalized.
|
|
147
|
+
|
|
148
|
+
## S3 copy with an access key and secret key
|
|
149
|
+
|
|
150
|
+
`credentials-copy-aksk.json`:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
[
|
|
154
|
+
{
|
|
155
|
+
"path_matchers": ["^s3://example-ingest-bucket/media/source\\.mxf$"],
|
|
156
|
+
"remote_access": {
|
|
157
|
+
"protocol": "s3",
|
|
158
|
+
"platform": "aws",
|
|
159
|
+
"path": "example-ingest-bucket/",
|
|
160
|
+
"storage_endpoint": {
|
|
161
|
+
"region": "us-west-2"
|
|
162
|
+
},
|
|
163
|
+
"cloud_credentials": {
|
|
164
|
+
"access_key_id": "YOUR_AWS_ACCESS_KEY_ID",
|
|
165
|
+
"secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
node utilities/MediaIngest.js \
|
|
174
|
+
--library-id ilib... \
|
|
175
|
+
--title "S3 copy, access key" \
|
|
176
|
+
--s3-copy \
|
|
177
|
+
--credentials ./credentials-copy-aksk.json \
|
|
178
|
+
--files s3://example-ingest-bucket/media/source.mxf
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The reference and copy credential schemas are identical; the command-line mode determines whether the S3 object is linked or copied.
|
|
182
|
+
|
|
183
|
+
Access-key credentials can alternatively come from the environment:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
export AWS_REGION="us-west-2"
|
|
187
|
+
export AWS_BUCKET="example-ingest-bucket"
|
|
188
|
+
export AWS_KEY="YOUR_AWS_ACCESS_KEY_ID"
|
|
189
|
+
export AWS_SECRET="YOUR_AWS_SECRET_ACCESS_KEY"
|
|
190
|
+
|
|
191
|
+
node utilities/MediaIngest.js \
|
|
192
|
+
--library-id ilib... \
|
|
193
|
+
--title "S3 copy, environment credentials" \
|
|
194
|
+
--s3-copy \
|
|
195
|
+
--files media/source.mxf
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## S3 credential matching and security
|
|
199
|
+
|
|
200
|
+
Each entry in a credentials file applies to the `--files` values matching one of its `path_matchers` regular expressions. Every S3 source must match an entry. For access-key credentials these values may be full `s3://` paths or object keys; for signed URLs they are paths relative to the signed URL's containing directory.
|
|
201
|
+
|
|
202
|
+
A normal S3 presigned URL authorizes one object. For multiple signed sources, use one credential entry and exact matcher per object:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
[
|
|
206
|
+
{
|
|
207
|
+
"path_matchers": ["^video\\.mxf$"],
|
|
208
|
+
"remote_access": {
|
|
209
|
+
"protocol": "s3",
|
|
210
|
+
"platform": "aws",
|
|
211
|
+
"cloud_credentials": {
|
|
212
|
+
"signed_url": "https://example-ingest-bucket.s3.amazonaws.com/media/video.mxf?X-Amz-Signature=REDACTED"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"path_matchers": ["^audio\\.wav$"],
|
|
218
|
+
"remote_access": {
|
|
219
|
+
"protocol": "s3",
|
|
220
|
+
"platform": "aws",
|
|
221
|
+
"cloud_credentials": {
|
|
222
|
+
"signed_url": "https://example-ingest-bucket.s3.amazonaws.com/media/audio.wav?X-Amz-Signature=REDACTED"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The corresponding argument is `--files video.mxf audio.wav`.
|
|
230
|
+
|
|
231
|
+
The access key, secret key, and presigned URL are sent for the S3 operation but are not stored by the client or in Fabric metadata.
|
|
232
|
+
|
|
233
|
+
## Audio
|
|
234
|
+
|
|
235
|
+
> TODO
|
|
236
|
+
|
|
237
|
+
## HDR10
|
|
238
|
+
|
|
239
|
+
Pass an HDR metadata file with `--hdr-info-file`:
|
|
240
|
+
|
|
241
|
+
`hdr-info.json`:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"master_display": "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
|
|
246
|
+
"max_cll": "1000,400"
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
node utilities/MediaIngest.js \
|
|
252
|
+
--library-id ilib... \
|
|
253
|
+
--title "HDR10 source" \
|
|
254
|
+
--files /absolute/path/to/hdr-source.mxf \
|
|
255
|
+
--hdr-info-file ./hdr-info.json
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## DRM and clear playback
|
|
259
|
+
|
|
260
|
+
The built-in profile contains both clear and DRM playout formats - MediaIngest selects only one group for each ingest:
|
|
261
|
+
|
|
262
|
+
| Command-line setting | Effective playout formats |
|
|
263
|
+
| --- | --- |
|
|
264
|
+
| No DRM argument | Clear DASH and clear HLS only |
|
|
265
|
+
| `--no-drm` | Clear DASH and clear HLS only |
|
|
266
|
+
| `--drm` | All DRM formats listed below; no clear formats |
|
|
267
|
+
|
|
268
|
+
The `--drm` option defaults to `false`.
|
|
269
|
+
|
|
270
|
+
With `--no-drm`, explicitly selects clear-only.
|
|
271
|
+
|
|
272
|
+
With `--drm`, MediaIngest retains these protected formats from the built-in profile:
|
|
273
|
+
|
|
274
|
+
- DASH Widevine
|
|
275
|
+
- HLS Widevine CENC
|
|
276
|
+
- HLS FairPlay
|
|
277
|
+
- HLS PlayReady CENC
|
|
278
|
+
- HLS Sample AES
|
|
279
|
+
- HLS AES-128
|
|
280
|
+
|
|
281
|
+
The profile stores mezzanine media encrypted at rest (`store_clear: false`) in either mode.
|
|
282
|
+
|
|
283
|
+
## ABR profile and variants
|
|
284
|
+
|
|
285
|
+
MediaIngest always uses the built-in [`abr_profile_4k_all.json`](utilities/lib/abr_profiles/abr_profile_4k_all.json). There is currently no command-line option for supplying a different ABR profile but you can modify the JSON file as needed.
|
|
286
|
+
|
|
287
|
+
The video profile is parametric. These are its base rungs for a 16:9, 30 fps source:
|
|
288
|
+
|
|
289
|
+
| Base rung | Base bitrate |
|
|
290
|
+
| --- | ---: |
|
|
291
|
+
| 3840x2160 | 14 Mbps |
|
|
292
|
+
| 2560x1440 | 11.5 Mbps |
|
|
293
|
+
| 1920x1080 | 9.5 Mbps |
|
|
294
|
+
| 1280x720 | 4.5 Mbps |
|
|
295
|
+
| 854x480 | 1.75 Mbps |
|
|
296
|
+
| 640x360 | 810 Kbps |
|
|
297
|
+
| 426x240 | 500 Kbps |
|
|
298
|
+
|
|
299
|
+
At ingest time, the profile generator adapts rung dimensions to the source aspect ratio, snaps near-standard aspect ratios when they are within 6%, and adjusts bitrates for the source frame rate relative to the 30 fps base. It never upscales (eg. a 1920x1080 source has no 1440p or 2160p output). The profile accepts source frame rates from 15 through 60 fps and caps a generated video rung at 30 Mbps. An audio-only variant produces no video ladder.
|
|
300
|
+
|
|
301
|
+
The base profile does not force a video codec. When `--hdr-info-file` is present, MediaIngest adds HEVC (`h265`) and 10-bit encoding requirements to the generated profile. Both audio and video use a two-second segment target with 15 segments per chunk. The audio ladder contains pregenerated rungs for mono at 128 Kbps, stereo at 192 Kbps, and 5.1 at 384 Kbps.
|
|
302
|
+
|
|
303
|
+
Variants and offerings are separate selections:
|
|
304
|
+
|
|
305
|
+
- `--variant-key KEY` selects an existing production-master variant and defaults to `default`. It does not create or modify a variant. MediaIngest does not pass a `streamKeys` filter, so all streams in the selected variant are included in mezzanine creation.
|
|
306
|
+
- `--offering-key KEY` selects the mezzanine offering to create and also defaults to `default`. Changing it does not change which production-master streams are selected.
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
## Existing objects and finalization
|
|
310
|
+
|
|
311
|
+
To ingest into an existing object, pass `--object-id` instead of `--library-id`. The utility discovers the object's library and opens a draft:
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
node utilities/MediaIngest.js \
|
|
315
|
+
--object-id iq__... \
|
|
316
|
+
--title "Replacement ingest" \
|
|
317
|
+
--files /absolute/path/to/source.mxf
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`--title` is only required for a newly created object
|
|
321
|
+
|
|
322
|
+
By default the completed object is finalized and MediaIngest waits for publication. `--no-finalize` leaves the object draft open and prints its write token and node. Source cleanup still occurs after ABR finalization even when object finalization is skipped.
|
|
323
|
+
|
|
324
|
+
## Operational notes
|
|
325
|
+
|
|
326
|
+
- Local upload, S3 copy, and S3 reference are mutually exclusive invocation-wide modes. Do not mix local and S3 paths in one run.
|
|
327
|
+
- Every input is stored at `path.basename(source)`. Inputs from different directories that share a filename therefore collide.
|
|
328
|
+
- Captions/subtitles, explicit language labels, custom stream maps, clipping, watermarks, and custom ABR profiles are not exposed by MediaIngest.
|
|
329
|
+
- Progress is written to `./ingest-status.json`, including the object ID and write token. Automatic restart/resume though is not currently implemented
|
|
330
|
+
- If ingest fails before cleanup, uploaded or copied sources may remain in the open draft. Use the reported object ID/write token and `ingest-status.json` for diagnosis and manual cleanup.
|
|
331
|
+
|
|
332
|
+
Run `node utilities/MediaIngest.js --help` for the complete option list.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
{
|
|
2
|
+
"audio_individual_drm_keys": false,
|
|
3
|
+
"no_upscale": true,
|
|
4
|
+
"drm_optional": true,
|
|
5
|
+
"store_clear": false,
|
|
6
|
+
"video_parametric_ladder": {
|
|
7
|
+
"baseAspectRatio": "16/9",
|
|
8
|
+
"baseFrameRate": "30",
|
|
9
|
+
"rungSpecs": [
|
|
10
|
+
{
|
|
11
|
+
"dim": 2160,
|
|
12
|
+
"bitrate": 14000000
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"dim": 1440,
|
|
16
|
+
"bitrate": 11500000
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"dim": 1080,
|
|
20
|
+
"bitrate": 9500000
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"dim": 720,
|
|
24
|
+
"bitrate": 4500000
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"dim": 480,
|
|
28
|
+
"bitrate": 1750000
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"dim": 360,
|
|
32
|
+
"bitrate": 810000
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"dim": 240,
|
|
36
|
+
"bitrate": 500000
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"options": {
|
|
40
|
+
"upscale": false,
|
|
41
|
+
"snapAR": true,
|
|
42
|
+
"maxARSnap": 0.06,
|
|
43
|
+
"minDimStepdown": 0.12,
|
|
44
|
+
"frameRateScaleFactor": 0.5
|
|
45
|
+
},
|
|
46
|
+
"limits": {
|
|
47
|
+
"aspectRatioMax": "3",
|
|
48
|
+
"aspectRatioMin": "1/3",
|
|
49
|
+
"avgBitrateMax": 100000000,
|
|
50
|
+
"avgBitrateMin": 100000,
|
|
51
|
+
"fileSizeMax": 100000000,
|
|
52
|
+
"fileSizeMin": 10000,
|
|
53
|
+
"durationMax": 14400,
|
|
54
|
+
"durationMin": 1,
|
|
55
|
+
"finalBitrateMax": 30000000,
|
|
56
|
+
"frameRateMax": "60",
|
|
57
|
+
"frameRateMin": "15",
|
|
58
|
+
"heightMax": 5000,
|
|
59
|
+
"heightMin": 100,
|
|
60
|
+
"sampleAspectRatioMax": "3/2",
|
|
61
|
+
"sampleAspectRatioMin": "2/3",
|
|
62
|
+
"widthMax": 5000,
|
|
63
|
+
"widthMin": 100
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"ladder_specs": {
|
|
67
|
+
"{\"media_type\":\"audio\",\"channels\":1}": {
|
|
68
|
+
"rung_specs": [
|
|
69
|
+
{
|
|
70
|
+
"bit_rate": 128000,
|
|
71
|
+
"media_type": "audio",
|
|
72
|
+
"pregenerate": true
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"{\"media_type\":\"audio\",\"channels\":2}": {
|
|
77
|
+
"rung_specs": [
|
|
78
|
+
{
|
|
79
|
+
"bit_rate": 192000,
|
|
80
|
+
"media_type": "audio",
|
|
81
|
+
"pregenerate": true
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
"{\"media_type\":\"audio\",\"channels\":6}": {
|
|
86
|
+
"rung_specs": [
|
|
87
|
+
{
|
|
88
|
+
"bit_rate": 384000,
|
|
89
|
+
"media_type": "audio",
|
|
90
|
+
"pregenerate": true
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"playout_formats": {
|
|
96
|
+
"dash-clear": {
|
|
97
|
+
"drm": null,
|
|
98
|
+
"protocol": {
|
|
99
|
+
"min_buffer_length": 2,
|
|
100
|
+
"type": "ProtoDash"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"dash-widevine": {
|
|
104
|
+
"drm": {
|
|
105
|
+
"content_id": "",
|
|
106
|
+
"enc_scheme_name": "cenc",
|
|
107
|
+
"lic_srvs_in_manifest": true,
|
|
108
|
+
"license_servers": [],
|
|
109
|
+
"mp4ff_encryptor": true,
|
|
110
|
+
"type": "DrmWidevine"
|
|
111
|
+
},
|
|
112
|
+
"protocol": {
|
|
113
|
+
"min_buffer_length": 2,
|
|
114
|
+
"type": "ProtoDash"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"hls-aes128": {
|
|
118
|
+
"drm": {
|
|
119
|
+
"enc_scheme_name": "aes-128",
|
|
120
|
+
"type": "DrmAes128"
|
|
121
|
+
},
|
|
122
|
+
"protocol": {
|
|
123
|
+
"type": "ProtoHls"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"hls-clear": {
|
|
127
|
+
"drm": null,
|
|
128
|
+
"protocol": {
|
|
129
|
+
"type": "ProtoHls"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"hls-fairplay": {
|
|
133
|
+
"drm": {
|
|
134
|
+
"enc_scheme_name": "cbcs",
|
|
135
|
+
"license_servers": [],
|
|
136
|
+
"type": "DrmFairplay"
|
|
137
|
+
},
|
|
138
|
+
"protocol": {
|
|
139
|
+
"type": "ProtoHls"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"hls-playready-cenc": {
|
|
143
|
+
"drm": {
|
|
144
|
+
"enc_scheme_name": "cenc",
|
|
145
|
+
"type": "DrmPlayReady"
|
|
146
|
+
},
|
|
147
|
+
"protocol": {
|
|
148
|
+
"type": "ProtoHls"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"hls-sample-aes": {
|
|
152
|
+
"drm": {
|
|
153
|
+
"enc_scheme_name": "cbcs",
|
|
154
|
+
"type": "DrmSampleAes"
|
|
155
|
+
},
|
|
156
|
+
"protocol": {
|
|
157
|
+
"type": "ProtoHls"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"hls-widevine-cenc": {
|
|
161
|
+
"drm": {
|
|
162
|
+
"content_id": "",
|
|
163
|
+
"enc_scheme_name": "cenc",
|
|
164
|
+
"type": "DrmWidevine"
|
|
165
|
+
},
|
|
166
|
+
"protocol": {
|
|
167
|
+
"type": "ProtoHls"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"segment_specs": {
|
|
172
|
+
"audio": {
|
|
173
|
+
"segs_per_chunk": 15,
|
|
174
|
+
"target_dur": 2
|
|
175
|
+
},
|
|
176
|
+
"video": {
|
|
177
|
+
"segs_per_chunk": 15,
|
|
178
|
+
"target_dur": 2
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
const {ElvClient} = require("../../../src/ElvClient");
|
|
2
2
|
|
|
3
3
|
const elvRegions = require("../data/elv_regions");
|
|
4
|
+
const networkSpecs = require("../data/network_specs.json");
|
|
4
5
|
|
|
5
6
|
const {NewOpt} = require("../options");
|
|
6
7
|
|
|
7
8
|
const Logger = require("./Logger");
|
|
8
9
|
|
|
10
|
+
const DEFAULT_NETWORK = "main";
|
|
11
|
+
const networkChoices = [...new Set([
|
|
12
|
+
...Object.keys(ElvClient.Networks()),
|
|
13
|
+
...Object.keys(networkSpecs)
|
|
14
|
+
])].sort();
|
|
15
|
+
|
|
9
16
|
const blueprint = {
|
|
10
17
|
name: "Client",
|
|
11
18
|
concerns: [Logger],
|
|
@@ -15,6 +22,12 @@ const blueprint = {
|
|
|
15
22
|
group: "API",
|
|
16
23
|
type: "string"
|
|
17
24
|
}),
|
|
25
|
+
NewOpt("network", {
|
|
26
|
+
choices: networkChoices,
|
|
27
|
+
descTemplate: `Network to use. Defaults to "${DEFAULT_NETWORK}" when neither --configUrl nor FABRIC_CONFIG_URL is supplied. Custom networks in utilities/lib/data/network_specs.json are initialized directly.`,
|
|
28
|
+
group: "API",
|
|
29
|
+
type: "string"
|
|
30
|
+
}),
|
|
18
31
|
NewOpt("debug", {
|
|
19
32
|
descTemplate: "Print debug logging for API calls",
|
|
20
33
|
group: "API",
|
|
@@ -49,7 +62,7 @@ const New = (context) => {
|
|
|
49
62
|
if(/^".+"$/.test(configUrl)) {
|
|
50
63
|
configUrl = configUrl.slice(1,-1);
|
|
51
64
|
}
|
|
52
|
-
const {debug, ethContractTimeout} = context.args;
|
|
65
|
+
const {debug, ethContractTimeout, network, node} = context.args;
|
|
53
66
|
const region = context.args.elvGeo;
|
|
54
67
|
const logger = context.concerns.Logger;
|
|
55
68
|
const privateKey = context.env.PRIVATE_KEY;
|
|
@@ -65,16 +78,64 @@ const New = (context) => {
|
|
|
65
78
|
if(!privateKey) {
|
|
66
79
|
throw Error("Please set environment variable PRIVATE_KEY");
|
|
67
80
|
}
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
|
|
82
|
+
if(network && networkSpecs[network]) {
|
|
83
|
+
// Initialize directly from a custom network spec, no config URL fetched.
|
|
84
|
+
const spec = networkSpecs[network];
|
|
85
|
+
const fabricURIs = spec.fabricURIs;
|
|
86
|
+
const ethereumURIs = spec.ethereumURIs;
|
|
87
|
+
const fileServiceURIs = spec.fileServiceURIs || fabricURIs;
|
|
88
|
+
|
|
89
|
+
logger.log(`Initializing elv-client-js from network spec "${network}" (no config URL)...`);
|
|
90
|
+
logger.log(` Content space: ${spec.contentSpaceId}`);
|
|
91
|
+
logger.log(` Fabric API: ${fabricURIs.join(", ")}`);
|
|
92
|
+
logger.log(` Ethereum API: ${ethereumURIs.join(", ")}`);
|
|
93
|
+
|
|
94
|
+
elvClient = new ElvClient({
|
|
95
|
+
contentSpaceId: spec.contentSpaceId,
|
|
96
|
+
networkId: spec.networkId,
|
|
97
|
+
networkName: spec.networkName || network,
|
|
98
|
+
fabricVersion: spec.fabricVersion || 3,
|
|
99
|
+
fabricURIs,
|
|
100
|
+
ethereumURIs,
|
|
101
|
+
fileServiceURIs,
|
|
102
|
+
authServiceURIs: spec.authServiceURIs || [],
|
|
103
|
+
searchURIs: spec.searchURIs || [],
|
|
104
|
+
ethereumContractTimeout: ethContractTimeout
|
|
105
|
+
});
|
|
106
|
+
} else if(network) {
|
|
107
|
+
logger.log(`Initializing elv-client-js from network "${network}"...`);
|
|
108
|
+
elvClient = await ElvClient.FromNetworkName({
|
|
109
|
+
networkName: network,
|
|
110
|
+
region,
|
|
111
|
+
ethereumContractTimeout: ethContractTimeout
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
if(configUrl) {
|
|
115
|
+
logger.log(`Initializing elv-client-js... (config URL: ${configUrl})`);
|
|
116
|
+
elvClient = await ElvClient.FromConfigurationUrl({
|
|
117
|
+
configUrl,
|
|
118
|
+
region,
|
|
119
|
+
ethereumContractTimeout: ethContractTimeout
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
logger.log(`Initializing elv-client-js from default network "${DEFAULT_NETWORK}"...`);
|
|
123
|
+
elvClient = await ElvClient.FromNetworkName({
|
|
124
|
+
networkName: DEFAULT_NETWORK,
|
|
125
|
+
region,
|
|
126
|
+
ethereumContractTimeout: ethContractTimeout
|
|
127
|
+
});
|
|
128
|
+
}
|
|
70
129
|
}
|
|
71
130
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
if(node) {
|
|
132
|
+
const nodeUrl = /^https?:\/\//i.test(node) ? node : `https://${node}`;
|
|
133
|
+
logger.log(`Pinning fabric and file-service requests to ${nodeUrl}...`);
|
|
134
|
+
elvClient.SetNodes({
|
|
135
|
+
fabricURIs: [nodeUrl],
|
|
136
|
+
fileServiceURIs: [nodeUrl]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
78
139
|
|
|
79
140
|
let wallet = elvClient.GenerateWallet();
|
|
80
141
|
let signer = wallet.AddAccount({privateKey});
|
|
@@ -98,4 +159,4 @@ const New = (context) => {
|
|
|
98
159
|
module.exports = {
|
|
99
160
|
blueprint,
|
|
100
161
|
New
|
|
101
|
-
};
|
|
162
|
+
};
|
|
@@ -8,7 +8,7 @@ const blueprint = {
|
|
|
8
8
|
concerns: [JSON],
|
|
9
9
|
options: [
|
|
10
10
|
NewOpt("credentials", {
|
|
11
|
-
descTemplate: "Path to JSON file containing credential sets for files stored in cloud",
|
|
11
|
+
descTemplate: "Path to JSON file containing access key or signed URL credential sets for files stored in cloud",
|
|
12
12
|
group: "Cloud",
|
|
13
13
|
normalize: true,
|
|
14
14
|
type: "string"
|
|
@@ -66,4 +66,4 @@ const New = context => {
|
|
|
66
66
|
return {credentialSet};
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
module.exports = {blueprint, New};
|
|
69
|
+
module.exports = {blueprint, New};
|
|
@@ -34,7 +34,10 @@ const accessMatchIndex = R.curry(
|
|
|
34
34
|
const groupByPathMatch = (access, fileList) => R.groupBy(accessMatchIndex(access), fileList);
|
|
35
35
|
|
|
36
36
|
const validateBucketMatch = R.curry((credentialSet, sourceFilePath) => {
|
|
37
|
-
const
|
|
37
|
+
const remotePath = credentialSet.remote_access.path;
|
|
38
|
+
if(!remotePath) return;
|
|
39
|
+
|
|
40
|
+
const credentialSetBucket = removeTrailingSlash(remotePath);
|
|
38
41
|
// if full s3 path supplied, check bucket name
|
|
39
42
|
const s3prefixMatch = s3BucketRegex.exec(sourceFilePath);
|
|
40
43
|
if(s3prefixMatch) {
|
|
@@ -96,17 +99,20 @@ const New = context => {
|
|
|
96
99
|
if(kindOf(copy) === "undefined") copy = isCopy();
|
|
97
100
|
files = files || context.args.files;
|
|
98
101
|
const groupedFiles = groupByPathMatch(access, files);
|
|
99
|
-
validatePathMatchGroups(groupedFiles);
|
|
102
|
+
validatePathMatchGroups(access, groupedFiles);
|
|
100
103
|
|
|
101
104
|
const client = await context.concerns.Client.get();
|
|
102
105
|
|
|
103
106
|
// iterate over file groups, add to fabric object using credential set for group
|
|
104
107
|
for(const [index, fileList] of R.toPairs(groupedFiles)) {
|
|
105
108
|
const credentialSet = access[index];
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
109
|
+
const remoteAccess = credentialSet.remote_access;
|
|
110
|
+
const cloudCredentials = remoteAccess.cloud_credentials;
|
|
111
|
+
const region = remoteAccess.storage_endpoint && remoteAccess.storage_endpoint.region;
|
|
112
|
+
const bucket = remoteAccess.path && removeTrailingSlash(remoteAccess.path);
|
|
113
|
+
const accessKey = cloudCredentials.access_key_id;
|
|
114
|
+
const secret = cloudCredentials.secret_access_key;
|
|
115
|
+
const signedUrl = cloudCredentials.signed_url;
|
|
110
116
|
|
|
111
117
|
await client.UploadFilesFromS3({
|
|
112
118
|
libraryId,
|
|
@@ -117,6 +123,7 @@ const New = context => {
|
|
|
117
123
|
bucket,
|
|
118
124
|
accessKey,
|
|
119
125
|
secret,
|
|
126
|
+
signedUrl,
|
|
120
127
|
copy,
|
|
121
128
|
callback,
|
|
122
129
|
encryption: encrypt ? "cgck" : "none"
|
|
@@ -134,4 +141,4 @@ const New = context => {
|
|
|
134
141
|
return {add, callback, credentialSet, fileInfo, isCopy};
|
|
135
142
|
};
|
|
136
143
|
|
|
137
|
-
module.exports = {blueprint, New};
|
|
144
|
+
module.exports = {blueprint, New};
|