@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/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 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";
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 ?? path.join(os.tmpdir(), "ffs-transient");
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 fs.readdir(dir, { withFileTypes: true });
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 = path.join(dir, entry.name);
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 fs.rmdir(fullPath);
351
+ await fs2.rmdir(fullPath);
193
352
  } catch {
194
353
  }
195
354
  } else if (entry.isFile()) {
196
355
  try {
197
- const stat = await fs.stat(fullPath);
356
+ const stat = await fs2.stat(fullPath);
198
357
  if (now - stat.mtimeMs > this.maxTtlMs) {
199
- await fs.rm(fullPath, { force: true });
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 fs.mkdir(path.dirname(filePath), { recursive: true });
366
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true });
208
367
  this.initialized = true;
209
368
  }
210
369
  filePath(key) {
211
- return path.join(this.baseDir, key);
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 = createWriteStream(tmpPath);
223
- await pipeline(stream, writeStream);
224
- await fs.rename(tmpPath, fp);
381
+ const writeStream = createWriteStream2(tmpPath);
382
+ await pipeline2(stream, writeStream);
383
+ await fs2.rename(tmpPath, fp);
225
384
  } catch (err) {
226
- await fs.rm(tmpPath, { force: true }).catch(() => {
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 fs.access(this.filePath(key));
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 fs.rm(this.filePath(key), { force: true });
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 fs.writeFile(tmpPath, JSON.stringify(data));
259
- await fs.rename(tmpPath, fp);
417
+ await fs2.writeFile(tmpPath, JSON.stringify(data));
418
+ await fs2.rename(tmpPath, fp);
260
419
  } catch (err) {
261
- await fs.rm(tmpPath, { force: true }).catch(() => {
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 fs.readFile(this.filePath(key), "utf-8");
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
  });
@@ -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":";;;;;;;;;;;;;AAAA,OAAO,aAAa;AACpB,OAAO,gBAAgB;AAYvB,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;AACrC,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":[]}
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.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.3.0"
36
+ "@effing/effie": "0.4.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/body-parser": "^1.19.5",