@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/server.js CHANGED
@@ -1,511 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createTransientStore,
4
+ ffsFetch,
5
+ storeKeys
6
+ } from "./chunk-QPZEAH3J.js";
2
7
 
3
8
  // src/server.ts
4
9
  import express5 from "express";
5
10
  import bodyParser from "body-parser";
6
11
 
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
-
166
12
  // src/handlers/shared.ts
167
13
  import "express";
168
14
 
169
- // src/storage.ts
170
- import {
171
- S3Client,
172
- PutObjectCommand,
173
- GetObjectCommand,
174
- HeadObjectCommand,
175
- DeleteObjectCommand
176
- } from "@aws-sdk/client-s3";
177
- import { Upload } from "@aws-sdk/lib-storage";
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";
183
- import crypto from "crypto";
184
- var DEFAULT_SOURCE_TTL_MS = 60 * 60 * 1e3;
185
- var DEFAULT_JOB_METADATA_TTL_MS = 8 * 60 * 60 * 1e3;
186
- var S3TransientStore = class {
187
- client;
188
- bucket;
189
- prefix;
190
- sourceTtlMs;
191
- jobMetadataTtlMs;
192
- constructor(options) {
193
- this.client = new S3Client({
194
- endpoint: options.endpoint,
195
- region: options.region ?? "auto",
196
- credentials: options.accessKeyId ? {
197
- accessKeyId: options.accessKeyId,
198
- secretAccessKey: options.secretAccessKey
199
- } : void 0,
200
- forcePathStyle: !!options.endpoint
201
- });
202
- this.bucket = options.bucket;
203
- this.prefix = options.prefix ?? "";
204
- this.sourceTtlMs = options.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
205
- this.jobMetadataTtlMs = options.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
206
- }
207
- getExpires(ttlMs) {
208
- return new Date(Date.now() + ttlMs);
209
- }
210
- getFullKey(key) {
211
- return `${this.prefix}${key}`;
212
- }
213
- async put(key, stream, ttlMs) {
214
- const upload = new Upload({
215
- client: this.client,
216
- params: {
217
- Bucket: this.bucket,
218
- Key: this.getFullKey(key),
219
- Body: stream,
220
- Expires: this.getExpires(ttlMs ?? this.sourceTtlMs)
221
- }
222
- });
223
- await upload.done();
224
- }
225
- async getStream(key) {
226
- try {
227
- const response = await this.client.send(
228
- new GetObjectCommand({
229
- Bucket: this.bucket,
230
- Key: this.getFullKey(key)
231
- })
232
- );
233
- return response.Body;
234
- } catch (err) {
235
- const error = err;
236
- if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
237
- return null;
238
- }
239
- throw err;
240
- }
241
- }
242
- async exists(key) {
243
- try {
244
- await this.client.send(
245
- new HeadObjectCommand({
246
- Bucket: this.bucket,
247
- Key: this.getFullKey(key)
248
- })
249
- );
250
- return true;
251
- } catch (err) {
252
- const error = err;
253
- if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) {
254
- return false;
255
- }
256
- throw err;
257
- }
258
- }
259
- async existsMany(keys) {
260
- const results = await Promise.all(
261
- keys.map(async (key) => [key, await this.exists(key)])
262
- );
263
- return new Map(results);
264
- }
265
- async delete(key) {
266
- try {
267
- await this.client.send(
268
- new DeleteObjectCommand({
269
- Bucket: this.bucket,
270
- Key: this.getFullKey(key)
271
- })
272
- );
273
- } catch (err) {
274
- const error = err;
275
- if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
276
- return;
277
- }
278
- throw err;
279
- }
280
- }
281
- async putJson(key, data, ttlMs) {
282
- await this.client.send(
283
- new PutObjectCommand({
284
- Bucket: this.bucket,
285
- Key: this.getFullKey(key),
286
- Body: JSON.stringify(data),
287
- ContentType: "application/json",
288
- Expires: this.getExpires(ttlMs ?? this.jobMetadataTtlMs)
289
- })
290
- );
291
- }
292
- async getJson(key) {
293
- try {
294
- const response = await this.client.send(
295
- new GetObjectCommand({
296
- Bucket: this.bucket,
297
- Key: this.getFullKey(key)
298
- })
299
- );
300
- const body = await response.Body?.transformToString();
301
- if (!body) return null;
302
- return JSON.parse(body);
303
- } catch (err) {
304
- const error = err;
305
- if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
306
- return null;
307
- }
308
- throw err;
309
- }
310
- }
311
- close() {
312
- }
313
- };
314
- var LocalTransientStore = class {
315
- baseDir;
316
- initialized = false;
317
- cleanupInterval;
318
- sourceTtlMs;
319
- jobMetadataTtlMs;
320
- /** For cleanup, use the longer of the two TTLs */
321
- maxTtlMs;
322
- constructor(options) {
323
- this.baseDir = options?.baseDir ?? path2.join(os2.tmpdir(), "ffs-transient");
324
- this.sourceTtlMs = options?.sourceTtlMs ?? DEFAULT_SOURCE_TTL_MS;
325
- this.jobMetadataTtlMs = options?.jobMetadataTtlMs ?? DEFAULT_JOB_METADATA_TTL_MS;
326
- this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobMetadataTtlMs);
327
- this.cleanupInterval = setInterval(() => {
328
- this.cleanupExpired().catch(console.error);
329
- }, 3e5);
330
- }
331
- /**
332
- * Remove files older than max TTL
333
- */
334
- async cleanupExpired() {
335
- if (!this.initialized) return;
336
- const now = Date.now();
337
- await this.cleanupDir(this.baseDir, now);
338
- }
339
- async cleanupDir(dir, now) {
340
- let entries;
341
- try {
342
- entries = await fs2.readdir(dir, { withFileTypes: true });
343
- } catch {
344
- return;
345
- }
346
- for (const entry of entries) {
347
- const fullPath = path2.join(dir, entry.name);
348
- if (entry.isDirectory()) {
349
- await this.cleanupDir(fullPath, now);
350
- try {
351
- await fs2.rmdir(fullPath);
352
- } catch {
353
- }
354
- } else if (entry.isFile()) {
355
- try {
356
- const stat = await fs2.stat(fullPath);
357
- if (now - stat.mtimeMs > this.maxTtlMs) {
358
- await fs2.rm(fullPath, { force: true });
359
- }
360
- } catch {
361
- }
362
- }
363
- }
364
- }
365
- async ensureDir(filePath) {
366
- await fs2.mkdir(path2.dirname(filePath), { recursive: true });
367
- this.initialized = true;
368
- }
369
- filePath(key) {
370
- return path2.join(this.baseDir, key);
371
- }
372
- tmpPathFor(finalPath) {
373
- const rand = crypto.randomBytes(8).toString("hex");
374
- return `${finalPath}.tmp-${process.pid}-${rand}`;
375
- }
376
- async put(key, stream, _ttlMs) {
377
- const fp = this.filePath(key);
378
- await this.ensureDir(fp);
379
- const tmpPath = this.tmpPathFor(fp);
380
- try {
381
- const writeStream = createWriteStream2(tmpPath);
382
- await pipeline2(stream, writeStream);
383
- await fs2.rename(tmpPath, fp);
384
- } catch (err) {
385
- await fs2.rm(tmpPath, { force: true }).catch(() => {
386
- });
387
- throw err;
388
- }
389
- }
390
- async getStream(key) {
391
- const fp = this.filePath(key);
392
- if (!existsSync(fp)) return null;
393
- return createReadStream(fp);
394
- }
395
- async exists(key) {
396
- try {
397
- await fs2.access(this.filePath(key));
398
- return true;
399
- } catch {
400
- return false;
401
- }
402
- }
403
- async existsMany(keys) {
404
- const results = await Promise.all(
405
- keys.map(async (key) => [key, await this.exists(key)])
406
- );
407
- return new Map(results);
408
- }
409
- async delete(key) {
410
- await fs2.rm(this.filePath(key), { force: true });
411
- }
412
- async putJson(key, data, _ttlMs) {
413
- const fp = this.filePath(key);
414
- await this.ensureDir(fp);
415
- const tmpPath = this.tmpPathFor(fp);
416
- try {
417
- await fs2.writeFile(tmpPath, JSON.stringify(data));
418
- await fs2.rename(tmpPath, fp);
419
- } catch (err) {
420
- await fs2.rm(tmpPath, { force: true }).catch(() => {
421
- });
422
- throw err;
423
- }
424
- }
425
- async getJson(key) {
426
- try {
427
- const content = await fs2.readFile(this.filePath(key), "utf-8");
428
- return JSON.parse(content);
429
- } catch {
430
- return null;
431
- }
432
- }
433
- close() {
434
- if (this.cleanupInterval) {
435
- clearInterval(this.cleanupInterval);
436
- this.cleanupInterval = void 0;
437
- }
438
- }
439
- };
440
- function createTransientStore() {
441
- const sourceTtlMs = process.env.FFS_SOURCE_CACHE_TTL_MS ? parseInt(process.env.FFS_SOURCE_CACHE_TTL_MS, 10) : DEFAULT_SOURCE_TTL_MS;
442
- const jobMetadataTtlMs = process.env.FFS_JOB_METADATA_TTL_MS ? parseInt(process.env.FFS_JOB_METADATA_TTL_MS, 10) : DEFAULT_JOB_METADATA_TTL_MS;
443
- if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
444
- return new S3TransientStore({
445
- endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
446
- region: process.env.FFS_TRANSIENT_STORE_REGION ?? "auto",
447
- bucket: process.env.FFS_TRANSIENT_STORE_BUCKET,
448
- prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
449
- accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
450
- secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
451
- sourceTtlMs,
452
- jobMetadataTtlMs
453
- });
454
- }
455
- return new LocalTransientStore({
456
- baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
457
- sourceTtlMs,
458
- jobMetadataTtlMs
459
- });
460
- }
461
- function hashUrl(url) {
462
- return crypto.createHash("sha256").update(url).digest("hex").slice(0, 16);
463
- }
464
- function sourceStoreKey(url) {
465
- return `sources/${hashUrl(url)}`;
466
- }
467
- function warmupJobStoreKey(jobId) {
468
- return `jobs/warmup/${jobId}.json`;
469
- }
470
- function renderJobStoreKey(jobId) {
471
- return `jobs/render/${jobId}.json`;
472
- }
473
- function warmupAndRenderJobStoreKey(jobId) {
474
- return `jobs/warmup-and-render/${jobId}.json`;
475
- }
476
- var storeKeys = {
477
- source: sourceStoreKey,
478
- warmupJob: warmupJobStoreKey,
479
- renderJob: renderJobStoreKey,
480
- warmupAndRenderJob: warmupAndRenderJobStoreKey
481
- };
482
-
483
15
  // src/proxy.ts
