@effing/ffs 0.4.1 → 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-3SM6XYCZ.js → chunk-N3D6I2BD.js} +179 -504
- package/dist/chunk-N3D6I2BD.js.map +1 -0
- package/dist/chunk-QPZEAH3J.js +342 -0
- package/dist/{chunk-JDRYI7SR.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 -1264
- package/dist/server.js.map +1 -1
- package/package.json +5 -3
- package/dist/chunk-3SM6XYCZ.js.map +0 -1
- package/dist/chunk-JDRYI7SR.js.map +0 -1
|
@@ -1,165 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 ffmpegBin = process.env.FFMPEG ?? pathToFFmpeg;
|
|
13
|
-
function getFFmpegVersion() {
|
|
14
|
-
return execFileSync(ffmpegBin, ["-version"], { encoding: "utf8" }).split("\n")[0].trim();
|
|
15
|
-
}
|
|
16
|
-
var FFmpegCommand = class {
|
|
17
|
-
globalArgs;
|
|
18
|
-
inputs;
|
|
19
|
-
filterComplex;
|
|
20
|
-
outputArgs;
|
|
21
|
-
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
22
|
-
this.globalArgs = globalArgs;
|
|
23
|
-
this.inputs = inputs;
|
|
24
|
-
this.filterComplex = filterComplex;
|
|
25
|
-
this.outputArgs = outputArgs;
|
|
26
|
-
}
|
|
27
|
-
buildArgs(inputResolver) {
|
|
28
|
-
const inputArgs = [];
|
|
29
|
-
for (const input of this.inputs) {
|
|
30
|
-
if (input.type === "color") {
|
|
31
|
-
inputArgs.push(...input.preArgs);
|
|
32
|
-
} else if (input.type === "animation") {
|
|
33
|
-
inputArgs.push(
|
|
34
|
-
...input.preArgs,
|
|
35
|
-
"-i",
|
|
36
|
-
path.join(inputResolver(input), "frame_%05d")
|
|
37
|
-
);
|
|
38
|
-
} else {
|
|
39
|
-
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const args = [
|
|
43
|
-
...this.globalArgs,
|
|
44
|
-
...inputArgs,
|
|
45
|
-
"-filter_complex",
|
|
46
|
-
this.filterComplex,
|
|
47
|
-
...this.outputArgs
|
|
48
|
-
];
|
|
49
|
-
return args;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
var FFmpegRunner = class {
|
|
53
|
-
command;
|
|
54
|
-
ffmpegProc;
|
|
55
|
-
constructor(command) {
|
|
56
|
-
this.command = command;
|
|
57
|
-
}
|
|
58
|
-
async run(sourceFetcher, imageTransformer, referenceResolver, urlTransformer) {
|
|
59
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffs-"));
|
|
60
|
-
const fileMapping = /* @__PURE__ */ new Map();
|
|
61
|
-
const fetchCache = /* @__PURE__ */ new Map();
|
|
62
|
-
const fetchAndSaveSource = async (input, sourceUrl, inputName) => {
|
|
63
|
-
const stream = await sourceFetcher({
|
|
64
|
-
type: input.type,
|
|
65
|
-
src: sourceUrl
|
|
66
|
-
});
|
|
67
|
-
if (input.type === "animation") {
|
|
68
|
-
const extractionDir = path.join(tempDir, inputName);
|
|
69
|
-
await fs.mkdir(extractionDir, { recursive: true });
|
|
70
|
-
const extract = tar.extract();
|
|
71
|
-
const extractPromise = new Promise((resolve, reject) => {
|
|
72
|
-
extract.on("entry", async (header, stream2, next) => {
|
|
73
|
-
if (header.name.startsWith("frame_")) {
|
|
74
|
-
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
75
|
-
const outputPath = path.join(extractionDir, header.name);
|
|
76
|
-
const writeStream = createWriteStream(outputPath);
|
|
77
|
-
transformedStream.pipe(writeStream);
|
|
78
|
-
writeStream.on("finish", next);
|
|
79
|
-
writeStream.on("error", reject);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
extract.on("finish", resolve);
|
|
83
|
-
extract.on("error", reject);
|
|
84
|
-
});
|
|
85
|
-
stream.pipe(extract);
|
|
86
|
-
await extractPromise;
|
|
87
|
-
return extractionDir;
|
|
88
|
-
} else if (input.type === "image" && imageTransformer) {
|
|
89
|
-
const tempFile = path.join(tempDir, inputName);
|
|
90
|
-
const transformedStream = await imageTransformer(stream);
|
|
91
|
-
const writeStream = createWriteStream(tempFile);
|
|
92
|
-
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
93
|
-
await pump(transformedStream, writeStream);
|
|
94
|
-
return tempFile;
|
|
95
|
-
} else {
|
|
96
|
-
const tempFile = path.join(tempDir, inputName);
|
|
97
|
-
const writeStream = createWriteStream(tempFile);
|
|
98
|
-
stream.on("error", (e) => writeStream.destroy(e));
|
|
99
|
-
await pump(stream, writeStream);
|
|
100
|
-
return tempFile;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
await Promise.all(
|
|
104
|
-
this.command.inputs.map(async (input) => {
|
|
105
|
-
if (input.type === "color") return;
|
|
106
|
-
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
107
|
-
const sourceUrl = referenceResolver ? referenceResolver(input.source) : input.source;
|
|
108
|
-
if ((input.type === "video" || input.type === "audio") && (sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
|
|
109
|
-
const finalUrl = urlTransformer ? urlTransformer(sourceUrl) : sourceUrl;
|
|
110
|
-
fileMapping.set(input.index, finalUrl);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const shouldCache = input.source.startsWith("#");
|
|
114
|
-
if (shouldCache) {
|
|
115
|
-
let fetchPromise = fetchCache.get(input.source);
|
|
116
|
-
if (!fetchPromise) {
|
|
117
|
-
fetchPromise = fetchAndSaveSource(input, sourceUrl, inputName);
|
|
118
|
-
fetchCache.set(input.source, fetchPromise);
|
|
119
|
-
}
|
|
120
|
-
const filePath = await fetchPromise;
|
|
121
|
-
fileMapping.set(input.index, filePath);
|
|
122
|
-
} else {
|
|
123
|
-
const filePath = await fetchAndSaveSource(
|
|
124
|
-
input,
|
|
125
|
-
sourceUrl,
|
|
126
|
-
inputName
|
|
127
|
-
);
|
|
128
|
-
fileMapping.set(input.index, filePath);
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
const finalArgs = this.command.buildArgs((input) => {
|
|
133
|
-
const filePath = fileMapping.get(input.index);
|
|
134
|
-
if (!filePath)
|
|
135
|
-
throw new Error(`File for input index ${input.index} not found`);
|
|
136
|
-
return filePath;
|
|
137
|
-
});
|
|
138
|
-
const ffmpegProc = spawn(ffmpegBin, finalArgs);
|
|
139
|
-
ffmpegProc.stderr.on("data", (data) => {
|
|
140
|
-
console.error(data.toString());
|
|
141
|
-
});
|
|
142
|
-
ffmpegProc.on("close", async () => {
|
|
143
|
-
try {
|
|
144
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
145
|
-
} catch (err) {
|
|
146
|
-
console.error("Error removing temp directory:", err);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
this.ffmpegProc = ffmpegProc;
|
|
150
|
-
return ffmpegProc.stdout;
|
|
151
|
-
}
|
|
152
|
-
close() {
|
|
153
|
-
if (this.ffmpegProc) {
|
|
154
|
-
this.ffmpegProc.kill("SIGTERM");
|
|
155
|
-
this.ffmpegProc = void 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
ffsFetch,
|
|
3
|
+
storeKeys
|
|
4
|
+
} from "./chunk-5SGOYTM2.js";
|
|
159
5
|
|
|
160
6
|
// src/render.ts
|
|
161
7
|
import { Readable } from "stream";
|
|
162
|
-
import { createReadStream
|
|
8
|
+
import { createReadStream } from "fs";
|
|
163
9
|
|
|
164
10
|
// src/motion.ts
|
|
165
11
|
function getEasingExpression(tNormExpr, easingType) {
|
|
@@ -352,6 +198,178 @@ function processEffects(effects, frameRate, frameWidth, frameHeight) {
|
|
|
352
198
|
return filters.join(",");
|
|
353
199
|
}
|
|
354
200
|
|
|
201
|
+
// src/ffmpeg.ts
|
|
202
|
+
import { spawn } from "child_process";
|
|
203
|
+
import { pipeline } from "stream";
|
|
204
|
+
import fs from "fs/promises";
|
|
205
|
+
import os from "os";
|
|
206
|
+
import path from "path";
|
|
207
|
+
import tar from "tar-stream";
|
|
208
|
+
import { createWriteStream } from "fs";
|
|
209
|
+
import { promisify } from "util";
|
|
210
|
+
var pump = promisify(pipeline);
|
|
211
|
+
var resolvedBin;
|
|
212
|
+
async function getFFmpegBin() {
|
|
213
|
+
if (resolvedBin) return resolvedBin;
|
|
214
|
+
if (process.env.FFMPEG) {
|
|
215
|
+
resolvedBin = process.env.FFMPEG;
|
|
216
|
+
return resolvedBin;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const { pathToFFmpeg } = await import("@effing/ffmpeg");
|
|
220
|
+
if (pathToFFmpeg) {
|
|
221
|
+
resolvedBin = pathToFFmpeg;
|
|
222
|
+
return resolvedBin;
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
resolvedBin = "ffmpeg";
|
|
227
|
+
return resolvedBin;
|
|
228
|
+
}
|
|
229
|
+
var FFmpegCommand = class {
|
|
230
|
+
globalArgs;
|
|
231
|
+
inputs;
|
|
232
|
+
filterComplex;
|
|
233
|
+
outputArgs;
|
|
234
|
+
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
235
|
+
this.globalArgs = globalArgs;
|
|
236
|
+
this.inputs = inputs;
|
|
237
|
+
this.filterComplex = filterComplex;
|
|
238
|
+
this.outputArgs = outputArgs;
|
|
239
|
+
}
|
|
240
|
+
buildArgs(inputResolver) {
|
|
241
|
+
const inputArgs = [];
|
|
242
|
+
for (const input of this.inputs) {
|
|
243
|
+
if (input.type === "color") {
|
|
244
|
+
inputArgs.push(...input.preArgs);
|
|
245
|
+
} else if (input.type === "animation") {
|
|
246
|
+
inputArgs.push(
|
|
247
|
+
...input.preArgs,
|
|
248
|
+
"-i",
|
|
249
|
+
path.join(inputResolver(input), "frame_%05d")
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const args = [
|
|
256
|
+
...this.globalArgs,
|
|
257
|
+
...inputArgs,
|
|
258
|
+
"-filter_complex",
|
|
259
|
+
this.filterComplex,
|
|
260
|
+
...this.outputArgs
|
|
261
|
+
];
|
|
262
|
+
return args;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
var FFmpegRunner = class {
|
|
266
|
+
command;
|
|
267
|
+
ffmpegProc;
|
|
268
|
+
constructor(command) {
|
|
269
|
+
this.command = command;
|
|
270
|
+
}
|
|
271
|
+
async run(sourceFetcher, imageTransformer, referenceResolver, urlTransformer) {
|
|
272
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffs-"));
|
|
273
|
+
const fileMapping = /* @__PURE__ */ new Map();
|
|
274
|
+
const fetchCache = /* @__PURE__ */ new Map();
|
|
275
|
+
const fetchAndSaveSource = async (input, sourceUrl, inputName) => {
|
|
276
|
+
const stream = await sourceFetcher({
|
|
277
|
+
type: input.type,
|
|
278
|
+
src: sourceUrl
|
|
279
|
+
});
|
|
280
|
+
if (input.type === "animation") {
|
|
281
|
+
const extractionDir = path.join(tempDir, inputName);
|
|
282
|
+
await fs.mkdir(extractionDir, { recursive: true });
|
|
283
|
+
const extract = tar.extract();
|
|
284
|
+
const extractPromise = new Promise((resolve, reject) => {
|
|
285
|
+
extract.on("entry", async (header, stream2, next) => {
|
|
286
|
+
if (header.name.startsWith("frame_")) {
|
|
287
|
+
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
288
|
+
const outputPath = path.join(extractionDir, header.name);
|
|
289
|
+
const writeStream = createWriteStream(outputPath);
|
|
290
|
+
transformedStream.pipe(writeStream);
|
|
291
|
+
writeStream.on("finish", next);
|
|
292
|
+
writeStream.on("error", reject);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
extract.on("finish", resolve);
|
|
296
|
+
extract.on("error", reject);
|
|
297
|
+
});
|
|
298
|
+
stream.pipe(extract);
|
|
299
|
+
await extractPromise;
|
|
300
|
+
return extractionDir;
|
|
301
|
+
} else if (input.type === "image" && imageTransformer) {
|
|
302
|
+
const tempFile = path.join(tempDir, inputName);
|
|
303
|
+
const transformedStream = await imageTransformer(stream);
|
|
304
|
+
const writeStream = createWriteStream(tempFile);
|
|
305
|
+
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
306
|
+
await pump(transformedStream, writeStream);
|
|
307
|
+
return tempFile;
|
|
308
|
+
} else {
|
|
309
|
+
const tempFile = path.join(tempDir, inputName);
|
|
310
|
+
const writeStream = createWriteStream(tempFile);
|
|
311
|
+
stream.on("error", (e) => writeStream.destroy(e));
|
|
312
|
+
await pump(stream, writeStream);
|
|
313
|
+
return tempFile;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
await Promise.all(
|
|
317
|
+
this.command.inputs.map(async (input) => {
|
|
318
|
+
if (input.type === "color") return;
|
|
319
|
+
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
320
|
+
const sourceUrl = referenceResolver ? referenceResolver(input.source) : input.source;
|
|
321
|
+
if ((input.type === "video" || input.type === "audio") && (sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
|
|
322
|
+
const finalUrl = urlTransformer ? urlTransformer(sourceUrl) : sourceUrl;
|
|
323
|
+
fileMapping.set(input.index, finalUrl);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const shouldCache = input.source.startsWith("#");
|
|
327
|
+
if (shouldCache) {
|
|
328
|
+
let fetchPromise = fetchCache.get(input.source);
|
|
329
|
+
if (!fetchPromise) {
|
|
330
|
+
fetchPromise = fetchAndSaveSource(input, sourceUrl, inputName);
|
|
331
|
+
fetchCache.set(input.source, fetchPromise);
|
|
332
|
+
}
|
|
333
|
+
const filePath = await fetchPromise;
|
|
334
|
+
fileMapping.set(input.index, filePath);
|
|
335
|
+
} else {
|
|
336
|
+
const filePath = await fetchAndSaveSource(
|
|
337
|
+
input,
|
|
338
|
+
sourceUrl,
|
|
339
|
+
inputName
|
|
340
|
+
);
|
|
341
|
+
fileMapping.set(input.index, filePath);
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
const finalArgs = this.command.buildArgs((input) => {
|
|
346
|
+
const filePath = fileMapping.get(input.index);
|
|
347
|
+
if (!filePath)
|
|
348
|
+
throw new Error(`File for input index ${input.index} not found`);
|
|
349
|
+
return filePath;
|
|
350
|
+
});
|
|
351
|
+
const ffmpegProc = spawn(await getFFmpegBin(), finalArgs);
|
|
352
|
+
ffmpegProc.stderr.on("data", (data) => {
|
|
353
|
+
console.error(data.toString());
|
|
354
|
+
});
|
|
355
|
+
ffmpegProc.on("close", async () => {
|
|
356
|
+
try {
|
|
357
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error("Error removing temp directory:", err);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
this.ffmpegProc = ffmpegProc;
|
|
363
|
+
return ffmpegProc.stdout;
|
|
364
|
+
}
|
|
365
|
+
close() {
|
|
366
|
+
if (this.ffmpegProc) {
|
|
367
|
+
this.ffmpegProc.kill("SIGTERM");
|
|
368
|
+
this.ffmpegProc = void 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
355
373
|
// src/transition.ts
|
|
356
374
|
function processTransition(transition) {
|
|
357
375
|
switch (transition.type) {
|
|
@@ -409,346 +427,7 @@ function processTransition(transition) {
|
|
|
409
427
|
|
|
410
428
|
// src/render.ts
|
|
411
429
|
import sharp from "sharp";
|
|
412
|
-
|
|
413
|
-
// src/fetch.ts
|
|
414
|
-
import { fetch, Agent } from "undici";
|
|
415
|
-
async function ffsFetch(url, options) {
|
|
416
|
-
const {
|
|
417
|
-
method,
|
|
418
|
-
body,
|
|
419
|
-
headers,
|
|
420
|
-
headersTimeout = 3e5,
|
|
421
|
-
// 5 minutes
|
|
422
|
-
bodyTimeout = 3e5
|
|
423
|
-
// 5 minutes
|
|
424
|
-
} = options ?? {};
|
|
425
|
-
const agent = new Agent({ headersTimeout, bodyTimeout });
|
|
426
|
-
return fetch(url, {
|
|
427
|
-
method,
|
|
428
|
-
body,
|
|
429
|
-
headers: { "User-Agent": "FFS (+https://effing.dev/ffs)", ...headers },
|
|
430
|
-
dispatcher: agent
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// src/render.ts
|
|
435
430
|
import { fileURLToPath } from "url";
|
|
436
|
-
|
|
437
|
-
// src/storage.ts
|
|
438
|
-
import {
|
|
439
|
-
S3Client,
|
|
440
|
-
PutObjectCommand,
|
|
441
|
-
GetObjectCommand,
|
|
442
|
-
HeadObjectCommand,
|
|
443
|
-
DeleteObjectCommand
|
|
444
|
-
} from "@aws-sdk/client-s3";
|
|
445
|
-
import { Upload } from "@aws-sdk/lib-storage";
|
|
446
|
-
import fs2 from "fs/promises";
|
|
447
|
-
import { createReadStream, createWriteStream as createWriteStream2, existsSync } from "fs";
|
|
448
|
-
import { pipeline as pipeline2 } from "stream/promises";
|
|
449
|
-
import path2 from "path";
|
|
450
|
-
import os2 from "os";
|
|
451
|
-
import crypto from "crypto";
|
|
452
|
-
var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
|
|
453
|
-
var DEFAULT_JOB_METADATA_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
454
|
-
var S3TransientStore = class {
|
|
455
|
-
client;
|
|
456
|
-
bucket;
|
|
457
|
-
prefix;
|
|
458
|
-
sourceTtlMs;
|
|
459
|
-
jobMetadataTtlMs;
|
|
460
|
-
constructor(options) {
|
|
461
|
-
this.client = new S3Client({
|
|
462
|
-
endpoint: options.endpoint,
|
|
463
|
-
region: options.region ?? "auto",
|
|
464
|
-
credentials: options.accessKeyId ? {
|
|
465
|
-
accessKeyId: options.accessKeyId,
|
|
466
|
-
secretAccessKey: options.secretAccessKey
|
|
467
|
-
} : void 0,
|
|
468
|
-
forcePathStyle: !!options.endpoint
|
|
469
|
-
});
|
|
470
|
-
this.bucket = options.bucket;
|
|
471
|
-
this.prefix = options.prefix ?? "";
|
|
472
|
-
this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
473
|
-
this.jobMetadataTtlMs = options.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
474
|
-
}
|
|
475
|
-
getExpires(ttlMs) {
|
|
476
|
-
return new Date(Date.now() + ttlMs);
|
|
477
|
-
}
|
|
478
|
-
getFullKey(key) {
|
|
479
|
-
return `${this.prefix}${key}`;
|
|
480
|
-
}
|
|
481
|
-
async put(key, stream, ttlMs) {
|
|
482
|
-
const upload = new Upload({
|
|
483
|
-
client: this.client,
|
|
484
|
-
params: {
|
|
485
|
-
Bucket: this.bucket,
|
|
486
|
-
Key: this.getFullKey(key),
|
|
487
|
-
Body: stream,
|
|
488
|
-
Expires: this.getExpires(ttlMs ?? this.sourceTtlMs)
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
await upload.done();
|
|
492
|
-
}
|
|
493
|
-
async getStream(key) {
|
|
494
|
-
try {
|
|
495
|
-
const response = await this.client.send(
|
|
496
|
-
new GetObjectCommand({
|
|
497
|
-
Bucket: this.bucket,
|
|
498
|
-
Key: this.getFullKey(key)
|
|
499
|
-
})
|
|
500
|
-
);
|
|
501
|
-
return response.Body;
|
|
502
|
-
} catch (err) {
|
|
503
|
-
const error = err;
|
|
504
|
-
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
throw err;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
async exists(key) {
|
|
511
|
-
try {
|
|
512
|
-
await this.client.send(
|
|
513
|
-
new HeadObjectCommand({
|
|
514
|
-
Bucket: this.bucket,
|
|
515
|
-
Key: this.getFullKey(key)
|
|
516
|
-
})
|
|
517
|
-
);
|
|
518
|
-
return true;
|
|
519
|
-
} catch (err) {
|
|
520
|
-
const error = err;
|
|
521
|
-
if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) {
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
throw err;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async existsMany(keys) {
|
|
528
|
-
const results = await Promise.all(
|
|
529
|
-
keys.map(async (key) => [key, await this.exists(key)])
|
|
530
|
-
);
|
|
531
|
-
return new Map(results);
|
|
532
|
-
}
|
|
533
|
-
async delete(key) {
|
|
534
|
-
try {
|
|
535
|
-
await this.client.send(
|
|
536
|
-
new DeleteObjectCommand({
|
|
537
|
-
Bucket: this.bucket,
|
|
538
|
-
Key: this.getFullKey(key)
|
|
539
|
-
})
|
|
540
|
-
);
|
|
541
|
-
} catch (err) {
|
|
542
|
-
const error = err;
|
|
543
|
-
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
throw err;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
async putJson(key, data, ttlMs) {
|
|
550
|
-
await this.client.send(
|
|
551
|
-
new PutObjectCommand({
|
|
552
|
-
Bucket: this.bucket,
|
|
553
|
-
Key: this.getFullKey(key),
|
|
554
|
-
Body: JSON.stringify(data),
|
|
555
|
-
ContentType: "application/json",
|
|
556
|
-
Expires: this.getExpires(ttlMs ?? this.jobMetadataTtlMs)
|
|
557
|
-
})
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
async getJson(key) {
|
|
561
|
-
try {
|
|
562
|
-
const response = await this.client.send(
|
|
563
|
-
new GetObjectCommand({
|
|
564
|
-
Bucket: this.bucket,
|
|
565
|
-
Key: this.getFullKey(key)
|
|
566
|
-
})
|
|
567
|
-
);
|
|
568
|
-
const body = await response.Body?.transformToString();
|
|
569
|
-
if (!body) return null;
|
|
570
|
-
return JSON.parse(body);
|
|
571
|
-
} catch (err) {
|
|
572
|
-
const error = err;
|
|
573
|
-
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
throw err;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
close() {
|
|
580
|
-
}
|
|
581
|
-
};
|
|
582
|
-
var LocalTransientStore = class {
|
|
583
|
-
baseDir;
|
|
584
|
-
initialized = false;
|
|
585
|
-
cleanupInterval;
|
|
586
|
-
sourceTtlMs;
|
|
587
|
-
jobMetadataTtlMs;
|
|
588
|
-
/** For cleanup, use the longer of the two TTLs */
|
|
589
|
-
maxTtlMs;
|
|
590
|
-
constructor(options) {
|
|
591
|
-
this.baseDir = options?.baseDir ?? path2.join(os2.tmpdir(), "ffs-transient");
|
|
592
|
-
this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
593
|
-
this.jobMetadataTtlMs = options?.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
594
|
-
this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobMetadataTtlMs);
|
|
595
|
-
this.cleanupInterval = setInterval(() => {
|
|
596
|
-
this.cleanupExpired().catch(console.error);
|
|
597
|
-
}, 3e5);
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Remove files older than max TTL
|
|
601
|
-
*/
|
|
602
|
-
async cleanupExpired() {
|
|
603
|
-
if (!this.initialized) return;
|
|
604
|
-
const now = Date.now();
|
|
605
|
-
await this.cleanupDir(this.baseDir, now);
|
|
606
|
-
}
|
|
607
|
-
async cleanupDir(dir, now) {
|
|
608
|
-
let entries;
|
|
609
|
-
try {
|
|
610
|
-
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
611
|
-
} catch {
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
for (const entry of entries) {
|
|
615
|
-
const fullPath = path2.join(dir, entry.name);
|
|
616
|
-
if (entry.isDirectory()) {
|
|
617
|
-
await this.cleanupDir(fullPath, now);
|
|
618
|
-
try {
|
|
619
|
-
await fs2.rmdir(fullPath);
|
|
620
|
-
} catch {
|
|
621
|
-
}
|
|
622
|
-
} else if (entry.isFile()) {
|
|
623
|
-
try {
|
|
624
|
-
const stat = await fs2.stat(fullPath);
|
|
625
|
-
if (now - stat.mtimeMs > this.maxTtlMs) {
|
|
626
|
-
await fs2.rm(fullPath, { force: true });
|
|
627
|
-
}
|
|
628
|
-
} catch {
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
async ensureDir(filePath) {
|
|
634
|
-
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
635
|
-
this.initialized = true;
|
|
636
|
-
}
|
|
637
|
-
filePath(key) {
|
|
638
|
-
return path2.join(this.baseDir, key);
|
|
639
|
-
}
|
|
640
|
-
tmpPathFor(finalPath) {
|
|
641
|
-
const rand = crypto.randomBytes(8).toString("hex");
|
|
642
|
-
return `${finalPath}.tmp-${process.pid}-${rand}`;
|
|
643
|
-
}
|
|
644
|
-
async put(key, stream, _ttlMs) {
|
|
645
|
-
const fp = this.filePath(key);
|
|
646
|
-
await this.ensureDir(fp);
|
|
647
|
-
const tmpPath = this.tmpPathFor(fp);
|
|
648
|
-
try {
|
|
649
|
-
const writeStream = createWriteStream2(tmpPath);
|
|
650
|
-
await pipeline2(stream, writeStream);
|
|
651
|
-
await fs2.rename(tmpPath, fp);
|
|
652
|
-
} catch (err) {
|
|
653
|
-
await fs2.rm(tmpPath, { force: true }).catch(() => {
|
|
654
|
-
});
|
|
655
|
-
throw err;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
async getStream(key) {
|
|
659
|
-
const fp = this.filePath(key);
|
|
660
|
-
if (!existsSync(fp)) return null;
|
|
661
|
-
return createReadStream(fp);
|
|
662
|
-
}
|
|
663
|
-
async exists(key) {
|
|
664
|
-
try {
|
|
665
|
-
await fs2.access(this.filePath(key));
|
|
666
|
-
return true;
|
|
667
|
-
} catch {
|
|
668
|
-
return false;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
async existsMany(keys) {
|
|
672
|
-
const results = await Promise.all(
|
|
673
|
-
keys.map(async (key) => [key, await this.exists(key)])
|
|
674
|
-
);
|
|
675
|
-
return new Map(results);
|
|
676
|
-
}
|
|
677
|
-
async delete(key) {
|
|
678
|
-
await fs2.rm(this.filePath(key), { force: true });
|
|
679
|
-
}
|
|
680
|
-
async putJson(key, data, _ttlMs) {
|
|
681
|
-
const fp = this.filePath(key);
|
|
682
|
-
await this.ensureDir(fp);
|
|
683
|
-
const tmpPath = this.tmpPathFor(fp);
|
|
684
|
-
try {
|
|
685
|
-
await fs2.writeFile(tmpPath, JSON.stringify(data));
|
|
686
|
-
await fs2.rename(tmpPath, fp);
|
|
687
|
-
} catch (err) {
|
|
688
|
-
await fs2.rm(tmpPath, { force: true }).catch(() => {
|
|
689
|
-
});
|
|
690
|
-
throw err;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
async getJson(key) {
|
|
694
|
-
try {
|
|
695
|
-
const content = await fs2.readFile(this.filePath(key), "utf-8");
|
|
696
|
-
return JSON.parse(content);
|
|
697
|
-
} catch {
|
|
698
|
-
return null;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
close() {
|
|
702
|
-
if (this.cleanupInterval) {
|
|
703
|
-
clearInterval(this.cleanupInterval);
|
|
704
|
-
this.cleanupInterval = void 0;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
};
|
|
708
|
-
function createTransientStore() {
|
|
709
|
-
const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10) : DEFAULT_SOURCE_TTL_MS;
|
|
710
|
-
const jobMetadataTtlMs = process.env.FFS_JOB_METADATA_TTL_MS ? parseInt(process.env.FFS_JOB_METADATA_TTL_MS, 10) : DEFAULT_JOB_METADATA_TTL_MS;
|
|
711
|
-
if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
|
|
712
|
-
return new S3TransientStore({
|
|
713
|
-
endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
|
|
714
|
-
region: process.env.FFS_TRANSIENT_STORE_REGION ?? "auto",
|
|
715
|
-
bucket: process.env.FFS_TRANSIENT_STORE_BUCKET,
|
|
716
|
-
prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
|
|
717
|
-
accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
|
|
718
|
-
secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
|
|
719
|
-
sourceTtlMs,
|
|
720
|
-
jobMetadataTtlMs
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
return new LocalTransientStore({
|
|
724
|
-
baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
|
|
725
|
-
sourceTtlMs,
|
|
726
|
-
jobMetadataTtlMs
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
function hashUrl(url) {
|
|
730
|
-
return crypto.createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
731
|
-
}
|
|
732
|
-
function sourceStoreKey(url) {
|
|
733
|
-
return `sources/${hashUrl(url)}`;
|
|
734
|
-
}
|
|
735
|
-
function warmupJobStoreKey(jobId) {
|
|
736
|
-
return `jobs/warmup/${jobId}.json`;
|
|
737
|
-
}
|
|
738
|
-
function renderJobStoreKey(jobId) {
|
|
739
|
-
return `jobs/render/${jobId}.json`;
|
|
740
|
-
}
|
|
741
|
-
function warmupAndRenderJobStoreKey(jobId) {
|
|
742
|
-
return `jobs/warmup-and-render/${jobId}.json`;
|
|
743
|
-
}
|
|
744
|
-
var storeKeys = {
|
|
745
|
-
source: sourceStoreKey,
|
|
746
|
-
warmupJob: warmupJobStoreKey,
|
|
747
|
-
renderJob: renderJobStoreKey,
|
|
748
|
-
warmupAndRenderJob: warmupAndRenderJobStoreKey
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
// src/render.ts
|
|
752
431
|
var EffieRenderer = class {
|
|
753
432
|
effieData;
|
|
754
433
|
ffmpegRunner;
|
|
@@ -779,7 +458,7 @@ var EffieRenderer = class {
|
|
|
779
458
|
"Local file paths are not allowed. Use allowLocalFiles option for trusted operations."
|
|
780
459
|
);
|
|
781
460
|
}
|
|
782
|
-
return
|
|
461
|
+
return createReadStream(fileURLToPath(src));
|
|
783
462
|
}
|
|
784
463
|
if (this.transientStore) {
|
|
785
464
|
const cachedStream = await this.transientStore.getStream(
|
|
@@ -1253,12 +932,8 @@ var EffieRenderer = class {
|
|
|
1253
932
|
};
|
|
1254
933
|
|
|
1255
934
|
export {
|
|
1256
|
-
getFFmpegVersion,
|
|
1257
935
|
FFmpegCommand,
|
|
1258
936
|
FFmpegRunner,
|
|
1259
|
-
ffsFetch,
|
|
1260
|
-
createTransientStore,
|
|
1261
|
-
storeKeys,
|
|
1262
937
|
EffieRenderer
|
|
1263
938
|
};
|
|
1264
|
-
//# sourceMappingURL=chunk-
|
|
939
|
+
//# sourceMappingURL=chunk-N3D6I2BD.js.map
|