@effing/ffs 0.4.0 → 0.5.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/dist/chunk-5SGOYTM2.js +341 -0
- package/dist/chunk-5SGOYTM2.js.map +1 -0
- package/dist/{chunk-J64HSZNQ.js → chunk-N3D6I2BD.js} +179 -499
- package/dist/chunk-N3D6I2BD.js.map +1 -0
- package/dist/chunk-QPZEAH3J.js +342 -0
- package/dist/{chunk-7FMPCMLO.js → chunk-ZERUSI5T.js} +10 -5
- package/dist/chunk-ZERUSI5T.js.map +1 -0
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.js +2 -2
- package/dist/index.js +2 -1
- package/dist/render-NEDCS65O.js +8 -0
- package/dist/render-NEDCS65O.js.map +1 -0
- package/dist/render-VWBOR3Y2.js +936 -0
- package/dist/server.js +22 -1259
- package/dist/server.js.map +1 -1
- package/package.json +5 -3
- package/dist/chunk-7FMPCMLO.js.map +0 -1
- package/dist/chunk-J64HSZNQ.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createTransientStore,
|
|
4
|
+
ffsFetch,
|
|
5
|
+
storeKeys
|
|
6
|
+
} from "./chunk-QPZEAH3J.js";
|
|
2
7
|
|
|
3
8
|
// src/server.ts
|
|
4
9
|
import express5 from "express";
|
|
@@ -7,346 +12,9 @@ import bodyParser from "body-parser";
|
|
|
7
12
|
// src/handlers/shared.ts
|
|
8
13
|
import "express";
|
|
9
14
|
|
|
10
|
-
// src/storage.ts
|
|
11
|
-
import {
|
|
12
|
-
S3Client,
|
|
13
|
-
PutObjectCommand,
|
|
14
|
-
GetObjectCommand,
|
|
15
|
-
HeadObjectCommand,
|
|
16
|
-
DeleteObjectCommand
|
|
17
|
-
} from "@aws-sdk/client-s3";
|
|
18
|
-
import { Upload } from "@aws-sdk/lib-storage";
|
|
19
|
-
import fs from "fs/promises";
|
|
20
|
-
import { createReadStream, createWriteStream, existsSync } from "fs";
|
|
21
|
-
import { pipeline } from "stream/promises";
|
|
22
|
-
import path from "path";
|
|
23
|
-
import os from "os";
|
|
24
|
-
import crypto from "crypto";
|
|
25
|
-
var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
|
|
26
|
-
var DEFAULT_JOB_METADATA_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
27
|
-
var S3TransientStore = class {
|
|
28
|
-
client;
|
|
29
|
-
bucket;
|
|
30
|
-
prefix;
|
|
31
|
-
sourceTtlMs;
|
|
32
|
-
jobMetadataTtlMs;
|
|
33
|
-
constructor(options) {
|
|
34
|
-
this.client = new S3Client({
|
|
35
|
-
endpoint: options.endpoint,
|
|
36
|
-
region: options.region ?? "auto",
|
|
37
|
-
credentials: options.accessKeyId ? {
|
|
38
|
-
accessKeyId: options.accessKeyId,
|
|
39
|
-
secretAccessKey: options.secretAccessKey
|
|
40
|
-
} : void 0,
|
|
41
|
-
forcePathStyle: !!options.endpoint
|
|
42
|
-
});
|
|
43
|
-
this.bucket = options.bucket;
|
|
44
|
-
this.prefix = options.prefix ?? "";
|
|
45
|
-
this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
46
|
-
this.jobMetadataTtlMs = options.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
47
|
-
}
|
|
48
|
-
getExpires(ttlMs) {
|
|
49
|
-
return new Date(Date.now() + ttlMs);
|
|
50
|
-
}
|
|
51
|
-
getFullKey(key) {
|
|
52
|
-
return `${this.prefix}${key}`;
|
|
53
|
-
}
|
|
54
|
-
async put(key, stream, ttlMs) {
|
|
55
|
-
const upload = new Upload({
|
|
56
|
-
client: this.client,
|
|
57
|
-
params: {
|
|
58
|
-
Bucket: this.bucket,
|
|
59
|
-
Key: this.getFullKey(key),
|
|
60
|
-
Body: stream,
|
|
61
|
-
Expires: this.getExpires(ttlMs ?? this.sourceTtlMs)
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
await upload.done();
|
|
65
|
-
}
|
|
66
|
-
async getStream(key) {
|
|
67
|
-
try {
|
|
68
|
-
const response = await this.client.send(
|
|
69
|
-
new GetObjectCommand({
|
|
70
|
-
Bucket: this.bucket,
|
|
71
|
-
Key: this.getFullKey(key)
|
|
72
|
-
})
|
|
73
|
-
);
|
|
74
|
-
return response.Body;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const error = err;
|
|
77
|
-
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async exists(key) {
|
|
84
|
-
try {
|
|
85
|
-
await this.client.send(
|
|
86
|
-
new HeadObjectCommand({
|
|
87
|
-
Bucket: this.bucket,
|
|
88
|
-
Key: this.getFullKey(key)
|
|
89
|
-
})
|
|
90
|
-
);
|
|
91
|
-
return true;
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const error = err;
|
|
94
|
-
if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
throw err;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
async existsMany(keys) {
|
|
101
|
-
const results = await Promise.all(
|
|
102
|
-
keys.map(async (key) => [key, await this.exists(key)])
|
|
103
|
-
);
|
|
104
|
-
return new Map(results);
|
|
105
|
-
}
|
|
106
|
-
async delete(key) {
|
|
107
|
-
try {
|
|
108
|
-
await this.client.send(
|
|
109
|
-
new DeleteObjectCommand({
|
|
110
|
-
Bucket: this.bucket,
|
|
111
|
-
Key: this.getFullKey(key)
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
const error = err;
|
|
116
|
-
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
throw err;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async putJson(key, data, ttlMs) {
|
|
123
|
-
await this.client.send(
|
|
124
|
-
new PutObjectCommand({
|
|
125
|
-
Bucket: this.bucket,
|
|
126
|
-
Key: this.getFullKey(key),
|
|
127
|
-
Body: JSON.stringify(data),
|
|
128
|
-
ContentType: "application/json",
|
|
129
|
-
Expires: this.getExpires(ttlMs ?? this.jobMetadataTtlMs)
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
async getJson(key) {
|
|
134
|
-
try {
|
|
135
|
-
const response = await this.client.send(
|
|
136
|
-
new GetObjectCommand({
|
|
137
|
-
Bucket: this.bucket,
|
|
138
|
-
Key: this.getFullKey(key)
|
|
139
|
-
})
|
|
140
|
-
);
|
|
141
|
-
const body = await response.Body?.transformToString();
|
|
142
|
-
if (!body) return null;
|
|
143
|
-
return JSON.parse(body);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
const error = err;
|
|
146
|
-
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
throw err;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
close() {
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
var LocalTransientStore = class {
|
|
156
|
-
baseDir;
|
|
157
|
-
initialized = false;
|
|
158
|
-
cleanupInterval;
|
|
159
|
-
sourceTtlMs;
|
|
160
|
-
jobMetadataTtlMs;
|
|
161
|
-
/** For cleanup, use the longer of the two TTLs */
|
|
162
|
-
maxTtlMs;
|
|
163
|
-
constructor(options) {
|
|
164
|
-
this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), "ffs-transient");
|
|
165
|
-
this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
166
|
-
this.jobMetadataTtlMs = options?.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
167
|
-
this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobMetadataTtlMs);
|
|
168
|
-
this.cleanupInterval = setInterval(() => {
|
|
169
|
-
this.cleanupExpired().catch(console.error);
|
|
170
|
-
}, 3e5);
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Remove files older than max TTL
|
|
174
|
-
*/
|
|
175
|
-
async cleanupExpired() {
|
|
176
|
-
if (!this.initialized) return;
|
|
177
|
-
const now = Date.now();
|
|
178
|
-
await this.cleanupDir(this.baseDir, now);
|
|
179
|
-
}
|
|
180
|
-
async cleanupDir(dir, now) {
|
|
181
|
-
let entries;
|
|
182
|
-
try {
|
|
183
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
184
|
-
} catch {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
for (const entry of entries) {
|
|
188
|
-
const fullPath = path.join(dir, entry.name);
|
|
189
|
-
if (entry.isDirectory()) {
|
|
190
|
-
await this.cleanupDir(fullPath, now);
|
|
191
|
-
try {
|
|
192
|
-
await fs.rmdir(fullPath);
|
|
193
|
-
} catch {
|
|
194
|
-
}
|
|
195
|
-
} else if (entry.isFile()) {
|
|
196
|
-
try {
|
|
197
|
-
const stat = await fs.stat(fullPath);
|
|
198
|
-
if (now - stat.mtimeMs > this.maxTtlMs) {
|
|
199
|
-
await fs.rm(fullPath, { force: true });
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async ensureDir(filePath) {
|
|
207
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
208
|
-
this.initialized = true;
|
|
209
|
-
}
|
|
210
|
-
filePath(key) {
|
|
211
|
-
return path.join(this.baseDir, key);
|
|
212
|
-
}
|
|
213
|
-
tmpPathFor(finalPath) {
|
|
214
|
-
const rand = crypto.randomBytes(8).toString("hex");
|
|
215
|
-
return `${finalPath}.tmp-${process.pid}-${rand}`;
|
|
216
|
-
}
|
|
217
|
-
async put(key, stream, _ttlMs) {
|
|
218
|
-
const fp = this.filePath(key);
|
|
219
|
-
await this.ensureDir(fp);
|
|
220
|
-
const tmpPath = this.tmpPathFor(fp);
|
|
221
|
-
try {
|
|
222
|
-
const writeStream = createWriteStream(tmpPath);
|
|
223
|
-
await pipeline(stream, writeStream);
|
|
224
|
-
await fs.rename(tmpPath, fp);
|
|
225
|
-
} catch (err) {
|
|
226
|
-
await fs.rm(tmpPath, { force: true }).catch(() => {
|
|
227
|
-
});
|
|
228
|
-
throw err;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async getStream(key) {
|
|
232
|
-
const fp = this.filePath(key);
|
|
233
|
-
if (!existsSync(fp)) return null;
|
|
234
|
-
return createReadStream(fp);
|
|
235
|
-
}
|
|
236
|
-
async exists(key) {
|
|
237
|
-
try {
|
|
238
|
-
await fs.access(this.filePath(key));
|
|
239
|
-
return true;
|
|
240
|
-
} catch {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
async existsMany(keys) {
|
|
245
|
-
const results = await Promise.all(
|
|
246
|
-
keys.map(async (key) => [key, await this.exists(key)])
|
|
247
|
-
);
|
|
248
|
-
return new Map(results);
|
|
249
|
-
}
|
|
250
|
-
async delete(key) {
|
|
251
|
-
await fs.rm(this.filePath(key), { force: true });
|
|
252
|
-
}
|
|
253
|
-
async putJson(key, data, _ttlMs) {
|
|
254
|
-
const fp = this.filePath(key);
|
|
255
|
-
await this.ensureDir(fp);
|
|
256
|
-
const tmpPath = this.tmpPathFor(fp);
|
|
257
|
-
try {
|
|
258
|
-
await fs.writeFile(tmpPath, JSON.stringify(data));
|
|
259
|
-
await fs.rename(tmpPath, fp);
|
|
260
|
-
} catch (err) {
|
|
261
|
-
await fs.rm(tmpPath, { force: true }).catch(() => {
|
|
262
|
-
});
|
|
263
|
-
throw err;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
async getJson(key) {
|
|
267
|
-
try {
|
|
268
|
-
const content = await fs.readFile(this.filePath(key), "utf-8");
|
|
269
|
-
return JSON.parse(content);
|
|
270
|
-
} catch {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
close() {
|
|
275
|
-
if (this.cleanupInterval) {
|
|
276
|
-
clearInterval(this.cleanupInterval);
|
|
277
|
-
this.cleanupInterval = void 0;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
function createTransientStore() {
|
|
282
|
-
const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10) : DEFAULT_SOURCE_TTL_MS;
|
|
283
|
-
const jobMetadataTtlMs = process.env.FFS_JOB_METADATA_TTL_MS ? parseInt(process.env.FFS_JOB_METADATA_TTL_MS, 10) : DEFAULT_JOB_METADATA_TTL_MS;
|
|
284
|
-
if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
|
|
285
|
-
return new S3TransientStore({
|
|
286
|
-
endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
|
|
287
|
-
region: process.env.FFS_TRANSIENT_STORE_REGION ?? "auto",
|
|
288
|
-
bucket: process.env.FFS_TRANSIENT_STORE_BUCKET,
|
|
289
|
-
prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
|
|
290
|
-
accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
|
|
291
|
-
secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
|
|
292
|
-
sourceTtlMs,
|
|
293
|
-
jobMetadataTtlMs
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
return new LocalTransientStore({
|
|
297
|
-
baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
|
|
298
|
-
sourceTtlMs,
|
|
299
|
-
jobMetadataTtlMs
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
function hashUrl(url) {
|
|
303
|
-
return crypto.createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
304
|
-
}
|
|
305
|
-
function sourceStoreKey(url) {
|
|
306
|
-
return `sources/${hashUrl(url)}`;
|
|
307
|
-
}
|
|
308
|
-
function warmupJobStoreKey(jobId) {
|
|
309
|
-
return `jobs/warmup/${jobId}.json`;
|
|
310
|
-
}
|
|
311
|
-
function renderJobStoreKey(jobId) {
|
|
312
|
-
return `jobs/render/${jobId}.json`;
|
|
313
|
-
}
|
|
314
|
-
function warmupAndRenderJobStoreKey(jobId) {
|
|
315
|
-
return `jobs/warmup-and-render/${jobId}.json`;
|
|
316
|
-
}
|
|
317
|
-
var storeKeys = {
|
|
318
|
-
source: sourceStoreKey,
|
|
319
|
-
warmupJob: warmupJobStoreKey,
|
|
320
|
-
renderJob: renderJobStoreKey,
|
|
321
|
-
warmupAndRenderJob: warmupAndRenderJobStoreKey
|
|
322
|
-
};
|
|
323
|
-
|
|
324
15
|
// src/proxy.ts
|
|
325
16
|
import http from "http";
|
|
326
17
|
import { Readable } from "stream";
|
|
327
|
-
|
|
328
|
-
// src/fetch.ts
|
|
329
|
-
import { fetch, Agent } from "undici";
|
|
330
|
-
async function ffsFetch(url, options) {
|
|
331
|
-
const {
|
|
332
|
-
method,
|
|
333
|
-
body,
|
|
334
|
-
headers,
|
|
335
|
-
headersTimeout = 3e5,
|
|
336
|
-
// 5 minutes
|
|
337
|
-
bodyTimeout = 3e5
|
|
338
|
-
// 5 minutes
|
|
339
|
-
} = options ?? {};
|
|
340
|
-
const agent = new Agent({ headersTimeout, bodyTimeout });
|
|
341
|
-
return fetch(url, {
|
|
342
|
-
method,
|
|
343
|
-
body,
|
|
344
|
-
headers: { "User-Agent": "FFS (+https://effing.dev/ffs)", ...headers },
|
|
345
|
-
dispatcher: agent
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// src/proxy.ts
|
|
350
18
|
var HttpProxy = class {
|
|
351
19
|
server = null;
|
|
352
20
|
_port = null;
|
|
@@ -425,11 +93,11 @@ var HttpProxy = class {
|
|
|
425
93
|
* Parse the proxy path to extract the original URL.
|
|
426
94
|
* Path format: /{originalUrl}
|
|
427
95
|
*/
|
|
428
|
-
parseProxyPath(
|
|
429
|
-
if (!
|
|
96
|
+
parseProxyPath(path) {
|
|
97
|
+
if (!path.startsWith("/http://") && !path.startsWith("/https://")) {
|
|
430
98
|
return null;
|
|
431
99
|
}
|
|
432
|
-
return
|
|
100
|
+
return path.slice(1);
|
|
433
101
|
}
|
|
434
102
|
/**
|
|
435
103
|
* Filter headers to forward to the upstream server.
|
|
@@ -470,8 +138,12 @@ var HttpProxy = class {
|
|
|
470
138
|
import { effieDataSchema } from "@effing/effie";
|
|
471
139
|
async function createServerContext() {
|
|
472
140
|
const port2 = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
473
|
-
const
|
|
474
|
-
|
|
141
|
+
const renderBackendBaseUrl = process.env.FFS_RENDER_BACKEND_BASE_URL;
|
|
142
|
+
let httpProxy;
|
|
143
|
+
if (!renderBackendBaseUrl) {
|
|
144
|
+
httpProxy = new HttpProxy();
|
|
145
|
+
await httpProxy.start();
|
|
146
|
+
}
|
|
475
147
|
return {
|
|
476
148
|
transientStore: createTransientStore(),
|
|
477
149
|
httpProxy,
|
|
@@ -528,7 +200,7 @@ data: ${JSON.stringify(data)}
|
|
|
528
200
|
|
|
529
201
|
// src/handlers/caching.ts
|
|
530
202
|
import "express";
|
|
531
|
-
import { Readable as
|
|
203
|
+
import { Readable as Readable2, Transform } from "stream";
|
|
532
204
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
533
205
|
import {
|
|
534
206
|
extractEffieSources,
|
|
@@ -543,919 +215,6 @@ import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } fro
|
|
|
543
215
|
// src/handlers/rendering.ts
|
|
544
216
|
import "express";
|
|
545
217
|
import { randomUUID } from "crypto";
|
|
546
|
-
|
|
547
|
-
// src/render.ts
|
|
548
|
-
import { Readable as Readable2 } from "stream";
|
|
549
|
-
import { createReadStream as createReadStream2 } from "fs";
|
|
550
|
-
|
|
551
|
-
// src/motion.ts
|
|
552
|
-
function getEasingExpression(tNormExpr, easingType) {
|
|
553
|
-
switch (easingType) {
|
|
554
|
-
case "ease-in":
|
|
555
|
-
return `pow(${tNormExpr},2)`;
|
|
556
|
-
case "ease-out":
|
|
557
|
-
return `(1-pow(1-(${tNormExpr}),2))`;
|
|
558
|
-
case "ease-in-out":
|
|
559
|
-
return `if(lt(${tNormExpr},0.5),2*pow(${tNormExpr},2),1-pow(-2*(${tNormExpr})+2,2)/2)`;
|
|
560
|
-
case "linear":
|
|
561
|
-
default:
|
|
562
|
-
return `(${tNormExpr})`;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function processSlideMotion(motion, relativeTimeExpr) {
|
|
566
|
-
const duration = motion.duration ?? 1;
|
|
567
|
-
const distance = motion.distance ?? 1;
|
|
568
|
-
const reverse = motion.reverse ?? false;
|
|
569
|
-
const easing = motion.easing ?? "linear";
|
|
570
|
-
const tNormExpr = `(${relativeTimeExpr})/${duration}`;
|
|
571
|
-
const easedProgressExpr = getEasingExpression(tNormExpr, easing);
|
|
572
|
-
const finalTimeFactorExpr = reverse ? easedProgressExpr : `(1-(${easedProgressExpr}))`;
|
|
573
|
-
let activeX;
|
|
574
|
-
let activeY;
|
|
575
|
-
let initialX;
|
|
576
|
-
let initialY;
|
|
577
|
-
let finalX;
|
|
578
|
-
let finalY;
|
|
579
|
-
switch (motion.direction) {
|
|
580
|
-
case "left": {
|
|
581
|
-
const offsetXLeft = `${distance}*W`;
|
|
582
|
-
activeX = `(${offsetXLeft})*${finalTimeFactorExpr}`;
|
|
583
|
-
activeY = "0";
|
|
584
|
-
initialX = reverse ? "0" : offsetXLeft;
|
|
585
|
-
initialY = "0";
|
|
586
|
-
finalX = reverse ? offsetXLeft : "0";
|
|
587
|
-
finalY = "0";
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
590
|
-
case "right": {
|
|
591
|
-
const offsetXRight = `-${distance}*W`;
|
|
592
|
-
activeX = `(${offsetXRight})*${finalTimeFactorExpr}`;
|
|
593
|
-
activeY = "0";
|
|
594
|
-
initialX = reverse ? "0" : offsetXRight;
|
|
595
|
-
initialY = "0";
|
|
596
|
-
finalX = reverse ? offsetXRight : "0";
|
|
597
|
-
finalY = "0";
|
|
598
|
-
break;
|
|
599
|
-
}
|
|
600
|
-
case "up": {
|
|
601
|
-
const offsetYUp = `${distance}*H`;
|
|
602
|
-
activeX = "0";
|
|
603
|
-
activeY = `(${offsetYUp})*${finalTimeFactorExpr}`;
|
|
604
|
-
initialX = "0";
|
|
605
|
-
initialY = reverse ? "0" : offsetYUp;
|
|
606
|
-
finalX = "0";
|
|
607
|
-
finalY = reverse ? offsetYUp : "0";
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
case "down": {
|
|
611
|
-
const offsetYDown = `-${distance}*H`;
|
|
612
|
-
activeX = "0";
|
|
613
|
-
activeY = `(${offsetYDown})*${finalTimeFactorExpr}`;
|
|
614
|
-
initialX = "0";
|
|
615
|
-
initialY = reverse ? "0" : offsetYDown;
|
|
616
|
-
finalX = "0";
|
|
617
|
-
finalY = reverse ? offsetYDown : "0";
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return { initialX, initialY, activeX, activeY, finalX, finalY, duration };
|
|
622
|
-
}
|
|
623
|
-
function processBounceMotion(motion, relativeTimeExpr) {
|
|
624
|
-
const amplitude = motion.amplitude ?? 0.5;
|
|
625
|
-
const duration = motion.duration ?? 1;
|
|
626
|
-
const initialY = `-overlay_h*${amplitude}`;
|
|
627
|
-
const finalY = "0";
|
|
628
|
-
const tNormExpr = `(${relativeTimeExpr})/${duration}`;
|
|
629
|
-
const activeBounceExpression = `if(lt(${tNormExpr},0.363636),${initialY}+overlay_h*${amplitude}*(7.5625*${tNormExpr}*${tNormExpr}),if(lt(${tNormExpr},0.727273),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.545455)*(${tNormExpr}-0.545455)+0.75),if(lt(${tNormExpr},0.909091),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.818182)*(${tNormExpr}-0.818182)+0.9375),if(lt(${tNormExpr},0.954545),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.954545)*(${tNormExpr}-0.954545)+0.984375),${finalY}))))`;
|
|
630
|
-
return {
|
|
631
|
-
initialX: "0",
|
|
632
|
-
initialY,
|
|
633
|
-
activeX: "0",
|
|
634
|
-
activeY: activeBounceExpression,
|
|
635
|
-
// This expression now scales with duration
|
|
636
|
-
finalX: "0",
|
|
637
|
-
finalY,
|
|
638
|
-
duration
|
|
639
|
-
// Return the actual duration used
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
function processShakeMotion(motion, relativeTimeExpr) {
|
|
643
|
-
const intensity = motion.intensity ?? 10;
|
|
644
|
-
const frequency = motion.frequency ?? 4;
|
|
645
|
-
const duration = motion.duration ?? 1;
|
|
646
|
-
const activeX = `${intensity}*sin(${relativeTimeExpr}*PI*${frequency})`;
|
|
647
|
-
const activeY = `${intensity}*cos(${relativeTimeExpr}*PI*${frequency})`;
|
|
648
|
-
return {
|
|
649
|
-
initialX: "0",
|
|
650
|
-
initialY: "0",
|
|
651
|
-
activeX,
|
|
652
|
-
activeY,
|
|
653
|
-
finalX: "0",
|
|
654
|
-
finalY: "0",
|
|
655
|
-
duration
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
function processMotion(delay, motion) {
|
|
659
|
-
if (!motion) return "x=0:y=0";
|
|
660
|
-
const start = delay + (motion.start ?? 0);
|
|
661
|
-
const relativeTimeExpr = `(t-${start})`;
|
|
662
|
-
let components;
|
|
663
|
-
switch (motion.type) {
|
|
664
|
-
case "bounce":
|
|
665
|
-
components = processBounceMotion(motion, relativeTimeExpr);
|
|
666
|
-
break;
|
|
667
|
-
case "shake":
|
|
668
|
-
components = processShakeMotion(motion, relativeTimeExpr);
|
|
669
|
-
break;
|
|
670
|
-
case "slide":
|
|
671
|
-
components = processSlideMotion(motion, relativeTimeExpr);
|
|
672
|
-
break;
|
|
673
|
-
default:
|
|
674
|
-
motion;
|
|
675
|
-
throw new Error(
|
|
676
|
-
`Unsupported motion type: ${motion.type}`
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
const motionEndTime = start + components.duration;
|
|
680
|
-
const xArg = `if(lt(t,${start}),${components.initialX},if(lt(t,${motionEndTime}),${components.activeX},${components.finalX}))`;
|
|
681
|
-
const yArg = `if(lt(t,${start}),${components.initialY},if(lt(t,${motionEndTime}),${components.activeY},${components.finalY}))`;
|
|
682
|
-
return `x='${xArg}':y='${yArg}'`;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// src/effect.ts
|
|
686
|
-
function processFadeIn(effect, _frameRate, _frameWidth, _frameHeight) {
|
|
687
|
-
return `fade=t=in:st=${effect.start}:d=${effect.duration}:alpha=1`;
|
|
688
|
-
}
|
|
689
|
-
function processFadeOut(effect, _frameRate, _frameWidth, _frameHeight) {
|
|
690
|
-
return `fade=t=out:st=${effect.start}:d=${effect.duration}:alpha=1`;
|
|
691
|
-
}
|
|
692
|
-
function processSaturateIn(effect, _frameRate, _frameWidth, _frameHeight) {
|
|
693
|
-
return `hue='s=max(0,min(1,(t-${effect.start})/${effect.duration}))'`;
|
|
694
|
-
}
|
|
695
|
-
function processSaturateOut(effect, _frameRate, _frameWidth, _frameHeight) {
|
|
696
|
-
return `hue='s=max(0,min(1,(${effect.start + effect.duration}-t)/${effect.duration}))'`;
|
|
697
|
-
}
|
|
698
|
-
function processScroll(effect, frameRate, _frameWidth, _frameHeight) {
|
|
699
|
-
const distance = effect.distance ?? 1;
|
|
700
|
-
const scroll = distance / (1 + distance);
|
|
701
|
-
const speed = scroll / (effect.duration * frameRate);
|
|
702
|
-
switch (effect.direction) {
|
|
703
|
-
case "left":
|
|
704
|
-
return `scroll=h=${speed}`;
|
|
705
|
-
case "right":
|
|
706
|
-
return `scroll=hpos=${1 - scroll}:h=-${speed}`;
|
|
707
|
-
case "up":
|
|
708
|
-
return `scroll=v=${speed}`;
|
|
709
|
-
case "down":
|
|
710
|
-
return `scroll=vpos=${1 - scroll}:v=-${speed}`;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
function processEffect(effect, frameRate, frameWidth, frameHeight) {
|
|
714
|
-
switch (effect.type) {
|
|
715
|
-
case "fade-in":
|
|
716
|
-
return processFadeIn(effect, frameRate, frameWidth, frameHeight);
|
|
717
|
-
case "fade-out":
|
|
718
|
-
return processFadeOut(effect, frameRate, frameWidth, frameHeight);
|
|
719
|
-
case "saturate-in":
|
|
720
|
-
return processSaturateIn(effect, frameRate, frameWidth, frameHeight);
|
|
721
|
-
case "saturate-out":
|
|
722
|
-
return processSaturateOut(effect, frameRate, frameWidth, frameHeight);
|
|
723
|
-
case "scroll":
|
|
724
|
-
return processScroll(effect, frameRate, frameWidth, frameHeight);
|
|
725
|
-
default:
|
|
726
|
-
effect;
|
|
727
|
-
throw new Error(
|
|
728
|
-
`Unsupported effect type: ${effect.type}`
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
function processEffects(effects, frameRate, frameWidth, frameHeight) {
|
|
733
|
-
if (!effects || effects.length === 0) return "";
|
|
734
|
-
const filters = [];
|
|
735
|
-
for (const effect of effects) {
|
|
736
|
-
const filter = processEffect(effect, frameRate, frameWidth, frameHeight);
|
|
737
|
-
filters.push(filter);
|
|
738
|
-
}
|
|
739
|
-
return filters.join(",");
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// src/ffmpeg.ts
|
|
743
|
-
import { spawn } from "child_process";
|
|
744
|
-
import { pipeline as pipeline2 } from "stream";
|
|
745
|
-
import fs2 from "fs/promises";
|
|
746
|
-
import os2 from "os";
|
|
747
|
-
import path2 from "path";
|
|
748
|
-
import pathToFFmpeg from "ffmpeg-static";
|
|
749
|
-
import tar from "tar-stream";
|
|
750
|
-
import { createWriteStream as createWriteStream2 } from "fs";
|
|
751
|
-
import { promisify } from "util";
|
|
752
|
-
var pump = promisify(pipeline2);
|
|
753
|
-
var FFmpegCommand = class {
|
|
754
|
-
globalArgs;
|
|
755
|
-
inputs;
|
|
756
|
-
filterComplex;
|
|
757
|
-
outputArgs;
|
|
758
|
-
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
759
|
-
this.globalArgs = globalArgs;
|
|
760
|
-
this.inputs = inputs;
|
|
761
|
-
this.filterComplex = filterComplex;
|
|
762
|
-
this.outputArgs = outputArgs;
|
|
763
|
-
}
|
|
764
|
-
buildArgs(inputResolver) {
|
|
765
|
-
const inputArgs = [];
|
|
766
|
-
for (const input of this.inputs) {
|
|
767
|
-
if (input.type === "color") {
|
|
768
|
-
inputArgs.push(...input.preArgs);
|
|
769
|
-
} else if (input.type === "animation") {
|
|
770
|
-
inputArgs.push(
|
|
771
|
-
...input.preArgs,
|
|
772
|
-
"-i",
|
|
773
|
-
path2.join(inputResolver(input), "frame_%05d")
|
|
774
|
-
);
|
|
775
|
-
} else {
|
|
776
|
-
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
const args = [
|
|
780
|
-
...this.globalArgs,
|
|
781
|
-
...inputArgs,
|
|
782
|
-
"-filter_complex",
|
|
783
|
-
this.filterComplex,
|
|
784
|
-
...this.outputArgs
|
|
785
|
-
];
|
|
786
|
-
return args;
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
var FFmpegRunner = class {
|
|
790
|
-
command;
|
|
791
|
-
ffmpegProc;
|
|
792
|
-
constructor(command) {
|
|
793
|
-
this.command = command;
|
|
794
|
-
}
|
|
795
|
-
async run(sourceFetcher, imageTransformer, referenceResolver, urlTransformer) {
|
|
796
|
-
const tempDir = await fs2.mkdtemp(path2.join(os2.tmpdir(), "ffs-"));
|
|
797
|
-
const fileMapping = /* @__PURE__ */ new Map();
|
|
798
|
-
const fetchCache = /* @__PURE__ */ new Map();
|
|
799
|
-
const fetchAndSaveSource = async (input, sourceUrl, inputName) => {
|
|
800
|
-
const stream = await sourceFetcher({
|
|
801
|
-
type: input.type,
|
|
802
|
-
src: sourceUrl
|
|
803
|
-
});
|
|
804
|
-
if (input.type === "animation") {
|
|
805
|
-
const extractionDir = path2.join(tempDir, inputName);
|
|
806
|
-
await fs2.mkdir(extractionDir, { recursive: true });
|
|
807
|
-
const extract = tar.extract();
|
|
808
|
-
const extractPromise = new Promise((resolve, reject) => {
|
|
809
|
-
extract.on("entry", async (header, stream2, next) => {
|
|
810
|
-
if (header.name.startsWith("frame_")) {
|
|
811
|
-
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
812
|
-
const outputPath = path2.join(extractionDir, header.name);
|
|
813
|
-
const writeStream = createWriteStream2(outputPath);
|
|
814
|
-
transformedStream.pipe(writeStream);
|
|
815
|
-
writeStream.on("finish", next);
|
|
816
|
-
writeStream.on("error", reject);
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
extract.on("finish", resolve);
|
|
820
|
-
extract.on("error", reject);
|
|
821
|
-
});
|
|
822
|
-
stream.pipe(extract);
|
|
823
|
-
await extractPromise;
|
|
824
|
-
return extractionDir;
|
|
825
|
-
} else if (input.type === "image" && imageTransformer) {
|
|
826
|
-
const tempFile = path2.join(tempDir, inputName);
|
|
827
|
-
const transformedStream = await imageTransformer(stream);
|
|
828
|
-
const writeStream = createWriteStream2(tempFile);
|
|
829
|
-
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
830
|
-
await pump(transformedStream, writeStream);
|
|
831
|
-
return tempFile;
|
|
832
|
-
} else {
|
|
833
|
-
const tempFile = path2.join(tempDir, inputName);
|
|
834
|
-
const writeStream = createWriteStream2(tempFile);
|
|
835
|
-
stream.on("error", (e) => writeStream.destroy(e));
|
|
836
|
-
await pump(stream, writeStream);
|
|
837
|
-
return tempFile;
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
await Promise.all(
|
|
841
|
-
this.command.inputs.map(async (input) => {
|
|
842
|
-
if (input.type === "color") return;
|
|
843
|
-
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
844
|
-
const sourceUrl = referenceResolver ? referenceResolver(input.source) : input.source;
|
|
845
|
-
if ((input.type === "video" || input.type === "audio") && (sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
|
|
846
|
-
const finalUrl = urlTransformer ? urlTransformer(sourceUrl) : sourceUrl;
|
|
847
|
-
fileMapping.set(input.index, finalUrl);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
const shouldCache = input.source.startsWith("#");
|
|
851
|
-
if (shouldCache) {
|
|
852
|
-
let fetchPromise = fetchCache.get(input.source);
|
|
853
|
-
if (!fetchPromise) {
|
|
854
|
-
fetchPromise = fetchAndSaveSource(input, sourceUrl, inputName);
|
|
855
|
-
fetchCache.set(input.source, fetchPromise);
|
|
856
|
-
}
|
|
857
|
-
const filePath = await fetchPromise;
|
|
858
|
-
fileMapping.set(input.index, filePath);
|
|
859
|
-
} else {
|
|
860
|
-
const filePath = await fetchAndSaveSource(
|
|
861
|
-
input,
|
|
862
|
-
sourceUrl,
|
|
863
|
-
inputName
|
|
864
|
-
);
|
|
865
|
-
fileMapping.set(input.index, filePath);
|
|
866
|
-
}
|
|
867
|
-
})
|
|
868
|
-
);
|
|
869
|
-
const finalArgs = this.command.buildArgs((input) => {
|
|
870
|
-
const filePath = fileMapping.get(input.index);
|
|
871
|
-
if (!filePath)
|
|
872
|
-
throw new Error(`File for input index ${input.index} not found`);
|
|
873
|
-
return filePath;
|
|
874
|
-
});
|
|
875
|
-
const ffmpegProc = spawn(process.env.FFMPEG ?? pathToFFmpeg, finalArgs);
|
|
876
|
-
ffmpegProc.stderr.on("data", (data) => {
|
|
877
|
-
console.error(data.toString());
|
|
878
|
-
});
|
|
879
|
-
ffmpegProc.on("close", async () => {
|
|
880
|
-
try {
|
|
881
|
-
await fs2.rm(tempDir, { recursive: true, force: true });
|
|
882
|
-
} catch (err) {
|
|
883
|
-
console.error("Error removing temp directory:", err);
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
this.ffmpegProc = ffmpegProc;
|
|
887
|
-
return ffmpegProc.stdout;
|
|
888
|
-
}
|
|
889
|
-
close() {
|
|
890
|
-
if (this.ffmpegProc) {
|
|
891
|
-
this.ffmpegProc.kill("SIGTERM");
|
|
892
|
-
this.ffmpegProc = void 0;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
// src/transition.ts
|
|
898
|
-
function processTransition(transition) {
|
|
899
|
-
switch (transition.type) {
|
|
900
|
-
case "fade": {
|
|
901
|
-
if ("through" in transition) {
|
|
902
|
-
return `fade${transition.through}`;
|
|
903
|
-
}
|
|
904
|
-
const easing = transition.easing ?? "linear";
|
|
905
|
-
return {
|
|
906
|
-
linear: "fade",
|
|
907
|
-
"ease-in": "fadeslow",
|
|
908
|
-
"ease-out": "fadefast"
|
|
909
|
-
}[easing];
|
|
910
|
-
}
|
|
911
|
-
case "barn": {
|
|
912
|
-
const orientation = transition.orientation ?? "horizontal";
|
|
913
|
-
const mode = transition.mode ?? "open";
|
|
914
|
-
const prefix = orientation === "vertical" ? "vert" : "horz";
|
|
915
|
-
return `${prefix}${mode}`;
|
|
916
|
-
}
|
|
917
|
-
case "circle": {
|
|
918
|
-
const mode = transition.mode ?? "open";
|
|
919
|
-
return `circle${mode}`;
|
|
920
|
-
}
|
|
921
|
-
case "wipe":
|
|
922
|
-
case "slide":
|
|
923
|
-
case "smooth": {
|
|
924
|
-
const direction = transition.direction ?? "left";
|
|
925
|
-
return `${transition.type}${direction}`;
|
|
926
|
-
}
|
|
927
|
-
case "slice": {
|
|
928
|
-
const direction = transition.direction ?? "left";
|
|
929
|
-
const prefix = {
|
|
930
|
-
left: "hl",
|
|
931
|
-
right: "hr",
|
|
932
|
-
up: "vu",
|
|
933
|
-
down: "vd"
|
|
934
|
-
}[direction];
|
|
935
|
-
return `${prefix}${transition.type}`;
|
|
936
|
-
}
|
|
937
|
-
case "zoom": {
|
|
938
|
-
return "zoomin";
|
|
939
|
-
}
|
|
940
|
-
case "dissolve":
|
|
941
|
-
case "pixelize":
|
|
942
|
-
case "radial":
|
|
943
|
-
return transition.type;
|
|
944
|
-
default:
|
|
945
|
-
transition;
|
|
946
|
-
throw new Error(
|
|
947
|
-
`Unsupported transition type: ${transition.type}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// src/render.ts
|
|
953
|
-
import sharp from "sharp";
|
|
954
|
-
import { fileURLToPath } from "url";
|
|
955
|
-
var EffieRenderer = class {
|
|
956
|
-
effieData;
|
|
957
|
-
ffmpegRunner;
|
|
958
|
-
allowLocalFiles;
|
|
959
|
-
transientStore;
|
|
960
|
-
httpProxy;
|
|
961
|
-
constructor(effieData, options) {
|
|
962
|
-
this.effieData = effieData;
|
|
963
|
-
this.allowLocalFiles = options?.allowLocalFiles ?? false;
|
|
964
|
-
this.transientStore = options?.transientStore;
|
|
965
|
-
this.httpProxy = options?.httpProxy;
|
|
966
|
-
}
|
|
967
|
-
async fetchSource(src) {
|
|
968
|
-
if (src.startsWith("data:")) {
|
|
969
|
-
const commaIndex = src.indexOf(",");
|
|
970
|
-
if (commaIndex === -1) {
|
|
971
|
-
throw new Error("Invalid data URL");
|
|
972
|
-
}
|
|
973
|
-
const meta = src.slice(5, commaIndex);
|
|
974
|
-
const isBase64 = meta.endsWith(";base64");
|
|
975
|
-
const data = src.slice(commaIndex + 1);
|
|
976
|
-
const buffer = isBase64 ? Buffer.from(data, "base64") : Buffer.from(decodeURIComponent(data));
|
|
977
|
-
return Readable2.from(buffer);
|
|
978
|
-
}
|
|
979
|
-
if (src.startsWith("file:")) {
|
|
980
|
-
if (!this.allowLocalFiles) {
|
|
981
|
-
throw new Error(
|
|
982
|
-
"Local file paths are not allowed. Use allowLocalFiles option for trusted operations."
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
return createReadStream2(fileURLToPath(src));
|
|
986
|
-
}
|
|
987
|
-
if (this.transientStore) {
|
|
988
|
-
const cachedStream = await this.transientStore.getStream(
|
|
989
|
-
storeKeys.source(src)
|
|
990
|
-
);
|
|
991
|
-
if (cachedStream) {
|
|
992
|
-
return cachedStream;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
const response = await ffsFetch(src, {
|
|
996
|
-
headersTimeout: 10 * 60 * 1e3,
|
|
997
|
-
// 10 minutes
|
|
998
|
-
bodyTimeout: 20 * 60 * 1e3
|
|
999
|
-
// 20 minutes
|
|
1000
|
-
});
|
|
1001
|
-
if (!response.ok) {
|
|
1002
|
-
throw new Error(
|
|
1003
|
-
`Failed to fetch ${src}: ${response.status} ${response.statusText}`
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
1006
|
-
if (!response.body) {
|
|
1007
|
-
throw new Error(`No body for ${src}`);
|
|
1008
|
-
}
|
|
1009
|
-
return Readable2.fromWeb(response.body);
|
|
1010
|
-
}
|
|
1011
|
-
buildAudioFilter({
|
|
1012
|
-
duration,
|
|
1013
|
-
volume,
|
|
1014
|
-
fadeIn,
|
|
1015
|
-
fadeOut
|
|
1016
|
-
}) {
|
|
1017
|
-
const filters = [];
|
|
1018
|
-
if (volume !== void 0) {
|
|
1019
|
-
filters.push(`volume=${volume}`);
|
|
1020
|
-
}
|
|
1021
|
-
if (fadeIn !== void 0) {
|
|
1022
|
-
filters.push(`afade=type=in:start_time=0:duration=${fadeIn}`);
|
|
1023
|
-
}
|
|
1024
|
-
if (fadeOut !== void 0) {
|
|
1025
|
-
filters.push(
|
|
1026
|
-
`afade=type=out:start_time=${duration - fadeOut}:duration=${fadeOut}`
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
1029
|
-
return filters.length ? filters.join(",") : "anull";
|
|
1030
|
-
}
|
|
1031
|
-
getFrameDimensions(scaleFactor) {
|
|
1032
|
-
return {
|
|
1033
|
-
frameWidth: Math.floor(this.effieData.width * scaleFactor / 2) * 2,
|
|
1034
|
-
frameHeight: Math.floor(this.effieData.height * scaleFactor / 2) * 2
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Builds an FFmpeg input for a background (global or segment).
|
|
1039
|
-
*/
|
|
1040
|
-
buildBackgroundInput(background, inputIndex, frameWidth, frameHeight) {
|
|
1041
|
-
if (background.type === "image") {
|
|
1042
|
-
return {
|
|
1043
|
-
index: inputIndex,
|
|
1044
|
-
source: background.source,
|
|
1045
|
-
preArgs: ["-loop", "1", "-framerate", this.effieData.fps.toString()],
|
|
1046
|
-
type: "image"
|
|
1047
|
-
};
|
|
1048
|
-
} else if (background.type === "video") {
|
|
1049
|
-
return {
|
|
1050
|
-
index: inputIndex,
|
|
1051
|
-
source: background.source,
|
|
1052
|
-
preArgs: ["-stream_loop", "-1"],
|
|
1053
|
-
type: "video"
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
return {
|
|
1057
|
-
index: inputIndex,
|
|
1058
|
-
source: "",
|
|
1059
|
-
preArgs: [
|
|
1060
|
-
"-f",
|
|
1061
|
-
"lavfi",
|
|
1062
|
-
"-i",
|
|
1063
|
-
`color=${background.color}:size=${frameWidth}x${frameHeight}:rate=${this.effieData.fps}`
|
|
1064
|
-
],
|
|
1065
|
-
type: "color"
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
buildOutputArgs(outputFilename) {
|
|
1069
|
-
return [
|
|
1070
|
-
"-map",
|
|
1071
|
-
"[outv]",
|
|
1072
|
-
"-map",
|
|
1073
|
-
"[outa]",
|
|
1074
|
-
"-c:v",
|
|
1075
|
-
"libx264",
|
|
1076
|
-
"-r",
|
|
1077
|
-
this.effieData.fps.toString(),
|
|
1078
|
-
"-pix_fmt",
|
|
1079
|
-
"yuv420p",
|
|
1080
|
-
"-preset",
|
|
1081
|
-
"fast",
|
|
1082
|
-
"-crf",
|
|
1083
|
-
"28",
|
|
1084
|
-
"-c:a",
|
|
1085
|
-
"aac",
|
|
1086
|
-
"-movflags",
|
|
1087
|
-
"frag_keyframe+empty_moov",
|
|
1088
|
-
"-f",
|
|
1089
|
-
"mp4",
|
|
1090
|
-
outputFilename
|
|
1091
|
-
];
|
|
1092
|
-
}
|
|
1093
|
-
buildLayerInput(layer, duration, inputIndex) {
|
|
1094
|
-
let preArgs = [];
|
|
1095
|
-
if (layer.type === "image") {
|
|
1096
|
-
preArgs = [
|
|
1097
|
-
"-loop",
|
|
1098
|
-
"1",
|
|
1099
|
-
"-t",
|
|
1100
|
-
duration.toString(),
|
|
1101
|
-
"-framerate",
|
|
1102
|
-
this.effieData.fps.toString()
|
|
1103
|
-
];
|
|
1104
|
-
} else if (layer.type === "animation") {
|
|
1105
|
-
preArgs = ["-f", "image2", "-framerate", this.effieData.fps.toString()];
|
|
1106
|
-
}
|
|
1107
|
-
return {
|
|
1108
|
-
index: inputIndex,
|
|
1109
|
-
source: layer.source,
|
|
1110
|
-
preArgs,
|
|
1111
|
-
type: layer.type
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Builds filter chain for all layers in a segment.
|
|
1116
|
-
* @param segment - The segment containing layers
|
|
1117
|
-
* @param bgLabel - Label for the background input (e.g., "bg_seg0" or "bg_seg")
|
|
1118
|
-
* @param labelPrefix - Prefix for generated labels (e.g., "seg0_" or "")
|
|
1119
|
-
* @param layerInputOffset - Starting input index for layers
|
|
1120
|
-
* @param frameWidth - Frame width for nullsrc
|
|
1121
|
-
* @param frameHeight - Frame height for nullsrc
|
|
1122
|
-
* @param outputLabel - Label for the final video output
|
|
1123
|
-
* @returns Array of filter parts to add to the filter chain
|
|
1124
|
-
*/
|
|
1125
|
-
buildLayerFilters(segment, bgLabel, labelPrefix, layerInputOffset, frameWidth, frameHeight, outputLabel) {
|
|
1126
|
-
const filterParts = [];
|
|
1127
|
-
let currentVidLabel = bgLabel;
|
|
1128
|
-
for (let l = 0; l < segment.layers.length; l++) {
|
|
1129
|
-
const inputIdx = layerInputOffset + l;
|
|
1130
|
-
const layerLabel = `${labelPrefix}layer${l}`;
|
|
1131
|
-
const layer = segment.layers[l];
|
|
1132
|
-
const effectChain = layer.effects ? processEffects(
|
|
1133
|
-
layer.effects,
|
|
1134
|
-
this.effieData.fps,
|
|
1135
|
-
frameWidth,
|
|
1136
|
-
frameHeight
|
|
1137
|
-
) : "";
|
|
1138
|
-
filterParts.push(
|
|
1139
|
-
`[${inputIdx}:v]trim=start=0:duration=${segment.duration},${effectChain ? effectChain + "," : ""}setsar=1,setpts=PTS-STARTPTS[${layerLabel}]`
|
|
1140
|
-
);
|
|
1141
|
-
let overlayInputLabel = layerLabel;
|
|
1142
|
-
const delay = layer.delay ?? 0;
|
|
1143
|
-
if (delay > 0) {
|
|
1144
|
-
filterParts.push(
|
|
1145
|
-
`nullsrc=size=${frameWidth}x${frameHeight}:duration=${delay},setpts=PTS-STARTPTS[null_${layerLabel}]`
|
|
1146
|
-
);
|
|
1147
|
-
filterParts.push(
|
|
1148
|
-
`[null_${layerLabel}][${layerLabel}]concat=n=2:v=1:a=0[delayed_${layerLabel}]`
|
|
1149
|
-
);
|
|
1150
|
-
overlayInputLabel = `delayed_${layerLabel}`;
|
|
1151
|
-
}
|
|
1152
|
-
const overlayOutputLabel = `${labelPrefix}tmp${l}`;
|
|
1153
|
-
const offset = layer.motion ? processMotion(delay, layer.motion) : "0:0";
|
|
1154
|
-
const fromTime = layer.from ?? 0;
|
|
1155
|
-
const untilTime = layer.until ?? segment.duration;
|
|
1156
|
-
filterParts.push(
|
|
1157
|
-
`[${currentVidLabel}][${overlayInputLabel}]overlay=${offset}:enable='between(t,${fromTime},${untilTime})',fps=${this.effieData.fps}[${overlayOutputLabel}]`
|
|
1158
|
-
);
|
|
1159
|
-
currentVidLabel = overlayOutputLabel;
|
|
1160
|
-
}
|
|
1161
|
-
filterParts.push(`[${currentVidLabel}]null[${outputLabel}]`);
|
|
1162
|
-
return filterParts;
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Applies xfade/concat transitions between video segments.
|
|
1166
|
-
* Modifies videoSegmentLabels in place to update labels after transitions.
|
|
1167
|
-
* @param filterParts - Array to append filter parts to
|
|
1168
|
-
* @param videoSegmentLabels - Array of video segment labels (modified in place)
|
|
1169
|
-
*/
|
|
1170
|
-
applyTransitions(filterParts, videoSegmentLabels) {
|
|
1171
|
-
let transitionOffset = 0;
|
|
1172
|
-
this.effieData.segments.forEach((segment, i) => {
|
|
1173
|
-
if (i === 0) {
|
|
1174
|
-
transitionOffset = segment.duration;
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
const combineLabel = `[vid_com${i}]`;
|
|
1178
|
-
if (!segment.transition) {
|
|
1179
|
-
transitionOffset += segment.duration;
|
|
1180
|
-
filterParts.push(
|
|
1181
|
-
`${videoSegmentLabels[i - 1]}${videoSegmentLabels[i]}concat=n=2:v=1:a=0,fps=${this.effieData.fps}${combineLabel}`
|
|
1182
|
-
);
|
|
1183
|
-
videoSegmentLabels[i] = combineLabel;
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
const transitionName = processTransition(segment.transition);
|
|
1187
|
-
const transitionDuration = segment.transition.duration;
|
|
1188
|
-
transitionOffset -= transitionDuration;
|
|
1189
|
-
filterParts.push(
|
|
1190
|
-
`${videoSegmentLabels[i - 1]}${videoSegmentLabels[i]}xfade=transition=${transitionName}:duration=${transitionDuration}:offset=${transitionOffset}${combineLabel}`
|
|
1191
|
-
);
|
|
1192
|
-
videoSegmentLabels[i] = combineLabel;
|
|
1193
|
-
transitionOffset += segment.duration;
|
|
1194
|
-
});
|
|
1195
|
-
filterParts.push(`${videoSegmentLabels.at(-1)}null[outv]`);
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Applies general audio mixing: concats segment audio and mixes with global audio if present.
|
|
1199
|
-
* @param filterParts - Array to append filter parts to
|
|
1200
|
-
* @param audioSegmentLabels - Array of audio segment labels to concat
|
|
1201
|
-
* @param totalDuration - Total duration for audio trimming
|
|
1202
|
-
* @param generalAudioInputIndex - Input index for general audio (if present)
|
|
1203
|
-
*/
|
|
1204
|
-
applyGeneralAudio(filterParts, audioSegmentLabels, totalDuration, generalAudioInputIndex) {
|
|
1205
|
-
if (this.effieData.audio) {
|
|
1206
|
-
const audioSeek = this.effieData.audio.seek ?? 0;
|
|
1207
|
-
const generalAudioFilter = this.buildAudioFilter({
|
|
1208
|
-
duration: totalDuration,
|
|
1209
|
-
volume: this.effieData.audio.volume,
|
|
1210
|
-
fadeIn: this.effieData.audio.fadeIn,
|
|
1211
|
-
fadeOut: this.effieData.audio.fadeOut
|
|
1212
|
-
});
|
|
1213
|
-
filterParts.push(
|
|
1214
|
-
`[${generalAudioInputIndex}:a]atrim=start=${audioSeek}:duration=${totalDuration},${generalAudioFilter},asetpts=PTS-STARTPTS[general_audio]`
|
|
1215
|
-
);
|
|
1216
|
-
filterParts.push(
|
|
1217
|
-
`${audioSegmentLabels.join("")}concat=n=${this.effieData.segments.length}:v=0:a=1,atrim=start=0:duration=${totalDuration}[segments_audio]`
|
|
1218
|
-
);
|
|
1219
|
-
filterParts.push(
|
|
1220
|
-
`[general_audio][segments_audio]amix=inputs=2:duration=longest[outa]`
|
|
1221
|
-
);
|
|
1222
|
-
} else {
|
|
1223
|
-
filterParts.push(
|
|
1224
|
-
`${audioSegmentLabels.join("")}concat=n=${this.effieData.segments.length}:v=0:a=1[outa]`
|
|
1225
|
-
);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
buildFFmpegCommand(outputFilename, scaleFactor = 1) {
|
|
1229
|
-
const globalArgs = ["-y", "-loglevel", "error"];
|
|
1230
|
-
const inputs = [];
|
|
1231
|
-
let inputIndex = 0;
|
|
1232
|
-
const { frameWidth, frameHeight } = this.getFrameDimensions(scaleFactor);
|
|
1233
|
-
const backgroundSeek = this.effieData.background.type === "video" ? this.effieData.background.seek ?? 0 : 0;
|
|
1234
|
-
inputs.push(
|
|
1235
|
-
this.buildBackgroundInput(
|
|
1236
|
-
this.effieData.background,
|
|
1237
|
-
inputIndex,
|
|
1238
|
-
frameWidth,
|
|
1239
|
-
frameHeight
|
|
1240
|
-
)
|
|
1241
|
-
);
|
|
1242
|
-
const globalBgInputIdx = inputIndex;
|
|
1243
|
-
inputIndex++;
|
|
1244
|
-
const segmentBgInputIndices = [];
|
|
1245
|
-
for (const segment of this.effieData.segments) {
|
|
1246
|
-
if (segment.background) {
|
|
1247
|
-
inputs.push(
|
|
1248
|
-
this.buildBackgroundInput(
|
|
1249
|
-
segment.background,
|
|
1250
|
-
inputIndex,
|
|
1251
|
-
frameWidth,
|
|
1252
|
-
frameHeight
|
|
1253
|
-
)
|
|
1254
|
-
);
|
|
1255
|
-
segmentBgInputIndices.push(inputIndex);
|
|
1256
|
-
inputIndex++;
|
|
1257
|
-
} else {
|
|
1258
|
-
segmentBgInputIndices.push(null);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
const globalBgSegmentIndices = [];
|
|
1262
|
-
for (let i = 0; i < this.effieData.segments.length; i++) {
|
|
1263
|
-
if (segmentBgInputIndices[i] === null) {
|
|
1264
|
-
globalBgSegmentIndices.push(i);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
for (const segment of this.effieData.segments) {
|
|
1268
|
-
for (const layer of segment.layers) {
|
|
1269
|
-
inputs.push(this.buildLayerInput(layer, segment.duration, inputIndex));
|
|
1270
|
-
inputIndex++;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
for (const segment of this.effieData.segments) {
|
|
1274
|
-
if (segment.audio) {
|
|
1275
|
-
inputs.push({
|
|
1276
|
-
index: inputIndex,
|
|
1277
|
-
source: segment.audio.source,
|
|
1278
|
-
preArgs: [],
|
|
1279
|
-
type: "audio"
|
|
1280
|
-
});
|
|
1281
|
-
inputIndex++;
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
if (this.effieData.audio) {
|
|
1285
|
-
inputs.push({
|
|
1286
|
-
index: inputIndex,
|
|
1287
|
-
source: this.effieData.audio.source,
|
|
1288
|
-
preArgs: [],
|
|
1289
|
-
type: "audio"
|
|
1290
|
-
});
|
|
1291
|
-
inputIndex++;
|
|
1292
|
-
}
|
|
1293
|
-
const numSegmentBgInputs = segmentBgInputIndices.filter(
|
|
1294
|
-
(i) => i !== null
|
|
1295
|
-
).length;
|
|
1296
|
-
const numVideoInputs = 1 + numSegmentBgInputs + this.effieData.segments.reduce((sum, seg) => sum + seg.layers.length, 0);
|
|
1297
|
-
let audioCounter = 0;
|
|
1298
|
-
let currentTime = 0;
|
|
1299
|
-
let layerInputOffset = 1 + numSegmentBgInputs;
|
|
1300
|
-
const filterParts = [];
|
|
1301
|
-
const videoSegmentLabels = [];
|
|
1302
|
-
const audioSegmentLabels = [];
|
|
1303
|
-
const globalBgFifoLabels = /* @__PURE__ */ new Map();
|
|
1304
|
-
const bgFilter = `fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight}:force_original_aspect_ratio=increase,crop=${frameWidth}:${frameHeight}`;
|
|
1305
|
-
if (globalBgSegmentIndices.length === 1) {
|
|
1306
|
-
const fifoLabel = `bg_fifo_0`;
|
|
1307
|
-
filterParts.push(`[${globalBgInputIdx}:v]${bgFilter},fifo[${fifoLabel}]`);
|
|
1308
|
-
globalBgFifoLabels.set(globalBgSegmentIndices[0], fifoLabel);
|
|
1309
|
-
} else if (globalBgSegmentIndices.length > 1) {
|
|
1310
|
-
const splitCount = globalBgSegmentIndices.length;
|
|
1311
|
-
const splitOutputLabels = globalBgSegmentIndices.map(
|
|
1312
|
-
(_, i) => `bg_split_${i}`
|
|
1313
|
-
);
|
|
1314
|
-
filterParts.push(
|
|
1315
|
-
`[${globalBgInputIdx}:v]${bgFilter},split=${splitCount}${splitOutputLabels.map((l) => `[${l}]`).join("")}`
|
|
1316
|
-
);
|
|
1317
|
-
for (let i = 0; i < splitCount; i++) {
|
|
1318
|
-
const fifoLabel = `bg_fifo_${i}`;
|
|
1319
|
-
filterParts.push(`[${splitOutputLabels[i]}]fifo[${fifoLabel}]`);
|
|
1320
|
-
globalBgFifoLabels.set(globalBgSegmentIndices[i], fifoLabel);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
for (let segIdx = 0; segIdx < this.effieData.segments.length; segIdx++) {
|
|
1324
|
-
const segment = this.effieData.segments[segIdx];
|
|
1325
|
-
const bgLabel = `bg_seg${segIdx}`;
|
|
1326
|
-
if (segment.background) {
|
|
1327
|
-
const segBgInputIdx = segmentBgInputIndices[segIdx];
|
|
1328
|
-
const segBgSeek = segment.background.type === "video" ? segment.background.seek ?? 0 : 0;
|
|
1329
|
-
filterParts.push(
|
|
1330
|
-
`[${segBgInputIdx}:v]fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight},trim=start=${segBgSeek}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`
|
|
1331
|
-
);
|
|
1332
|
-
} else {
|
|
1333
|
-
const fifoLabel = globalBgFifoLabels.get(segIdx);
|
|
1334
|
-
if (fifoLabel) {
|
|
1335
|
-
filterParts.push(
|
|
1336
|
-
`[${fifoLabel}]trim=start=${backgroundSeek + currentTime}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
const vidLabel = `vid_seg${segIdx}`;
|
|
1341
|
-
filterParts.push(
|
|
1342
|
-
...this.buildLayerFilters(
|
|
1343
|
-
segment,
|
|
1344
|
-
bgLabel,
|
|
1345
|
-
`seg${segIdx}_`,
|
|
1346
|
-
layerInputOffset,
|
|
1347
|
-
frameWidth,
|
|
1348
|
-
frameHeight,
|
|
1349
|
-
vidLabel
|
|
1350
|
-
)
|
|
1351
|
-
);
|
|
1352
|
-
layerInputOffset += segment.layers.length;
|
|
1353
|
-
videoSegmentLabels.push(`[${vidLabel}]`);
|
|
1354
|
-
const nextSegment = this.effieData.segments[segIdx + 1];
|
|
1355
|
-
const transitionDuration = nextSegment?.transition?.duration ?? 0;
|
|
1356
|
-
const realDuration = Math.max(
|
|
1357
|
-
1e-3,
|
|
1358
|
-
segment.duration - transitionDuration
|
|
1359
|
-
);
|
|
1360
|
-
if (segment.audio) {
|
|
1361
|
-
const audioInputIndex = numVideoInputs + audioCounter;
|
|
1362
|
-
const audioFilter = this.buildAudioFilter({
|
|
1363
|
-
duration: realDuration,
|
|
1364
|
-
volume: segment.audio.volume,
|
|
1365
|
-
fadeIn: segment.audio.fadeIn,
|
|
1366
|
-
fadeOut: segment.audio.fadeOut
|
|
1367
|
-
});
|
|
1368
|
-
filterParts.push(
|
|
1369
|
-
`[${audioInputIndex}:a]atrim=start=0:duration=${realDuration},${audioFilter},asetpts=PTS-STARTPTS[aud_seg${segIdx}]`
|
|
1370
|
-
);
|
|
1371
|
-
audioCounter++;
|
|
1372
|
-
} else {
|
|
1373
|
-
filterParts.push(
|
|
1374
|
-
`anullsrc=r=44100:cl=stereo,atrim=start=0:duration=${realDuration},asetpts=PTS-STARTPTS[aud_seg${segIdx}]`
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
audioSegmentLabels.push(`[aud_seg${segIdx}]`);
|
|
1378
|
-
currentTime += realDuration;
|
|
1379
|
-
}
|
|
1380
|
-
this.applyGeneralAudio(
|
|
1381
|
-
filterParts,
|
|
1382
|
-
audioSegmentLabels,
|
|
1383
|
-
currentTime,
|
|
1384
|
-
numVideoInputs + audioCounter
|
|
1385
|
-
);
|
|
1386
|
-
this.applyTransitions(filterParts, videoSegmentLabels);
|
|
1387
|
-
const filterComplex = filterParts.join(";");
|
|
1388
|
-
const outputArgs = this.buildOutputArgs(outputFilename);
|
|
1389
|
-
return new FFmpegCommand(globalArgs, inputs, filterComplex, outputArgs);
|
|
1390
|
-
}
|
|
1391
|
-
createImageTransformer(scaleFactor) {
|
|
1392
|
-
return async (imageStream) => {
|
|
1393
|
-
if (scaleFactor === 1) return imageStream;
|
|
1394
|
-
const sharpTransformer = sharp();
|
|
1395
|
-
imageStream.on("error", (err) => {
|
|
1396
|
-
if (!sharpTransformer.destroyed) {
|
|
1397
|
-
sharpTransformer.destroy(err);
|
|
1398
|
-
}
|
|
1399
|
-
});
|
|
1400
|
-
sharpTransformer.on("error", (err) => {
|
|
1401
|
-
if (!imageStream.destroyed) {
|
|
1402
|
-
imageStream.destroy(err);
|
|
1403
|
-
}
|
|
1404
|
-
});
|
|
1405
|
-
imageStream.pipe(sharpTransformer);
|
|
1406
|
-
try {
|
|
1407
|
-
const metadata = await sharpTransformer.metadata();
|
|
1408
|
-
const imageWidth = metadata.width ?? this.effieData.width;
|
|
1409
|
-
const imageHeight = metadata.height ?? this.effieData.height;
|
|
1410
|
-
return sharpTransformer.resize({
|
|
1411
|
-
width: Math.floor(imageWidth * scaleFactor),
|
|
1412
|
-
height: Math.floor(imageHeight * scaleFactor)
|
|
1413
|
-
});
|
|
1414
|
-
} catch (error) {
|
|
1415
|
-
if (!sharpTransformer.destroyed) {
|
|
1416
|
-
sharpTransformer.destroy(error);
|
|
1417
|
-
}
|
|
1418
|
-
throw error;
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
/**
|
|
1423
|
-
* Resolves a source reference to its actual URL.
|
|
1424
|
-
* If the source is a #reference, returns the resolved URL.
|
|
1425
|
-
* Otherwise, returns the source as-is.
|
|
1426
|
-
*/
|
|
1427
|
-
resolveReference(src) {
|
|
1428
|
-
if (src.startsWith("#")) {
|
|
1429
|
-
const sourceName = src.slice(1);
|
|
1430
|
-
if (sourceName in this.effieData.sources) {
|
|
1431
|
-
return this.effieData.sources[sourceName];
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
return src;
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Renders the effie data to a video stream.
|
|
1438
|
-
* @param scaleFactor - Scale factor for output dimensions
|
|
1439
|
-
*/
|
|
1440
|
-
async render(scaleFactor = 1) {
|
|
1441
|
-
const ffmpegCommand = this.buildFFmpegCommand("-", scaleFactor);
|
|
1442
|
-
this.ffmpegRunner = new FFmpegRunner(ffmpegCommand);
|
|
1443
|
-
const urlTransformer = this.httpProxy ? (url) => this.httpProxy.transformUrl(url) : void 0;
|
|
1444
|
-
return this.ffmpegRunner.run(
|
|
1445
|
-
async ({ src }) => this.fetchSource(src),
|
|
1446
|
-
this.createImageTransformer(scaleFactor),
|
|
1447
|
-
(src) => this.resolveReference(src),
|
|
1448
|
-
urlTransformer
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
close() {
|
|
1452
|
-
if (this.ffmpegRunner) {
|
|
1453
|
-
this.ffmpegRunner.close();
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
};
|
|
1457
|
-
|
|
1458
|
-
// src/handlers/rendering.ts
|
|
1459
218
|
import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
|
|
1460
219
|
async function createRenderJob(req, res, ctx2) {
|
|
1461
220
|
try {
|
|
@@ -1555,6 +314,7 @@ async function streamRenderJob(req, res, ctx2) {
|
|
|
1555
314
|
}
|
|
1556
315
|
}
|
|
1557
316
|
async function streamRenderDirect(res, job, ctx2) {
|
|
317
|
+
const { EffieRenderer } = await import("./render-VWBOR3Y2.js");
|
|
1558
318
|
const renderer = new EffieRenderer(job.effie, {
|
|
1559
319
|
transientStore: ctx2.transientStore,
|
|
1560
320
|
httpProxy: ctx2.httpProxy
|
|
@@ -1619,6 +379,7 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
|
|
|
1619
379
|
timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
|
|
1620
380
|
}
|
|
1621
381
|
const renderStartTime = Date.now();
|
|
382
|
+
const { EffieRenderer } = await import("./render-VWBOR3Y2.js");
|
|
1622
383
|
const renderer = new EffieRenderer(effie, {
|
|
1623
384
|
transientStore: ctx2.transientStore,
|
|
1624
385
|
httpProxy: ctx2.httpProxy
|
|
@@ -2148,7 +909,7 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx2) {
|
|
|
2148
909
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
2149
910
|
}
|
|
2150
911
|
sendEvent("downloading", { url, status: "started", bytesReceived: 0 });
|
|
2151
|
-
const sourceStream =
|
|
912
|
+
const sourceStream = Readable2.fromWeb(
|
|
2152
913
|
response.body
|
|
2153
914
|
);
|
|
2154
915
|
let totalBytes = 0;
|
|
@@ -2181,7 +942,9 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx2) {
|
|
|
2181
942
|
var app = express5();
|
|
2182
943
|
app.use(bodyParser.json({ limit: "50mb" }));
|
|
2183
944
|
var ctx = await createServerContext();
|
|
2184
|
-
|
|
945
|
+
if (ctx.httpProxy) {
|
|
946
|
+
console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);
|
|
947
|
+
}
|
|
2185
948
|
function validateAuth(req, res) {
|
|
2186
949
|
const apiKey = process.env.FFS_API_KEY;
|
|
2187
950
|
if (!apiKey) return true;
|
|
@@ -2220,7 +983,7 @@ var server = app.listen(port, () => {
|
|
|
2220
983
|
});
|
|
2221
984
|
function shutdown() {
|
|
2222
985
|
console.log("Shutting down FFS server...");
|
|
2223
|
-
ctx.httpProxy
|
|
986
|
+
ctx.httpProxy?.close();
|
|
2224
987
|
ctx.transientStore.close();
|
|
2225
988
|
server.close(() => {
|
|
2226
989
|
console.log("FFS server stopped");
|