484
16
  import http from "http";
485
17
  import { Readable } from "stream";
486
-
487
- // src/fetch.ts
488
- import { fetch, Agent } from "undici";
489
- async function ffsFetch(url, options) {
490
- const {
491
- method,
492
- body,
493
- headers,
494
- headersTimeout = 3e5,
495
- // 5 minutes
496
- bodyTimeout = 3e5
497
- // 5 minutes
498
- } = options ?? {};
499
- const agent = new Agent({ headersTimeout, bodyTimeout });
500
- return fetch(url, {
501
- method,
502
- body,
503
- headers: { "User-Agent": "FFS (+https://effing.dev/ffs)", ...headers },
504
- dispatcher: agent
505
- });
506
- }
507
-
508
- // src/proxy.ts
509
18
  var HttpProxy = class {
510
19
  server = null;
511
20
  _port = null;
@@ -584,11 +93,11 @@ var HttpProxy = class {
584
93
  * Parse the proxy path to extract the original URL.
585
94
  * Path format: /{originalUrl}
586
95
  */
587
- parseProxyPath(path3) {
588
- if (!path3.startsWith("/http://") && !path3.startsWith("/https://")) {
96
+ parseProxyPath(path) {
97
+ if (!path.startsWith("/http://") && !path.startsWith("/https://")) {
589
98
  return null;
590
99
  }
591
- return path3.slice(1);
100
+ return path.slice(1);
592
101
  }
593
102
  /**
594
103
  * Filter headers to forward to the upstream server.
@@ -629,8 +138,12 @@ var HttpProxy = class {
629
138
  import { effieDataSchema } from "@effing/effie";
630
139
  async function createServerContext() {
631
140
  const port2 = process.env.FFS_PORT || process.env.PORT || 2e3;
632
- const httpProxy = new HttpProxy();
633
- await httpProxy.start();
141
+ const renderBackendBaseUrl = process.env.FFS_RENDER_BACKEND_BASE_URL;
142
+ let httpProxy;
143
+ if (!renderBackendBaseUrl) {
144
+ httpProxy = new HttpProxy();
145
+ await httpProxy.start();
146
+ }
634
147
  return {
635
148
  transientStore: createTransientStore(),
636
149
  httpProxy,
@@ -687,7 +200,7 @@ data: ${JSON.stringify(data)}
687
200
 
688
201
  // src/handlers/caching.ts
689
202
  import "express";
690
- import { Readable as Readable3, Transform } from "stream";
203
+ import { Readable as Readable2, Transform } from "stream";
691
204
  import { randomUUID as randomUUID3 } from "crypto";
692
205
  import {
693
206
  extractEffieSources,
@@ -702,764 +215,6 @@ import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } fro
702
215
  // src/handlers/rendering.ts
703
216
  import "express";
704
217
  import { randomUUID } from "crypto";
705
-
706
- // src/render.ts
707
- import { Readable as Readable2 } from "stream";
708
- import { createReadStream as createReadStream2 } from "fs";
709
-
710
- // src/motion.ts
711
- function getEasingExpression(tNormExpr, easingType) {
712
- switch (easingType) {
713
- case "ease-in":
714
- return `pow(${tNormExpr},2)`;
715
- case "ease-out":
716
- return `(1-pow(1-(${tNormExpr}),2))`;
717
- case "ease-in-out":
718
- return `if(lt(${tNormExpr},0.5),2*pow(${tNormExpr},2),1-pow(-2*(${tNormExpr})+2,2)/2)`;
719
- case "linear":
720
- default:
721
- return `(${tNormExpr})`;
722
- }
723
- }
724
- function processSlideMotion(motion, relativeTimeExpr) {
725
- const duration = motion.duration ?? 1;
726
- const distance = motion.distance ?? 1;
727
- const reverse = motion.reverse ?? false;
728
- const easing = motion.easing ?? "linear";
729
- const tNormExpr = `(${relativeTimeExpr})/${duration}`;
730
- const easedProgressExpr = getEasingExpression(tNormExpr, easing);
731
- const finalTimeFactorExpr = reverse ? easedProgressExpr : `(1-(${easedProgressExpr}))`;
732
- let activeX;
733
- let activeY;
734
- let initialX;
735
- let initialY;
736
- let finalX;
737
- let finalY;
738
- switch (motion.direction) {
739
- case "left": {
740
- const offsetXLeft = `${distance}*W`;
741
- activeX = `(${offsetXLeft})*${finalTimeFactorExpr}`;
742
- activeY = "0";
743
- initialX = reverse ? "0" : offsetXLeft;
744
- initialY = "0";
745
- finalX = reverse ? offsetXLeft : "0";
746
- finalY = "0";
747
- break;
748
- }
749
- case "right": {
750
- const offsetXRight = `-${distance}*W`;
751
- activeX = `(${offsetXRight})*${finalTimeFactorExpr}`;
752
- activeY = "0";
753
- initialX = reverse ? "0" : offsetXRight;
754
- initialY = "0";
755
- finalX = reverse ? offsetXRight : "0";
756
- finalY = "0";
757
- break;
758
- }
759
- case "up": {
760
- const offsetYUp = `${distance}*H`;
761
- activeX = "0";
762
- activeY = `(${offsetYUp})*${finalTimeFactorExpr}`;
763
- initialX = "0";
764
- initialY = reverse ? "0" : offsetYUp;
765
- finalX = "0";
766
- finalY = reverse ? offsetYUp : "0";
767
- break;
768
- }
769
- case "down": {
770
- const offsetYDown = `-${distance}*H`;
771
- activeX = "0";
772
- activeY = `(${offsetYDown})*${finalTimeFactorExpr}`;
773
- initialX = "0";
774
- initialY = reverse ? "0" : offsetYDown;
775
- finalX = "0";
776
- finalY = reverse ? offsetYDown : "0";
777
- break;
778
- }
779
- }
780
- return { initialX, initialY, activeX, activeY, finalX, finalY, duration };
781
- }
782
- function processBounceMotion(motion, relativeTimeExpr) {
783
- const amplitude = motion.amplitude ?? 0.5;
784
- const duration = motion.duration ?? 1;
785
- const initialY = `-overlay_h*${amplitude}`;
786
- const finalY = "0";
787
- const tNormExpr = `(${relativeTimeExpr})/${duration}`;
788
- const activeBounceExpression = `if(lt(${tNormExpr},0.363636),${initialY}+overlay_h*${amplitude}*(7.5625*${tNormExpr}*${tNormExpr}),if(lt(${tNormExpr},0.727273),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.545455)*(${tNormExpr}-0.545455)+0.75),if(lt(${tNormExpr},0.909091),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.818182)*(${tNormExpr}-0.818182)+0.9375),if(lt(${tNormExpr},0.954545),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.954545)*(${tNormExpr}-0.954545)+0.984375),${finalY}))))`;
789
- return {
790
- initialX: "0",
791
- initialY,
792
- activeX: "0",
793
- activeY: activeBounceExpression,
794
- // This expression now scales with duration
795
- finalX: "0",
796
- finalY,
797
- duration
798
- // Return the actual duration used
799
- };
800
- }
801
- function processShakeMotion(motion, relativeTimeExpr) {
802
- const intensity = motion.intensity ?? 10;
803
- const frequency = motion.frequency ?? 4;
804
- const duration = motion.duration ?? 1;
805
- const activeX = `${intensity}*sin(${relativeTimeExpr}*PI*${frequency})`;
806
- const activeY = `${intensity}*cos(${relativeTimeExpr}*PI*${frequency})`;
807
- return {
808
- initialX: "0",
809
- initialY: "0",
810
- activeX,
811
- activeY,
812
- finalX: "0",
813
- finalY: "0",
814
- duration
815
- };
816
- }
817
- function processMotion(delay, motion) {
818
- if (!motion) return "x=0:y=0";
819
- const start = delay + (motion.start ?? 0);
820
- const relativeTimeExpr = `(t-${start})`;
821
- let components;
822
- switch (motion.type) {
823
- case "bounce":
824
- components = processBounceMotion(motion, relativeTimeExpr);
825
- break;
826
- case "shake":
827
- components = processShakeMotion(motion, relativeTimeExpr);
828
- break;
829
- case "slide":
830
- components = processSlideMotion(motion, relativeTimeExpr);
831
- break;
832
- default:
833
- motion;
834
- throw new Error(
835
- `Unsupported motion type: ${motion.type}`
836
- );
837
- }
838
- const motionEndTime = start + components.duration;
839
- const xArg = `if(lt(t,${start}),${components.initialX},if(lt(t,${motionEndTime}),${components.activeX},${components.finalX}))`;
840
- const yArg = `if(lt(t,${start}),${components.initialY},if(lt(t,${motionEndTime}),${components.activeY},${components.finalY}))`;
841
- return `x='${xArg}':y='${yArg}'`;
842
- }
843
-
844
- // src/effect.ts
845
- function processFadeIn(effect, _frameRate, _frameWidth, _frameHeight) {
846
- return `fade=t=in:st=${effect.start}:d=${effect.duration}:alpha=1`;
847
- }
848
- function processFadeOut(effect, _frameRate, _frameWidth, _frameHeight) {
849
- return `fade=t=out:st=${effect.start}:d=${effect.duration}:alpha=1`;
850
- }
851
- function processSaturateIn(effect, _frameRate, _frameWidth, _frameHeight) {
852
- return `hue='s=max(0,min(1,(t-${effect.start})/${effect.duration}))'`;
853
- }
854
- function processSaturateOut(effect, _frameRate, _frameWidth, _frameHeight) {
855
- return `hue='s=max(0,min(1,(${effect.start + effect.duration}-t)/${effect.duration}))'`;
856
- }
857
- function processScroll(effect, frameRate, _frameWidth, _frameHeight) {
858
- const distance = effect.distance ?? 1;
859
- const scroll = distance / (1 + distance);
860
- const speed = scroll / (effect.duration * frameRate);
861
- switch (effect.direction) {
862
- case "left":
863
- return `scroll=h=${speed}`;
864
- case "right":
865
- return `scroll=hpos=${1 - scroll}:h=-${speed}`;
866
- case "up":
867
- return `scroll=v=${speed}`;
868
- case "down":
869
- return `scroll=vpos=${1 - scroll}:v=-${speed}`;
870
- }
871
- }
872
- function processEffect(effect, frameRate, frameWidth, frameHeight) {
873
- switch (effect.type) {
874
- case "fade-in":
875
- return processFadeIn(effect, frameRate, frameWidth, frameHeight);
876
- case "fade-out":
877
- return processFadeOut(effect, frameRate, frameWidth, frameHeight);
878
- case "saturate-in":
879
- return processSaturateIn(effect, frameRate, frameWidth, frameHeight);
880
- case "saturate-out":
881
- return processSaturateOut(effect, frameRate, frameWidth, frameHeight);
882
- case "scroll":
883
- return processScroll(effect, frameRate, frameWidth, frameHeight);
884
- default:
885
- effect;
886
- throw new Error(
887
- `Unsupported effect type: ${effect.type}`
888
- );
889
- }
890
- }
891
- function processEffects(effects, frameRate, frameWidth, frameHeight) {
892
- if (!effects || effects.length === 0) return "";
893
- const filters = [];
894
- for (const effect of effects) {
895
- const filter = processEffect(effect, frameRate, frameWidth, frameHeight);
896
- filters.push(filter);
897
- }
898
- return filters.join(",");
899
- }
900
-
901
- // src/transition.ts
902
- function processTransition(transition) {
903
- switch (transition.type) {
904
- case "fade": {
905
- if ("through" in transition) {
906
- return `fade${transition.through}`;
907
- }
908
- const easing = transition.easing ?? "linear";
909
- return {
910
- linear: "fade",
911
- "ease-in": "fadeslow",
912
- "ease-out": "fadefast"
913
- }[easing];
914
- }
915
- case "barn": {
916
- const orientation = transition.orientation ?? "horizontal";
917
- const mode = transition.mode ?? "open";
918
- const prefix = orientation === "vertical" ? "vert" : "horz";
919
- return `${prefix}${mode}`;
920
- }
921
- case "circle": {
922
- const mode = transition.mode ?? "open";
923
- return `circle${mode}`;
924
- }
925
- case "wipe":
926
- case "slide":
927
- case "smooth": {
928
- const direction = transition.direction ?? "left";
929
- return `${transition.type}${direction}`;
930
- }
931
- case "slice": {
932
- const direction = transition.direction ?? "left";
933
- const prefix = {
934
- left: "hl",
935
- right: "hr",
936
- up: "vu",
937
- down: "vd"
938
- }[direction];
939
- return `${prefix}${transition.type}`;
940
- }
941
- case "zoom": {
942
- return "zoomin";
943
- }
944
- case "dissolve":
945
- case "pixelize":
946
- case "radial":
947
- return transition.type;
948
- default:
949
- transition;
950
- throw new Error(
951
- `Unsupported transition type: ${transition.type}`
952
- );
953
- }
954
- }
955
-
956
- // src/render.ts
957
- import sharp from "sharp";
958
- import { fileURLToPath } from "url";
959
- var EffieRenderer = class {
960
- effieData;
961
- ffmpegRunner;
962
- allowLocalFiles;
963
- transientStore;
964
- httpProxy;
965
- constructor(effieData, options) {
966
- this.effieData = effieData;
967
- this.allowLocalFiles = options?.allowLocalFiles ?? false;
968
- this.transientStore = options?.transientStore;
969
- this.httpProxy = options?.httpProxy;
970
- }
971
- async fetchSource(src) {
972
- if (src.startsWith("data:")) {
973
- const commaIndex = src.indexOf(",");
974
- if (commaIndex === -1) {
975
- throw new Error("Invalid data URL");
976
- }
977
- const meta = src.slice(5, commaIndex);
978
- const isBase64 = meta.endsWith(";base64");
979
- const data = src.slice(commaIndex + 1);
980
- const buffer = isBase64 ? Buffer.from(data, "base64") : Buffer.from(decodeURIComponent(data));
981
- return Readable2.from(buffer);
982
- }
983
- if (src.startsWith("file:")) {
984
- if (!this.allowLocalFiles) {
985
- throw new Error(
986
- "Local file paths are not allowed. Use allowLocalFiles option for trusted operations."
987
- );
988
- }
989
- return createReadStream2(fileURLToPath(src));
990
- }
991
- if (this.transientStore) {
992
- const cachedStream = await this.transientStore.getStream(
993
- storeKeys.source(src)
994
- );
995
- if (cachedStream) {
996
- return cachedStream;
997
- }
998
- }
999
- const response = await ffsFetch(src, {
1000
- headersTimeout: 10 * 60 * 1e3,
1001
- // 10 minutes
1002
- bodyTimeout: 20 * 60 * 1e3
1003
- // 20 minutes
1004
- });
1005
- if (!response.ok) {
1006
- throw new Error(
1007
- `Failed to fetch ${src}: ${response.status} ${response.statusText}`
1008
- );
1009
- }
1010
- if (!response.body) {
1011
- throw new Error(`No body for ${src}`);
1012
- }
1013
- return Readable2.fromWeb(response.body);
1014
- }
1015
- buildAudioFilter({
1016
- duration,
1017
- volume,
1018
- fadeIn,
1019
- fadeOut
1020
- }) {
1021
- const filters = [];
1022
- if (volume !== void 0) {
1023
- filters.push(`volume=${volume}`);
1024
- }
1025
- if (fadeIn !== void 0) {
1026
- filters.push(`afade=type=in:start_time=0:duration=${fadeIn}`);
1027
- }
1028
- if (fadeOut !== void 0) {
1029
- filters.push(
1030
- `afade=type=out:start_time=${duration - fadeOut}:duration=${fadeOut}`
1031
- );
1032
- }
1033
- return filters.length ? filters.join(",") : "anull";
1034
- }
1035
- getFrameDimensions(scaleFactor) {
1036
- return {
1037
- frameWidth: Math.floor(this.effieData.width * scaleFactor / 2) * 2,
1038
- frameHeight: Math.floor(this.effieData.height * scaleFactor / 2) * 2
1039
- };
1040
- }
1041
- /**
1042
- * Builds an FFmpeg input for a background (global or segment).
1043
- */
1044
- buildBackgroundInput(background, inputIndex, frameWidth, frameHeight) {
1045
- if (background.type === "image") {
1046
- return {
1047
- index: inputIndex,
1048
- source: background.source,
1049
- preArgs: ["-loop", "1", "-framerate", this.effieData.fps.toString()],
1050
- type: "image"
1051
- };
1052
- } else if (background.type === "video") {
1053
- return {
1054
- index: inputIndex,
1055
- source: background.source,
1056
- preArgs: ["-stream_loop", "-1"],
1057
- type: "video"
1058
- };
1059
- }
1060
- return {
1061
- index: inputIndex,
1062
- source: "",
1063
- preArgs: [
1064
- "-f",
1065
- "lavfi",
1066
- "-i",
1067
- `color=${background.color}:size=${frameWidth}x${frameHeight}:rate=${this.effieData.fps}`
1068
- ],
1069
- type: "color"
1070
- };
1071
- }
1072
- buildOutputArgs(outputFilename) {
1073
- return [
1074
- "-map",
1075
- "[outv]",
1076
- "-map",
1077
- "[outa]",
1078
- "-c:v",
1079
- "libx264",
1080
- "-r",
1081
- this.effieData.fps.toString(),
1082
- "-pix_fmt",
1083
- "yuv420p",
1084
- "-preset",
1085
- "fast",
1086
- "-crf",
1087
- "28",
1088
- "-c:a",
1089
- "aac",
1090
- "-movflags",
1091
- "frag_keyframe+empty_moov",
1092
- "-f",
1093
- "mp4",
1094
- outputFilename
1095
- ];
1096
- }
1097
- buildLayerInput(layer, duration, inputIndex) {
1098
- let preArgs = [];
1099
- if (layer.type === "image") {
1100
- preArgs = [
1101
- "-loop",
1102
- "1",
1103
- "-t",
1104
- duration.toString(),
1105
- "-framerate",
1106
- this.effieData.fps.toString()
1107
- ];
1108
- } else if (layer.type === "animation") {
1109
- preArgs = ["-f", "image2", "-framerate", this.effieData.fps.toString()];
1110
- }
1111
- return {
1112
- index: inputIndex,
1113
- source: layer.source,
1114
- preArgs,
1115
- type: layer.type
1116
- };
1117
- }
1118
- /**
1119
- * Builds filter chain for all layers in a segment.
1120
- * @param segment - The segment containing layers
1121
- * @param bgLabel - Label for the background input (e.g., "bg_seg0" or "bg_seg")
1122
- * @param labelPrefix - Prefix for generated labels (e.g., "seg0_" or "")
1123
- * @param layerInputOffset - Starting input index for layers
1124
- * @param frameWidth - Frame width for nullsrc
1125
- * @param frameHeight - Frame height for nullsrc
1126
- * @param outputLabel - Label for the final video output
1127
- * @returns Array of filter parts to add to the filter chain
1128
- */
1129
- buildLayerFilters(segment, bgLabel, labelPrefix, layerInputOffset, frameWidth, frameHeight, outputLabel) {
1130
- const filterParts = [];
1131
- let currentVidLabel = bgLabel;
1132
- for (let l = 0; l < segment.layers.length; l++) {
1133
- const inputIdx = layerInputOffset + l;
1134
- const layerLabel = `${labelPrefix}layer${l}`;
1135
- const layer = segment.layers[l];
1136
- const effectChain = layer.effects ? processEffects(
1137
- layer.effects,
1138
- this.effieData.fps,
1139
- frameWidth,
1140
- frameHeight
1141
- ) : "";
1142
- filterParts.push(
1143
- `[${inputIdx}:v]trim=start=0:duration=${segment.duration},${effectChain ? effectChain + "," : ""}setsar=1,setpts=PTS-STARTPTS[${layerLabel}]`
1144
- );
1145
- let overlayInputLabel = layerLabel;
1146
- const delay = layer.delay ?? 0;
1147
- if (delay > 0) {
1148
- filterParts.push(
1149
- `nullsrc=size=${frameWidth}x${frameHeight}:duration=${delay},setpts=PTS-STARTPTS[null_${layerLabel}]`
1150
- );
1151
- filterParts.push(
1152
- `[null_${layerLabel}][${layerLabel}]concat=n=2:v=1:a=0[delayed_${layerLabel}]`
1153
- );
1154
- overlayInputLabel = `delayed_${layerLabel}`;
1155
- }
1156
- const overlayOutputLabel = `${labelPrefix}tmp${l}`;
1157
- const offset = layer.motion ? processMotion(delay, layer.motion) : "0:0";
1158
- const fromTime = layer.from ?? 0;
1159
- const untilTime = layer.until ?? segment.duration;
1160
- filterParts.push(
1161
- `[${currentVidLabel}][${overlayInputLabel}]overlay=${offset}:enable='between(t,${fromTime},${untilTime})',fps=${this.effieData.fps}[${overlayOutputLabel}]`
1162
- );
1163
- currentVidLabel = overlayOutputLabel;
1164
- }
1165
- filterParts.push(`[${currentVidLabel}]null[${outputLabel}]`);
1166
- return filterParts;
1167
- }
1168
- /**
1169
- * Applies xfade/concat transitions between video segments.
1170
- * Modifies videoSegmentLabels in place to update labels after transitions.
1171
- * @param filterParts - Array to append filter parts to
1172
- * @param videoSegmentLabels - Array of video segment labels (modified in place)
1173
- */
1174
- applyTransitions(filterParts, videoSegmentLabels) {
1175
- let transitionOffset = 0;
1176
- this.effieData.segments.forEach((segment, i) => {
1177
- if (i === 0) {
1178
- transitionOffset = segment.duration;
1179
- return;
1180
- }
1181
- const combineLabel = `[vid_com${i}]`;
1182
- if (!segment.transition) {
1183
- transitionOffset += segment.duration;
1184
- filterParts.push(
1185
- `${videoSegmentLabels[i - 1]}${videoSegmentLabels[i]}concat=n=2:v=1:a=0,fps=${this.effieData.fps}${combineLabel}`
1186
- );
1187
- videoSegmentLabels[i] = combineLabel;
1188
- return;
1189
- }
1190
- const transitionName = processTransition(segment.transition);
1191
- const transitionDuration = segment.transition.duration;
1192
- transitionOffset -= transitionDuration;
1193
- filterParts.push(
1194
- `${videoSegmentLabels[i - 1]}${videoSegmentLabels[i]}xfade=transition=${transitionName}:duration=${transitionDuration}:offset=${transitionOffset}${combineLabel}`
1195
- );
1196
- videoSegmentLabels[i] = combineLabel;
1197
- transitionOffset += segment.duration;
1198
- });
1199
- filterParts.push(`${videoSegmentLabels.at(-1)}null[outv]`);
1200
- }
1201
- /**
1202
- * Applies general audio mixing: concats segment audio and mixes with global audio if present.
1203
- * @param filterParts - Array to append filter parts to
1204
- * @param audioSegmentLabels - Array of audio segment labels to concat
1205
- * @param totalDuration - Total duration for audio trimming
1206
- * @param generalAudioInputIndex - Input index for general audio (if present)
1207
- */
1208
- applyGeneralAudio(filterParts, audioSegmentLabels, totalDuration, generalAudioInputIndex) {
1209
- if (this.effieData.audio) {
1210
- const audioSeek = this.effieData.audio.seek ?? 0;
1211
- const generalAudioFilter = this.buildAudioFilter({
1212
- duration: totalDuration,
1213
- volume: this.effieData.audio.volume,
1214
- fadeIn: this.effieData.audio.fadeIn,
1215
- fadeOut: this.effieData.audio.fadeOut
1216
- });
1217
- filterParts.push(
1218
- `[${generalAudioInputIndex}:a]atrim=start=${audioSeek}:duration=${totalDuration},${generalAudioFilter},asetpts=PTS-STARTPTS[general_audio]`
1219
- );
1220
- filterParts.push(
1221
- `${audioSegmentLabels.join("")}concat=n=${this.effieData.segments.length}:v=0:a=1,atrim=start=0:duration=${totalDuration}[segments_audio]`
1222
- );
1223
- filterParts.push(
1224
- `[general_audio][segments_audio]amix=inputs=2:duration=longest[outa]`
1225
- );
1226
- } else {
1227
- filterParts.push(
1228
- `${audioSegmentLabels.join("")}concat=n=${this.effieData.segments.length}:v=0:a=1[outa]`
1229
- );
1230
- }
1231
- }
1232
- buildFFmpegCommand(outputFilename, scaleFactor = 1) {
1233
- const globalArgs = ["-y", "-loglevel", "error"];
1234
- const inputs = [];
1235
- let inputIndex = 0;
1236
- const { frameWidth, frameHeight } = this.getFrameDimensions(scaleFactor);
1237
- const backgroundSeek = this.effieData.background.type === "video" ? this.effieData.background.seek ?? 0 : 0;
1238
- inputs.push(
1239
- this.buildBackgroundInput(
1240
- this.effieData.background,
1241
- inputIndex,
1242
- frameWidth,
1243
- frameHeight
1244
- )
1245
- );
1246
- const globalBgInputIdx = inputIndex;
1247
- inputIndex++;
1248
- const segmentBgInputIndices = [];
1249
- for (const segment of this.effieData.segments) {
1250
- if (segment.background) {
1251
- inputs.push(
1252
- this.buildBackgroundInput(
1253
- segment.background,
1254
- inputIndex,
1255
- frameWidth,
1256
- frameHeight
1257
- )
1258
- );
1259
- segmentBgInputIndices.push(inputIndex);
1260
- inputIndex++;
1261
- } else {
1262
- segmentBgInputIndices.push(null);
1263
- }
1264
- }
1265
- const globalBgSegmentIndices = [];
1266
- for (let i = 0; i < this.effieData.segments.length; i++) {
1267
- if (segmentBgInputIndices[i] === null) {
1268
- globalBgSegmentIndices.push(i);
1269
- }
1270
- }
1271
- for (const segment of this.effieData.segments) {
1272
- for (const layer of segment.layers) {
1273
- inputs.push(this.buildLayerInput(layer, segment.duration, inputIndex));
1274
- inputIndex++;
1275
- }
1276
- }
1277
- for (const segment of this.effieData.segments) {
1278
- if (segment.audio) {
1279
- inputs.push({
1280
- index: inputIndex,
1281
- source: segment.audio.source,
1282
- preArgs: [],
1283
- type: "audio"
1284
- });
1285
- inputIndex++;
1286
- }
1287
- }
1288
- if (this.effieData.audio) {
1289
- inputs.push({
1290
- index: inputIndex,
1291
- source: this.effieData.audio.source,
1292
- preArgs: [],
1293
- type: "audio"
1294
- });
1295
- inputIndex++;
1296
- }
1297
- const numSegmentBgInputs = segmentBgInputIndices.filter(
1298
- (i) => i !== null
1299
- ).length;
1300
- const numVideoInputs = 1 + numSegmentBgInputs + this.effieData.segments.reduce((sum, seg) => sum + seg.layers.length, 0);
1301
- let audioCounter = 0;
1302
- let currentTime = 0;
1303
- let layerInputOffset = 1 + numSegmentBgInputs;
1304
- const filterParts = [];
1305
- const videoSegmentLabels = [];
1306
- const audioSegmentLabels = [];
1307
- const globalBgFifoLabels = /* @__PURE__ */ new Map();
1308
- const bgFilter = `fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight}:force_original_aspect_ratio=increase,crop=${frameWidth}:${frameHeight}`;
1309
- if (globalBgSegmentIndices.length === 1) {
1310
- const fifoLabel = `bg_fifo_0`;
1311
- filterParts.push(`[${globalBgInputIdx}:v]${bgFilter},fifo[${fifoLabel}]`);
1312
- globalBgFifoLabels.set(globalBgSegmentIndices[0], fifoLabel);
1313
- } else if (globalBgSegmentIndices.length > 1) {
1314
- const splitCount = globalBgSegmentIndices.length;
1315
- const splitOutputLabels = globalBgSegmentIndices.map(
1316
- (_, i) => `bg_split_${i}`
1317
- );
1318
- filterParts.push(
1319
- `[${globalBgInputIdx}:v]${bgFilter},split=${splitCount}${splitOutputLabels.map((l) => `[${l}]`).join("")}`
1320
- );
1321
- for (let i = 0; i < splitCount; i++) {
1322
- const fifoLabel = `bg_fifo_${i}`;
1323
- filterParts.push(`[${splitOutputLabels[i]}]fifo[${fifoLabel}]`);
1324
- globalBgFifoLabels.set(globalBgSegmentIndices[i], fifoLabel);
1325
- }
1326
- }
1327
- for (let segIdx = 0; segIdx < this.effieData.segments.length; segIdx++) {
1328
- const segment = this.effieData.segments[segIdx];
1329
- const bgLabel = `bg_seg${segIdx}`;
1330
- if (segment.background) {
1331
- const segBgInputIdx = segmentBgInputIndices[segIdx];
1332
- const segBgSeek = segment.background.type === "video" ? segment.background.seek ?? 0 : 0;
1333
- filterParts.push(
1334
- `[${segBgInputIdx}:v]fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight},trim=start=${segBgSeek}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`
1335
- );
1336
- } else {
1337
- const fifoLabel = globalBgFifoLabels.get(segIdx);
1338
- if (fifoLabel) {
1339
- filterParts.push(
1340
- `[${fifoLabel}]trim=start=${backgroundSeek + currentTime}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`
1341
- );
1342
- }
1343
- }
1344
- const vidLabel = `vid_seg${segIdx}`;
1345
- filterParts.push(
1346
- ...this.buildLayerFilters(
1347
- segment,
1348
- bgLabel,
1349
- `seg${segIdx}_`,
1350
- layerInputOffset,
1351
- frameWidth,
1352
- frameHeight,
1353
- vidLabel
1354
- )
1355
- );
1356
- layerInputOffset += segment.layers.length;
1357
- videoSegmentLabels.push(`[${vidLabel}]`);
1358
- const nextSegment = this.effieData.segments[segIdx + 1];
1359
- const transitionDuration = nextSegment?.transition?.duration ?? 0;
1360
- const realDuration = Math.max(
1361
- 1e-3,
1362
- segment.duration - transitionDuration
1363
- );
1364
- if (segment.audio) {
1365
- const audioInputIndex = numVideoInputs + audioCounter;
1366
- const audioFilter = this.buildAudioFilter({
1367
- duration: realDuration,
1368
- volume: segment.audio.volume,
1369
- fadeIn: segment.audio.fadeIn,
1370
- fadeOut: segment.audio.fadeOut
1371
- });
1372
- filterParts.push(
1373
- `[${audioInputIndex}:a]atrim=start=0:duration=${realDuration},${audioFilter},asetpts=PTS-STARTPTS[aud_seg${segIdx}]`
1374
- );
1375
- audioCounter++;
1376
- } else {
1377
- filterParts.push(
1378
- `anullsrc=r=44100:cl=stereo,atrim=start=0:duration=${realDuration},asetpts=PTS-STARTPTS[aud_seg${segIdx}]`
1379
- );
1380
- }
1381
- audioSegmentLabels.push(`[aud_seg${segIdx}]`);
1382
- currentTime += realDuration;
1383
- }
1384
- this.applyGeneralAudio(
1385
- filterParts,
1386
- audioSegmentLabels,
1387
- currentTime,
1388
- numVideoInputs + audioCounter
1389
- );
1390
- this.applyTransitions(filterParts, videoSegmentLabels);
1391
- const filterComplex = filterParts.join(";");
1392
- const outputArgs = this.buildOutputArgs(outputFilename);
1393
- return new FFmpegCommand(globalArgs, inputs, filterComplex, outputArgs);
1394
- }
1395
- createImageTransformer(scaleFactor) {
1396
- return async (imageStream) => {
1397
- if (scaleFactor === 1) return imageStream;
1398
- const sharpTransformer = sharp();
1399
- imageStream.on("error", (err) => {
1400
- if (!sharpTransformer.destroyed) {
1401
- sharpTransformer.destroy(err);
1402
- }
1403
- });
1404
- sharpTransformer.on("error", (err) => {
1405
- if (!imageStream.destroyed) {
1406
- imageStream.destroy(err);
1407
- }
1408
- });
1409
- imageStream.pipe(sharpTransformer);
1410
- try {
1411
- const metadata = await sharpTransformer.metadata();
1412
- const imageWidth = metadata.width ?? this.effieData.width;
1413
- const imageHeight = metadata.height ?? this.effieData.height;
1414
- return sharpTransformer.resize({
1415
- width: Math.floor(imageWidth * scaleFactor),
1416
- height: Math.floor(imageHeight * scaleFactor)
1417
- });
1418
- } catch (error) {
1419
- if (!sharpTransformer.destroyed) {
1420
- sharpTransformer.destroy(error);
1421
- }
1422
- throw error;
1423
- }
1424
- };
1425
- }
1426
- /**
1427
- * Resolves a source reference to its actual URL.
1428
- * If the source is a #reference, returns the resolved URL.
1429
- * Otherwise, returns the source as-is.
1430
- */
1431
- resolveReference(src) {
1432
- if (src.startsWith("#")) {
1433
- const sourceName = src.slice(1);
1434
- if (sourceName in this.effieData.sources) {
1435
- return this.effieData.sources[sourceName];
1436
- }
1437
- }
1438
- return src;
1439
- }
1440
- /**
1441
- * Renders the effie data to a video stream.
1442
- * @param scaleFactor - Scale factor for output dimensions
1443
- */
1444
- async render(scaleFactor = 1) {
1445
- const ffmpegCommand = this.buildFFmpegCommand("-", scaleFactor);
1446
- this.ffmpegRunner = new FFmpegRunner(ffmpegCommand);
1447
- const urlTransformer = this.httpProxy ? (url) => this.httpProxy.transformUrl(url) : void 0;
1448
- return this.ffmpegRunner.run(
1449
- async ({ src }) => this.fetchSource(src),
1450
- this.createImageTransformer(scaleFactor),
1451
- (src) => this.resolveReference(src),
1452
- urlTransformer
1453
- );
1454
- }
1455
- close() {
1456
- if (this.ffmpegRunner) {
1457
- this.ffmpegRunner.close();
1458
- }
1459
- }
1460
- };
1461
-
1462
- // src/handlers/rendering.ts
1463
218
  import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
1464
219
  async function createRenderJob(req, res, ctx2) {
1465
220
  try {
@@ -1559,6 +314,7 @@ async function streamRenderJob(req, res, ctx2) {
1559
314
  }
1560
315
  }
1561
316
  async function streamRenderDirect(res, job, ctx2) {
317
+ const { EffieRenderer } = await import("./render-VWBOR3Y2.js");
1562
318
  const renderer = new EffieRenderer(job.effie, {
1563
319
  transientStore: ctx2.transientStore,
1564
320
  httpProxy: ctx2.httpProxy
@@ -1623,6 +379,7 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
1623
379
  timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
1624
380
  }
1625
381
  const renderStartTime = Date.now();
382
+ const { EffieRenderer } = await import("./render-VWBOR3Y2.js");
1626
383
  const renderer = new EffieRenderer(effie, {
1627
384
  transientStore: ctx2.transientStore,
1628
385
  httpProxy: ctx2.httpProxy
@@ -2152,7 +909,7 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx2) {
2152
909
  throw new Error(`${response.status} ${response.statusText}`);
2153
910
  }
2154
911
  sendEvent("downloading", { url, status: "started", bytesReceived: 0 });
2155
- const sourceStream = Readable3.fromWeb(
912
+ const sourceStream = Readable2.fromWeb(
2156
913
  response.body
2157
914
  );
2158
915
  let totalBytes = 0;
@@ -2182,11 +939,12 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx2) {
2182
939
  }
2183
940
 
2184
941
  // src/server.ts
2185
- console.log("FFS", getFFmpegVersion());
2186
942
  var app = express5();
2187
943
  app.use(bodyParser.json({ limit: "50mb" }));
2188
944
  var ctx = await createServerContext();
2189
- console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);
945
+ if (ctx.httpProxy) {
946
+ console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);
947
+ }
2190
948
  function validateAuth(req, res) {
2191
949
  const apiKey = process.env.FFS_API_KEY;
2192
950
  if (!apiKey) return true;
@@ -2225,7 +983,7 @@ var server = app.listen(port, () => {
2225
983
  });
2226
984
  function shutdown() {
2227
985
  console.log("Shutting down FFS server...");
2228
- ctx.httpProxy.close();
986
+ ctx.httpProxy?.close();
2229
987
  ctx.transientStore.close();
2230
988
  server.close(() => {
2231
989
  console.log("FFS server stopped");