@effing/ffs 0.1.2 → 0.3.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/README.md +138 -16
- package/dist/{chunk-RNE6TKMF.js → chunk-J64HSZNQ.js} +276 -207
- package/dist/chunk-J64HSZNQ.js.map +1 -0
- package/dist/chunk-XSCNUWZJ.js +935 -0
- package/dist/chunk-XSCNUWZJ.js.map +1 -0
- package/dist/handlers/index.d.ts +40 -7
- package/dist/handlers/index.js +10 -2
- package/dist/index.d.ts +23 -15
- package/dist/index.js +3 -9
- package/dist/proxy-qTA69nOV.d.ts +72 -0
- package/dist/server.js +853 -283
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/cache-BUVFfGZF.d.ts +0 -25
- package/dist/chunk-LK5K4SQV.js +0 -439
- package/dist/chunk-LK5K4SQV.js.map +0 -1
- package/dist/chunk-RNE6TKMF.js.map +0 -1
|
@@ -1,3 +1,162 @@
|
|
|
1
|
+
// src/ffmpeg.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { pipeline } from "stream";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import pathToFFmpeg from "ffmpeg-static";
|
|
8
|
+
import tar from "tar-stream";
|
|
9
|
+
import { createWriteStream } from "fs";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
var pump = promisify(pipeline);
|
|
12
|
+
var FFmpegCommand = class {
|
|
13
|
+
globalArgs;
|
|
14
|
+
inputs;
|
|
15
|
+
filterComplex;
|
|
16
|
+
outputArgs;
|
|
17
|
+
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
18
|
+
this.globalArgs = globalArgs;
|
|
19
|
+
this.inputs = inputs;
|
|
20
|
+
this.filterComplex = filterComplex;
|
|
21
|
+
this.outputArgs = outputArgs;
|
|
22
|
+
}
|
|
23
|
+
buildArgs(inputResolver) {
|
|
24
|
+
const inputArgs = [];
|
|
25
|
+
for (const input of this.inputs) {
|
|
26
|
+
if (input.type === "color") {
|
|
27
|
+
inputArgs.push(...input.preArgs);
|
|
28
|
+
} else if (input.type === "animation") {
|
|
29
|
+
inputArgs.push(
|
|
30
|
+
...input.preArgs,
|
|
31
|
+
"-i",
|
|
32
|
+
path.join(inputResolver(input), "frame_%05d")
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const args = [
|
|
39
|
+
...this.globalArgs,
|
|
40
|
+
...inputArgs,
|
|
41
|
+
"-filter_complex",
|
|
42
|
+
this.filterComplex,
|
|
43
|
+
...this.outputArgs
|
|
44
|
+
];
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var FFmpegRunner = class {
|
|
49
|
+
command;
|
|
50
|
+
ffmpegProc;
|
|
51
|
+
constructor(command) {
|
|
52
|
+
this.command = command;
|
|
53
|
+
}
|
|
54
|
+
async run(sourceFetcher, imageTransformer, referenceResolver, urlTransformer) {
|
|
55
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffs-"));
|
|
56
|
+
const fileMapping = /* @__PURE__ */ new Map();
|
|
57
|
+
const fetchCache = /* @__PURE__ */ new Map();
|
|
58
|
+
const fetchAndSaveSource = async (input, sourceUrl, inputName) => {
|
|
59
|
+
const stream = await sourceFetcher({
|
|
60
|
+
type: input.type,
|
|
61
|
+
src: sourceUrl
|
|
62
|
+
});
|
|
63
|
+
if (input.type === "animation") {
|
|
64
|
+
const extractionDir = path.join(tempDir, inputName);
|
|
65
|
+
await fs.mkdir(extractionDir, { recursive: true });
|
|
66
|
+
const extract = tar.extract();
|
|
67
|
+
const extractPromise = new Promise((resolve, reject) => {
|
|
68
|
+
extract.on("entry", async (header, stream2, next) => {
|
|
69
|
+
if (header.name.startsWith("frame_")) {
|
|
70
|
+
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
71
|
+
const outputPath = path.join(extractionDir, header.name);
|
|
72
|
+
const writeStream = createWriteStream(outputPath);
|
|
73
|
+
transformedStream.pipe(writeStream);
|
|
74
|
+
writeStream.on("finish", next);
|
|
75
|
+
writeStream.on("error", reject);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
extract.on("finish", resolve);
|
|
79
|
+
extract.on("error", reject);
|
|
80
|
+
});
|
|
81
|
+
stream.pipe(extract);
|
|
82
|
+
await extractPromise;
|
|
83
|
+
return extractionDir;
|
|
84
|
+
} else if (input.type === "image" && imageTransformer) {
|
|
85
|
+
const tempFile = path.join(tempDir, inputName);
|
|
86
|
+
const transformedStream = await imageTransformer(stream);
|
|
87
|
+
const writeStream = createWriteStream(tempFile);
|
|
88
|
+
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
89
|
+
await pump(transformedStream, writeStream);
|
|
90
|
+
return tempFile;
|
|
91
|
+
} else {
|
|
92
|
+
const tempFile = path.join(tempDir, inputName);
|
|
93
|
+
const writeStream = createWriteStream(tempFile);
|
|
94
|
+
stream.on("error", (e) => writeStream.destroy(e));
|
|
95
|
+
await pump(stream, writeStream);
|
|
96
|
+
return tempFile;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
await Promise.all(
|
|
100
|
+
this.command.inputs.map(async (input) => {
|
|
101
|
+
if (input.type === "color") return;
|
|
102
|
+
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
103
|
+
const sourceUrl = referenceResolver ? referenceResolver(input.source) : input.source;
|
|
104
|
+
if ((input.type === "video" || input.type === "audio") && (sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
|
|
105
|
+
const finalUrl = urlTransformer ? urlTransformer(sourceUrl) : sourceUrl;
|
|
106
|
+
fileMapping.set(input.index, finalUrl);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const shouldCache = input.source.startsWith("#");
|
|
110
|
+
if (shouldCache) {
|
|
111
|
+
let fetchPromise = fetchCache.get(input.source);
|
|
112
|
+
if (!fetchPromise) {
|
|
113
|
+
fetchPromise = fetchAndSaveSource(input, sourceUrl, inputName);
|
|
114
|
+
fetchCache.set(input.source, fetchPromise);
|
|
115
|
+
}
|
|
116
|
+
const filePath = await fetchPromise;
|
|
117
|
+
fileMapping.set(input.index, filePath);
|
|
118
|
+
} else {
|
|
119
|
+
const filePath = await fetchAndSaveSource(
|
|
120
|
+
input,
|
|
121
|
+
sourceUrl,
|
|
122
|
+
inputName
|
|
123
|
+
);
|
|
124
|
+
fileMapping.set(input.index, filePath);
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
const finalArgs = this.command.buildArgs((input) => {
|
|
129
|
+
const filePath = fileMapping.get(input.index);
|
|
130
|
+
if (!filePath)
|
|
131
|
+
throw new Error(`File for input index ${input.index} not found`);
|
|
132
|
+
return filePath;
|
|
133
|
+
});
|
|
134
|
+
const ffmpegProc = spawn(process.env.FFMPEG ?? pathToFFmpeg, finalArgs);
|
|
135
|
+
ffmpegProc.stderr.on("data", (data) => {
|
|
136
|
+
console.error(data.toString());
|
|
137
|
+
});
|
|
138
|
+
ffmpegProc.on("close", async () => {
|
|
139
|
+
try {
|
|
140
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error("Error removing temp directory:", err);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
this.ffmpegProc = ffmpegProc;
|
|
146
|
+
return ffmpegProc.stdout;
|
|
147
|
+
}
|
|
148
|
+
close() {
|
|
149
|
+
if (this.ffmpegProc) {
|
|
150
|
+
this.ffmpegProc.kill("SIGTERM");
|
|
151
|
+
this.ffmpegProc = void 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/render.ts
|
|
157
|
+
import { Readable } from "stream";
|
|
158
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
159
|
+
|
|
1
160
|
// src/motion.ts
|
|
2
161
|
function getEasingExpression(tNormExpr, easingType) {
|
|
3
162
|
switch (easingType) {
|
|
@@ -189,151 +348,6 @@ function processEffects(effects, frameRate, frameWidth, frameHeight) {
|
|
|
189
348
|
return filters.join(",");
|
|
190
349
|
}
|
|
191
350
|
|
|
192
|
-
// src/ffmpeg.ts
|
|
193
|
-
import { spawn } from "child_process";
|
|
194
|
-
import { pipeline } from "stream";
|
|
195
|
-
import fs from "fs/promises";
|
|
196
|
-
import os from "os";
|
|
197
|
-
import path from "path";
|
|
198
|
-
import pathToFFmpeg from "ffmpeg-static";
|
|
199
|
-
import tar from "tar-stream";
|
|
200
|
-
import { createWriteStream } from "fs";
|
|
201
|
-
import { promisify } from "util";
|
|
202
|
-
var pump = promisify(pipeline);
|
|
203
|
-
var FFmpegCommand = class {
|
|
204
|
-
globalArgs;
|
|
205
|
-
inputs;
|
|
206
|
-
filterComplex;
|
|
207
|
-
outputArgs;
|
|
208
|
-
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
209
|
-
this.globalArgs = globalArgs;
|
|
210
|
-
this.inputs = inputs;
|
|
211
|
-
this.filterComplex = filterComplex;
|
|
212
|
-
this.outputArgs = outputArgs;
|
|
213
|
-
}
|
|
214
|
-
buildArgs(inputResolver) {
|
|
215
|
-
const inputArgs = [];
|
|
216
|
-
for (const input of this.inputs) {
|
|
217
|
-
if (input.type === "color") {
|
|
218
|
-
inputArgs.push(...input.preArgs);
|
|
219
|
-
} else if (input.type === "animation") {
|
|
220
|
-
inputArgs.push(
|
|
221
|
-
...input.preArgs,
|
|
222
|
-
"-i",
|
|
223
|
-
path.join(inputResolver(input), "frame_%05d")
|
|
224
|
-
);
|
|
225
|
-
} else {
|
|
226
|
-
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const args = [
|
|
230
|
-
...this.globalArgs,
|
|
231
|
-
...inputArgs,
|
|
232
|
-
"-filter_complex",
|
|
233
|
-
this.filterComplex,
|
|
234
|
-
...this.outputArgs
|
|
235
|
-
];
|
|
236
|
-
return args;
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
var FFmpegRunner = class {
|
|
240
|
-
command;
|
|
241
|
-
ffmpegProc;
|
|
242
|
-
constructor(command) {
|
|
243
|
-
this.command = command;
|
|
244
|
-
}
|
|
245
|
-
async run(sourceResolver, imageTransformer) {
|
|
246
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffs-"));
|
|
247
|
-
const fileMapping = /* @__PURE__ */ new Map();
|
|
248
|
-
const sourceCache = /* @__PURE__ */ new Map();
|
|
249
|
-
const fetchAndSaveSource = async (input, inputName) => {
|
|
250
|
-
const stream = await sourceResolver({
|
|
251
|
-
type: input.type,
|
|
252
|
-
src: input.source
|
|
253
|
-
});
|
|
254
|
-
if (input.type === "animation") {
|
|
255
|
-
const extractionDir = path.join(tempDir, inputName);
|
|
256
|
-
await fs.mkdir(extractionDir, { recursive: true });
|
|
257
|
-
const extract = tar.extract();
|
|
258
|
-
const extractPromise = new Promise((resolve, reject) => {
|
|
259
|
-
extract.on("entry", async (header, stream2, next) => {
|
|
260
|
-
if (header.name.startsWith("frame_")) {
|
|
261
|
-
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
262
|
-
const outputPath = path.join(extractionDir, header.name);
|
|
263
|
-
const writeStream = createWriteStream(outputPath);
|
|
264
|
-
transformedStream.pipe(writeStream);
|
|
265
|
-
writeStream.on("finish", next);
|
|
266
|
-
writeStream.on("error", reject);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
extract.on("finish", resolve);
|
|
270
|
-
extract.on("error", reject);
|
|
271
|
-
});
|
|
272
|
-
stream.pipe(extract);
|
|
273
|
-
await extractPromise;
|
|
274
|
-
return extractionDir;
|
|
275
|
-
} else if (input.type === "image" && imageTransformer) {
|
|
276
|
-
const tempFile = path.join(tempDir, inputName);
|
|
277
|
-
const transformedStream = await imageTransformer(stream);
|
|
278
|
-
const writeStream = createWriteStream(tempFile);
|
|
279
|
-
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
280
|
-
await pump(transformedStream, writeStream);
|
|
281
|
-
return tempFile;
|
|
282
|
-
} else {
|
|
283
|
-
const tempFile = path.join(tempDir, inputName);
|
|
284
|
-
const writeStream = createWriteStream(tempFile);
|
|
285
|
-
stream.on("error", (e) => writeStream.destroy(e));
|
|
286
|
-
await pump(stream, writeStream);
|
|
287
|
-
return tempFile;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
await Promise.all(
|
|
291
|
-
this.command.inputs.map(async (input) => {
|
|
292
|
-
if (input.type === "color") return;
|
|
293
|
-
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
294
|
-
const shouldCache = input.source.startsWith("#");
|
|
295
|
-
if (shouldCache) {
|
|
296
|
-
let fetchPromise = sourceCache.get(input.source);
|
|
297
|
-
if (!fetchPromise) {
|
|
298
|
-
fetchPromise = fetchAndSaveSource(input, inputName);
|
|
299
|
-
sourceCache.set(input.source, fetchPromise);
|
|
300
|
-
}
|
|
301
|
-
const filePath = await fetchPromise;
|
|
302
|
-
fileMapping.set(input.index, filePath);
|
|
303
|
-
} else {
|
|
304
|
-
const filePath = await fetchAndSaveSource(input, inputName);
|
|
305
|
-
fileMapping.set(input.index, filePath);
|
|
306
|
-
}
|
|
307
|
-
})
|
|
308
|
-
);
|
|
309
|
-
const finalArgs = this.command.buildArgs((input) => {
|
|
310
|
-
const filePath = fileMapping.get(input.index);
|
|
311
|
-
if (!filePath)
|
|
312
|
-
throw new Error(`File for input index ${input.index} not found`);
|
|
313
|
-
return filePath;
|
|
314
|
-
});
|
|
315
|
-
const ffmpegProc = spawn(process.env.FFMPEG ?? pathToFFmpeg, finalArgs);
|
|
316
|
-
ffmpegProc.stderr.on("data", (data) => {
|
|
317
|
-
console.error(data.toString());
|
|
318
|
-
});
|
|
319
|
-
ffmpegProc.on("close", async () => {
|
|
320
|
-
try {
|
|
321
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
322
|
-
} catch (err) {
|
|
323
|
-
console.error("Error removing temp directory:", err);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
this.ffmpegProc = ffmpegProc;
|
|
327
|
-
return ffmpegProc.stdout;
|
|
328
|
-
}
|
|
329
|
-
close() {
|
|
330
|
-
if (this.ffmpegProc) {
|
|
331
|
-
this.ffmpegProc.kill("SIGTERM");
|
|
332
|
-
this.ffmpegProc = void 0;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
351
|
// src/transition.ts
|
|
338
352
|
function processTransition(transition) {
|
|
339
353
|
switch (transition.type) {
|
|
@@ -390,8 +404,6 @@ function processTransition(transition) {
|
|
|
390
404
|
}
|
|
391
405
|
|
|
392
406
|
// src/render.ts
|
|
393
|
-
import { Readable } from "stream";
|
|
394
|
-
import { createReadStream as createReadStream2 } from "fs";
|
|
395
407
|
import sharp from "sharp";
|
|
396
408
|
|
|
397
409
|
// src/fetch.ts
|
|
@@ -418,7 +430,7 @@ async function ffsFetch(url, options) {
|
|
|
418
430
|
// src/render.ts
|
|
419
431
|
import { fileURLToPath } from "url";
|
|
420
432
|
|
|
421
|
-
// src/
|
|
433
|
+
// src/storage.ts
|
|
422
434
|
import {
|
|
423
435
|
S3Client,
|
|
424
436
|
PutObjectCommand,
|
|
@@ -433,11 +445,14 @@ import { pipeline as pipeline2 } from "stream/promises";
|
|
|
433
445
|
import path2 from "path";
|
|
434
446
|
import os2 from "os";
|
|
435
447
|
import crypto from "crypto";
|
|
436
|
-
var
|
|
448
|
+
var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
|
|
449
|
+
var DEFAULT_JOB_METADATA_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
450
|
+
var S3TransientStore = class {
|
|
437
451
|
client;
|
|
438
452
|
bucket;
|
|
439
453
|
prefix;
|
|
440
|
-
|
|
454
|
+
sourceTtlMs;
|
|
455
|
+
jobMetadataTtlMs;
|
|
441
456
|
constructor(options) {
|
|
442
457
|
this.client = new S3Client({
|
|
443
458
|
endpoint: options.endpoint,
|
|
@@ -450,22 +465,23 @@ var S3CacheStorage = class {
|
|
|
450
465
|
});
|
|
451
466
|
this.bucket = options.bucket;
|
|
452
467
|
this.prefix = options.prefix ?? "";
|
|
453
|
-
this.
|
|
468
|
+
this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
469
|
+
this.jobMetadataTtlMs = options.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
454
470
|
}
|
|
455
|
-
getExpires() {
|
|
456
|
-
return new Date(Date.now() +
|
|
471
|
+
getExpires(ttlMs) {
|
|
472
|
+
return new Date(Date.now() + ttlMs);
|
|
457
473
|
}
|
|
458
474
|
getFullKey(key) {
|
|
459
475
|
return `${this.prefix}${key}`;
|
|
460
476
|
}
|
|
461
|
-
async put(key, stream) {
|
|
477
|
+
async put(key, stream, ttlMs) {
|
|
462
478
|
const upload = new Upload({
|
|
463
479
|
client: this.client,
|
|
464
480
|
params: {
|
|
465
481
|
Bucket: this.bucket,
|
|
466
482
|
Key: this.getFullKey(key),
|
|
467
483
|
Body: stream,
|
|
468
|
-
Expires: this.getExpires()
|
|
484
|
+
Expires: this.getExpires(ttlMs ?? this.sourceTtlMs)
|
|
469
485
|
}
|
|
470
486
|
});
|
|
471
487
|
await upload.done();
|
|
@@ -526,14 +542,14 @@ var S3CacheStorage = class {
|
|
|
526
542
|
throw err;
|
|
527
543
|
}
|
|
528
544
|
}
|
|
529
|
-
async putJson(key, data) {
|
|
545
|
+
async putJson(key, data, ttlMs) {
|
|
530
546
|
await this.client.send(
|
|
531
547
|
new PutObjectCommand({
|
|
532
548
|
Bucket: this.bucket,
|
|
533
549
|
Key: this.getFullKey(key),
|
|
534
550
|
Body: JSON.stringify(data),
|
|
535
551
|
ContentType: "application/json",
|
|
536
|
-
Expires: this.getExpires()
|
|
552
|
+
Expires: this.getExpires(ttlMs ?? this.jobMetadataTtlMs)
|
|
537
553
|
})
|
|
538
554
|
);
|
|
539
555
|
}
|
|
@@ -559,20 +575,25 @@ var S3CacheStorage = class {
|
|
|
559
575
|
close() {
|
|
560
576
|
}
|
|
561
577
|
};
|
|
562
|
-
var
|
|
578
|
+
var LocalTransientStore = class {
|
|
563
579
|
baseDir;
|
|
564
580
|
initialized = false;
|
|
565
581
|
cleanupInterval;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
582
|
+
sourceTtlMs;
|
|
583
|
+
jobMetadataTtlMs;
|
|
584
|
+
/** For cleanup, use the longer of the two TTLs */
|
|
585
|
+
maxTtlMs;
|
|
586
|
+
constructor(options) {
|
|
587
|
+
this.baseDir = options?.baseDir ?? path2.join(os2.tmpdir(), "ffs-transient");
|
|
588
|
+
this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
589
|
+
this.jobMetadataTtlMs = options?.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
590
|
+
this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobMetadataTtlMs);
|
|
570
591
|
this.cleanupInterval = setInterval(() => {
|
|
571
592
|
this.cleanupExpired().catch(console.error);
|
|
572
593
|
}, 3e5);
|
|
573
594
|
}
|
|
574
595
|
/**
|
|
575
|
-
* Remove files older than TTL
|
|
596
|
+
* Remove files older than max TTL
|
|
576
597
|
*/
|
|
577
598
|
async cleanupExpired() {
|
|
578
599
|
if (!this.initialized) return;
|
|
@@ -597,7 +618,7 @@ var LocalCacheStorage = class {
|
|
|
597
618
|
} else if (entry.isFile()) {
|
|
598
619
|
try {
|
|
599
620
|
const stat = await fs2.stat(fullPath);
|
|
600
|
-
if (now - stat.mtimeMs > this.
|
|
621
|
+
if (now - stat.mtimeMs > this.maxTtlMs) {
|
|
601
622
|
await fs2.rm(fullPath, { force: true });
|
|
602
623
|
}
|
|
603
624
|
} catch {
|
|
@@ -616,7 +637,7 @@ var LocalCacheStorage = class {
|
|
|
616
637
|
const rand = crypto.randomBytes(8).toString("hex");
|
|
617
638
|
return `${finalPath}.tmp-${process.pid}-${rand}`;
|
|
618
639
|
}
|
|
619
|
-
async put(key, stream) {
|
|
640
|
+
async put(key, stream, _ttlMs) {
|
|
620
641
|
const fp = this.filePath(key);
|
|
621
642
|
await this.ensureDir(fp);
|
|
622
643
|
const tmpPath = this.tmpPathFor(fp);
|
|
@@ -652,7 +673,7 @@ var LocalCacheStorage = class {
|
|
|
652
673
|
async delete(key) {
|
|
653
674
|
await fs2.rm(this.filePath(key), { force: true });
|
|
654
675
|
}
|
|
655
|
-
async putJson(key, data) {
|
|
676
|
+
async putJson(key, data, _ttlMs) {
|
|
656
677
|
const fp = this.filePath(key);
|
|
657
678
|
await this.ensureDir(fp);
|
|
658
679
|
const tmpPath = this.tmpPathFor(fp);
|
|
@@ -680,37 +701,47 @@ var LocalCacheStorage = class {
|
|
|
680
701
|
}
|
|
681
702
|
}
|
|
682
703
|
};
|
|
683
|
-
function
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
704
|
+
function createTransientStore() {
|
|
705
|
+
const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10) : DEFAULT_SOURCE_TTL_MS;
|
|
706
|
+
const jobMetadataTtlMs = process.env.FFS_JOB_METADATA_TTL_MS ? parseInt(process.env.FFS_JOB_METADATA_TTL_MS, 10) : DEFAULT_JOB_METADATA_TTL_MS;
|
|
707
|
+
if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
|
|
708
|
+
return new S3TransientStore({
|
|
709
|
+
endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
|
|
710
|
+
region: process.env.FFS_TRANSIENT_STORE_REGION ?? "auto",
|
|
711
|
+
bucket: process.env.FFS_TRANSIENT_STORE_BUCKET,
|
|
712
|
+
prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
|
|
713
|
+
accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
|
|
714
|
+
secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
|
|
715
|
+
sourceTtlMs,
|
|
716
|
+
jobMetadataTtlMs
|
|
694
717
|
});
|
|
695
718
|
}
|
|
696
|
-
return new
|
|
719
|
+
return new LocalTransientStore({
|
|
720
|
+
baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
|
|
721
|
+
sourceTtlMs,
|
|
722
|
+
jobMetadataTtlMs
|
|
723
|
+
});
|
|
697
724
|
}
|
|
698
725
|
function hashUrl(url) {
|
|
699
726
|
return crypto.createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
700
727
|
}
|
|
701
|
-
function
|
|
728
|
+
function sourceStoreKey(url) {
|
|
702
729
|
return `sources/${hashUrl(url)}`;
|
|
703
730
|
}
|
|
704
|
-
function
|
|
731
|
+
function warmupJobStoreKey(jobId) {
|
|
705
732
|
return `jobs/warmup/${jobId}.json`;
|
|
706
733
|
}
|
|
707
|
-
function
|
|
734
|
+
function renderJobStoreKey(jobId) {
|
|
708
735
|
return `jobs/render/${jobId}.json`;
|
|
709
736
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
737
|
+
function warmupAndRenderJobStoreKey(jobId) {
|
|
738
|
+
return `jobs/warmup-and-render/${jobId}.json`;
|
|
739
|
+
}
|
|
740
|
+
var storeKeys = {
|
|
741
|
+
source: sourceStoreKey,
|
|
742
|
+
warmupJob: warmupJobStoreKey,
|
|
743
|
+
renderJob: renderJobStoreKey,
|
|
744
|
+
warmupAndRenderJob: warmupAndRenderJobStoreKey
|
|
714
745
|
};
|
|
715
746
|
|
|
716
747
|
// src/render.ts
|
|
@@ -718,20 +749,15 @@ var EffieRenderer = class {
|
|
|
718
749
|
effieData;
|
|
719
750
|
ffmpegRunner;
|
|
720
751
|
allowLocalFiles;
|
|
721
|
-
|
|
752
|
+
transientStore;
|
|
753
|
+
httpProxy;
|
|
722
754
|
constructor(effieData, options) {
|
|
723
755
|
this.effieData = effieData;
|
|
724
756
|
this.allowLocalFiles = options?.allowLocalFiles ?? false;
|
|
725
|
-
this.
|
|
757
|
+
this.transientStore = options?.transientStore;
|
|
758
|
+
this.httpProxy = options?.httpProxy;
|
|
726
759
|
}
|
|
727
760
|
async fetchSource(src) {
|
|
728
|
-
if (src.startsWith("#")) {
|
|
729
|
-
const sourceName = src.slice(1);
|
|
730
|
-
if (!(sourceName in this.effieData.sources)) {
|
|
731
|
-
throw new Error(`Named source "${sourceName}" not found`);
|
|
732
|
-
}
|
|
733
|
-
src = this.effieData.sources[sourceName];
|
|
734
|
-
}
|
|
735
761
|
if (src.startsWith("data:")) {
|
|
736
762
|
const commaIndex = src.indexOf(",");
|
|
737
763
|
if (commaIndex === -1) {
|
|
@@ -751,9 +777,9 @@ var EffieRenderer = class {
|
|
|
751
777
|
}
|
|
752
778
|
return createReadStream2(fileURLToPath(src));
|
|
753
779
|
}
|
|
754
|
-
if (this.
|
|
755
|
-
const cachedStream = await this.
|
|
756
|
-
|
|
780
|
+
if (this.transientStore) {
|
|
781
|
+
const cachedStream = await this.transientStore.getStream(
|
|
782
|
+
storeKeys.source(src)
|
|
757
783
|
);
|
|
758
784
|
if (cachedStream) {
|
|
759
785
|
return cachedStream;
|
|
@@ -1025,6 +1051,12 @@ var EffieRenderer = class {
|
|
|
1025
1051
|
segmentBgInputIndices.push(null);
|
|
1026
1052
|
}
|
|
1027
1053
|
}
|
|
1054
|
+
const globalBgSegmentIndices = [];
|
|
1055
|
+
for (let i = 0; i < this.effieData.segments.length; i++) {
|
|
1056
|
+
if (segmentBgInputIndices[i] === null) {
|
|
1057
|
+
globalBgSegmentIndices.push(i);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1028
1060
|
for (const segment of this.effieData.segments) {
|
|
1029
1061
|
for (const layer of segment.layers) {
|
|
1030
1062
|
inputs.push(this.buildLayerInput(layer, segment.duration, inputIndex));
|
|
@@ -1061,6 +1093,26 @@ var EffieRenderer = class {
|
|
|
1061
1093
|
const filterParts = [];
|
|
1062
1094
|
const videoSegmentLabels = [];
|
|
1063
1095
|
const audioSegmentLabels = [];
|
|
1096
|
+
const globalBgFifoLabels = /* @__PURE__ */ new Map();
|
|
1097
|
+
const bgFilter = `fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight}:force_original_aspect_ratio=increase,crop=${frameWidth}:${frameHeight}`;
|
|
1098
|
+
if (globalBgSegmentIndices.length === 1) {
|
|
1099
|
+
const fifoLabel = `bg_fifo_0`;
|
|
1100
|
+
filterParts.push(`[${globalBgInputIdx}:v]${bgFilter},fifo[${fifoLabel}]`);
|
|
1101
|
+
globalBgFifoLabels.set(globalBgSegmentIndices[0], fifoLabel);
|
|
1102
|
+
} else if (globalBgSegmentIndices.length > 1) {
|
|
1103
|
+
const splitCount = globalBgSegmentIndices.length;
|
|
1104
|
+
const splitOutputLabels = globalBgSegmentIndices.map(
|
|
1105
|
+
(_, i) => `bg_split_${i}`
|
|
1106
|
+
);
|
|
1107
|
+
filterParts.push(
|
|
1108
|
+
`[${globalBgInputIdx}:v]${bgFilter},split=${splitCount}${splitOutputLabels.map((l) => `[${l}]`).join("")}`
|
|
1109
|
+
);
|
|
1110
|
+
for (let i = 0; i < splitCount; i++) {
|
|
1111
|
+
const fifoLabel = `bg_fifo_${i}`;
|
|
1112
|
+
filterParts.push(`[${splitOutputLabels[i]}]fifo[${fifoLabel}]`);
|
|
1113
|
+
globalBgFifoLabels.set(globalBgSegmentIndices[i], fifoLabel);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1064
1116
|
for (let segIdx = 0; segIdx < this.effieData.segments.length; segIdx++) {
|
|
1065
1117
|
const segment = this.effieData.segments[segIdx];
|
|
1066
1118
|
const bgLabel = `bg_seg${segIdx}`;
|
|
@@ -1071,9 +1123,12 @@ var EffieRenderer = class {
|
|
|
1071
1123
|
`[${segBgInputIdx}:v]fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight},trim=start=${segBgSeek}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`
|
|
1072
1124
|
);
|
|
1073
1125
|
} else {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1126
|
+
const fifoLabel = globalBgFifoLabels.get(segIdx);
|
|
1127
|
+
if (fifoLabel) {
|
|
1128
|
+
filterParts.push(
|
|
1129
|
+
`[${fifoLabel}]trim=start=${backgroundSeek + currentTime}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1077
1132
|
}
|
|
1078
1133
|
const vidLabel = `vid_seg${segIdx}`;
|
|
1079
1134
|
filterParts.push(
|
|
@@ -1157,6 +1212,20 @@ var EffieRenderer = class {
|
|
|
1157
1212
|
}
|
|
1158
1213
|
};
|
|
1159
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Resolves a source reference to its actual URL.
|
|
1217
|
+
* If the source is a #reference, returns the resolved URL.
|
|
1218
|
+
* Otherwise, returns the source as-is.
|
|
1219
|
+
*/
|
|
1220
|
+
resolveReference(src) {
|
|
1221
|
+
if (src.startsWith("#")) {
|
|
1222
|
+
const sourceName = src.slice(1);
|
|
1223
|
+
if (sourceName in this.effieData.sources) {
|
|
1224
|
+
return this.effieData.sources[sourceName];
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return src;
|
|
1228
|
+
}
|
|
1160
1229
|
/**
|
|
1161
1230
|
* Renders the effie data to a video stream.
|
|
1162
1231
|
* @param scaleFactor - Scale factor for output dimensions
|
|
@@ -1164,9 +1233,12 @@ var EffieRenderer = class {
|
|
|
1164
1233
|
async render(scaleFactor = 1) {
|
|
1165
1234
|
const ffmpegCommand = this.buildFFmpegCommand("-", scaleFactor);
|
|
1166
1235
|
this.ffmpegRunner = new FFmpegRunner(ffmpegCommand);
|
|
1236
|
+
const urlTransformer = this.httpProxy ? (url) => this.httpProxy.transformUrl(url) : void 0;
|
|
1167
1237
|
return this.ffmpegRunner.run(
|
|
1168
1238
|
async ({ src }) => this.fetchSource(src),
|
|
1169
|
-
this.createImageTransformer(scaleFactor)
|
|
1239
|
+
this.createImageTransformer(scaleFactor),
|
|
1240
|
+
(src) => this.resolveReference(src),
|
|
1241
|
+
urlTransformer
|
|
1170
1242
|
);
|
|
1171
1243
|
}
|
|
1172
1244
|
close() {
|
|
@@ -1177,14 +1249,11 @@ var EffieRenderer = class {
|
|
|
1177
1249
|
};
|
|
1178
1250
|
|
|
1179
1251
|
export {
|
|
1180
|
-
processMotion,
|
|
1181
|
-
processEffects,
|
|
1182
1252
|
FFmpegCommand,
|
|
1183
1253
|
FFmpegRunner,
|
|
1184
|
-
processTransition,
|
|
1185
1254
|
ffsFetch,
|
|
1186
|
-
|
|
1187
|
-
|
|
1255
|
+
createTransientStore,
|
|
1256
|
+
storeKeys,
|
|
1188
1257
|
EffieRenderer
|
|
1189
1258
|
};
|
|
1190
|
-
//# sourceMappingURL=chunk-
|
|
1259
|
+
//# sourceMappingURL=chunk-J64HSZNQ.js.map
|