@alpha2-sh/editor 0.0.0-stage → 0.1.0
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.md +21 -0
- package/README.md +108 -2
- package/dist/browser/bridge.d.ts +44 -0
- package/dist/browser/bridge.js +2176 -0
- package/dist/browser/extensions-worker-entry.d.ts +1 -0
- package/dist/browser/extensions-worker-entry.js +3 -0
- package/dist/browser/extensions-worker.d.ts +46 -0
- package/dist/browser/extensions-worker.js +149 -0
- package/dist/browser/hash-worker-entry.d.ts +1 -0
- package/dist/browser/hash-worker-entry.js +3 -0
- package/dist/browser/hash-worker.d.ts +24 -0
- package/dist/browser/hash-worker.js +87 -0
- package/dist/browser/index.d.ts +11 -0
- package/dist/browser/index.js +9 -0
- package/dist/browser/jobs.d.ts +82 -0
- package/dist/browser/jobs.js +291 -0
- package/dist/browser/resources.d.ts +40 -0
- package/dist/browser/resources.js +83 -0
- package/dist/browser/runtime/assets.d.ts +233 -0
- package/dist/browser/runtime/assets.js +633 -0
- package/dist/browser/runtime/audio.d.ts +60 -0
- package/dist/browser/runtime/audio.js +549 -0
- package/dist/browser/runtime/budget.d.ts +11 -0
- package/dist/browser/runtime/budget.js +26 -0
- package/dist/browser/runtime/caption-fonts.d.ts +34 -0
- package/dist/browser/runtime/caption-fonts.js +69 -0
- package/dist/browser/runtime/color.d.ts +7 -0
- package/dist/browser/runtime/color.js +12 -0
- package/dist/browser/runtime/engine.d.ts +91 -0
- package/dist/browser/runtime/engine.js +352 -0
- package/dist/browser/runtime/export-audio.d.ts +23 -0
- package/dist/browser/runtime/export-audio.js +75 -0
- package/dist/browser/runtime/exporter.d.ts +140 -0
- package/dist/browser/runtime/exporter.js +751 -0
- package/dist/browser/runtime/extension-import.d.ts +196 -0
- package/dist/browser/runtime/extension-import.js +521 -0
- package/dist/browser/runtime/fingerprint.d.ts +57 -0
- package/dist/browser/runtime/fingerprint.js +157 -0
- package/dist/browser/runtime/generators.d.ts +188 -0
- package/dist/browser/runtime/generators.js +173 -0
- package/dist/browser/runtime/locks.d.ts +32 -0
- package/dist/browser/runtime/locks.js +86 -0
- package/dist/browser/runtime/output.d.ts +87 -0
- package/dist/browser/runtime/output.js +230 -0
- package/dist/browser/runtime/preview-utils.d.ts +3 -0
- package/dist/browser/runtime/preview-utils.js +49 -0
- package/dist/browser/runtime/proxies.d.ts +72 -0
- package/dist/browser/runtime/proxies.js +369 -0
- package/dist/browser/runtime/proxy-storage.d.ts +59 -0
- package/dist/browser/runtime/proxy-storage.js +119 -0
- package/dist/browser/runtime/sources.d.ts +82 -0
- package/dist/browser/runtime/sources.js +819 -0
- package/dist/browser/runtime/staging.d.ts +10 -0
- package/dist/browser/runtime/staging.js +32 -0
- package/dist/browser/runtime/still.d.ts +21 -0
- package/dist/browser/runtime/still.js +52 -0
- package/dist/browser/runtime/wasm.d.ts +9 -0
- package/dist/browser/runtime/wasm.js +20 -0
- package/dist/browser/types.d.ts +549 -0
- package/dist/browser/types.js +1 -0
- package/dist/browser/worker-entry.d.ts +1 -0
- package/dist/browser/worker-entry.js +2 -0
- package/dist/browser/worker.d.ts +24 -0
- package/dist/browser/worker.js +239 -0
- package/dist/core/captions.d.ts +34 -0
- package/dist/core/captions.js +108 -0
- package/dist/core/clip-time.d.ts +31 -0
- package/dist/core/clip-time.js +125 -0
- package/dist/core/commands.d.ts +413 -0
- package/dist/core/commands.js +1118 -0
- package/dist/core/compositions.d.ts +234 -0
- package/dist/core/compositions.js +851 -0
- package/dist/core/document.d.ts +245 -0
- package/dist/core/document.js +683 -0
- package/dist/core/keyframes.d.ts +82 -0
- package/dist/core/keyframes.js +153 -0
- package/dist/core/presets.d.ts +64 -0
- package/dist/core/presets.js +241 -0
- package/dist/core/session.d.ts +150 -0
- package/dist/core/session.js +541 -0
- package/dist/core/text.d.ts +17 -0
- package/dist/core/text.js +17 -0
- package/dist/core/time.d.ts +19 -0
- package/dist/core/time.js +39 -0
- package/dist/core/timeline-index.d.ts +44 -0
- package/dist/core/timeline-index.js +59 -0
- package/dist/core/transitions.d.ts +20 -0
- package/dist/core/transitions.js +118 -0
- package/dist/core/validated.d.ts +23 -0
- package/dist/core/validated.js +52 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/native-client/index.d.ts +131 -0
- package/dist/native-client/index.js +354 -0
- package/dist/ports/index.d.ts +146 -0
- package/dist/ports/index.js +1 -0
- package/dist/react/index.d.ts +37 -0
- package/dist/react/index.js +94 -0
- package/package.json +79 -4
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { classifyBrowserFailure, failureOf, withSecondaryFailure, } from "alpha2/export-failure";
|
|
2
|
+
import { decideRetry, RETRY_PROFILES, } from "alpha2/export-retry";
|
|
3
|
+
import { validFrameRate } from "alpha2/project";
|
|
4
|
+
import { EditorError } from "../core/document.js";
|
|
5
|
+
/**
|
|
6
|
+
* Refuses an export `frameRate` the engine cannot read, before an export starts, for every
|
|
7
|
+
* backend: one rate, the same everywhere.
|
|
8
|
+
*/
|
|
9
|
+
export function requireExportFrameRate(frameRate) {
|
|
10
|
+
if (frameRate !== undefined && !validFrameRate(frameRate))
|
|
11
|
+
throw new EditorError("invalid_frame_rate", `Export frameRate must be { num, den }, positive 32-bit integers with a frame at least one tick long; received ${JSON.stringify(frameRate)}`, "export");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One export of a pinned document, run in bounded attempts.
|
|
15
|
+
*
|
|
16
|
+
* Every attempt renders the same document from the start, on a fresh device or host, with the same
|
|
17
|
+
* output settings; a failed one is retried only as the retry policy allows, with less in flight
|
|
18
|
+
* when the failure is evidence of memory exhaustion. Progress from an attempt that has ended is
|
|
19
|
+
* ignored, so a late answer cannot move a newer attempt's progress or change its result. Cancelling
|
|
20
|
+
* stops the running attempt and any pending retry; no new attempt starts after it.
|
|
21
|
+
*/
|
|
22
|
+
export class ExportJob {
|
|
23
|
+
revision;
|
|
24
|
+
id;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves to the artifact. Rejects with `EditorError` `cancelled` after {@link cancel}, or
|
|
27
|
+
* with the last attempt's error, whose `failure` property (and {@link failure}) holds the
|
|
28
|
+
* classified `ExportFailure`.
|
|
29
|
+
*/
|
|
30
|
+
result;
|
|
31
|
+
#controller = new AbortController();
|
|
32
|
+
#listeners = new Set();
|
|
33
|
+
#progress = { phase: "preparing", fraction: 0 };
|
|
34
|
+
#settled = false;
|
|
35
|
+
#failure;
|
|
36
|
+
#attempts = [];
|
|
37
|
+
#current = 0;
|
|
38
|
+
/**
|
|
39
|
+
* Starts the job at once, on the next microtask.
|
|
40
|
+
*
|
|
41
|
+
* @param revision The editor revision `document` was pinned at.
|
|
42
|
+
* @param backend Runs each attempt; its `dispose` runs once when the job settles.
|
|
43
|
+
* @param release Called once when the job settles, whatever the outcome.
|
|
44
|
+
*/
|
|
45
|
+
constructor(revision, document, backend, release = () => { }, options = {}) {
|
|
46
|
+
this.revision = revision;
|
|
47
|
+
this.id = options.id ?? crypto.randomUUID();
|
|
48
|
+
const signal = this.#controller.signal;
|
|
49
|
+
const progress = (value) => {
|
|
50
|
+
this.#progress = Object.freeze(value);
|
|
51
|
+
for (const listener of this.#listeners)
|
|
52
|
+
listener(this.#progress);
|
|
53
|
+
};
|
|
54
|
+
const cancelledBy = (cause) => signal.aborted &&
|
|
55
|
+
!(cause instanceof EditorError && cause.code === "cancel_failed");
|
|
56
|
+
this.result = this.#attempt(document, backend, progress, options)
|
|
57
|
+
.then(async (value) => {
|
|
58
|
+
// A cancel wins until the job settles, even over an attempt that already finished: what it
|
|
59
|
+
// made is released and the job settles as cancelled.
|
|
60
|
+
if (signal.aborted) {
|
|
61
|
+
this.#record(this.#current, {
|
|
62
|
+
outcome: "cancelled",
|
|
63
|
+
endedAt: Date.now(),
|
|
64
|
+
});
|
|
65
|
+
await releaseArtifact(value);
|
|
66
|
+
signal.throwIfAborted();
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
})
|
|
70
|
+
.then(async (value) => {
|
|
71
|
+
await backend.dispose?.({ ok: true, cancelled: false });
|
|
72
|
+
progress({
|
|
73
|
+
phase: "completed",
|
|
74
|
+
fraction: 1,
|
|
75
|
+
attempt: this.#current,
|
|
76
|
+
});
|
|
77
|
+
return value;
|
|
78
|
+
}, async (cause) => {
|
|
79
|
+
const cancelled = cancelledBy(cause);
|
|
80
|
+
let failure = cancelled
|
|
81
|
+
? undefined
|
|
82
|
+
: {
|
|
83
|
+
...(failureOf(cause) ??
|
|
84
|
+
classifyBrowserFailure(cause, {
|
|
85
|
+
phase: phaseOf(this.#progress),
|
|
86
|
+
})),
|
|
87
|
+
jobId: this.id,
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
await backend.dispose?.({ ok: false, cancelled });
|
|
91
|
+
}
|
|
92
|
+
catch (cleanup) {
|
|
93
|
+
// A failed job keeps its failure and notes the cleanup beside it.
|
|
94
|
+
if (cancelled)
|
|
95
|
+
throw cleanup;
|
|
96
|
+
if (failure)
|
|
97
|
+
failure = withSecondaryFailure(failure, {
|
|
98
|
+
code: "UNKNOWN",
|
|
99
|
+
phase: "cleanup",
|
|
100
|
+
message: cleanup instanceof Error ? cleanup.message : String(cleanup),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (failure)
|
|
104
|
+
this.#failure = Object.freeze(failure);
|
|
105
|
+
throw cause;
|
|
106
|
+
})
|
|
107
|
+
.catch((cause) => {
|
|
108
|
+
const cancelled = cancelledBy(cause);
|
|
109
|
+
progress({
|
|
110
|
+
phase: cancelled ? "cancelled" : "failed",
|
|
111
|
+
fraction: this.#progress.fraction,
|
|
112
|
+
attempt: this.#current,
|
|
113
|
+
});
|
|
114
|
+
throw cancelled
|
|
115
|
+
? new EditorError("cancelled", "Export cancelled", "export", undefined, true, { cause })
|
|
116
|
+
: cause;
|
|
117
|
+
})
|
|
118
|
+
.finally(() => {
|
|
119
|
+
this.#settled = true;
|
|
120
|
+
release();
|
|
121
|
+
});
|
|
122
|
+
// Jobs may be cancelled before the caller starts awaiting result.
|
|
123
|
+
void this.result.catch(() => { });
|
|
124
|
+
}
|
|
125
|
+
async #attempt(document, backend, progress, options) {
|
|
126
|
+
const signal = this.#controller.signal;
|
|
127
|
+
const policy = options.retry === false ? undefined : (options.retry ?? {});
|
|
128
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
129
|
+
const history = [];
|
|
130
|
+
let profile = policy?.profiles?.[0] ?? RETRY_PROFILES[0];
|
|
131
|
+
await Promise.resolve();
|
|
132
|
+
for (let number = 1;; number += 1) {
|
|
133
|
+
signal.throwIfAborted();
|
|
134
|
+
const attempt = Object.freeze({
|
|
135
|
+
id: `${this.id}/${number}`,
|
|
136
|
+
number,
|
|
137
|
+
profile,
|
|
138
|
+
...(history.length > 0 ? { previous: history.at(-1).failure } : {}),
|
|
139
|
+
});
|
|
140
|
+
this.#current = number;
|
|
141
|
+
this.#record(number, {
|
|
142
|
+
id: attempt.id,
|
|
143
|
+
number,
|
|
144
|
+
profile,
|
|
145
|
+
startedAt: Date.now(),
|
|
146
|
+
outcome: "running",
|
|
147
|
+
});
|
|
148
|
+
// Progress from an attempt that has ended belongs to nothing.
|
|
149
|
+
const report = (value) => {
|
|
150
|
+
if (this.#current === number && !this.#settled)
|
|
151
|
+
progress({ ...value, attempt: number });
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const value = await backend.run(document, signal, report, attempt);
|
|
155
|
+
this.#record(number, { outcome: "succeeded", endedAt: Date.now() });
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
catch (cause) {
|
|
159
|
+
if (signal.aborted) {
|
|
160
|
+
this.#record(number, { outcome: "cancelled", endedAt: Date.now() });
|
|
161
|
+
throw cause;
|
|
162
|
+
}
|
|
163
|
+
// The next attempt owns progress from here: a late report from this one is dropped.
|
|
164
|
+
this.#current = number + 1;
|
|
165
|
+
const failure = {
|
|
166
|
+
...(failureOf(cause) ??
|
|
167
|
+
classifyBrowserFailure(cause, {
|
|
168
|
+
phase: phaseOf(this.#progress),
|
|
169
|
+
})),
|
|
170
|
+
jobId: this.id,
|
|
171
|
+
attemptId: attempt.id,
|
|
172
|
+
};
|
|
173
|
+
history.push({ failure, profile });
|
|
174
|
+
const decision = policy
|
|
175
|
+
? decideRetry(history, policy)
|
|
176
|
+
: { retry: false, reason: "retries are off for this job" };
|
|
177
|
+
this.#record(number, {
|
|
178
|
+
outcome: "failed",
|
|
179
|
+
endedAt: Date.now(),
|
|
180
|
+
failure,
|
|
181
|
+
decision: { retry: decision.retry, reason: decision.reason },
|
|
182
|
+
});
|
|
183
|
+
if (!decision.retry) {
|
|
184
|
+
this.#current = number;
|
|
185
|
+
if (cause instanceof Error)
|
|
186
|
+
Object.defineProperty(cause, "failure", {
|
|
187
|
+
value: failure,
|
|
188
|
+
configurable: true,
|
|
189
|
+
});
|
|
190
|
+
throw cause;
|
|
191
|
+
}
|
|
192
|
+
progress({
|
|
193
|
+
phase: "retrying",
|
|
194
|
+
fraction: this.#progress.fraction,
|
|
195
|
+
attempt: number + 1,
|
|
196
|
+
retry: { reason: decision.reason, delayMs: decision.delayMs },
|
|
197
|
+
});
|
|
198
|
+
await sleep(decision.delayMs, signal);
|
|
199
|
+
profile = decision.profile;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
#record(number, patch) {
|
|
204
|
+
const index = number - 1;
|
|
205
|
+
this.#attempts[index] = Object.freeze({
|
|
206
|
+
...this.#attempts[index],
|
|
207
|
+
...patch,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** The latest progress, frozen. Reports from an attempt that has ended are not reflected. */
|
|
211
|
+
get progress() {
|
|
212
|
+
return this.#progress;
|
|
213
|
+
}
|
|
214
|
+
/** Why the job failed, classified; undefined unless it failed. */
|
|
215
|
+
get failure() {
|
|
216
|
+
return this.#failure;
|
|
217
|
+
}
|
|
218
|
+
/** Every attempt so far, oldest first. */
|
|
219
|
+
get attempts() {
|
|
220
|
+
return [...this.#attempts];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Calls `listener` with the current progress at once and with every later change. Returns
|
|
224
|
+
* the unsubscribe function.
|
|
225
|
+
*/
|
|
226
|
+
subscribe(listener) {
|
|
227
|
+
this.#listeners.add(listener);
|
|
228
|
+
listener(this.#progress);
|
|
229
|
+
return () => this.#listeners.delete(listener);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Stops the running attempt and any pending retry, and resolves once the job has settled. An
|
|
233
|
+
* attempt that finished before the job settled has what it made released (a temporary file
|
|
234
|
+
* deleted). Resolves without effect on a settled job. Rejects only when cleanup failed
|
|
235
|
+
* (`EditorError` `cancel_failed`).
|
|
236
|
+
*/
|
|
237
|
+
async cancel() {
|
|
238
|
+
if (!this.#settled) {
|
|
239
|
+
this.#progress = {
|
|
240
|
+
phase: "cancelling",
|
|
241
|
+
fraction: this.#progress.fraction,
|
|
242
|
+
attempt: this.#current,
|
|
243
|
+
};
|
|
244
|
+
for (const listener of this.#listeners)
|
|
245
|
+
listener(this.#progress);
|
|
246
|
+
this.#controller.abort();
|
|
247
|
+
}
|
|
248
|
+
await this.result.catch((error) => {
|
|
249
|
+
if (error instanceof EditorError && error.code === "cancel_failed")
|
|
250
|
+
throw error;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Deletes what a finished attempt made, when its kind can be deleted. */
|
|
255
|
+
async function releaseArtifact(artifact) {
|
|
256
|
+
if ("release" in artifact && typeof artifact.release === "function")
|
|
257
|
+
await artifact.release();
|
|
258
|
+
}
|
|
259
|
+
/** Waits `ms`, or rejects as soon as `signal` aborts. */
|
|
260
|
+
function abortableSleep(ms, signal) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const stop = () => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
reject(signal.reason instanceof Error
|
|
265
|
+
? signal.reason
|
|
266
|
+
: new DOMException("Cancelled", "AbortError"));
|
|
267
|
+
};
|
|
268
|
+
const timer = setTimeout(() => {
|
|
269
|
+
signal.removeEventListener("abort", stop);
|
|
270
|
+
resolve();
|
|
271
|
+
}, ms);
|
|
272
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
273
|
+
if (signal.aborted)
|
|
274
|
+
stop();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** The export step a job's progress was in, for an error that did not say. */
|
|
278
|
+
function phaseOf(progress) {
|
|
279
|
+
switch (progress.phase) {
|
|
280
|
+
case "rendering":
|
|
281
|
+
return "render";
|
|
282
|
+
case "finalizing":
|
|
283
|
+
return "mux";
|
|
284
|
+
case "publishing":
|
|
285
|
+
return "publish";
|
|
286
|
+
case "downloading":
|
|
287
|
+
return "download";
|
|
288
|
+
default:
|
|
289
|
+
return "prepare";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** A counted reference to a registry entry. */
|
|
2
|
+
export interface Lease<T> {
|
|
3
|
+
readonly value: T;
|
|
4
|
+
/**
|
|
5
|
+
* Drops this reference. Idempotent. The entry's `dispose` runs when its last lease is
|
|
6
|
+
* released.
|
|
7
|
+
*/
|
|
8
|
+
release(): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Holds runtime values (decoders, bitmaps, files) that must never enter the project JSON or
|
|
12
|
+
* its history, keyed by id and revision and kept alive by explicit leases from owners and
|
|
13
|
+
* export jobs.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ResourceRegistry<T> {
|
|
16
|
+
#private;
|
|
17
|
+
readonly limit: number;
|
|
18
|
+
/** @param limit Most entries held at once (default 128). */
|
|
19
|
+
constructor(limit?: number);
|
|
20
|
+
/**
|
|
21
|
+
* Stores `value` under `id` at `revision` and returns its first lease. `dispose` runs once,
|
|
22
|
+
* when the last lease is released or the registry is disposed.
|
|
23
|
+
*
|
|
24
|
+
* Throws `EditorError` `disposed` after {@link dispose}, `duplicate_resource` if that
|
|
25
|
+
* revision is already stored, and `resource_budget` when `limit` entries are held.
|
|
26
|
+
*/
|
|
27
|
+
put(id: string, revision: number, value: T, dispose: () => void): Lease<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Takes another lease on a stored entry. Throws `EditorError` `missing_asset` if no entry
|
|
30
|
+
* exists for `id` at `revision`.
|
|
31
|
+
*/
|
|
32
|
+
acquire(id: string, revision: number): Lease<T>;
|
|
33
|
+
/** Entries currently held. */
|
|
34
|
+
get size(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Disposes every entry regardless of outstanding leases and refuses later `put` calls.
|
|
37
|
+
* Idempotent.
|
|
38
|
+
*/
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { EditorError } from "../core/document.js";
|
|
2
|
+
/**
|
|
3
|
+
* Holds runtime values (decoders, bitmaps, files) that must never enter the project JSON or
|
|
4
|
+
* its history, keyed by id and revision and kept alive by explicit leases from owners and
|
|
5
|
+
* export jobs.
|
|
6
|
+
*/
|
|
7
|
+
export class ResourceRegistry {
|
|
8
|
+
limit;
|
|
9
|
+
#entries = new Map();
|
|
10
|
+
#closed = false;
|
|
11
|
+
/** @param limit Most entries held at once (default 128). */
|
|
12
|
+
constructor(limit = 128) {
|
|
13
|
+
this.limit = limit;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Stores `value` under `id` at `revision` and returns its first lease. `dispose` runs once,
|
|
17
|
+
* when the last lease is released or the registry is disposed.
|
|
18
|
+
*
|
|
19
|
+
* Throws `EditorError` `disposed` after {@link dispose}, `duplicate_resource` if that
|
|
20
|
+
* revision is already stored, and `resource_budget` when `limit` entries are held.
|
|
21
|
+
*/
|
|
22
|
+
put(id, revision, value, dispose) {
|
|
23
|
+
if (this.#closed)
|
|
24
|
+
throw new EditorError("disposed", "Resources are closed", "resource");
|
|
25
|
+
const key = JSON.stringify([id, revision]);
|
|
26
|
+
if (this.#entries.has(key))
|
|
27
|
+
throw new EditorError("duplicate_resource", "Resource revision already exists", "resource", id);
|
|
28
|
+
if (this.#entries.size >= this.limit)
|
|
29
|
+
throw new EditorError("resource_budget", "Resource budget reached", "resource", id);
|
|
30
|
+
let closed = false;
|
|
31
|
+
this.#entries.set(key, {
|
|
32
|
+
value,
|
|
33
|
+
refs: 0,
|
|
34
|
+
dispose: () => {
|
|
35
|
+
if (!closed) {
|
|
36
|
+
closed = true;
|
|
37
|
+
dispose();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return this.acquire(id, revision);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Takes another lease on a stored entry. Throws `EditorError` `missing_asset` if no entry
|
|
45
|
+
* exists for `id` at `revision`.
|
|
46
|
+
*/
|
|
47
|
+
acquire(id, revision) {
|
|
48
|
+
const key = JSON.stringify([id, revision]);
|
|
49
|
+
const entry = this.#entries.get(key);
|
|
50
|
+
if (!entry)
|
|
51
|
+
throw new EditorError("missing_asset", "Runtime resource is missing", "resource", id);
|
|
52
|
+
entry.refs++;
|
|
53
|
+
let released = false;
|
|
54
|
+
return {
|
|
55
|
+
value: entry.value,
|
|
56
|
+
release: () => {
|
|
57
|
+
if (released)
|
|
58
|
+
return;
|
|
59
|
+
released = true;
|
|
60
|
+
if (--entry.refs === 0) {
|
|
61
|
+
this.#entries.delete(key);
|
|
62
|
+
entry.dispose();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Entries currently held. */
|
|
68
|
+
get size() {
|
|
69
|
+
return this.#entries.size;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Disposes every entry regardless of outstanding leases and refuses later `put` calls.
|
|
73
|
+
* Idempotent.
|
|
74
|
+
*/
|
|
75
|
+
dispose() {
|
|
76
|
+
if (this.#closed)
|
|
77
|
+
return;
|
|
78
|
+
this.#closed = true;
|
|
79
|
+
for (const entry of this.#entries.values())
|
|
80
|
+
entry.dispose();
|
|
81
|
+
this.#entries.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a video container holds, read by the demuxer: its picture (`video`: size, frame rate, an HDR
|
|
3
|
+
* declaration), or null when it has no video track, and whether it has an audio stream. Null when
|
|
4
|
+
* the demuxer cannot read the container.
|
|
5
|
+
*/
|
|
6
|
+
export function probeVideo(file: any): Promise<{
|
|
7
|
+
video: {
|
|
8
|
+
colorMetadata: import("alpha2").ColorMetadata | null;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
frameRate: number;
|
|
12
|
+
} | null;
|
|
13
|
+
hasAudio: boolean;
|
|
14
|
+
audioDecodable: boolean;
|
|
15
|
+
} | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Streams a file's primary audio track into per-bucket peak magnitudes (`bucketsPerSecond`, capped
|
|
18
|
+
* at `maxBuckets`) with bounded memory. A bucket's peak is its loudest sample in any channel,
|
|
19
|
+
* since decoders order channels differently. `null` without audio.
|
|
20
|
+
*/
|
|
21
|
+
export function streamPeaks(file: any, { signal, bucketsPerSecond, maxBuckets }?: {
|
|
22
|
+
bucketsPerSecond?: number | undefined;
|
|
23
|
+
maxBuckets?: number | undefined;
|
|
24
|
+
}): Promise<Float32Array<ArrayBuffer> | null>;
|
|
25
|
+
/** Per-bucket peak magnitudes used to draw clip waveforms. */
|
|
26
|
+
export function computePeaks(buffer: any, buckets?: number): Float32Array<ArrayBuffer>;
|
|
27
|
+
export const IMAGE_TYPES: string[];
|
|
28
|
+
export const VIDEO_TYPES: string[];
|
|
29
|
+
export const AUDIO_TYPES: string[];
|
|
30
|
+
export const ACCEPTED_TYPES: string;
|
|
31
|
+
/** A title's style where its authoring leaves a field out (`TEXT_DEFAULTS`, exported from core). */
|
|
32
|
+
export const DEFAULT_TEXT: Readonly<{
|
|
33
|
+
readonly content: "alpha2";
|
|
34
|
+
readonly fontSize: 120;
|
|
35
|
+
readonly fontFamily: "Inter, Segoe UI, system-ui, sans-serif";
|
|
36
|
+
readonly color: "#ffffff";
|
|
37
|
+
readonly align: "center";
|
|
38
|
+
readonly maxWidth: 0;
|
|
39
|
+
readonly lineHeight: 1.15;
|
|
40
|
+
}>;
|
|
41
|
+
export class AssetLibrary {
|
|
42
|
+
get map(): Map<any, any>;
|
|
43
|
+
get all(): any[];
|
|
44
|
+
get(id: any): any;
|
|
45
|
+
/** Metadata persisted in an editor project. Media bytes stay in local files. */
|
|
46
|
+
projectManifest(): {
|
|
47
|
+
id: any;
|
|
48
|
+
kind: any;
|
|
49
|
+
name: any;
|
|
50
|
+
locator: any;
|
|
51
|
+
type: any;
|
|
52
|
+
size: any;
|
|
53
|
+
lastModified: any;
|
|
54
|
+
width: any;
|
|
55
|
+
height: any;
|
|
56
|
+
frameRate: any;
|
|
57
|
+
duration: any;
|
|
58
|
+
hasAudio: boolean;
|
|
59
|
+
fit: any;
|
|
60
|
+
colorMetadata: any;
|
|
61
|
+
text: any;
|
|
62
|
+
generator: any;
|
|
63
|
+
}[];
|
|
64
|
+
/** Replaces the library with embedded text assets and missing local-file references. */
|
|
65
|
+
restoreProjectManifest(manifest: any): any[];
|
|
66
|
+
get missing(): any[];
|
|
67
|
+
/** Imports a non-media resource consumed by an engine effect. */
|
|
68
|
+
importEffectResource(file: any, kind: any): Promise<{
|
|
69
|
+
id: any;
|
|
70
|
+
kind: any;
|
|
71
|
+
name: any;
|
|
72
|
+
locator: any;
|
|
73
|
+
file: any;
|
|
74
|
+
text: any;
|
|
75
|
+
}>;
|
|
76
|
+
usedBy(doc: any): Set<any>;
|
|
77
|
+
remove(id: any): void;
|
|
78
|
+
/**
|
|
79
|
+
* Imports browser files; each one that does not import is reported back with its name and why
|
|
80
|
+
* (see {@link importFile}).
|
|
81
|
+
*/
|
|
82
|
+
importFiles(files: any, onProgress: any): Promise<{
|
|
83
|
+
imported: ({
|
|
84
|
+
id: any;
|
|
85
|
+
kind: string;
|
|
86
|
+
name: any;
|
|
87
|
+
locator: any;
|
|
88
|
+
file: any;
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
duration: number;
|
|
92
|
+
fit: string;
|
|
93
|
+
thumbnail: HTMLCanvasElement;
|
|
94
|
+
} | {
|
|
95
|
+
id: any;
|
|
96
|
+
kind: string;
|
|
97
|
+
name: any;
|
|
98
|
+
locator: any;
|
|
99
|
+
file: any;
|
|
100
|
+
url: string;
|
|
101
|
+
video: HTMLVideoElement;
|
|
102
|
+
width: any;
|
|
103
|
+
height: any;
|
|
104
|
+
frameRate: any;
|
|
105
|
+
duration: number;
|
|
106
|
+
hasAudio: any;
|
|
107
|
+
audioDecodable: any;
|
|
108
|
+
colorMetadata: any;
|
|
109
|
+
fit: string;
|
|
110
|
+
thumbnail: null;
|
|
111
|
+
peaks: null;
|
|
112
|
+
} | {
|
|
113
|
+
id: any;
|
|
114
|
+
kind: string;
|
|
115
|
+
name: any;
|
|
116
|
+
locator: any;
|
|
117
|
+
file: any;
|
|
118
|
+
duration: number;
|
|
119
|
+
hasAudio: boolean;
|
|
120
|
+
audioDecodable: boolean;
|
|
121
|
+
peaks: null;
|
|
122
|
+
thumbnail: null;
|
|
123
|
+
})[];
|
|
124
|
+
rejected: {
|
|
125
|
+
name: any;
|
|
126
|
+
error: unknown;
|
|
127
|
+
}[];
|
|
128
|
+
relinked: any[];
|
|
129
|
+
}>;
|
|
130
|
+
/**
|
|
131
|
+
* Imports one browser file as an image, video or audio asset. Rejects with an `EditorError`:
|
|
132
|
+
* `unsupported_media` for a file that is none of those, `unreadable_media` for one that is but
|
|
133
|
+
* that this browser could not read (a codec it does not decode, a damaged file), with the
|
|
134
|
+
* decoder's or demuxer's reason in the message and as the `cause`.
|
|
135
|
+
*/
|
|
136
|
+
importFile(file: any): Promise<{
|
|
137
|
+
id: any;
|
|
138
|
+
kind: string;
|
|
139
|
+
name: any;
|
|
140
|
+
locator: any;
|
|
141
|
+
file: any;
|
|
142
|
+
width: number;
|
|
143
|
+
height: number;
|
|
144
|
+
duration: number;
|
|
145
|
+
fit: string;
|
|
146
|
+
thumbnail: HTMLCanvasElement;
|
|
147
|
+
} | {
|
|
148
|
+
id: any;
|
|
149
|
+
kind: string;
|
|
150
|
+
name: any;
|
|
151
|
+
locator: any;
|
|
152
|
+
file: any;
|
|
153
|
+
url: string;
|
|
154
|
+
video: HTMLVideoElement;
|
|
155
|
+
width: any;
|
|
156
|
+
height: any;
|
|
157
|
+
frameRate: any;
|
|
158
|
+
duration: number;
|
|
159
|
+
hasAudio: any;
|
|
160
|
+
audioDecodable: any;
|
|
161
|
+
colorMetadata: any;
|
|
162
|
+
fit: string;
|
|
163
|
+
thumbnail: null;
|
|
164
|
+
peaks: null;
|
|
165
|
+
} | {
|
|
166
|
+
id: any;
|
|
167
|
+
kind: string;
|
|
168
|
+
name: any;
|
|
169
|
+
locator: any;
|
|
170
|
+
file: any;
|
|
171
|
+
duration: number;
|
|
172
|
+
hasAudio: boolean;
|
|
173
|
+
audioDecodable: boolean;
|
|
174
|
+
peaks: null;
|
|
175
|
+
thumbnail: null;
|
|
176
|
+
}>;
|
|
177
|
+
/** Registers a project-rate audio buffer synthesized in the page rather than read from a file. */
|
|
178
|
+
addGeneratedAudio(name: any, audioBuffer: any): {
|
|
179
|
+
id: string;
|
|
180
|
+
kind: string;
|
|
181
|
+
name: any;
|
|
182
|
+
generated: boolean;
|
|
183
|
+
duration: any;
|
|
184
|
+
hasAudio: boolean;
|
|
185
|
+
audioBuffer: any;
|
|
186
|
+
peaks: Float32Array<ArrayBuffer>;
|
|
187
|
+
thumbnail: null;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Creates a text source rasterized by the packaged `OffscreenTextRasterizer`, the browser host
|
|
191
|
+
* for the engine's host-rasterized text contract.
|
|
192
|
+
*/
|
|
193
|
+
createText(style?: {}, { id }?: {
|
|
194
|
+
id?: string | undefined;
|
|
195
|
+
}): {
|
|
196
|
+
id: string;
|
|
197
|
+
kind: string;
|
|
198
|
+
name: string;
|
|
199
|
+
text: {
|
|
200
|
+
content: "alpha2";
|
|
201
|
+
fontSize: 120;
|
|
202
|
+
fontFamily: "Inter, Segoe UI, system-ui, sans-serif";
|
|
203
|
+
color: "#ffffff";
|
|
204
|
+
align: "center";
|
|
205
|
+
maxWidth: 0;
|
|
206
|
+
lineHeight: 1.15;
|
|
207
|
+
};
|
|
208
|
+
duration: number;
|
|
209
|
+
fit: string;
|
|
210
|
+
raster: null;
|
|
211
|
+
thumbnail: null;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Creates a source the engine draws itself from `generator` (see `generators.js`); it needs no
|
|
215
|
+
* file and has no duration.
|
|
216
|
+
*/
|
|
217
|
+
createGenerated(generator: any, { id, name }?: {
|
|
218
|
+
id?: string | undefined;
|
|
219
|
+
name?: null | undefined;
|
|
220
|
+
}): {
|
|
221
|
+
id: string;
|
|
222
|
+
kind: string;
|
|
223
|
+
name: string;
|
|
224
|
+
generator: any;
|
|
225
|
+
duration: number;
|
|
226
|
+
thumbnail: null;
|
|
227
|
+
};
|
|
228
|
+
updateGenerated(asset: any, generator: any): void;
|
|
229
|
+
rasterizeText(asset: any, text: any): any;
|
|
230
|
+
dispose(): void;
|
|
231
|
+
clear(): void;
|
|
232
|
+
#private;
|
|
233
|
+
}
|