@effing/ffs 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/{chunk-J64HSZNQ.js → chunk-3SM6XYCZ.js} +8 -3
- package/dist/chunk-3SM6XYCZ.js.map +1 -0
- package/dist/{chunk-XSCNUWZJ.js → chunk-JDRYI7SR.js} +18 -10
- package/dist/chunk-JDRYI7SR.js.map +1 -0
- package/dist/handlers/index.d.ts +3 -1
- package/dist/handlers/index.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +200 -187
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-J64HSZNQ.js.map +0 -1
- package/dist/chunk-XSCNUWZJ.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -4,6 +4,165 @@
|
|
|
4
4
|
import express5 from "express";
|
|
5
5
|
import bodyParser from "body-parser";
|
|
6
6
|
|
|
7
|
+
// src/ffmpeg.ts
|
|
8
|
+
import { execFileSync, spawn } from "child_process";
|
|
9
|
+
import { pipeline } from "stream";
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import pathToFFmpeg from "ffmpeg-static";
|
|
14
|
+
import tar from "tar-stream";
|
|
15
|
+
import { createWriteStream } from "fs";
|
|
16
|
+
import { promisify } from "util";
|
|
17
|
+
var pump = promisify(pipeline);
|
|
18
|
+
var ffmpegBin = process.env.FFMPEG ?? pathToFFmpeg;
|
|
19
|
+
function getFFmpegVersion() {
|
|
20
|
+
return execFileSync(ffmpegBin, ["-version"], { encoding: "utf8" }).split("\n")[0].trim();
|
|
21
|
+
}
|
|
22
|
+
var FFmpegCommand = class {
|
|
23
|
+
globalArgs;
|
|
24
|
+
inputs;
|
|
25
|
+
filterComplex;
|
|
26
|
+
outputArgs;
|
|
27
|
+
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
28
|
+
this.globalArgs = globalArgs;
|
|
29
|
+
this.inputs = inputs;
|
|
30
|
+
this.filterComplex = filterComplex;
|
|
31
|
+
this.outputArgs = outputArgs;
|
|
32
|
+
}
|
|
33
|
+
buildArgs(inputResolver) {
|
|
34
|
+
const inputArgs = [];
|
|
35
|
+
for (const input of this.inputs) {
|
|
36
|
+
if (input.type === "color") {
|
|
37
|
+
inputArgs.push(...input.preArgs);
|
|
38
|
+
} else if (input.type === "animation") {
|
|
39
|
+
inputArgs.push(
|
|
40
|
+
...input.preArgs,
|
|
41
|
+
"-i",
|
|
42
|
+
path.join(inputResolver(input), "frame_%05d")
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const args = [
|
|
49
|
+
...this.globalArgs,
|
|
50
|
+
...inputArgs,
|
|
51
|
+
"-filter_complex",
|
|
52
|
+
this.filterComplex,
|
|
53
|
+
...this.outputArgs
|
|
54
|
+
];
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var FFmpegRunner = class {
|
|
59
|
+
command;
|
|
60
|
+
ffmpegProc;
|
|
61
|
+
constructor(command) {
|
|
62
|
+
this.command = command;
|
|
63
|
+
}
|
|
64
|
+
async run(sourceFetcher, imageTransformer, referenceResolver, urlTransformer) {
|
|
65
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ffs-"));
|
|
66
|
+
const fileMapping = /* @__PURE__ */ new Map();
|
|
67
|
+
const fetchCache = /* @__PURE__ */ new Map();
|
|
68
|
+
const fetchAndSaveSource = async (input, sourceUrl, inputName) => {
|
|
69
|
+
const stream = await sourceFetcher({
|
|
70
|
+
type: input.type,
|
|
71
|
+
src: sourceUrl
|
|
72
|
+
});
|
|
73
|
+
if (input.type === "animation") {
|
|
74
|
+
const extractionDir = path.join(tempDir, inputName);
|
|
75
|
+
await fs.mkdir(extractionDir, { recursive: true });
|
|
76
|
+
const extract = tar.extract();
|
|
77
|
+
const extractPromise = new Promise((resolve, reject) => {
|
|
78
|
+
extract.on("entry", async (header, stream2, next) => {
|
|
79
|
+
if (header.name.startsWith("frame_")) {
|
|
80
|
+
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
81
|
+
const outputPath = path.join(extractionDir, header.name);
|
|
82
|
+
const writeStream = createWriteStream(outputPath);
|
|
83
|
+
transformedStream.pipe(writeStream);
|
|
84
|
+
writeStream.on("finish", next);
|
|
85
|
+
writeStream.on("error", reject);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
extract.on("finish", resolve);
|
|
89
|
+
extract.on("error", reject);
|
|
90
|
+
});
|
|
91
|
+
stream.pipe(extract);
|
|
92
|
+
await extractPromise;
|
|
93
|
+
return extractionDir;
|
|
94
|
+
} else if (input.type === "image" && imageTransformer) {
|
|
95
|
+
const tempFile = path.join(tempDir, inputName);
|
|
96
|
+
const transformedStream = await imageTransformer(stream);
|
|
97
|
+
const writeStream = createWriteStream(tempFile);
|
|
98
|
+
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
99
|
+
await pump(transformedStream, writeStream);
|
|
100
|
+
return tempFile;
|
|
101
|
+
} else {
|
|
102
|
+
const tempFile = path.join(tempDir, inputName);
|
|
103
|
+
const writeStream = createWriteStream(tempFile);
|
|
104
|
+
stream.on("error", (e) => writeStream.destroy(e));
|
|
105
|
+
await pump(stream, writeStream);
|
|
106
|
+
return tempFile;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
await Promise.all(
|
|
110
|
+
this.command.inputs.map(async (input) => {
|
|
111
|
+
if (input.type === "color") return;
|
|
112
|
+
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
113
|
+
const sourceUrl = referenceResolver ? referenceResolver(input.source) : input.source;
|
|
114
|
+
if ((input.type === "video" || input.type === "audio") && (sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
|
|
115
|
+
const finalUrl = urlTransformer ? urlTransformer(sourceUrl) : sourceUrl;
|
|
116
|
+
fileMapping.set(input.index, finalUrl);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const shouldCache = input.source.startsWith("#");
|
|
120
|
+
if (shouldCache) {
|
|
121
|
+
let fetchPromise = fetchCache.get(input.source);
|
|
122
|
+
if (!fetchPromise) {
|
|
123
|
+
fetchPromise = fetchAndSaveSource(input, sourceUrl, inputName);
|
|
124
|
+
fetchCache.set(input.source, fetchPromise);
|
|
125
|
+
}
|
|
126
|
+
const filePath = await fetchPromise;
|
|
127
|
+
fileMapping.set(input.index, filePath);
|
|
128
|
+
} else {
|
|
129
|
+
const filePath = await fetchAndSaveSource(
|
|
130
|
+
input,
|
|
131
|
+
sourceUrl,
|
|
132
|
+
inputName
|
|
133
|
+
);
|
|
134
|
+
fileMapping.set(input.index, filePath);
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
const finalArgs = this.command.buildArgs((input) => {
|
|
139
|
+
const filePath = fileMapping.get(input.index);
|
|
140
|
+
if (!filePath)
|
|
141
|
+
throw new Error(`File for input index ${input.index} not found`);
|
|
142
|
+
return filePath;
|
|
143
|
+
});
|
|
144
|
+
const ffmpegProc = spawn(ffmpegBin, finalArgs);
|
|
145
|
+
ffmpegProc.stderr.on("data", (data) => {
|
|
146
|
+
console.error(data.toString());
|
|
147
|
+
});
|
|
148
|
+
ffmpegProc.on("close", async () => {
|
|
149
|
+
try {
|
|
150
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error("Error removing temp directory:", err);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
this.ffmpegProc = ffmpegProc;
|
|
156
|
+
return ffmpegProc.stdout;
|
|
157
|
+
}
|
|
158
|
+
close() {
|
|
159
|
+
if (this.ffmpegProc) {
|
|
160
|
+
this.ffmpegProc.kill("SIGTERM");
|
|
161
|
+
this.ffmpegProc = void 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
7
166
|
// src/handlers/shared.ts
|
|
8
167
|
import "express";
|
|
9
168
|
|
|
@@ -16,11 +175,11 @@ import {
|
|
|
16
175
|
DeleteObjectCommand
|
|
17
176
|
} from "@aws-sdk/client-s3";
|
|
18
177
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
19
|
-
import
|
|
20
|
-
import { createReadStream, createWriteStream, existsSync } from "fs";
|
|
21
|
-
import { pipeline } from "stream/promises";
|
|
22
|
-
import
|
|
23
|
-
import
|
|
178
|
+
import fs2 from "fs/promises";
|
|
179
|
+
import { createReadStream, createWriteStream as createWriteStream2, existsSync } from "fs";
|
|
180
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
181
|
+
import path2 from "path";
|
|
182
|
+
import os2 from "os";
|
|
24
183
|
import crypto from "crypto";
|
|
25
184
|
var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
|
|
26
185
|
var DEFAULT_JOB_METADATA_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
@@ -161,7 +320,7 @@ var LocalTransientStore = class {
|
|
|
161
320
|
/** For cleanup, use the longer of the two TTLs */
|
|
162
321
|
maxTtlMs;
|
|
163
322
|
constructor(options) {
|
|
164
|
-
this.baseDir = options?.baseDir ??
|
|
323
|
+
this.baseDir = options?.baseDir ?? path2.join(os2.tmpdir(), "ffs-transient");
|
|
165
324
|
this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
|
|
166
325
|
this.jobMetadataTtlMs = options?.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
|
|
167
326
|
this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobMetadataTtlMs);
|
|
@@ -180,23 +339,23 @@ var LocalTransientStore = class {
|
|
|
180
339
|
async cleanupDir(dir, now) {
|
|
181
340
|
let entries;
|
|
182
341
|
try {
|
|
183
|
-
entries = await
|
|
342
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
184
343
|
} catch {
|
|
185
344
|
return;
|
|
186
345
|
}
|
|
187
346
|
for (const entry of entries) {
|
|
188
|
-
const fullPath =
|
|
347
|
+
const fullPath = path2.join(dir, entry.name);
|
|
189
348
|
if (entry.isDirectory()) {
|
|
190
349
|
await this.cleanupDir(fullPath, now);
|
|
191
350
|
try {
|
|
192
|
-
await
|
|
351
|
+
await fs2.rmdir(fullPath);
|
|
193
352
|
} catch {
|
|
194
353
|
}
|
|
195
354
|
} else if (entry.isFile()) {
|
|
196
355
|
try {
|
|
197
|
-
const stat = await
|
|
356
|
+
const stat = await fs2.stat(fullPath);
|
|
198
357
|
if (now - stat.mtimeMs > this.maxTtlMs) {
|
|
199
|
-
await
|
|
358
|
+
await fs2.rm(fullPath, { force: true });
|
|
200
359
|
}
|
|
201
360
|
} catch {
|
|
202
361
|
}
|
|
@@ -204,11 +363,11 @@ var LocalTransientStore = class {
|
|
|
204
363
|
}
|
|
205
364
|
}
|
|
206
365
|
async ensureDir(filePath) {
|
|
207
|
-
await
|
|
366
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
208
367
|
this.initialized = true;
|
|
209
368
|
}
|
|
210
369
|
filePath(key) {
|
|
211
|
-
return
|
|
370
|
+
return path2.join(this.baseDir, key);
|
|
212
371
|
}
|
|
213
372
|
tmpPathFor(finalPath) {
|
|
214
373
|
const rand = crypto.randomBytes(8).toString("hex");
|
|
@@ -219,11 +378,11 @@ var LocalTransientStore = class {
|
|
|
219
378
|
await this.ensureDir(fp);
|
|
220
379
|
const tmpPath = this.tmpPathFor(fp);
|
|
221
380
|
try {
|
|
222
|
-
const writeStream =
|
|
223
|
-
await
|
|
224
|
-
await
|
|
381
|
+
const writeStream = createWriteStream2(tmpPath);
|
|
382
|
+
await pipeline2(stream, writeStream);
|
|
383
|
+
await fs2.rename(tmpPath, fp);
|
|
225
384
|
} catch (err) {
|
|
226
|
-
await
|
|
385
|
+
await fs2.rm(tmpPath, { force: true }).catch(() => {
|
|
227
386
|
});
|
|
228
387
|
throw err;
|
|
229
388
|
}
|
|
@@ -235,7 +394,7 @@ var LocalTransientStore = class {
|
|
|
235
394
|
}
|
|
236
395
|
async exists(key) {
|
|
237
396
|
try {
|
|
238
|
-
await
|
|
397
|
+
await fs2.access(this.filePath(key));
|
|
239
398
|
return true;
|
|
240
399
|
} catch {
|
|
241
400
|
return false;
|
|
@@ -248,24 +407,24 @@ var LocalTransientStore = class {
|
|
|
248
407
|
return new Map(results);
|
|
249
408
|
}
|
|
250
409
|
async delete(key) {
|
|
251
|
-
await
|
|
410
|
+
await fs2.rm(this.filePath(key), { force: true });
|
|
252
411
|
}
|
|
253
412
|
async putJson(key, data, _ttlMs) {
|
|
254
413
|
const fp = this.filePath(key);
|
|
255
414
|
await this.ensureDir(fp);
|
|
256
415
|
const tmpPath = this.tmpPathFor(fp);
|
|
257
416
|
try {
|
|
258
|
-
await
|
|
259
|
-
await
|
|
417
|
+
await fs2.writeFile(tmpPath, JSON.stringify(data));
|
|
418
|
+
await fs2.rename(tmpPath, fp);
|
|
260
419
|
} catch (err) {
|
|
261
|
-
await
|
|
420
|
+
await fs2.rm(tmpPath, { force: true }).catch(() => {
|
|
262
421
|
});
|
|
263
422
|
throw err;
|
|
264
423
|
}
|
|
265
424
|
}
|
|
266
425
|
async getJson(key) {
|
|
267
426
|
try {
|
|
268
|
-
const content = await
|
|
427
|
+
const content = await fs2.readFile(this.filePath(key), "utf-8");
|
|
269
428
|
return JSON.parse(content);
|
|
270
429
|
} catch {
|
|
271
430
|
return null;
|
|
@@ -469,7 +628,7 @@ var HttpProxy = class {
|
|
|
469
628
|
// src/handlers/shared.ts
|
|
470
629
|
import { effieDataSchema } from "@effing/effie";
|
|
471
630
|
async function createServerContext() {
|
|
472
|
-
const port2 = process.env.FFS_PORT || 2e3;
|
|
631
|
+
const port2 = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
473
632
|
const httpProxy = new HttpProxy();
|
|
474
633
|
await httpProxy.start();
|
|
475
634
|
return {
|
|
@@ -479,7 +638,9 @@ async function createServerContext() {
|
|
|
479
638
|
skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
|
|
480
639
|
warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
|
|
481
640
|
warmupBackendBaseUrl: process.env.FFS_WARMUP_BACKEND_BASE_URL,
|
|
482
|
-
renderBackendBaseUrl: process.env.FFS_RENDER_BACKEND_BASE_URL
|
|
641
|
+
renderBackendBaseUrl: process.env.FFS_RENDER_BACKEND_BASE_URL,
|
|
642
|
+
warmupBackendApiKey: process.env.FFS_WARMUP_BACKEND_API_KEY,
|
|
643
|
+
renderBackendApiKey: process.env.FFS_RENDER_BACKEND_API_KEY
|
|
483
644
|
};
|
|
484
645
|
}
|
|
485
646
|
function parseEffieData(body, skipValidation) {
|
|
@@ -737,161 +898,6 @@ function processEffects(effects, frameRate, frameWidth, frameHeight) {
|
|
|
737
898
|
return filters.join(",");
|
|
738
899
|
}
|
|
739
900
|
|
|
740
|
-
// src/ffmpeg.ts
|
|
741
|
-
import { spawn } from "child_process";
|
|
742
|
-
import { pipeline as pipeline2 } from "stream";
|
|
743
|
-
import fs2 from "fs/promises";
|
|
744
|
-
import os2 from "os";
|
|
745
|
-
import path2 from "path";
|
|
746
|
-
import pathToFFmpeg from "ffmpeg-static";
|
|
747
|
-
import tar from "tar-stream";
|
|
748
|
-
import { createWriteStream as createWriteStream2 } from "fs";
|
|
749
|
-
import { promisify } from "util";
|
|
750
|
-
var pump = promisify(pipeline2);
|
|
751
|
-
var FFmpegCommand = class {
|
|
752
|
-
globalArgs;
|
|
753
|
-
inputs;
|
|
754
|
-
filterComplex;
|
|
755
|
-
outputArgs;
|
|
756
|
-
constructor(globalArgs, inputs, filterComplex, outputArgs) {
|
|
757
|
-
this.globalArgs = globalArgs;
|
|
758
|
-
this.inputs = inputs;
|
|
759
|
-
this.filterComplex = filterComplex;
|
|
760
|
-
this.outputArgs = outputArgs;
|
|
761
|
-
}
|
|
762
|
-
buildArgs(inputResolver) {
|
|
763
|
-
const inputArgs = [];
|
|
764
|
-
for (const input of this.inputs) {
|
|
765
|
-
if (input.type === "color") {
|
|
766
|
-
inputArgs.push(...input.preArgs);
|
|
767
|
-
} else if (input.type === "animation") {
|
|
768
|
-
inputArgs.push(
|
|
769
|
-
...input.preArgs,
|
|
770
|
-
"-i",
|
|
771
|
-
path2.join(inputResolver(input), "frame_%05d")
|
|
772
|
-
);
|
|
773
|
-
} else {
|
|
774
|
-
inputArgs.push(...input.preArgs, "-i", inputResolver(input));
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
const args = [
|
|
778
|
-
...this.globalArgs,
|
|
779
|
-
...inputArgs,
|
|
780
|
-
"-filter_complex",
|
|
781
|
-
this.filterComplex,
|
|
782
|
-
...this.outputArgs
|
|
783
|
-
];
|
|
784
|
-
return args;
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
var FFmpegRunner = class {
|
|
788
|
-
command;
|
|
789
|
-
ffmpegProc;
|
|
790
|
-
constructor(command) {
|
|
791
|
-
this.command = command;
|
|
792
|
-
}
|
|
793
|
-
async run(sourceFetcher, imageTransformer, referenceResolver, urlTransformer) {
|
|
794
|
-
const tempDir = await fs2.mkdtemp(path2.join(os2.tmpdir(), "ffs-"));
|
|
795
|
-
const fileMapping = /* @__PURE__ */ new Map();
|
|
796
|
-
const fetchCache = /* @__PURE__ */ new Map();
|
|
797
|
-
const fetchAndSaveSource = async (input, sourceUrl, inputName) => {
|
|
798
|
-
const stream = await sourceFetcher({
|
|
799
|
-
type: input.type,
|
|
800
|
-
src: sourceUrl
|
|
801
|
-
});
|
|
802
|
-
if (input.type === "animation") {
|
|
803
|
-
const extractionDir = path2.join(tempDir, inputName);
|
|
804
|
-
await fs2.mkdir(extractionDir, { recursive: true });
|
|
805
|
-
const extract = tar.extract();
|
|
806
|
-
const extractPromise = new Promise((resolve, reject) => {
|
|
807
|
-
extract.on("entry", async (header, stream2, next) => {
|
|
808
|
-
if (header.name.startsWith("frame_")) {
|
|
809
|
-
const transformedStream = imageTransformer ? await imageTransformer(stream2) : stream2;
|
|
810
|
-
const outputPath = path2.join(extractionDir, header.name);
|
|
811
|
-
const writeStream = createWriteStream2(outputPath);
|
|
812
|
-
transformedStream.pipe(writeStream);
|
|
813
|
-
writeStream.on("finish", next);
|
|
814
|
-
writeStream.on("error", reject);
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
extract.on("finish", resolve);
|
|
818
|
-
extract.on("error", reject);
|
|
819
|
-
});
|
|
820
|
-
stream.pipe(extract);
|
|
821
|
-
await extractPromise;
|
|
822
|
-
return extractionDir;
|
|
823
|
-
} else if (input.type === "image" && imageTransformer) {
|
|
824
|
-
const tempFile = path2.join(tempDir, inputName);
|
|
825
|
-
const transformedStream = await imageTransformer(stream);
|
|
826
|
-
const writeStream = createWriteStream2(tempFile);
|
|
827
|
-
transformedStream.on("error", (e) => writeStream.destroy(e));
|
|
828
|
-
await pump(transformedStream, writeStream);
|
|
829
|
-
return tempFile;
|
|
830
|
-
} else {
|
|
831
|
-
const tempFile = path2.join(tempDir, inputName);
|
|
832
|
-
const writeStream = createWriteStream2(tempFile);
|
|
833
|
-
stream.on("error", (e) => writeStream.destroy(e));
|
|
834
|
-
await pump(stream, writeStream);
|
|
835
|
-
return tempFile;
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
await Promise.all(
|
|
839
|
-
this.command.inputs.map(async (input) => {
|
|
840
|
-
if (input.type === "color") return;
|
|
841
|
-
const inputName = `ffmpeg_input_${input.index.toString().padStart(3, "0")}`;
|
|
842
|
-
const sourceUrl = referenceResolver ? referenceResolver(input.source) : input.source;
|
|
843
|
-
if ((input.type === "video" || input.type === "audio") && (sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))) {
|
|
844
|
-
const finalUrl = urlTransformer ? urlTransformer(sourceUrl) : sourceUrl;
|
|
845
|
-
fileMapping.set(input.index, finalUrl);
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
const shouldCache = input.source.startsWith("#");
|
|
849
|
-
if (shouldCache) {
|
|
850
|
-
let fetchPromise = fetchCache.get(input.source);
|
|
851
|
-
if (!fetchPromise) {
|
|
852
|
-
fetchPromise = fetchAndSaveSource(input, sourceUrl, inputName);
|
|
853
|
-
fetchCache.set(input.source, fetchPromise);
|
|
854
|
-
}
|
|
855
|
-
const filePath = await fetchPromise;
|
|
856
|
-
fileMapping.set(input.index, filePath);
|
|
857
|
-
} else {
|
|
858
|
-
const filePath = await fetchAndSaveSource(
|
|
859
|
-
input,
|
|
860
|
-
sourceUrl,
|
|
861
|
-
inputName
|
|
862
|
-
);
|
|
863
|
-
fileMapping.set(input.index, filePath);
|
|
864
|
-
}
|
|
865
|
-
})
|
|
866
|
-
);
|
|
867
|
-
const finalArgs = this.command.buildArgs((input) => {
|
|
868
|
-
const filePath = fileMapping.get(input.index);
|
|
869
|
-
if (!filePath)
|
|
870
|
-
throw new Error(`File for input index ${input.index} not found`);
|
|
871
|
-
return filePath;
|
|
872
|
-
});
|
|
873
|
-
const ffmpegProc = spawn(process.env.FFMPEG ?? pathToFFmpeg, finalArgs);
|
|
874
|
-
ffmpegProc.stderr.on("data", (data) => {
|
|
875
|
-
console.error(data.toString());
|
|
876
|
-
});
|
|
877
|
-
ffmpegProc.on("close", async () => {
|
|
878
|
-
try {
|
|
879
|
-
await fs2.rm(tempDir, { recursive: true, force: true });
|
|
880
|
-
} catch (err) {
|
|
881
|
-
console.error("Error removing temp directory:", err);
|
|
882
|
-
}
|
|
883
|
-
});
|
|
884
|
-
this.ffmpegProc = ffmpegProc;
|
|
885
|
-
return ffmpegProc.stdout;
|
|
886
|
-
}
|
|
887
|
-
close() {
|
|
888
|
-
if (this.ffmpegProc) {
|
|
889
|
-
this.ffmpegProc.kill("SIGTERM");
|
|
890
|
-
this.ffmpegProc = void 0;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
};
|
|
894
|
-
|
|
895
901
|
// src/transition.ts
|
|
896
902
|
function processTransition(transition) {
|
|
897
903
|
switch (transition.type) {
|
|
@@ -1648,7 +1654,9 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
|
|
|
1648
1654
|
}
|
|
1649
1655
|
async function proxyRenderFromBackend(res, jobId, ctx2) {
|
|
1650
1656
|
const backendUrl = `${ctx2.renderBackendBaseUrl}/render/${jobId}`;
|
|
1651
|
-
const response = await ffsFetch(backendUrl
|
|
1657
|
+
const response = await ffsFetch(backendUrl, {
|
|
1658
|
+
headers: ctx2.renderBackendApiKey ? { Authorization: `Bearer ${ctx2.renderBackendApiKey}` } : void 0
|
|
1659
|
+
});
|
|
1652
1660
|
if (!response.ok) {
|
|
1653
1661
|
res.status(response.status).json({ error: "Backend render failed" });
|
|
1654
1662
|
return;
|
|
@@ -1808,7 +1816,8 @@ async function streamWarmupAndRenderJob(req, res, ctx2) {
|
|
|
1808
1816
|
`${ctx2.warmupBackendBaseUrl}/warmup/${job.warmupJobId}`,
|
|
1809
1817
|
sendEvent,
|
|
1810
1818
|
"warmup:",
|
|
1811
|
-
res
|
|
1819
|
+
res,
|
|
1820
|
+
ctx2.warmupBackendApiKey ? { Authorization: `Bearer ${ctx2.warmupBackendApiKey}` } : void 0
|
|
1812
1821
|
);
|
|
1813
1822
|
} else {
|
|
1814
1823
|
const warmupSender = prefixEventSender(sendEvent, "warmup:");
|
|
@@ -1821,7 +1830,8 @@ async function streamWarmupAndRenderJob(req, res, ctx2) {
|
|
|
1821
1830
|
`${ctx2.renderBackendBaseUrl}/render/${job.renderJobId}`,
|
|
1822
1831
|
sendEvent,
|
|
1823
1832
|
"render:",
|
|
1824
|
-
res
|
|
1833
|
+
res,
|
|
1834
|
+
ctx2.renderBackendApiKey ? { Authorization: `Bearer ${ctx2.renderBackendApiKey}` } : void 0
|
|
1825
1835
|
);
|
|
1826
1836
|
} else {
|
|
1827
1837
|
const renderSender = prefixEventSender(sendEvent, "render:");
|
|
@@ -1866,10 +1876,11 @@ function prefixEventSender(sendEvent, prefix) {
|
|
|
1866
1876
|
sendEvent(`${prefix}${event}`, data);
|
|
1867
1877
|
};
|
|
1868
1878
|
}
|
|
1869
|
-
async function proxyRemoteSSE(url, sendEvent, prefix, res) {
|
|
1879
|
+
async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
1870
1880
|
const response = await ffsFetch(url, {
|
|
1871
1881
|
headers: {
|
|
1872
|
-
Accept: "text/event-stream"
|
|
1882
|
+
Accept: "text/event-stream",
|
|
1883
|
+
...headers
|
|
1873
1884
|
}
|
|
1874
1885
|
});
|
|
1875
1886
|
if (!response.ok) {
|
|
@@ -1979,7 +1990,8 @@ async function streamWarmupJob(req, res, ctx2) {
|
|
|
1979
1990
|
`${ctx2.warmupBackendBaseUrl}/warmup/${jobId}`,
|
|
1980
1991
|
sendEvent2,
|
|
1981
1992
|
"",
|
|
1982
|
-
res
|
|
1993
|
+
res,
|
|
1994
|
+
ctx2.warmupBackendApiKey ? { Authorization: `Bearer ${ctx2.warmupBackendApiKey}` } : void 0
|
|
1983
1995
|
);
|
|
1984
1996
|
} finally {
|
|
1985
1997
|
res.end();
|
|
@@ -2170,6 +2182,7 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx2) {
|
|
|
2170
2182
|
}
|
|
2171
2183
|
|
|
2172
2184
|
// src/server.ts
|
|
2185
|
+
console.log("FFS", getFFmpegVersion());
|
|
2173
2186
|
var app = express5();
|
|
2174
2187
|
app.use(bodyParser.json({ limit: "50mb" }));
|
|
2175
2188
|
var ctx = await createServerContext();
|
|
@@ -2206,7 +2219,7 @@ app.get(
|
|
|
2206
2219
|
"/warmup-and-render/:id",
|
|
2207
2220
|
(req, res) => streamWarmupAndRenderJob(req, res, ctx)
|
|
2208
2221
|
);
|
|
2209
|
-
var port = process.env.FFS_PORT || 2e3;
|
|
2222
|
+
var port = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
2210
2223
|
var server = app.listen(port, () => {
|
|
2211
2224
|
console.log(`FFS server listening on port ${port}`);
|
|
2212
2225
|
});
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import express from \"express\";\nimport bodyParser from \"body-parser\";\nimport {\n createServerContext,\n createWarmupJob,\n streamWarmupJob,\n purgeCache,\n createRenderJob,\n streamRenderJob,\n createWarmupAndRenderJob,\n streamWarmupAndRenderJob,\n} from \"./handlers\";\n\nconst app: express.Express = express();\napp.use(bodyParser.json({ limit: \"50mb\" })); // Support large JSON requests\n\nconst ctx = await createServerContext();\nconsole.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);\n\nfunction validateAuth(req: express.Request, res: express.Response): boolean {\n const apiKey = process.env.FFS_API_KEY;\n if (!apiKey) return true; // No auth required if api key not set\n\n const authHeader = req.headers.authorization;\n if (!authHeader || authHeader !== `Bearer ${apiKey}`) {\n res.status(401).json({ error: \"Unauthorized\" });\n return false;\n }\n return true;\n}\n\n// Routes with auth (POST endpoints)\napp.post(\"/warmup\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupJob(req, res, ctx);\n});\napp.post(\"/purge\", (req, res) => {\n if (!validateAuth(req, res)) return;\n purgeCache(req, res, ctx);\n});\napp.post(\"/render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createRenderJob(req, res, ctx);\n});\napp.post(\"/warmup-and-render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupAndRenderJob(req, res, ctx);\n});\n\n// Routes without auth (GET endpoints use job ID as capability token)\napp.get(\"/warmup/:id\", (req, res) => streamWarmupJob(req, res, ctx));\napp.get(\"/render/:id\", (req, res) => streamRenderJob(req, res, ctx));\napp.get(\"/warmup-and-render/:id\", (req, res) =>\n streamWarmupAndRenderJob(req, res, ctx),\n);\n\n// Server lifecycle\nconst port = process.env.FFS_PORT || 2000; // ffmpeg was conceived in the year 2000\nconst server = app.listen(port, () => {\n console.log(`FFS server listening on port ${port}`);\n});\n\nfunction shutdown() {\n console.log(\"Shutting down FFS server...\");\n ctx.httpProxy.close();\n ctx.transientStore.close();\n server.close(() => {\n console.log(\"FFS server stopped\");\n process.exit(0);\n });\n}\n\nprocess.on(\"SIGTERM\", shutdown);\nprocess.on(\"SIGINT\", shutdown);\n\nexport { app };\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import express from \"express\";\nimport bodyParser from \"body-parser\";\nimport { getFFmpegVersion } from \"./ffmpeg\";\nimport {\n createServerContext,\n createWarmupJob,\n streamWarmupJob,\n purgeCache,\n createRenderJob,\n streamRenderJob,\n createWarmupAndRenderJob,\n streamWarmupAndRenderJob,\n} from \"./handlers\";\n\nconsole.log(\"FFS\", getFFmpegVersion());\n\nconst app: express.Express = express();\napp.use(bodyParser.json({ limit: \"50mb\" })); // Support large JSON requests\n\nconst ctx = await createServerContext();\nconsole.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);\n\nfunction validateAuth(req: express.Request, res: express.Response): boolean {\n const apiKey = process.env.FFS_API_KEY;\n if (!apiKey) return true; // No auth required if api key not set\n\n const authHeader = req.headers.authorization;\n if (!authHeader || authHeader !== `Bearer ${apiKey}`) {\n res.status(401).json({ error: \"Unauthorized\" });\n return false;\n }\n return true;\n}\n\n// Routes with auth (POST endpoints)\napp.post(\"/warmup\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupJob(req, res, ctx);\n});\napp.post(\"/purge\", (req, res) => {\n if (!validateAuth(req, res)) return;\n purgeCache(req, res, ctx);\n});\napp.post(\"/render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createRenderJob(req, res, ctx);\n});\napp.post(\"/warmup-and-render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupAndRenderJob(req, res, ctx);\n});\n\n// Routes without auth (GET endpoints use job ID as capability token)\napp.get(\"/warmup/:id\", (req, res) => streamWarmupJob(req, res, ctx));\napp.get(\"/render/:id\", (req, res) => streamRenderJob(req, res, ctx));\napp.get(\"/warmup-and-render/:id\", (req, res) =>\n streamWarmupAndRenderJob(req, res, ctx),\n);\n\n// Server lifecycle\nconst port = process.env.FFS_PORT || process.env.PORT || 2000; // ffmpeg was conceived in the year 2000\nconst server = app.listen(port, () => {\n console.log(`FFS server listening on port ${port}`);\n});\n\nfunction shutdown() {\n console.log(\"Shutting down FFS server...\");\n ctx.httpProxy.close();\n ctx.transientStore.close();\n server.close(() => {\n console.log(\"FFS server stopped\");\n process.exit(0);\n });\n}\n\nprocess.on(\"SIGTERM\", shutdown);\nprocess.on(\"SIGINT\", shutdown);\n\nexport { app };\n"],"mappings":";;;;;;;;;;;;;;;AAAA,OAAO,aAAa;AACpB,OAAO,gBAAgB;AAavB,QAAQ,IAAI,OAAO,iBAAiB,CAAC;AAErC,IAAM,MAAuB,QAAQ;AACrC,IAAI,IAAI,WAAW,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC;AAE1C,IAAM,MAAM,MAAM,oBAAoB;AACtC,QAAQ,IAAI,oCAAoC,IAAI,UAAU,IAAI,EAAE;AAEpE,SAAS,aAAa,KAAsB,KAAgC;AAC1E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,IAAI,QAAQ;AAC/B,MAAI,CAAC,cAAc,eAAe,UAAU,MAAM,IAAI;AACpD,QAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,eAAe,CAAC;AAC9C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AACD,IAAI,KAAK,UAAU,CAAC,KAAK,QAAQ;AAC/B,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,aAAW,KAAK,KAAK,GAAG;AAC1B,CAAC;AACD,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AACD,IAAI,KAAK,sBAAsB,CAAC,KAAK,QAAQ;AAC3C,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,2BAAyB,KAAK,KAAK,GAAG;AACxC,CAAC;AAGD,IAAI,IAAI,eAAe,CAAC,KAAK,QAAQ,gBAAgB,KAAK,KAAK,GAAG,CAAC;AACnE,IAAI,IAAI,eAAe,CAAC,KAAK,QAAQ,gBAAgB,KAAK,KAAK,GAAG,CAAC;AACnE,IAAI;AAAA,EAAI;AAAA,EAA0B,CAAC,KAAK,QACtC,yBAAyB,KAAK,KAAK,GAAG;AACxC;AAGA,IAAM,OAAO,QAAQ,IAAI,YAAY,QAAQ,IAAI,QAAQ;AACzD,IAAM,SAAS,IAAI,OAAO,MAAM,MAAM;AACpC,UAAQ,IAAI,gCAAgC,IAAI,EAAE;AACpD,CAAC;AAED,SAAS,WAAW;AAClB,UAAQ,IAAI,6BAA6B;AACzC,MAAI,UAAU,MAAM;AACpB,MAAI,eAAe,MAAM;AACzB,SAAO,MAAM,MAAM;AACjB,YAAQ,IAAI,oBAAoB;AAChC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;AAEA,QAAQ,GAAG,WAAW,QAAQ;AAC9B,QAAQ,GAAG,UAAU,QAAQ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effing/ffs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "FFmpeg-based effie rendering service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"tar-stream": "^3.1.7",
|
|
34
34
|
"undici": "^7.3.0",
|
|
35
35
|
"zod": "^3.25.76",
|
|
36
|
-
"@effing/effie": "0.
|
|
36
|
+
"@effing/effie": "0.4.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/body-parser": "^1.19.5",
|