@effing/ffs 0.1.2 → 0.3.0

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