@eluvio/elv-client-js 4.2.14 → 4.2.15
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 +1 -1
- package/src/client/ContentAccess.js +1 -0
- package/utilities/ChannelCreate.js +1 -1
- package/utilities/CompositionCreate.js +517 -0
- package/utilities/LibraryDownloadMp4.js +371 -0
- package/utilities/MezDownloadMp4.js +177 -0
- package/utilities/lib/DownloadFile.js +88 -0
- package/utilities/lib/FrameAccurateVideo.js +431 -0
- package/utilities/lib/concerns/Metadata.js +2 -1
package/package.json
CHANGED
|
@@ -1505,6 +1505,7 @@ exports.PlayoutOptions = async function({
|
|
|
1505
1505
|
playoutMethods: {
|
|
1506
1506
|
...((playoutMap[protocol] || {}).playoutMethods || {}),
|
|
1507
1507
|
[drm || "clear"]: {
|
|
1508
|
+
properties: option.properties || {},
|
|
1508
1509
|
playoutUrl:
|
|
1509
1510
|
signedLink ?
|
|
1510
1511
|
await this.LinkUrl({
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* -----------------------------------------------------------------------------
|
|
3
|
+
* CompositionCreate.js — Usage Example
|
|
4
|
+
* -----------------------------------------------------------------------------
|
|
5
|
+
*
|
|
6
|
+
* This script creates (or updates) a CHANNEL / COMPOSITION offering on a
|
|
7
|
+
* base object by stitching together multiple mezzanine objects that all share
|
|
8
|
+
* identical playout parameters and can be clipped at the start and end time
|
|
9
|
+
* provided in fromat HH_MM_SS_MS
|
|
10
|
+
*
|
|
11
|
+
* Each item is specified as:
|
|
12
|
+
* - OBJECT_ID (default offering)
|
|
13
|
+
* - OBJECT_ID:OFFERING_KEY
|
|
14
|
+
* - OBJECT_ID:OFFERING_KEY:START_TC:END_TC
|
|
15
|
+
* - OBJECT_ID::START_TC:END_TC (default offering)
|
|
16
|
+
*
|
|
17
|
+
* Items are passed as a single comma-separated string.
|
|
18
|
+
*
|
|
19
|
+
* -----------------------------------------------------------------------------
|
|
20
|
+
* BASIC USAGE
|
|
21
|
+
* -----------------------------------------------------------------------------
|
|
22
|
+
*
|
|
23
|
+
* node CompositionCreate.js \
|
|
24
|
+
* --library-id ilib_xxxxxxxxxxxxxxxxx \
|
|
25
|
+
* --name "Full Game DASH" \
|
|
26
|
+
* --base-object-id iq__xxxxxxxxxxxxxxxx \
|
|
27
|
+
* --items iq__100:default_dash:1_2_3_4:2_3_4_5,iq__200:default_dash,iq__300:default_dash
|
|
28
|
+
* --base-range 1_2_3_4:2_3_4_5 => (hh_mm_ss_ms)
|
|
29
|
+
* --base-offering-key default_dash
|
|
30
|
+
*
|
|
31
|
+
* Without offering:
|
|
32
|
+
* node CompositionCreate.js \
|
|
33
|
+
* --library-id ilib_xxxxxxxxxxxxxxxxx \
|
|
34
|
+
* --name "Full Game DASH" \
|
|
35
|
+
* --base-object-id iq__xxxxxxxxxxxxxxxx \
|
|
36
|
+
* --items iq__100,iq__200,iq__300
|
|
37
|
+
* -----------------------------------------------------------------------------
|
|
38
|
+
* ARGUMENTS
|
|
39
|
+
* -----------------------------------------------------------------------------
|
|
40
|
+
*
|
|
41
|
+
* --library-id
|
|
42
|
+
* The content library where all objects exist.
|
|
43
|
+
*
|
|
44
|
+
* --name
|
|
45
|
+
* Display name for the channel offering.
|
|
46
|
+
* This value is sanitized and used as the offering key.
|
|
47
|
+
*
|
|
48
|
+
* --base-object-id
|
|
49
|
+
* The object ID where the channel metadata will be written.
|
|
50
|
+
* This object will receive /metadata/channel/offerings/<key>.
|
|
51
|
+
*
|
|
52
|
+
* --items
|
|
53
|
+
* Comma-separated list of OBJECT_ID:OFFERING_KEY pairs.
|
|
54
|
+
*
|
|
55
|
+
* Example:
|
|
56
|
+
* iq__100:default_dash
|
|
57
|
+
* iq__200:default_dash
|
|
58
|
+
* iq__300:default_dash
|
|
59
|
+
*
|
|
60
|
+
* All items MUST:
|
|
61
|
+
* - Have identical playout.streams.representations
|
|
62
|
+
* - Have identical playout.playout_formats
|
|
63
|
+
* - Have matching media_struct stream parameters
|
|
64
|
+
*
|
|
65
|
+
* -----------------------------------------------------------------------------
|
|
66
|
+
* WHAT THIS SCRIPT DOES
|
|
67
|
+
* -----------------------------------------------------------------------------
|
|
68
|
+
*
|
|
69
|
+
* 1. Fetches metadata for each OBJECT_ID.
|
|
70
|
+
* 2. Validates all items are playout-compatible.
|
|
71
|
+
* 3. Creates / merges metadata.channel.offerings on the base object.
|
|
72
|
+
* 4. Adds each item as a sequential channel source.
|
|
73
|
+
* 5. Sets createdAt on first item and updatedAt on last item.
|
|
74
|
+
* 6. Writes updated metadata back to Fabric.
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
const R = require("ramda");
|
|
79
|
+
const Fraction = require("fraction.js");
|
|
80
|
+
const { FrameAccurateVideo } = require("./lib/FrameAccurateVideo");
|
|
81
|
+
|
|
82
|
+
const { ModOpt, NewOpt, StdOpt } = require("./lib/options");
|
|
83
|
+
const Utility = require("./lib/Utility");
|
|
84
|
+
|
|
85
|
+
const ArgLibraryId = require("./lib/concerns/ArgLibraryId");
|
|
86
|
+
const FabricObject = require("./lib/concerns/FabricObject");
|
|
87
|
+
|
|
88
|
+
const STREAM_FIELDS = [
|
|
89
|
+
"aspect_ratio",
|
|
90
|
+
"channel_layout",
|
|
91
|
+
"channels",
|
|
92
|
+
"codec_name",
|
|
93
|
+
"codec_type",
|
|
94
|
+
"height",
|
|
95
|
+
"rate",
|
|
96
|
+
"width"
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse item string
|
|
101
|
+
* Formats:
|
|
102
|
+
* - OBJECT_ID
|
|
103
|
+
* - OBJECT_ID:OFFERING_KEY
|
|
104
|
+
* - OBJECT_ID:OFFERING_KEY:START_TC:END_TC
|
|
105
|
+
* - OBJECT_ID::START_TC:END_TC (default offering)
|
|
106
|
+
*/
|
|
107
|
+
const itemParser = (itemStr) => {
|
|
108
|
+
if (typeof itemStr !== "string" || !itemStr.trim()) {
|
|
109
|
+
throw new Error("Item must be a non-empty string");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const parts = itemStr.split(":");
|
|
113
|
+
|
|
114
|
+
const objectId = parts[0];
|
|
115
|
+
if(!objectId) {
|
|
116
|
+
throw new Error(`Invalid item format: '${itemStr}'`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const offering = parts[1] || "default";
|
|
120
|
+
|
|
121
|
+
const startTC = parts.length >= 3 ? parts[2] : undefined;
|
|
122
|
+
const endTC = parts.length >= 4? parts[3] : undefined;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
objectId,
|
|
126
|
+
offering,
|
|
127
|
+
startTC,
|
|
128
|
+
endTC
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const mediaStructStreamFields = R.pick(STREAM_FIELDS);
|
|
133
|
+
const msStreamsFieldSubset = R.map(mediaStructStreamFields);
|
|
134
|
+
|
|
135
|
+
// return media_struct stream keys appearing in playout.streams.representations
|
|
136
|
+
const poStreamMsStreamKeys = poStream =>
|
|
137
|
+
R.uniq(R.map(R.prop("media_struct_stream_key"), poStream.representations));
|
|
138
|
+
|
|
139
|
+
// return fields that need to be checked from media_struct.streams
|
|
140
|
+
const msStreamFields = (poStreams, msStreams) => {
|
|
141
|
+
const msStreamKeys = R.uniq(R.values(R.map(poStreamMsStreamKeys, poStreams)));
|
|
142
|
+
const usedMsStreams = R.pick(msStreamKeys, msStreams);
|
|
143
|
+
return R.map(msStreamsFieldSubset, usedMsStreams);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// convert HH_MM_SS_MS format to seconds
|
|
147
|
+
const convertTimecodeToSeconds = (timecode) => {
|
|
148
|
+
if (typeof timecode !== "string") {
|
|
149
|
+
throw new Error("Timecode must be a string");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const match = timecode.match(
|
|
153
|
+
/^(\d{1,2})_(\d{1,2})_(\d{1,2})(?:_(\d{1,3}))?$/
|
|
154
|
+
);
|
|
155
|
+
if(!match) {
|
|
156
|
+
throw new Error(`Invalid timecode format: ${timecode}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const [,hh,mm,ss,msRaw = "0"] = match;
|
|
160
|
+
const hours = parseInt(hh, 10);
|
|
161
|
+
const minutes = parseInt(mm, 10);
|
|
162
|
+
const seconds = parseInt(ss, 10);
|
|
163
|
+
const ms = msRaw.padEnd(3, "0"); // normalize to 3 digits
|
|
164
|
+
|
|
165
|
+
return Fraction(hours).mul(3600)
|
|
166
|
+
.add(Fraction(minutes).mul(60))
|
|
167
|
+
.add(Fraction(seconds))
|
|
168
|
+
.add(Fraction(ms).div(1000));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
const deriveSliceAndDurationFromVideoStream = ({offering, startTC, endTC}) => {
|
|
173
|
+
const streams = offering?.media_struct?.streams;
|
|
174
|
+
if (!streams) {
|
|
175
|
+
throw new Error("Missing media_struct.streams in offering");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const videoStream = Object.values(streams).find(
|
|
179
|
+
s => s.codec_type === "video"
|
|
180
|
+
);
|
|
181
|
+
if (!videoStream) {
|
|
182
|
+
throw new Error("No video stream found in offering");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let clipStart = Fraction(0), clipEnd;
|
|
186
|
+
if(startTC) {
|
|
187
|
+
clipStart = convertTimecodeToSeconds(startTC);
|
|
188
|
+
}
|
|
189
|
+
if(endTC) {
|
|
190
|
+
clipEnd = convertTimecodeToSeconds(endTC);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const streamDuration = Fraction(videoStream.duration.rat);
|
|
194
|
+
if(!clipEnd) {
|
|
195
|
+
const frameDur = Fraction(videoStream.rate).inverse();
|
|
196
|
+
clipEnd = Fraction(streamDuration.div(frameDur).floor().mul(frameDur));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (clipEnd.compare(clipStart) <= 0) {
|
|
200
|
+
throw new Error("Invalid clip range: end must be after start");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (clipEnd.compare(streamDuration) > 0) {
|
|
204
|
+
throw new Error("Clip end exceeds video duration");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const videoHandler = new FrameAccurateVideo({
|
|
208
|
+
frameRateRat: videoStream.rate
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const clipInFrame = videoHandler.TimeToFrame(clipStart);
|
|
212
|
+
const clipOutFrame = videoHandler.TimeToFrame(clipEnd, true);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
slice_start_rat: videoHandler.FrameToRat(clipInFrame),
|
|
216
|
+
slice_end_rat: videoHandler.FrameToRat(clipOutFrame),
|
|
217
|
+
duration_rat: videoHandler.FrameToRat(clipOutFrame - clipInFrame)
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const withoutDrmContentId = poFormat => {
|
|
222
|
+
const clone = R.clone(poFormat);
|
|
223
|
+
if (clone.drm) clone.drm.content_id = null;
|
|
224
|
+
return clone;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const withoutEncryptionSchemes = poStream => {
|
|
228
|
+
const clone = R.clone(poStream);
|
|
229
|
+
clone.encryption_schemes = null;
|
|
230
|
+
return clone;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const sanitizeFilename = (name, fallback) => {
|
|
234
|
+
if (!name) return fallback;
|
|
235
|
+
return name
|
|
236
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
237
|
+
.replace(/_+/g, "_")
|
|
238
|
+
.substring(0, 180)
|
|
239
|
+
.toLowerCase();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
class CompositionCreate extends Utility {
|
|
243
|
+
blueprint() {
|
|
244
|
+
return {
|
|
245
|
+
concerns: [ArgLibraryId, FabricObject],
|
|
246
|
+
options: [
|
|
247
|
+
ModOpt("libraryId", { demand: true }),
|
|
248
|
+
StdOpt("name", { forX: "channel object" }),
|
|
249
|
+
NewOpt("baseObjectId", {
|
|
250
|
+
demand: true,
|
|
251
|
+
descTemplate: "Base object ID to write channel metadata to",
|
|
252
|
+
string: true
|
|
253
|
+
}),
|
|
254
|
+
NewOpt("items", {
|
|
255
|
+
descTemplate:
|
|
256
|
+
"Comma-separated list of OBJECT_ID:OFFERING_KEY:START_TIME:END_TIME, The start and end time are provide in form HH_MM_SS_MS (e.g. iq__100:default_dash:1_20_3_5:1_30_0_0,iq__200:default_dash)",
|
|
257
|
+
string: true
|
|
258
|
+
}),
|
|
259
|
+
NewOpt("force", {
|
|
260
|
+
descTemplate: "Overwrite existing composition if it already exists",
|
|
261
|
+
type: "boolean",
|
|
262
|
+
default: false
|
|
263
|
+
}),
|
|
264
|
+
NewOpt("baseRange", {
|
|
265
|
+
descTemplate: "Provide start and end time for base object id (format START_TIME:END_TIME)",
|
|
266
|
+
type: "string",
|
|
267
|
+
}),
|
|
268
|
+
NewOpt("baseOfferingKey", {
|
|
269
|
+
descTemplate: "Offering key to use for base object if no objects in items list",
|
|
270
|
+
type: "string",
|
|
271
|
+
default: ""
|
|
272
|
+
})
|
|
273
|
+
]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async body() {
|
|
278
|
+
const logger = this.logger;
|
|
279
|
+
const { items, libraryId, baseObjectId, baseRange, baseOfferingKey } = this.args;
|
|
280
|
+
|
|
281
|
+
let baseObjectStartTC , baseObjectEndTC ;
|
|
282
|
+
if(baseRange) {
|
|
283
|
+
const parts = baseRange.split(":");
|
|
284
|
+
baseObjectStartTC = parts.length >= 1 ? parts[0] : undefined;
|
|
285
|
+
baseObjectEndTC = parts.length >= 2? parts[1]:undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Fetch base object metadata first
|
|
289
|
+
const baseMetadata = await this.concerns.FabricObject.metadata({
|
|
290
|
+
libraryId,
|
|
291
|
+
objectId: baseObjectId
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// Determine base offering
|
|
297
|
+
const baseOfferingKeyUsed = baseOfferingKey || Object.keys(baseMetadata.offerings ?? {})[0] || "default";
|
|
298
|
+
|
|
299
|
+
const name = this.args.name || baseMetadata.public?.name + " - " + baseOfferingKeyUsed || baseObjectId;
|
|
300
|
+
logger.log(`Creating composition '${name}' on base object ${baseObjectId}`);
|
|
301
|
+
|
|
302
|
+
// Parse items
|
|
303
|
+
let itemList = items? items.split(",").map(itemParser):[];
|
|
304
|
+
|
|
305
|
+
// Add baseObjectId to itemList if not already included
|
|
306
|
+
if (!itemList.some(item => item.objectId === baseObjectId)) {
|
|
307
|
+
itemList.unshift({ objectId: baseObjectId, offering: baseOfferingKeyUsed, startTC: baseObjectStartTC, endTC: baseObjectEndTC});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
logger.log("\nChecking items for any parameter mismatches...");
|
|
311
|
+
|
|
312
|
+
const itemPublicMeta = [];
|
|
313
|
+
const itemOfferings = [];
|
|
314
|
+
|
|
315
|
+
// Fetch metadata for each item
|
|
316
|
+
for (const item of itemList) {
|
|
317
|
+
if (item.objectId === baseObjectId) {
|
|
318
|
+
itemPublicMeta.push(baseMetadata);
|
|
319
|
+
const offering = baseMetadata.offerings?.[item.offering];
|
|
320
|
+
if (!offering) {
|
|
321
|
+
throw Error(`Offering '${item.offering}' not found for ${item.objectId}`);
|
|
322
|
+
}
|
|
323
|
+
itemOfferings.push(offering);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const meta = await this.concerns.FabricObject.metadata({
|
|
327
|
+
libraryId,
|
|
328
|
+
objectId: item.objectId
|
|
329
|
+
});
|
|
330
|
+
itemPublicMeta.push(meta);
|
|
331
|
+
const offering = meta.offerings?.[item.offering];
|
|
332
|
+
if (!offering) throw Error(`Offering '${item.offering}' not found for ${item.objectId}`);
|
|
333
|
+
itemOfferings.push(offering);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Use first item as reference for checks
|
|
337
|
+
const firstPoStreams = R.map(
|
|
338
|
+
withoutEncryptionSchemes,
|
|
339
|
+
itemOfferings[0].playout.streams
|
|
340
|
+
);
|
|
341
|
+
const firstPoFormats = R.map(
|
|
342
|
+
withoutDrmContentId,
|
|
343
|
+
itemOfferings[0].playout.playout_formats
|
|
344
|
+
);
|
|
345
|
+
const firstMsStreamFields = msStreamFields(
|
|
346
|
+
firstPoStreams,
|
|
347
|
+
itemOfferings[0].media_struct.streams
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Validate all other items match first item
|
|
351
|
+
for (let i = 1; i < itemList.length; i++) {
|
|
352
|
+
const testPoStreams = R.map(
|
|
353
|
+
withoutEncryptionSchemes,
|
|
354
|
+
itemOfferings[i].playout.streams
|
|
355
|
+
);
|
|
356
|
+
const stripTranscodeId = R.map(
|
|
357
|
+
R.evolve({ representations: R.map(R.dissoc("transcode_id")) })
|
|
358
|
+
);
|
|
359
|
+
if (!R.equals(stripTranscodeId(firstPoStreams), stripTranscodeId(testPoStreams))) {
|
|
360
|
+
throw Error(
|
|
361
|
+
"ERROR: All items must have identical playout.streams.representations"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const testPoFormats = R.map(
|
|
366
|
+
withoutDrmContentId,
|
|
367
|
+
itemOfferings[i].playout.playout_formats
|
|
368
|
+
);
|
|
369
|
+
if (!R.equals(firstPoFormats, testPoFormats)) {
|
|
370
|
+
throw Error(
|
|
371
|
+
"ERROR: All items must have identical playout.playout_formats"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const testMsStreamFields = msStreamFields(
|
|
376
|
+
testPoStreams,
|
|
377
|
+
itemOfferings[i].media_struct.streams
|
|
378
|
+
);
|
|
379
|
+
if (!R.equals(firstMsStreamFields, testMsStreamFields)) {
|
|
380
|
+
throw Error(
|
|
381
|
+
"ERROR: All items must have matching media_struct stream fields: " +
|
|
382
|
+
STREAM_FIELDS.join(", ")
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
logger.log("Mezzanine item parameter checks passed.");
|
|
388
|
+
|
|
389
|
+
logger.log("\nAdding channel metadata to new object...");
|
|
390
|
+
|
|
391
|
+
// Prepare channel metadata
|
|
392
|
+
const key = sanitizeFilename(name, `${baseObjectId}.mp4`);
|
|
393
|
+
|
|
394
|
+
const existingChannelOffering = baseMetadata.channel?.offerings?.[key];
|
|
395
|
+
|
|
396
|
+
if (existingChannelOffering && !this.args.force) {
|
|
397
|
+
throw Error(
|
|
398
|
+
`ERROR: A composition named '${name}' already exists on object ${baseObjectId} ` +
|
|
399
|
+
`(key='${key}'). Use --force to overwrite it.`
|
|
400
|
+
);
|
|
401
|
+
} else if (existingChannelOffering && this.args.force) {
|
|
402
|
+
logger.log(
|
|
403
|
+
`Warning: Overwriting existing composition '${name}' (key='${key}') due to --force`
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
// Base offering for channel
|
|
410
|
+
const baseOffering = baseMetadata.offerings?.[baseOfferingKeyUsed] || {};
|
|
411
|
+
|
|
412
|
+
// Normalize playout_formats and streams to arrays
|
|
413
|
+
const basePlayoutFormats =
|
|
414
|
+
baseOffering.playout?.playout_formats &&
|
|
415
|
+
typeof baseOffering.playout.playout_formats === "object" &&
|
|
416
|
+
!Array.isArray(baseOffering.playout.playout_formats)
|
|
417
|
+
? baseOffering.playout.playout_formats
|
|
418
|
+
: {};
|
|
419
|
+
|
|
420
|
+
const baseStreams =
|
|
421
|
+
baseOffering.playout?.streams &&
|
|
422
|
+
typeof baseOffering.playout.streams === "object" &&
|
|
423
|
+
!Array.isArray(baseOffering.playout.streams)
|
|
424
|
+
? baseOffering.playout.streams
|
|
425
|
+
: {};
|
|
426
|
+
|
|
427
|
+
const newChannelOffering = {
|
|
428
|
+
created_at: "",
|
|
429
|
+
display_name: name,
|
|
430
|
+
items: [],
|
|
431
|
+
key,
|
|
432
|
+
offeringKey: baseOfferingKeyUsed,
|
|
433
|
+
playout: {
|
|
434
|
+
playout_formats: basePlayoutFormats,
|
|
435
|
+
streams: baseStreams
|
|
436
|
+
},
|
|
437
|
+
playout_type: "ch_vod",
|
|
438
|
+
source_info: {
|
|
439
|
+
frameRate: baseOffering.media_struct.streams.video.rate,
|
|
440
|
+
libraryId,
|
|
441
|
+
name: baseMetadata.public?.name || name,
|
|
442
|
+
objectId: baseObjectId,
|
|
443
|
+
offeringKey: baseOfferingKeyUsed,
|
|
444
|
+
profileKey: "",
|
|
445
|
+
prompt: "",
|
|
446
|
+
type: "elvmediatool"
|
|
447
|
+
},
|
|
448
|
+
sources: [],
|
|
449
|
+
updated_at: ""
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Add items and sources
|
|
453
|
+
for (let i = 0; i < itemList.length; i++) {
|
|
454
|
+
const item = itemList[i];
|
|
455
|
+
const offering = itemOfferings[i];
|
|
456
|
+
const publicMeta = itemPublicMeta[i];
|
|
457
|
+
const startTC = itemList[i].startTC;
|
|
458
|
+
const endTC = itemList[i].endTC;
|
|
459
|
+
|
|
460
|
+
const itemLatestVersion = await this.concerns.FabricObject.latestVersionHash({
|
|
461
|
+
libraryId,
|
|
462
|
+
objectId: item.objectId
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const derivedSlice = deriveSliceAndDurationFromVideoStream({offering, startTC, endTC});
|
|
466
|
+
|
|
467
|
+
newChannelOffering.items.push({
|
|
468
|
+
display_name: publicMeta.public.name,
|
|
469
|
+
duration_rat: derivedSlice.duration_rat,
|
|
470
|
+
slice_start_rat: derivedSlice.slice_start_rat,
|
|
471
|
+
slice_end_rat: derivedSlice.slice_end_rat,
|
|
472
|
+
source: {
|
|
473
|
+
".": { auto_update: { tag: "latest" } },
|
|
474
|
+
"/": `/qfab/${itemLatestVersion}/rep/playout/${item.offering}`
|
|
475
|
+
},
|
|
476
|
+
type: "mez_vod"
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Add item to sources array
|
|
480
|
+
newChannelOffering.sources.push(item.objectId);
|
|
481
|
+
|
|
482
|
+
const now = new Date().toISOString();
|
|
483
|
+
|
|
484
|
+
if (!newChannelOffering.created_at) {
|
|
485
|
+
newChannelOffering.created_at = now; // first write only
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
newChannelOffering.updated_at = now; // always overwrite
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
logger.log("Merging metadata...");
|
|
492
|
+
|
|
493
|
+
const versionHash = await this.concerns.Metadata.write({
|
|
494
|
+
libraryId,
|
|
495
|
+
metadataSubtree: `/channel/offerings/${key}`,
|
|
496
|
+
metadata: {
|
|
497
|
+
...newChannelOffering
|
|
498
|
+
},
|
|
499
|
+
objectId: baseObjectId,
|
|
500
|
+
commitMessage: "Ran CompositionCreate.js"
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
logger.log("");
|
|
504
|
+
logger.log(`Object ID: ${baseObjectId}`);
|
|
505
|
+
logger.log("New version hash: " + versionHash);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
header() {
|
|
509
|
+
return `Create composition in object ${this.args.baseObjectId}`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (require.main === module) {
|
|
514
|
+
Utility.cmdLineInvoke(CompositionCreate);
|
|
515
|
+
} else {
|
|
516
|
+
module.exports = CompositionCreate;
|
|
517
|
+
}
|