@aelionsdk/sdk 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/audio-controller.d.ts +25 -0
- package/dist/audio-controller.d.ts.map +1 -0
- package/dist/audio-controller.js +235 -0
- package/dist/audio-mastering.d.ts +27 -0
- package/dist/audio-mastering.d.ts.map +1 -0
- package/dist/audio-mastering.js +160 -0
- package/dist/composition.d.ts +75 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +146 -0
- package/dist/default-schemas.d.ts +7 -0
- package/dist/default-schemas.d.ts.map +1 -0
- package/dist/default-schemas.js +18 -0
- package/dist/export-job.d.ts +22 -0
- package/dist/export-job.d.ts.map +1 -0
- package/dist/export-job.js +126 -0
- package/dist/extension-host.d.ts +90 -0
- package/dist/extension-host.d.ts.map +1 -0
- package/dist/extension-host.js +367 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/media-provider.d.ts +49 -0
- package/dist/media-provider.d.ts.map +1 -0
- package/dist/media-provider.js +388 -0
- package/dist/migration-materials.d.ts +10 -0
- package/dist/migration-materials.d.ts.map +1 -0
- package/dist/migration-materials.js +148 -0
- package/dist/migration.d.ts +109 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +1232 -0
- package/dist/persistence.d.ts +73 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +245 -0
- package/dist/player.d.ts +22 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +522 -0
- package/dist/preview-controller.d.ts +62 -0
- package/dist/preview-controller.d.ts.map +1 -0
- package/dist/preview-controller.js +377 -0
- package/dist/preview-quality.d.ts +7 -0
- package/dist/preview-quality.d.ts.map +1 -0
- package/dist/preview-quality.js +8 -0
- package/dist/production-media-provider.d.ts +112 -0
- package/dist/production-media-provider.d.ts.map +1 -0
- package/dist/production-media-provider.js +749 -0
- package/dist/project-builder.d.ts +249 -0
- package/dist/project-builder.d.ts.map +1 -0
- package/dist/project-builder.js +953 -0
- package/dist/runtime-material-registry.d.ts +10 -0
- package/dist/runtime-material-registry.d.ts.map +1 -0
- package/dist/runtime-material-registry.js +23 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1114 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
import { AelionError, frameStartUs, normalizeRational, } from '@aelionsdk/core';
|
|
2
|
+
import { ProjectValidator, canonicalClone, } from '@aelionsdk/project-schema';
|
|
3
|
+
import { defaultSchemas } from './default-schemas.js';
|
|
4
|
+
const ENTITY_ID = /^[A-Za-z][A-Za-z0-9._:-]*$/u;
|
|
5
|
+
function assertEntityId(value, name) {
|
|
6
|
+
if (value.length > 128 || !ENTITY_ID.test(value)) {
|
|
7
|
+
throw new TypeError(`${name} must be a valid Aelion entity id`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function assertTime(value, name, positive = false) {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value < 0 || (positive && value === 0)) {
|
|
12
|
+
throw new RangeError(`${name} must be ${positive ? 'a positive' : 'a non-negative'} safe integer`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function assertFiniteNumber(value, name) {
|
|
16
|
+
if (!Number.isFinite(value))
|
|
17
|
+
throw new RangeError(`${name} must be finite`);
|
|
18
|
+
}
|
|
19
|
+
function deepFreeze(value) {
|
|
20
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
21
|
+
for (const entry of Object.values(value))
|
|
22
|
+
deepFreeze(entry);
|
|
23
|
+
Object.freeze(value);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function toMicroseconds(value, multiplier, name) {
|
|
28
|
+
assertFiniteNumber(value, name);
|
|
29
|
+
if (value < 0)
|
|
30
|
+
throw new RangeError(`${name} must be non-negative`);
|
|
31
|
+
const result = Math.round(value * multiplier);
|
|
32
|
+
if (!Number.isSafeInteger(result))
|
|
33
|
+
throw new RangeError(`${name} exceeds the safe time range`);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/** Convert seconds to canonical integer microseconds. */
|
|
37
|
+
export function seconds(value) {
|
|
38
|
+
return toMicroseconds(value, 1_000_000, 'seconds');
|
|
39
|
+
}
|
|
40
|
+
/** Convert milliseconds to canonical integer microseconds. */
|
|
41
|
+
export function milliseconds(value) {
|
|
42
|
+
return toMicroseconds(value, 1_000, 'milliseconds');
|
|
43
|
+
}
|
|
44
|
+
/** Return the exact quantized start time of a frame count. */
|
|
45
|
+
export function frames(value, frameRate = { numerator: 30, denominator: 1 }) {
|
|
46
|
+
return frameStartUs(value, frameRate);
|
|
47
|
+
}
|
|
48
|
+
/** Create a validated Project v1 document through small, type-safe operations. */
|
|
49
|
+
export class ProjectBuilder {
|
|
50
|
+
#project;
|
|
51
|
+
#validator = new ProjectValidator({
|
|
52
|
+
projectSchema: defaultSchemas.project,
|
|
53
|
+
materialInstanceSchema: defaultSchemas.materialInstance,
|
|
54
|
+
});
|
|
55
|
+
#counters = new Map();
|
|
56
|
+
constructor(options = {}) {
|
|
57
|
+
const projectId = options.projectId ?? 'project_1';
|
|
58
|
+
const sequenceId = options.sequenceId ?? 'sequence_1';
|
|
59
|
+
assertEntityId(projectId, 'projectId');
|
|
60
|
+
assertEntityId(sequenceId, 'sequenceId');
|
|
61
|
+
const width = options.width ?? 1920;
|
|
62
|
+
const height = options.height ?? 1080;
|
|
63
|
+
const frameRate = options.frameRate ?? { numerator: 30, denominator: 1 };
|
|
64
|
+
if (!Number.isSafeInteger(width) || width <= 0 || width > 65_535) {
|
|
65
|
+
throw new RangeError('width must be an integer from 1 to 65535');
|
|
66
|
+
}
|
|
67
|
+
if (!Number.isSafeInteger(height) || height <= 0 || height > 65_535) {
|
|
68
|
+
throw new RangeError('height must be an integer from 1 to 65535');
|
|
69
|
+
}
|
|
70
|
+
if (!Number.isSafeInteger(frameRate.numerator) ||
|
|
71
|
+
!Number.isSafeInteger(frameRate.denominator) ||
|
|
72
|
+
frameRate.numerator <= 0 ||
|
|
73
|
+
frameRate.denominator <= 0) {
|
|
74
|
+
throw new RangeError('frameRate must use positive safe integers');
|
|
75
|
+
}
|
|
76
|
+
if (options.durationUs !== undefined)
|
|
77
|
+
assertTime(options.durationUs, 'durationUs');
|
|
78
|
+
this.#project = {
|
|
79
|
+
$schema: 'https://schemas.aelion.dev/project/v1.json',
|
|
80
|
+
schemaVersion: '1.0.0',
|
|
81
|
+
projectId,
|
|
82
|
+
metadata: options.title === undefined ? {} : { title: options.title },
|
|
83
|
+
settings: {
|
|
84
|
+
defaultSequenceId: sequenceId,
|
|
85
|
+
defaultStillDurationUs: seconds(3),
|
|
86
|
+
missingAssetPolicy: 'error',
|
|
87
|
+
missingMaterialPolicy: 'error',
|
|
88
|
+
missingPluginPolicy: 'error',
|
|
89
|
+
},
|
|
90
|
+
assets: {},
|
|
91
|
+
sequences: {
|
|
92
|
+
[sequenceId]: {
|
|
93
|
+
id: sequenceId,
|
|
94
|
+
...(options.sequenceName === undefined ? {} : { name: options.sequenceName }),
|
|
95
|
+
format: {
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
pixelAspectRatio: { numerator: 1, denominator: 1 },
|
|
99
|
+
frameRate: { numerator: frameRate.numerator, denominator: frameRate.denominator },
|
|
100
|
+
sampleRate: options.sampleRate ?? 48_000,
|
|
101
|
+
channelLayout: options.channelLayout ?? 'stereo',
|
|
102
|
+
workingColorSpace: 'srgb-linear',
|
|
103
|
+
backgroundColor: options.backgroundColor === undefined
|
|
104
|
+
? { space: 'srgb-linear', rgba: [0, 0, 0, 1] }
|
|
105
|
+
: this.#color(options.backgroundColor),
|
|
106
|
+
},
|
|
107
|
+
duration: options.durationUs === undefined
|
|
108
|
+
? { mode: 'content' }
|
|
109
|
+
: { mode: 'fixed', durationUs: options.durationUs, overflow: 'clip' },
|
|
110
|
+
trackIds: [],
|
|
111
|
+
transitionIds: [],
|
|
112
|
+
materialInstanceIds: [],
|
|
113
|
+
markerIds: [],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
tracks: {},
|
|
117
|
+
items: {},
|
|
118
|
+
materialInstances: {},
|
|
119
|
+
transitions: {},
|
|
120
|
+
markers: {},
|
|
121
|
+
linkGroups: {},
|
|
122
|
+
extensions: {},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
get projectId() {
|
|
126
|
+
return this.#project.projectId;
|
|
127
|
+
}
|
|
128
|
+
get sequenceId() {
|
|
129
|
+
return this.#project.settings.defaultSequenceId;
|
|
130
|
+
}
|
|
131
|
+
addTrack(options) {
|
|
132
|
+
const id = options.id ?? this.#nextId(`track_${options.kind}`);
|
|
133
|
+
this.#assertUnused(id);
|
|
134
|
+
const track = {
|
|
135
|
+
id,
|
|
136
|
+
sequenceId: this.sequenceId,
|
|
137
|
+
kind: options.kind,
|
|
138
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
139
|
+
enabled: options.enabled ?? true,
|
|
140
|
+
locked: options.locked ?? false,
|
|
141
|
+
itemIds: [],
|
|
142
|
+
materialInstanceIds: [],
|
|
143
|
+
...(options.kind === 'audio'
|
|
144
|
+
? { audio: { gainDb: 0, pan: 0, muted: false, solo: false } }
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
this.#project.tracks[id] = track;
|
|
148
|
+
this.#sequence().trackIds.push(id);
|
|
149
|
+
return id;
|
|
150
|
+
}
|
|
151
|
+
addAsset(options) {
|
|
152
|
+
this.#assertUnused(options.id);
|
|
153
|
+
if (options.contentHash !== undefined && !/^[0-9a-f]{64}$/u.test(options.contentHash)) {
|
|
154
|
+
throw new TypeError('contentHash must be a lowercase SHA-256 value');
|
|
155
|
+
}
|
|
156
|
+
if (options.byteLength !== undefined)
|
|
157
|
+
assertTime(options.byteLength, 'byteLength');
|
|
158
|
+
this.#project.assets[options.id] = {
|
|
159
|
+
id: options.id,
|
|
160
|
+
kind: options.kind,
|
|
161
|
+
locator: options.locator ?? { type: 'runtime-binding', bindingId: options.id },
|
|
162
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
163
|
+
...(options.mimeType === undefined ? {} : { mimeType: options.mimeType }),
|
|
164
|
+
...(options.contentHash === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { contentHash: { algorithm: 'sha256', value: options.contentHash } }),
|
|
167
|
+
...(options.byteLength === undefined ? {} : { byteLength: options.byteLength }),
|
|
168
|
+
...(options.probeHint === undefined ? {} : { probeHint: options.probeHint }),
|
|
169
|
+
...(options.representations === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { representations: [...options.representations] }),
|
|
172
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
173
|
+
};
|
|
174
|
+
return options.id;
|
|
175
|
+
}
|
|
176
|
+
addMediaClip(options) {
|
|
177
|
+
const track = this.#project.tracks[options.trackId];
|
|
178
|
+
if (track === undefined)
|
|
179
|
+
throw new ReferenceError(`Unknown Track: ${options.trackId}`);
|
|
180
|
+
const expectedTrack = options.kind === 'audio' ? 'audio' : 'visual';
|
|
181
|
+
if (track.kind !== expectedTrack) {
|
|
182
|
+
throw new TypeError(`${options.kind} clips require a ${expectedTrack} Track`);
|
|
183
|
+
}
|
|
184
|
+
if (this.#project.assets[options.assetId] === undefined) {
|
|
185
|
+
throw new ReferenceError(`Unknown Asset: ${options.assetId}`);
|
|
186
|
+
}
|
|
187
|
+
const id = options.id ?? this.#nextId(`item_${options.kind}`);
|
|
188
|
+
this.#assertUnused(id);
|
|
189
|
+
const atUs = options.atUs ?? 0;
|
|
190
|
+
const sourceStartUs = options.sourceStartUs ?? 0;
|
|
191
|
+
const sourceDurationUs = options.sourceDurationUs ?? options.durationUs;
|
|
192
|
+
const streamIndex = options.streamIndex ?? 0;
|
|
193
|
+
const rate = normalizeRational(options.rate ?? { numerator: 1, denominator: 1 });
|
|
194
|
+
assertTime(atUs, 'atUs');
|
|
195
|
+
assertTime(options.durationUs, 'durationUs', true);
|
|
196
|
+
assertTime(sourceStartUs, 'sourceStartUs');
|
|
197
|
+
assertTime(sourceDurationUs, 'sourceDurationUs', true);
|
|
198
|
+
assertTime(streamIndex, 'streamIndex');
|
|
199
|
+
if (options.fadeInUs !== undefined)
|
|
200
|
+
assertTime(options.fadeInUs, 'fadeInUs');
|
|
201
|
+
if (options.fadeOutUs !== undefined)
|
|
202
|
+
assertTime(options.fadeOutUs, 'fadeOutUs');
|
|
203
|
+
const pitchPolicy = options.pitchPolicy;
|
|
204
|
+
if (pitchPolicy !== undefined && pitchPolicy !== 'varispeed' && pitchPolicy !== 'preserve') {
|
|
205
|
+
throw new RangeError('pitchPolicy must be varispeed or preserve');
|
|
206
|
+
}
|
|
207
|
+
if ((options.fadeInUs ?? 0) + (options.fadeOutUs ?? 0) > options.durationUs) {
|
|
208
|
+
throw new RangeError('audio fades cannot overlap past the Clip duration');
|
|
209
|
+
}
|
|
210
|
+
const source = {
|
|
211
|
+
assetId: options.assetId,
|
|
212
|
+
stream: { type: options.kind, index: streamIndex },
|
|
213
|
+
sourceRange: { startUs: sourceStartUs, durationUs: sourceDurationUs },
|
|
214
|
+
timeMapping: {
|
|
215
|
+
type: 'linear',
|
|
216
|
+
rate: { numerator: rate.numerator, denominator: rate.denominator },
|
|
217
|
+
reverse: false,
|
|
218
|
+
boundary: options.boundary ?? 'hold',
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const item = options.kind === 'video'
|
|
222
|
+
? {
|
|
223
|
+
id,
|
|
224
|
+
trackId: track.id,
|
|
225
|
+
type: 'video',
|
|
226
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
227
|
+
enabled: true,
|
|
228
|
+
range: { startUs: atUs, durationUs: options.durationUs },
|
|
229
|
+
source,
|
|
230
|
+
visual: this.#visual(options.fit ?? 'contain', options.opacity ?? 1),
|
|
231
|
+
materialInstanceIds: [],
|
|
232
|
+
}
|
|
233
|
+
: {
|
|
234
|
+
id,
|
|
235
|
+
trackId: track.id,
|
|
236
|
+
type: 'audio',
|
|
237
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
238
|
+
enabled: true,
|
|
239
|
+
range: { startUs: atUs, durationUs: options.durationUs },
|
|
240
|
+
source,
|
|
241
|
+
audio: {
|
|
242
|
+
gainDb: options.gainDb ?? 0,
|
|
243
|
+
pan: options.pan ?? 0,
|
|
244
|
+
...(options.fadeInUs === undefined ? {} : { fadeInUs: options.fadeInUs }),
|
|
245
|
+
...(options.fadeOutUs === undefined ? {} : { fadeOutUs: options.fadeOutUs }),
|
|
246
|
+
...(options.pitchPolicy === undefined ? {} : { pitchPolicy: options.pitchPolicy }),
|
|
247
|
+
},
|
|
248
|
+
materialInstanceIds: [],
|
|
249
|
+
};
|
|
250
|
+
this.#project.items[id] = item;
|
|
251
|
+
track.itemIds.push(id);
|
|
252
|
+
return id;
|
|
253
|
+
}
|
|
254
|
+
setBackgroundColor(value) {
|
|
255
|
+
this.#sequence().format.backgroundColor = this.#color(value);
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
setProjectExtension(namespace, value) {
|
|
259
|
+
this.#project.extensions[namespace] = structuredClone(value);
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
setTrackExtension(trackId, namespace, value) {
|
|
263
|
+
const track = this.#project.tracks[trackId];
|
|
264
|
+
if (track === undefined)
|
|
265
|
+
throw new ReferenceError(`Unknown Track: ${trackId}`);
|
|
266
|
+
const current = track.extensions !== null &&
|
|
267
|
+
typeof track.extensions === 'object' &&
|
|
268
|
+
!Array.isArray(track.extensions)
|
|
269
|
+
? track.extensions
|
|
270
|
+
: {};
|
|
271
|
+
track.extensions = {
|
|
272
|
+
...current,
|
|
273
|
+
[namespace]: structuredClone(value),
|
|
274
|
+
};
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
setItemMetadata(itemId, metadata) {
|
|
278
|
+
const item = this.#project.items[itemId];
|
|
279
|
+
if (item === undefined)
|
|
280
|
+
throw new ReferenceError(`Unknown Item: ${itemId}`);
|
|
281
|
+
item.metadata = structuredClone(metadata);
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/** Add a first-class still image Clip backed by an image Asset. */
|
|
285
|
+
addImageClip(options) {
|
|
286
|
+
const track = this.#project.tracks[options.trackId];
|
|
287
|
+
if (track === undefined)
|
|
288
|
+
throw new ReferenceError(`Unknown Track: ${options.trackId}`);
|
|
289
|
+
if (track.kind !== 'visual')
|
|
290
|
+
throw new TypeError('image clips require a visual Track');
|
|
291
|
+
const asset = this.#project.assets[options.assetId];
|
|
292
|
+
if (asset === undefined)
|
|
293
|
+
throw new ReferenceError(`Unknown Asset: ${options.assetId}`);
|
|
294
|
+
if (asset.kind !== 'image')
|
|
295
|
+
throw new TypeError('image clips require an image Asset');
|
|
296
|
+
const id = options.id ?? this.#nextId('item_image');
|
|
297
|
+
this.#assertUnused(id);
|
|
298
|
+
const atUs = options.atUs ?? 0;
|
|
299
|
+
const durationUs = options.durationUs ?? this.#project.settings.defaultStillDurationUs;
|
|
300
|
+
assertTime(atUs, 'atUs');
|
|
301
|
+
assertTime(durationUs, 'durationUs', true);
|
|
302
|
+
const item = {
|
|
303
|
+
id,
|
|
304
|
+
trackId: track.id,
|
|
305
|
+
type: 'image',
|
|
306
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
307
|
+
enabled: true,
|
|
308
|
+
range: { startUs: atUs, durationUs },
|
|
309
|
+
source: {
|
|
310
|
+
assetId: options.assetId,
|
|
311
|
+
stream: { type: 'video', index: 0 },
|
|
312
|
+
sourceRange: { startUs: 0, durationUs },
|
|
313
|
+
timeMapping: {
|
|
314
|
+
type: 'linear',
|
|
315
|
+
rate: { numerator: 1, denominator: 1 },
|
|
316
|
+
reverse: false,
|
|
317
|
+
boundary: 'hold',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
visual: this.#visual(options.fit ?? 'contain', options.opacity ?? 1),
|
|
321
|
+
materialInstanceIds: [],
|
|
322
|
+
};
|
|
323
|
+
this.#project.items[id] = item;
|
|
324
|
+
track.itemIds.push(id);
|
|
325
|
+
return id;
|
|
326
|
+
}
|
|
327
|
+
addTextClip(options) {
|
|
328
|
+
const track = this.#project.tracks[options.trackId];
|
|
329
|
+
if (track === undefined)
|
|
330
|
+
throw new ReferenceError(`Unknown Track: ${options.trackId}`);
|
|
331
|
+
if (track.kind !== 'visual')
|
|
332
|
+
throw new TypeError('text clips require a visual Track');
|
|
333
|
+
const id = options.id ?? this.#nextId('item_text');
|
|
334
|
+
this.#assertUnused(id);
|
|
335
|
+
const atUs = options.atUs ?? 0;
|
|
336
|
+
assertTime(atUs, 'atUs');
|
|
337
|
+
assertTime(options.durationUs, 'durationUs', true);
|
|
338
|
+
if (options.text.length > 1_000_000)
|
|
339
|
+
throw new RangeError('text exceeds 1,000,000 characters');
|
|
340
|
+
if (options.runs !== undefined &&
|
|
341
|
+
(options.runs.length === 0 || options.runs.map(run => run.text).join('') !== options.text)) {
|
|
342
|
+
throw new TypeError('text runs must be non-empty and concatenate to text');
|
|
343
|
+
}
|
|
344
|
+
const sequence = this.#sequence();
|
|
345
|
+
const format = sequence.format;
|
|
346
|
+
const width = typeof format.width === 'number' ? format.width : 1920;
|
|
347
|
+
const height = typeof format.height === 'number' ? format.height : 1080;
|
|
348
|
+
const box = options.box ?? {
|
|
349
|
+
x: width * 0.1,
|
|
350
|
+
y: height * 0.1,
|
|
351
|
+
width: width * 0.8,
|
|
352
|
+
height: height * 0.8,
|
|
353
|
+
};
|
|
354
|
+
this.#assertBox(box);
|
|
355
|
+
const item = {
|
|
356
|
+
id,
|
|
357
|
+
trackId: track.id,
|
|
358
|
+
type: 'text',
|
|
359
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
360
|
+
enabled: true,
|
|
361
|
+
range: { startUs: atUs, durationUs: options.durationUs },
|
|
362
|
+
box: { ...box },
|
|
363
|
+
overflow: options.overflow ?? 'auto-fit',
|
|
364
|
+
writingMode: options.writingMode ?? 'horizontal-tb',
|
|
365
|
+
paragraphs: [
|
|
366
|
+
{
|
|
367
|
+
style: options.paragraphStyle ?? {},
|
|
368
|
+
runs: options.runs?.map(run => ({
|
|
369
|
+
text: run.text,
|
|
370
|
+
style: {
|
|
371
|
+
...(options.style ?? {
|
|
372
|
+
fontFamilies: ['sans-serif'],
|
|
373
|
+
fontSizePx: 48,
|
|
374
|
+
fontWeight: 400,
|
|
375
|
+
fill: '#ffffff',
|
|
376
|
+
}),
|
|
377
|
+
...(run.style ?? {}),
|
|
378
|
+
},
|
|
379
|
+
})) ?? [
|
|
380
|
+
{
|
|
381
|
+
text: options.text,
|
|
382
|
+
style: options.style ?? {
|
|
383
|
+
fontFamilies: ['sans-serif'],
|
|
384
|
+
fontSizePx: 48,
|
|
385
|
+
fontWeight: 400,
|
|
386
|
+
fill: '#ffffff',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
visual: this.#visual('none', options.opacity ?? 1),
|
|
393
|
+
materialInstanceIds: [],
|
|
394
|
+
};
|
|
395
|
+
this.#project.items[id] = item;
|
|
396
|
+
track.itemIds.push(id);
|
|
397
|
+
return id;
|
|
398
|
+
}
|
|
399
|
+
addCaptionClip(options) {
|
|
400
|
+
const track = this.#project.tracks[options.trackId];
|
|
401
|
+
if (track === undefined)
|
|
402
|
+
throw new ReferenceError(`Unknown Track: ${options.trackId}`);
|
|
403
|
+
if (track.kind !== 'caption')
|
|
404
|
+
throw new TypeError('caption clips require a caption Track');
|
|
405
|
+
const id = options.id ?? this.#nextId('item_caption');
|
|
406
|
+
this.#assertUnused(id);
|
|
407
|
+
const atUs = options.atUs ?? 0;
|
|
408
|
+
assertTime(atUs, 'atUs');
|
|
409
|
+
assertTime(options.durationUs, 'durationUs', true);
|
|
410
|
+
if (options.text.length > 1_000_000) {
|
|
411
|
+
throw new RangeError('caption text exceeds 1,000,000 characters');
|
|
412
|
+
}
|
|
413
|
+
const sequence = this.#sequence();
|
|
414
|
+
const format = sequence.format;
|
|
415
|
+
const width = typeof format.width === 'number' ? format.width : 1920;
|
|
416
|
+
const height = typeof format.height === 'number' ? format.height : 1080;
|
|
417
|
+
const box = options.box ?? {
|
|
418
|
+
x: width * 0.1,
|
|
419
|
+
y: height * 0.72,
|
|
420
|
+
width: width * 0.8,
|
|
421
|
+
height: height * 0.2,
|
|
422
|
+
};
|
|
423
|
+
this.#assertBox(box);
|
|
424
|
+
const item = {
|
|
425
|
+
id,
|
|
426
|
+
trackId: track.id,
|
|
427
|
+
type: 'caption',
|
|
428
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
429
|
+
enabled: true,
|
|
430
|
+
range: { startUs: atUs, durationUs: options.durationUs },
|
|
431
|
+
text: options.text,
|
|
432
|
+
box: { ...box },
|
|
433
|
+
style: options.style ?? {
|
|
434
|
+
fontFamilies: ['sans-serif'],
|
|
435
|
+
fontSizePx: 42,
|
|
436
|
+
fontWeight: 600,
|
|
437
|
+
fill: '#ffffff',
|
|
438
|
+
stroke: '#000000',
|
|
439
|
+
strokeWidthPx: 2,
|
|
440
|
+
align: 'center',
|
|
441
|
+
},
|
|
442
|
+
overflow: options.overflow ?? 'auto-fit',
|
|
443
|
+
...(options.cueSettings === undefined ? {} : { cueSettings: options.cueSettings }),
|
|
444
|
+
visual: this.#visual('none', 1),
|
|
445
|
+
materialInstanceIds: [],
|
|
446
|
+
};
|
|
447
|
+
this.#project.items[id] = item;
|
|
448
|
+
track.itemIds.push(id);
|
|
449
|
+
return id;
|
|
450
|
+
}
|
|
451
|
+
addShapeClip(options) {
|
|
452
|
+
const track = this.#project.tracks[options.trackId];
|
|
453
|
+
if (track === undefined)
|
|
454
|
+
throw new ReferenceError(`Unknown Track: ${options.trackId}`);
|
|
455
|
+
if (track.kind !== 'visual')
|
|
456
|
+
throw new TypeError('shape clips require a visual Track');
|
|
457
|
+
const id = options.id ?? this.#nextId('item_shape');
|
|
458
|
+
this.#assertUnused(id);
|
|
459
|
+
const atUs = options.atUs ?? 0;
|
|
460
|
+
assertTime(atUs, 'atUs');
|
|
461
|
+
assertTime(options.durationUs, 'durationUs', true);
|
|
462
|
+
this.#assertBox(options.box);
|
|
463
|
+
if (options.strokeWidthPx !== undefined) {
|
|
464
|
+
assertFiniteNumber(options.strokeWidthPx, 'strokeWidthPx');
|
|
465
|
+
}
|
|
466
|
+
if (options.cornerRadiusPx !== undefined) {
|
|
467
|
+
assertFiniteNumber(options.cornerRadiusPx, 'cornerRadiusPx');
|
|
468
|
+
}
|
|
469
|
+
if (options.kind === 'polygon' && (options.points?.length ?? 0) < 3) {
|
|
470
|
+
throw new RangeError('polygon shapes require at least three points');
|
|
471
|
+
}
|
|
472
|
+
const item = {
|
|
473
|
+
id,
|
|
474
|
+
trackId: track.id,
|
|
475
|
+
type: 'shape',
|
|
476
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
477
|
+
enabled: true,
|
|
478
|
+
range: { startUs: atUs, durationUs: options.durationUs },
|
|
479
|
+
shape: {
|
|
480
|
+
kind: options.kind,
|
|
481
|
+
box: { ...options.box },
|
|
482
|
+
fill: this.#color(options.fill),
|
|
483
|
+
...(options.stroke === undefined ? {} : { stroke: this.#color(options.stroke) }),
|
|
484
|
+
...(options.strokeWidthPx === undefined ? {} : { strokeWidthPx: options.strokeWidthPx }),
|
|
485
|
+
...(options.cornerRadiusPx === undefined ? {} : { cornerRadiusPx: options.cornerRadiusPx }),
|
|
486
|
+
...(options.points === undefined
|
|
487
|
+
? {}
|
|
488
|
+
: { points: options.points.map(point => ({ ...point })) }),
|
|
489
|
+
},
|
|
490
|
+
visual: this.#visual('none', options.opacity ?? 1),
|
|
491
|
+
materialInstanceIds: [],
|
|
492
|
+
};
|
|
493
|
+
this.#project.items[id] = item;
|
|
494
|
+
track.itemIds.push(id);
|
|
495
|
+
return id;
|
|
496
|
+
}
|
|
497
|
+
addMaterialInstance(options) {
|
|
498
|
+
const id = options.id ?? this.#nextId('material');
|
|
499
|
+
this.#assertUnused(id);
|
|
500
|
+
if (!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u.test(options.packageId)) {
|
|
501
|
+
throw new TypeError('packageId is invalid');
|
|
502
|
+
}
|
|
503
|
+
if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(options.packageVersion)) {
|
|
504
|
+
throw new TypeError('packageVersion must be valid SemVer');
|
|
505
|
+
}
|
|
506
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(options.packageIntegrity)) {
|
|
507
|
+
throw new TypeError('packageIntegrity must be a sha256 integrity value');
|
|
508
|
+
}
|
|
509
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(options.materialId)) {
|
|
510
|
+
throw new TypeError('materialId is invalid');
|
|
511
|
+
}
|
|
512
|
+
if (options.randomSeed !== undefined &&
|
|
513
|
+
(!Number.isInteger(options.randomSeed) ||
|
|
514
|
+
options.randomSeed < 0 ||
|
|
515
|
+
options.randomSeed > 4_294_967_295)) {
|
|
516
|
+
throw new RangeError('randomSeed must be a uint32');
|
|
517
|
+
}
|
|
518
|
+
const instance = {
|
|
519
|
+
id,
|
|
520
|
+
definition: {
|
|
521
|
+
packageId: options.packageId,
|
|
522
|
+
packageVersion: options.packageVersion,
|
|
523
|
+
packageIntegrity: options.packageIntegrity,
|
|
524
|
+
materialId: options.materialId,
|
|
525
|
+
},
|
|
526
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
527
|
+
enabled: true,
|
|
528
|
+
previewPolicy: options.previewPolicy ?? 'required',
|
|
529
|
+
parameters: options.parameters ?? {},
|
|
530
|
+
...(options.resourceBindings === undefined
|
|
531
|
+
? {}
|
|
532
|
+
: { resourceBindings: options.resourceBindings }),
|
|
533
|
+
...(options.inputBindings === undefined ? {} : { inputBindings: options.inputBindings }),
|
|
534
|
+
...(options.randomSeed === undefined ? {} : { randomSeed: options.randomSeed }),
|
|
535
|
+
};
|
|
536
|
+
this.#project.materialInstances[id] = instance;
|
|
537
|
+
return id;
|
|
538
|
+
}
|
|
539
|
+
attachEffect(itemId, materialInstanceId) {
|
|
540
|
+
const item = this.#project.items[itemId];
|
|
541
|
+
if (item === undefined)
|
|
542
|
+
throw new ReferenceError(`Unknown Item: ${itemId}`);
|
|
543
|
+
if (this.#project.materialInstances[materialInstanceId] === undefined) {
|
|
544
|
+
throw new ReferenceError(`Unknown MaterialInstance: ${materialInstanceId}`);
|
|
545
|
+
}
|
|
546
|
+
if (!item.materialInstanceIds.includes(materialInstanceId)) {
|
|
547
|
+
item.materialInstanceIds.push(materialInstanceId);
|
|
548
|
+
}
|
|
549
|
+
return this;
|
|
550
|
+
}
|
|
551
|
+
addTransition(options) {
|
|
552
|
+
const from = this.#project.items[options.fromItemId];
|
|
553
|
+
const to = this.#project.items[options.toItemId];
|
|
554
|
+
if (from === undefined)
|
|
555
|
+
throw new ReferenceError(`Unknown Item: ${options.fromItemId}`);
|
|
556
|
+
if (to === undefined)
|
|
557
|
+
throw new ReferenceError(`Unknown Item: ${options.toItemId}`);
|
|
558
|
+
if (from.trackId !== to.trackId)
|
|
559
|
+
throw new TypeError('Transition Items must share a Track');
|
|
560
|
+
const track = this.#project.tracks[from.trackId];
|
|
561
|
+
if (track === undefined)
|
|
562
|
+
throw new ReferenceError(`Unknown Track: ${from.trackId}`);
|
|
563
|
+
if (this.#project.materialInstances[options.materialInstanceId] === undefined) {
|
|
564
|
+
throw new ReferenceError(`Unknown MaterialInstance: ${options.materialInstanceId}`);
|
|
565
|
+
}
|
|
566
|
+
assertTime(options.atUs, 'atUs');
|
|
567
|
+
assertTime(options.durationUs, 'durationUs', true);
|
|
568
|
+
const id = options.id ?? this.#nextId('transition');
|
|
569
|
+
this.#assertUnused(id);
|
|
570
|
+
const transition = {
|
|
571
|
+
id,
|
|
572
|
+
sequenceId: this.sequenceId,
|
|
573
|
+
trackId: track.id,
|
|
574
|
+
kind: track.kind === 'audio' ? 'audio' : 'visual',
|
|
575
|
+
fromItemId: from.id,
|
|
576
|
+
toItemId: to.id,
|
|
577
|
+
range: { startUs: options.atUs, durationUs: options.durationUs },
|
|
578
|
+
materialInstanceId: options.materialInstanceId,
|
|
579
|
+
};
|
|
580
|
+
this.#project.transitions[id] = transition;
|
|
581
|
+
this.#sequence().transitionIds.push(id);
|
|
582
|
+
return id;
|
|
583
|
+
}
|
|
584
|
+
setMask(itemId, options) {
|
|
585
|
+
const item = this.#project.items[itemId];
|
|
586
|
+
const source = this.#project.items[options.sourceItemId];
|
|
587
|
+
if (item === undefined)
|
|
588
|
+
throw new ReferenceError(`Unknown Item: ${itemId}`);
|
|
589
|
+
if (source === undefined)
|
|
590
|
+
throw new ReferenceError(`Unknown Item: ${options.sourceItemId}`);
|
|
591
|
+
const visual = item.visual;
|
|
592
|
+
if (visual === null || typeof visual !== 'object' || Array.isArray(visual)) {
|
|
593
|
+
throw new TypeError(`${itemId} is not a visual Item`);
|
|
594
|
+
}
|
|
595
|
+
const featherPx = options.featherPx ?? 0;
|
|
596
|
+
assertFiniteNumber(featherPx, 'featherPx');
|
|
597
|
+
if (featherPx < 0 || featherPx > 4096) {
|
|
598
|
+
throw new RangeError('featherPx must be from 0 to 4096');
|
|
599
|
+
}
|
|
600
|
+
visual.mask = {
|
|
601
|
+
sourceItemId: options.sourceItemId,
|
|
602
|
+
channel: options.channel ?? 'alpha',
|
|
603
|
+
invert: options.invert ?? false,
|
|
604
|
+
featherPx,
|
|
605
|
+
space: options.space ?? 'canvas',
|
|
606
|
+
consumeSource: options.consumeSource ?? true,
|
|
607
|
+
};
|
|
608
|
+
return this;
|
|
609
|
+
}
|
|
610
|
+
setVisual(itemId, options) {
|
|
611
|
+
const item = this.#project.items[itemId];
|
|
612
|
+
if (item === undefined)
|
|
613
|
+
throw new ReferenceError(`Unknown Item: ${itemId}`);
|
|
614
|
+
const entity = item;
|
|
615
|
+
const visual = entity.visual;
|
|
616
|
+
if (visual === null || typeof visual !== 'object' || Array.isArray(visual)) {
|
|
617
|
+
throw new TypeError(`${itemId} is not a visual Item`);
|
|
618
|
+
}
|
|
619
|
+
const target = visual;
|
|
620
|
+
if (options.fit !== undefined)
|
|
621
|
+
target.fit = options.fit;
|
|
622
|
+
if (options.opacity !== undefined) {
|
|
623
|
+
assertFiniteNumber(options.opacity, 'opacity');
|
|
624
|
+
if (options.opacity < 0 || options.opacity > 1) {
|
|
625
|
+
throw new RangeError('opacity must be from 0 to 1');
|
|
626
|
+
}
|
|
627
|
+
target.opacity = options.opacity;
|
|
628
|
+
}
|
|
629
|
+
if (options.blendMode !== undefined)
|
|
630
|
+
target.blendMode = options.blendMode;
|
|
631
|
+
const transform = target.transform;
|
|
632
|
+
if (transform === null || typeof transform !== 'object' || Array.isArray(transform)) {
|
|
633
|
+
throw new TypeError(`${itemId} has no visual transform`);
|
|
634
|
+
}
|
|
635
|
+
const transformTarget = transform;
|
|
636
|
+
const assignPoint = (key, value) => {
|
|
637
|
+
if (value === undefined)
|
|
638
|
+
return;
|
|
639
|
+
assertFiniteNumber(value.x, `${key}.x`);
|
|
640
|
+
assertFiniteNumber(value.y, `${key}.y`);
|
|
641
|
+
transformTarget[key] = { x: value.x, y: value.y };
|
|
642
|
+
};
|
|
643
|
+
assignPoint('positionPx', options.positionPx);
|
|
644
|
+
assignPoint('anchor', options.anchor);
|
|
645
|
+
assignPoint('scale', options.scale);
|
|
646
|
+
assignPoint('skewDeg', options.skewDeg);
|
|
647
|
+
if (options.rotationDeg !== undefined) {
|
|
648
|
+
assertFiniteNumber(options.rotationDeg, 'rotationDeg');
|
|
649
|
+
transformTarget.rotationDeg = options.rotationDeg;
|
|
650
|
+
}
|
|
651
|
+
return this;
|
|
652
|
+
}
|
|
653
|
+
setKeyframes(itemId, property, keyframes) {
|
|
654
|
+
const item = this.#project.items[itemId];
|
|
655
|
+
if (item === undefined)
|
|
656
|
+
throw new ReferenceError(`Unknown Item: ${itemId}`);
|
|
657
|
+
if (keyframes.length === 0)
|
|
658
|
+
throw new RangeError('keyframes must not be empty');
|
|
659
|
+
const sorted = [...keyframes].sort((left, right) => left.timeUs - right.timeUs);
|
|
660
|
+
sorted.forEach((keyframe, index) => {
|
|
661
|
+
assertTime(keyframe.timeUs, `keyframes[${index.toString()}].timeUs`);
|
|
662
|
+
if (index > 0 && sorted[index - 1]?.timeUs === keyframe.timeUs) {
|
|
663
|
+
throw new RangeError('keyframe times must be unique');
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
const animation = {
|
|
667
|
+
animation: {
|
|
668
|
+
timeSpace: 'item',
|
|
669
|
+
preInfinity: 'hold',
|
|
670
|
+
postInfinity: 'hold',
|
|
671
|
+
keyframes: sorted.map(keyframe => ({
|
|
672
|
+
timeUs: keyframe.timeUs,
|
|
673
|
+
value: keyframe.value,
|
|
674
|
+
interpolation: keyframe.interpolation ?? 'linear',
|
|
675
|
+
...(keyframe.easing === undefined ? {} : { easing: keyframe.easing }),
|
|
676
|
+
})),
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
const entity = item;
|
|
680
|
+
if (property === 'gain' || property === 'pan') {
|
|
681
|
+
const audio = entity.audio;
|
|
682
|
+
if (audio === null || typeof audio !== 'object' || Array.isArray(audio)) {
|
|
683
|
+
throw new TypeError(`${itemId} is not an audio Item`);
|
|
684
|
+
}
|
|
685
|
+
audio[property === 'gain' ? 'gainDb' : 'pan'] = animation;
|
|
686
|
+
return this;
|
|
687
|
+
}
|
|
688
|
+
const visual = entity.visual;
|
|
689
|
+
if (visual === null || typeof visual !== 'object' || Array.isArray(visual)) {
|
|
690
|
+
throw new TypeError(`${itemId} is not a visual Item`);
|
|
691
|
+
}
|
|
692
|
+
if (property === 'opacity') {
|
|
693
|
+
visual.opacity = animation;
|
|
694
|
+
return this;
|
|
695
|
+
}
|
|
696
|
+
const transform = visual.transform;
|
|
697
|
+
if (transform === null || typeof transform !== 'object' || Array.isArray(transform)) {
|
|
698
|
+
throw new TypeError(`${itemId} has no visual transform`);
|
|
699
|
+
}
|
|
700
|
+
const field = property === 'position' ? 'positionPx' : property === 'scale' ? 'scale' : 'rotationDeg';
|
|
701
|
+
transform[field] = animation;
|
|
702
|
+
return this;
|
|
703
|
+
}
|
|
704
|
+
async importMedia(options) {
|
|
705
|
+
assertEntityId(options.assetId, 'assetId');
|
|
706
|
+
const probe = await options.provider.probe(options.assetId, { purpose: 'export' });
|
|
707
|
+
const video = probe.index.tracks.find(track => track.kind === 'video');
|
|
708
|
+
const audio = probe.index.tracks.find(track => track.kind === 'audio');
|
|
709
|
+
const importVideo = (options.video ?? true) && video !== undefined;
|
|
710
|
+
const importAudio = (options.audio ?? true) && audio !== undefined;
|
|
711
|
+
if (!importVideo && !importAudio) {
|
|
712
|
+
throw new TypeError('Media import did not find an enabled video or audio stream');
|
|
713
|
+
}
|
|
714
|
+
const sourceStartUs = options.sourceStartUs ?? 0;
|
|
715
|
+
assertTime(sourceStartUs, 'sourceStartUs');
|
|
716
|
+
const availableDurationUs = probe.index.durationUs - sourceStartUs;
|
|
717
|
+
if (availableDurationUs <= 0)
|
|
718
|
+
throw new RangeError('sourceStartUs is outside the media');
|
|
719
|
+
const durationUs = options.durationUs ?? availableDurationUs;
|
|
720
|
+
assertTime(durationUs, 'durationUs', true);
|
|
721
|
+
if (durationUs > availableDurationUs) {
|
|
722
|
+
throw new RangeError('durationUs exceeds the available source media');
|
|
723
|
+
}
|
|
724
|
+
if (this.#project.assets[options.assetId] === undefined) {
|
|
725
|
+
this.addAsset({
|
|
726
|
+
id: options.assetId,
|
|
727
|
+
kind: importVideo ? 'video' : 'audio',
|
|
728
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
729
|
+
mimeType: options.mimeType ??
|
|
730
|
+
`${importVideo ? 'video' : 'audio'}/${probe.index.container === 'mp4' ? 'mp4' : 'webm'}`,
|
|
731
|
+
probeHint: {
|
|
732
|
+
durationUs: probe.index.durationUs,
|
|
733
|
+
...(video === undefined
|
|
734
|
+
? {}
|
|
735
|
+
: {
|
|
736
|
+
width: video.codedWidth,
|
|
737
|
+
height: video.codedHeight,
|
|
738
|
+
videoCodec: video.codec,
|
|
739
|
+
}),
|
|
740
|
+
...(audio === undefined ? {} : { audioCodec: audio.codec }),
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const atUs = options.atUs ?? 0;
|
|
745
|
+
assertTime(atUs, 'atUs');
|
|
746
|
+
let videoTrackId;
|
|
747
|
+
let audioTrackId;
|
|
748
|
+
let videoItemId;
|
|
749
|
+
let audioItemId;
|
|
750
|
+
if (importVideo) {
|
|
751
|
+
videoTrackId = this.#resolveTrack('visual', options.videoTrackId);
|
|
752
|
+
videoItemId = this.addMediaClip({
|
|
753
|
+
kind: 'video',
|
|
754
|
+
assetId: options.assetId,
|
|
755
|
+
trackId: videoTrackId,
|
|
756
|
+
atUs,
|
|
757
|
+
durationUs,
|
|
758
|
+
sourceStartUs,
|
|
759
|
+
sourceDurationUs: durationUs,
|
|
760
|
+
streamIndex: probe.index.tracks.filter(track => track.kind === 'video').indexOf(video),
|
|
761
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
762
|
+
...(options.fit === undefined ? {} : { fit: options.fit }),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (importAudio) {
|
|
766
|
+
audioTrackId = this.#resolveTrack('audio', options.audioTrackId);
|
|
767
|
+
audioItemId = this.addMediaClip({
|
|
768
|
+
kind: 'audio',
|
|
769
|
+
assetId: options.assetId,
|
|
770
|
+
trackId: audioTrackId,
|
|
771
|
+
atUs,
|
|
772
|
+
durationUs,
|
|
773
|
+
sourceStartUs,
|
|
774
|
+
sourceDurationUs: durationUs,
|
|
775
|
+
streamIndex: probe.index.tracks.filter(track => track.kind === 'audio').indexOf(audio),
|
|
776
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
let linkGroupId;
|
|
780
|
+
if (videoItemId !== undefined && audioItemId !== undefined) {
|
|
781
|
+
linkGroupId = this.#nextId('link_av');
|
|
782
|
+
this.#project.linkGroups[linkGroupId] = {
|
|
783
|
+
id: linkGroupId,
|
|
784
|
+
kind: 'av-sync',
|
|
785
|
+
itemIds: [videoItemId, audioItemId],
|
|
786
|
+
syncOffsetsUs: { [videoItemId]: 0, [audioItemId]: 0 },
|
|
787
|
+
};
|
|
788
|
+
this.#project.items[videoItemId] = {
|
|
789
|
+
...this.#project.items[videoItemId],
|
|
790
|
+
linkGroupId,
|
|
791
|
+
};
|
|
792
|
+
this.#project.items[audioItemId] = {
|
|
793
|
+
...this.#project.items[audioItemId],
|
|
794
|
+
linkGroupId,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
assetId: options.assetId,
|
|
799
|
+
durationUs,
|
|
800
|
+
probe,
|
|
801
|
+
...(videoTrackId === undefined ? {} : { videoTrackId }),
|
|
802
|
+
...(audioTrackId === undefined ? {} : { audioTrackId }),
|
|
803
|
+
...(videoItemId === undefined ? {} : { videoItemId }),
|
|
804
|
+
...(audioItemId === undefined ? {} : { audioItemId }),
|
|
805
|
+
...(linkGroupId === undefined ? {} : { linkGroupId }),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
addMarker(options) {
|
|
809
|
+
const id = options.id ?? this.#nextId('marker');
|
|
810
|
+
this.#assertUnused(id);
|
|
811
|
+
assertTime(options.timeUs, 'timeUs');
|
|
812
|
+
assertTime(options.durationUs ?? 0, 'durationUs');
|
|
813
|
+
if (options.itemId !== undefined && this.#project.items[options.itemId] === undefined) {
|
|
814
|
+
throw new ReferenceError(`Unknown Item: ${options.itemId}`);
|
|
815
|
+
}
|
|
816
|
+
const marker = {
|
|
817
|
+
id,
|
|
818
|
+
owner: options.itemId === undefined
|
|
819
|
+
? { type: 'sequence', id: this.sequenceId }
|
|
820
|
+
: { type: 'item', id: options.itemId },
|
|
821
|
+
timeUs: options.timeUs,
|
|
822
|
+
durationUs: options.durationUs ?? 0,
|
|
823
|
+
...(options.label === undefined ? {} : { label: options.label }),
|
|
824
|
+
...(options.color === undefined ? {} : { color: options.color }),
|
|
825
|
+
...(options.payload === undefined ? {} : { payload: options.payload }),
|
|
826
|
+
};
|
|
827
|
+
this.#project.markers[id] = marker;
|
|
828
|
+
if (options.itemId === undefined)
|
|
829
|
+
this.#sequence().markerIds.push(id);
|
|
830
|
+
else {
|
|
831
|
+
const item = this.#project.items[options.itemId];
|
|
832
|
+
if (item !== undefined)
|
|
833
|
+
item.markerIds = [...(item.markerIds ?? []), id];
|
|
834
|
+
}
|
|
835
|
+
return id;
|
|
836
|
+
}
|
|
837
|
+
build() {
|
|
838
|
+
const candidate = canonicalClone(this.#project);
|
|
839
|
+
const result = this.#validator.validate(candidate);
|
|
840
|
+
if (!result.ok)
|
|
841
|
+
throw new AelionError(result.diagnostics);
|
|
842
|
+
return deepFreeze(result.value.project);
|
|
843
|
+
}
|
|
844
|
+
#sequence() {
|
|
845
|
+
const sequence = this.#project.sequences[this.sequenceId];
|
|
846
|
+
if (sequence === undefined)
|
|
847
|
+
throw new Error('Default Sequence is missing');
|
|
848
|
+
return sequence;
|
|
849
|
+
}
|
|
850
|
+
#resolveTrack(kind, requestedId) {
|
|
851
|
+
if (requestedId !== undefined) {
|
|
852
|
+
const track = this.#project.tracks[requestedId];
|
|
853
|
+
if (track === undefined)
|
|
854
|
+
throw new ReferenceError(`Unknown Track: ${requestedId}`);
|
|
855
|
+
if (track.kind !== kind)
|
|
856
|
+
throw new TypeError(`${requestedId} is not a ${kind} Track`);
|
|
857
|
+
return requestedId;
|
|
858
|
+
}
|
|
859
|
+
const existing = Object.values(this.#project.tracks).find(track => track.kind === kind);
|
|
860
|
+
return existing?.id ?? this.addTrack({ kind });
|
|
861
|
+
}
|
|
862
|
+
#assertBox(box) {
|
|
863
|
+
assertFiniteNumber(box.x, 'box.x');
|
|
864
|
+
assertFiniteNumber(box.y, 'box.y');
|
|
865
|
+
assertFiniteNumber(box.width, 'box.width');
|
|
866
|
+
assertFiniteNumber(box.height, 'box.height');
|
|
867
|
+
if (box.width <= 0 || box.height <= 0) {
|
|
868
|
+
throw new RangeError('box width and height must be positive');
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
#color(value) {
|
|
872
|
+
let rgba;
|
|
873
|
+
if (typeof value === 'string') {
|
|
874
|
+
if (!/^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/u.test(value)) {
|
|
875
|
+
throw new TypeError('color strings must be #RRGGBB or #RRGGBBAA');
|
|
876
|
+
}
|
|
877
|
+
const channel = (offset) => Number.parseInt(value.slice(offset, offset + 2), 16) / 255;
|
|
878
|
+
const linear = (channelValue) => channelValue <= 0.04045 ? channelValue / 12.92 : ((channelValue + 0.055) / 1.055) ** 2.4;
|
|
879
|
+
rgba = [
|
|
880
|
+
linear(channel(1)),
|
|
881
|
+
linear(channel(3)),
|
|
882
|
+
linear(channel(5)),
|
|
883
|
+
value.length === 9 ? channel(7) : 1,
|
|
884
|
+
];
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
rgba = value;
|
|
888
|
+
}
|
|
889
|
+
if (rgba.length !== 4 || rgba.some(channelValue => !Number.isFinite(channelValue))) {
|
|
890
|
+
throw new TypeError('color must contain four finite channels');
|
|
891
|
+
}
|
|
892
|
+
return { space: 'srgb-linear', rgba: [...rgba] };
|
|
893
|
+
}
|
|
894
|
+
#visual(fit, opacity) {
|
|
895
|
+
assertFiniteNumber(opacity, 'opacity');
|
|
896
|
+
if (opacity < 0 || opacity > 1)
|
|
897
|
+
throw new RangeError('opacity must be from 0 to 1');
|
|
898
|
+
const sequence = this.#sequence();
|
|
899
|
+
const format = sequence.format;
|
|
900
|
+
const width = typeof format.width === 'number' ? format.width : 1920;
|
|
901
|
+
const height = typeof format.height === 'number' ? format.height : 1080;
|
|
902
|
+
return {
|
|
903
|
+
fit,
|
|
904
|
+
transform: {
|
|
905
|
+
positionPx: { x: width / 2, y: height / 2 },
|
|
906
|
+
anchor: { x: 0.5, y: 0.5 },
|
|
907
|
+
scale: { x: 1, y: 1 },
|
|
908
|
+
rotationDeg: 0,
|
|
909
|
+
skewDeg: { x: 0, y: 0 },
|
|
910
|
+
},
|
|
911
|
+
crop: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
912
|
+
opacity,
|
|
913
|
+
blendMode: 'normal',
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
#assertUnused(id) {
|
|
917
|
+
assertEntityId(id, 'entity id');
|
|
918
|
+
for (const collection of [
|
|
919
|
+
this.#project.assets,
|
|
920
|
+
this.#project.sequences,
|
|
921
|
+
this.#project.tracks,
|
|
922
|
+
this.#project.items,
|
|
923
|
+
this.#project.materialInstances,
|
|
924
|
+
this.#project.transitions,
|
|
925
|
+
this.#project.markers,
|
|
926
|
+
this.#project.linkGroups,
|
|
927
|
+
]) {
|
|
928
|
+
if (collection[id] !== undefined)
|
|
929
|
+
throw new TypeError(`Entity id is already used: ${id}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
#nextId(prefix) {
|
|
933
|
+
let counter = this.#counters.get(prefix) ?? 0;
|
|
934
|
+
for (;;) {
|
|
935
|
+
counter += 1;
|
|
936
|
+
const candidate = `${prefix}_${counter.toString()}`;
|
|
937
|
+
try {
|
|
938
|
+
this.#assertUnused(candidate);
|
|
939
|
+
this.#counters.set(prefix, counter);
|
|
940
|
+
return candidate;
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
if (!(error instanceof TypeError) ||
|
|
944
|
+
!error.message.startsWith('Entity id is already used')) {
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
export function createProject(options = {}) {
|
|
952
|
+
return new ProjectBuilder(options);
|
|
953
|
+
}
|