@effing/ffs 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -20
- package/dist/{chunk-5SGOYTM2.js → chunk-4N2GLGC5.js} +11 -11
- package/dist/chunk-4N2GLGC5.js.map +1 -0
- package/dist/{chunk-N3D6I2BD.js → chunk-O7Z6DV2I.js} +2 -2
- package/dist/{chunk-QPZEAH3J.js → chunk-PERB3C4S.js} +10 -10
- package/dist/{chunk-ZERUSI5T.js → chunk-WD7MF3GH.js} +86 -75
- package/dist/chunk-WD7MF3GH.js.map +1 -0
- package/dist/handlers/index.d.ts +27 -10
- package/dist/handlers/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{proxy-qTA69nOV.d.ts → proxy-CsZ5h2Ya.d.ts} +3 -3
- package/dist/render-IKGZZOBP.js +8 -0
- package/dist/{render-VWBOR3Y2.js → render-MUKKTCF6.js} +1 -1
- package/dist/server.js +85 -74
- package/package.json +3 -3
- package/dist/chunk-5SGOYTM2.js.map +0 -1
- package/dist/chunk-ZERUSI5T.js.map +0 -1
- package/dist/render-NEDCS65O.js +0 -8
- /package/dist/{chunk-N3D6I2BD.js.map → chunk-O7Z6DV2I.js.map} +0 -0
- /package/dist/{render-NEDCS65O.js.map → render-IKGZZOBP.js.map} +0 -0
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
createTransientStore,
|
|
3
3
|
ffsFetch,
|
|
4
4
|
storeKeys
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4N2GLGC5.js";
|
|
6
6
|
|
|
7
7
|
// src/handlers/shared.ts
|
|
8
8
|
import "express";
|
|
@@ -131,11 +131,11 @@ var HttpProxy = class {
|
|
|
131
131
|
|
|
132
132
|
// src/handlers/shared.ts
|
|
133
133
|
import { effieDataSchema } from "@effing/effie";
|
|
134
|
-
async function createServerContext() {
|
|
134
|
+
async function createServerContext(options) {
|
|
135
135
|
const port = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
136
|
-
const
|
|
136
|
+
const enableHttpProxy = options?.httpProxy ?? !options?.renderBackendResolver;
|
|
137
137
|
let httpProxy;
|
|
138
|
-
if (
|
|
138
|
+
if (enableHttpProxy) {
|
|
139
139
|
httpProxy = new HttpProxy();
|
|
140
140
|
await httpProxy.start();
|
|
141
141
|
}
|
|
@@ -145,10 +145,8 @@ async function createServerContext() {
|
|
|
145
145
|
baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,
|
|
146
146
|
skipValidation: !!process.env.FFS_SKIP_VALIDATION && process.env.FFS_SKIP_VALIDATION !== "false",
|
|
147
147
|
warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || "4", 10),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
warmupBackendApiKey: process.env.FFS_WARMUP_BACKEND_API_KEY,
|
|
151
|
-
renderBackendApiKey: process.env.FFS_RENDER_BACKEND_API_KEY
|
|
148
|
+
warmupBackendResolver: options?.warmupBackendResolver,
|
|
149
|
+
renderBackendResolver: options?.renderBackendResolver
|
|
152
150
|
};
|
|
153
151
|
}
|
|
154
152
|
function parseEffieData(body, skipValidation) {
|
|
@@ -211,16 +209,16 @@ import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } fro
|
|
|
211
209
|
import "express";
|
|
212
210
|
import { randomUUID } from "crypto";
|
|
213
211
|
import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
|
|
214
|
-
async function createRenderJob(req, res, ctx) {
|
|
212
|
+
async function createRenderJob(req, res, ctx, options) {
|
|
215
213
|
try {
|
|
216
214
|
const isWrapped = "effie" in req.body;
|
|
217
215
|
let rawEffieData;
|
|
218
216
|
let scale;
|
|
219
217
|
let upload;
|
|
220
218
|
if (isWrapped) {
|
|
221
|
-
const
|
|
222
|
-
if (typeof
|
|
223
|
-
const response = await ffsFetch(
|
|
219
|
+
const options2 = req.body;
|
|
220
|
+
if (typeof options2.effie === "string") {
|
|
221
|
+
const response = await ffsFetch(options2.effie);
|
|
224
222
|
if (!response.ok) {
|
|
225
223
|
throw new Error(
|
|
226
224
|
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
@@ -228,10 +226,10 @@ async function createRenderJob(req, res, ctx) {
|
|
|
228
226
|
}
|
|
229
227
|
rawEffieData = await response.json();
|
|
230
228
|
} else {
|
|
231
|
-
rawEffieData =
|
|
229
|
+
rawEffieData = options2.effie;
|
|
232
230
|
}
|
|
233
|
-
scale =
|
|
234
|
-
upload =
|
|
231
|
+
scale = options2.scale ?? 1;
|
|
232
|
+
upload = options2.upload;
|
|
235
233
|
} else {
|
|
236
234
|
rawEffieData = req.body;
|
|
237
235
|
scale = parseFloat(req.query.scale?.toString() || "1");
|
|
@@ -263,12 +261,13 @@ async function createRenderJob(req, res, ctx) {
|
|
|
263
261
|
effie,
|
|
264
262
|
scale,
|
|
265
263
|
upload,
|
|
266
|
-
createdAt: Date.now()
|
|
264
|
+
createdAt: Date.now(),
|
|
265
|
+
metadata: options?.metadata
|
|
267
266
|
};
|
|
268
267
|
await ctx.transientStore.putJson(
|
|
269
268
|
storeKeys.renderJob(jobId),
|
|
270
269
|
job,
|
|
271
|
-
ctx.transientStore.
|
|
270
|
+
ctx.transientStore.jobDataTtlMs
|
|
272
271
|
);
|
|
273
272
|
res.json({
|
|
274
273
|
id: jobId,
|
|
@@ -283,17 +282,20 @@ async function streamRenderJob(req, res, ctx) {
|
|
|
283
282
|
try {
|
|
284
283
|
setupCORSHeaders(res);
|
|
285
284
|
const jobId = req.params.id;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const jobCacheKey = storeKeys.renderJob(jobId);
|
|
291
|
-
const job = await ctx.transientStore.getJson(jobCacheKey);
|
|
292
|
-
ctx.transientStore.delete(jobCacheKey);
|
|
285
|
+
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
286
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
293
287
|
if (!job) {
|
|
294
288
|
res.status(404).json({ error: "Job not found or expired" });
|
|
295
289
|
return;
|
|
296
290
|
}
|
|
291
|
+
if (ctx.renderBackendResolver) {
|
|
292
|
+
const backend = ctx.renderBackendResolver(job.effie, job.metadata);
|
|
293
|
+
if (backend) {
|
|
294
|
+
await proxyRenderFromBackend(res, jobId, backend);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
297
299
|
if (job.upload) {
|
|
298
300
|
await streamRenderWithUpload(res, job, ctx);
|
|
299
301
|
} else {
|
|
@@ -309,7 +311,7 @@ async function streamRenderJob(req, res, ctx) {
|
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
313
|
async function streamRenderDirect(res, job, ctx) {
|
|
312
|
-
const { EffieRenderer } = await import("./render-
|
|
314
|
+
const { EffieRenderer } = await import("./render-IKGZZOBP.js");
|
|
313
315
|
const renderer = new EffieRenderer(job.effie, {
|
|
314
316
|
transientStore: ctx.transientStore,
|
|
315
317
|
httpProxy: ctx.httpProxy
|
|
@@ -320,6 +322,7 @@ async function streamRenderDirect(res, job, ctx) {
|
|
|
320
322
|
renderer.close();
|
|
321
323
|
});
|
|
322
324
|
res.set("Content-Type", "video/mp4");
|
|
325
|
+
res.set("Cache-Control", "public, immutable, max-age=86400");
|
|
323
326
|
videoStream.pipe(res);
|
|
324
327
|
}
|
|
325
328
|
async function streamRenderWithUpload(res, job, ctx) {
|
|
@@ -374,7 +377,7 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
|
374
377
|
timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
|
|
375
378
|
}
|
|
376
379
|
const renderStartTime = Date.now();
|
|
377
|
-
const { EffieRenderer } = await import("./render-
|
|
380
|
+
const { EffieRenderer } = await import("./render-IKGZZOBP.js");
|
|
378
381
|
const renderer = new EffieRenderer(effie, {
|
|
379
382
|
transientStore: ctx.transientStore,
|
|
380
383
|
httpProxy: ctx.httpProxy
|
|
@@ -404,10 +407,10 @@ async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
|
404
407
|
timings.uploadTime = Date.now() - uploadStartTime;
|
|
405
408
|
return timings;
|
|
406
409
|
}
|
|
407
|
-
async function proxyRenderFromBackend(res, jobId,
|
|
408
|
-
const backendUrl = `${
|
|
410
|
+
async function proxyRenderFromBackend(res, jobId, backend) {
|
|
411
|
+
const backendUrl = `${backend.baseUrl}/render/${jobId}`;
|
|
409
412
|
const response = await ffsFetch(backendUrl, {
|
|
410
|
-
headers:
|
|
413
|
+
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
411
414
|
});
|
|
412
415
|
if (!response.ok) {
|
|
413
416
|
res.status(response.status).json({ error: "Backend render failed" });
|
|
@@ -464,12 +467,12 @@ async function proxyRenderFromBackend(res, jobId, ctx) {
|
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
// src/handlers/orchestrating.ts
|
|
467
|
-
async function createWarmupAndRenderJob(req, res, ctx) {
|
|
470
|
+
async function createWarmupAndRenderJob(req, res, ctx, options) {
|
|
468
471
|
try {
|
|
469
|
-
const
|
|
472
|
+
const body = req.body;
|
|
470
473
|
let rawEffieData;
|
|
471
|
-
if (typeof
|
|
472
|
-
const response = await ffsFetch(
|
|
474
|
+
if (typeof body.effie === "string") {
|
|
475
|
+
const response = await ffsFetch(body.effie);
|
|
473
476
|
if (!response.ok) {
|
|
474
477
|
throw new Error(
|
|
475
478
|
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
@@ -477,7 +480,7 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
477
480
|
}
|
|
478
481
|
rawEffieData = await response.json();
|
|
479
482
|
} else {
|
|
480
|
-
rawEffieData =
|
|
483
|
+
rawEffieData = body.effie;
|
|
481
484
|
}
|
|
482
485
|
let effie;
|
|
483
486
|
if (!ctx.skipValidation) {
|
|
@@ -502,8 +505,8 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
502
505
|
effie = data;
|
|
503
506
|
}
|
|
504
507
|
const sources = extractEffieSourcesWithTypes(effie);
|
|
505
|
-
const scale =
|
|
506
|
-
const upload =
|
|
508
|
+
const scale = body.scale ?? 1;
|
|
509
|
+
const upload = body.upload;
|
|
507
510
|
const jobId = randomUUID2();
|
|
508
511
|
const warmupJobId = randomUUID2();
|
|
509
512
|
const renderJobId = randomUUID2();
|
|
@@ -514,17 +517,18 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
514
517
|
upload,
|
|
515
518
|
warmupJobId,
|
|
516
519
|
renderJobId,
|
|
517
|
-
createdAt: Date.now()
|
|
520
|
+
createdAt: Date.now(),
|
|
521
|
+
metadata: options?.metadata
|
|
518
522
|
};
|
|
519
523
|
await ctx.transientStore.putJson(
|
|
520
524
|
storeKeys.warmupAndRenderJob(jobId),
|
|
521
525
|
job,
|
|
522
|
-
ctx.transientStore.
|
|
526
|
+
ctx.transientStore.jobDataTtlMs
|
|
523
527
|
);
|
|
524
528
|
await ctx.transientStore.putJson(
|
|
525
529
|
storeKeys.warmupJob(warmupJobId),
|
|
526
|
-
{ sources },
|
|
527
|
-
ctx.transientStore.
|
|
530
|
+
{ sources, metadata: options?.metadata },
|
|
531
|
+
ctx.transientStore.jobDataTtlMs
|
|
528
532
|
);
|
|
529
533
|
await ctx.transientStore.putJson(
|
|
530
534
|
storeKeys.renderJob(renderJobId),
|
|
@@ -532,9 +536,10 @@ async function createWarmupAndRenderJob(req, res, ctx) {
|
|
|
532
536
|
effie,
|
|
533
537
|
scale,
|
|
534
538
|
upload,
|
|
535
|
-
createdAt: Date.now()
|
|
539
|
+
createdAt: Date.now(),
|
|
540
|
+
metadata: options?.metadata
|
|
536
541
|
},
|
|
537
|
-
ctx.transientStore.
|
|
542
|
+
ctx.transientStore.jobDataTtlMs
|
|
538
543
|
);
|
|
539
544
|
res.json({
|
|
540
545
|
id: jobId,
|
|
@@ -549,13 +554,15 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
549
554
|
try {
|
|
550
555
|
setupCORSHeaders(res);
|
|
551
556
|
const jobId = req.params.id;
|
|
552
|
-
const
|
|
553
|
-
const job = await ctx.transientStore.getJson(
|
|
554
|
-
ctx.transientStore.delete(
|
|
557
|
+
const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);
|
|
558
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
559
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
555
560
|
if (!job) {
|
|
556
561
|
res.status(404).json({ error: "Job not found" });
|
|
557
562
|
return;
|
|
558
563
|
}
|
|
564
|
+
const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
565
|
+
const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
|
|
559
566
|
setupSSEResponse(res);
|
|
560
567
|
const sendEvent = createSSEEventSender(res);
|
|
561
568
|
let keepalivePhase = "warmup";
|
|
@@ -563,13 +570,13 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
563
570
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
564
571
|
}, 25e3);
|
|
565
572
|
try {
|
|
566
|
-
if (
|
|
573
|
+
if (warmupBackend) {
|
|
567
574
|
await proxyRemoteSSE(
|
|
568
|
-
`${
|
|
575
|
+
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,
|
|
569
576
|
sendEvent,
|
|
570
577
|
"warmup:",
|
|
571
578
|
res,
|
|
572
|
-
|
|
579
|
+
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
573
580
|
);
|
|
574
581
|
} else {
|
|
575
582
|
const warmupSender = prefixEventSender(sendEvent, "warmup:");
|
|
@@ -577,13 +584,13 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
577
584
|
warmupSender("complete", { status: "ready" });
|
|
578
585
|
}
|
|
579
586
|
keepalivePhase = "render";
|
|
580
|
-
if (
|
|
587
|
+
if (renderBackend) {
|
|
581
588
|
await proxyRemoteSSE(
|
|
582
|
-
`${
|
|
589
|
+
`${renderBackend.baseUrl}/render/${job.renderJobId}`,
|
|
583
590
|
sendEvent,
|
|
584
591
|
"render:",
|
|
585
592
|
res,
|
|
586
|
-
|
|
593
|
+
renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
|
|
587
594
|
);
|
|
588
595
|
} else {
|
|
589
596
|
const renderSender = prefixEventSender(sendEvent, "render:");
|
|
@@ -602,7 +609,7 @@ async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
|
602
609
|
sendEvent("complete", { status: "ready", videoUrl });
|
|
603
610
|
}
|
|
604
611
|
}
|
|
605
|
-
if (job.upload && !
|
|
612
|
+
if (job.upload && !renderBackend) {
|
|
606
613
|
sendEvent("complete", { status: "done" });
|
|
607
614
|
}
|
|
608
615
|
} catch (error) {
|
|
@@ -707,7 +714,7 @@ function shouldSkipWarmup(source) {
|
|
|
707
714
|
return source.type === "video" || source.type === "audio";
|
|
708
715
|
}
|
|
709
716
|
var inFlightFetches = /* @__PURE__ */ new Map();
|
|
710
|
-
async function createWarmupJob(req, res, ctx) {
|
|
717
|
+
async function createWarmupJob(req, res, ctx, options) {
|
|
711
718
|
try {
|
|
712
719
|
const parseResult = parseEffieData(req.body, ctx.skipValidation);
|
|
713
720
|
if ("error" in parseResult) {
|
|
@@ -716,10 +723,11 @@ async function createWarmupJob(req, res, ctx) {
|
|
|
716
723
|
}
|
|
717
724
|
const sources = extractEffieSourcesWithTypes2(parseResult.effie);
|
|
718
725
|
const jobId = randomUUID3();
|
|
726
|
+
const job = { sources, metadata: options?.metadata };
|
|
719
727
|
await ctx.transientStore.putJson(
|
|
720
728
|
storeKeys.warmupJob(jobId),
|
|
721
|
-
|
|
722
|
-
ctx.transientStore.
|
|
729
|
+
job,
|
|
730
|
+
ctx.transientStore.jobDataTtlMs
|
|
723
731
|
);
|
|
724
732
|
res.json({
|
|
725
733
|
id: jobId,
|
|
@@ -734,29 +742,32 @@ async function streamWarmupJob(req, res, ctx) {
|
|
|
734
742
|
try {
|
|
735
743
|
setupCORSHeaders(res);
|
|
736
744
|
const jobId = req.params.id;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const sendEvent2 = createSSEEventSender(res);
|
|
740
|
-
try {
|
|
741
|
-
await proxyRemoteSSE(
|
|
742
|
-
`${ctx.warmupBackendBaseUrl}/warmup/${jobId}`,
|
|
743
|
-
sendEvent2,
|
|
744
|
-
"",
|
|
745
|
-
res,
|
|
746
|
-
ctx.warmupBackendApiKey ? { Authorization: `Bearer ${ctx.warmupBackendApiKey}` } : void 0
|
|
747
|
-
);
|
|
748
|
-
} finally {
|
|
749
|
-
res.end();
|
|
750
|
-
}
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const jobCacheKey = storeKeys.warmupJob(jobId);
|
|
754
|
-
const job = await ctx.transientStore.getJson(jobCacheKey);
|
|
755
|
-
ctx.transientStore.delete(jobCacheKey);
|
|
745
|
+
const jobStoreKey = storeKeys.warmupJob(jobId);
|
|
746
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
756
747
|
if (!job) {
|
|
757
748
|
res.status(404).json({ error: "Job not found" });
|
|
758
749
|
return;
|
|
759
750
|
}
|
|
751
|
+
if (ctx.warmupBackendResolver) {
|
|
752
|
+
const backend = ctx.warmupBackendResolver(job.sources, job.metadata);
|
|
753
|
+
if (backend) {
|
|
754
|
+
setupSSEResponse(res);
|
|
755
|
+
const sendEvent2 = createSSEEventSender(res);
|
|
756
|
+
try {
|
|
757
|
+
await proxyRemoteSSE(
|
|
758
|
+
`${backend.baseUrl}/warmup/${jobId}`,
|
|
759
|
+
sendEvent2,
|
|
760
|
+
"",
|
|
761
|
+
res,
|
|
762
|
+
backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
763
|
+
);
|
|
764
|
+
} finally {
|
|
765
|
+
res.end();
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
760
771
|
setupSSEResponse(res);
|
|
761
772
|
const sendEvent = createSSEEventSender(res);
|
|
762
773
|
try {
|
|
@@ -945,4 +956,4 @@ export {
|
|
|
945
956
|
streamWarmupJob,
|
|
946
957
|
purgeCache
|
|
947
958
|
};
|
|
948
|
-
//# sourceMappingURL=chunk-
|
|
959
|
+
//# sourceMappingURL=chunk-WD7MF3GH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/handlers/shared.ts","../src/proxy.ts","../src/handlers/caching.ts","../src/handlers/orchestrating.ts","../src/handlers/rendering.ts"],"sourcesContent":["import express from \"express\";\nimport type { TransientStore } from \"../storage\";\nimport { createTransientStore } from \"../storage\";\nimport { HttpProxy } from \"../proxy\";\nimport type {\n EffieData,\n EffieSources,\n EffieSourceWithType,\n} from \"@effing/effie\";\nimport { effieDataSchema } from \"@effing/effie\";\n\nexport type UploadOptions = {\n videoUrl: string;\n coverUrl?: string;\n};\n\nexport type BackendConfig = {\n baseUrl: string;\n apiKey?: string;\n};\n\nexport type WarmupBackendResolver = (\n sources: EffieSourceWithType[],\n metadata?: Record<string, unknown>,\n) => BackendConfig | null;\n\nexport type RenderBackendResolver = (\n effie: EffieData<EffieSources>,\n metadata?: Record<string, unknown>,\n) => BackendConfig | null;\n\nexport type WarmupJob = {\n sources: EffieSourceWithType[];\n metadata?: Record<string, unknown>;\n};\n\nexport type RenderJob = {\n effie: EffieData<EffieSources>;\n scale: number;\n upload?: UploadOptions;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type WarmupAndRenderJob = {\n effie: EffieData<EffieSources>;\n sources: EffieSourceWithType[];\n scale: number;\n upload?: UploadOptions;\n warmupJobId: string;\n renderJobId: string;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type ServerContext = {\n transientStore: TransientStore;\n httpProxy?: HttpProxy;\n baseUrl: string;\n skipValidation: boolean;\n warmupConcurrency: number;\n warmupBackendResolver?: WarmupBackendResolver;\n renderBackendResolver?: RenderBackendResolver;\n};\n\nexport type SSEEventSender = (event: string, data: object) => void;\n\nexport type ParseEffieResult =\n | { effie: EffieData<EffieSources> }\n | { error: string; issues?: object[] };\n\n/**\n * Create the server context with configuration from environment variables\n */\nexport async function createServerContext(options?: {\n warmupBackendResolver?: WarmupBackendResolver;\n renderBackendResolver?: RenderBackendResolver;\n httpProxy?: boolean;\n}): Promise<ServerContext> {\n const port = process.env.FFS_PORT || process.env.PORT || 2000;\n const enableHttpProxy = options?.httpProxy ?? !options?.renderBackendResolver;\n let httpProxy: HttpProxy | undefined;\n if (enableHttpProxy) {\n httpProxy = new HttpProxy();\n await httpProxy.start();\n }\n return {\n transientStore: createTransientStore(),\n httpProxy,\n baseUrl: process.env.FFS_BASE_URL || `http://localhost:${port}`,\n skipValidation:\n !!process.env.FFS_SKIP_VALIDATION &&\n process.env.FFS_SKIP_VALIDATION !== \"false\",\n warmupConcurrency: parseInt(process.env.FFS_WARMUP_CONCURRENCY || \"4\", 10),\n warmupBackendResolver: options?.warmupBackendResolver,\n renderBackendResolver: options?.renderBackendResolver,\n };\n}\n\n/**\n * Parse and validate Effie data from request body\n */\nexport function parseEffieData(\n body: unknown,\n skipValidation: boolean,\n): ParseEffieResult {\n // Wrapped format has `effie` property\n const isWrapped =\n typeof body === \"object\" && body !== null && \"effie\" in body;\n const rawEffieData = isWrapped ? (body as { effie: unknown }).effie : body;\n\n if (!skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n return {\n error: \"Invalid effie data\",\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n };\n }\n return { effie: result.data };\n } else {\n const effie = rawEffieData as EffieData<EffieSources>;\n if (!effie?.segments) {\n return { error: \"Invalid effie data: missing segments\" };\n }\n return { effie };\n }\n}\n\n/**\n * Set up CORS headers for public endpoints\n */\nexport function setupCORSHeaders(res: express.Response): void {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET\");\n}\n\n/**\n * Set up SSE response headers\n */\nexport function setupSSEResponse(res: express.Response): void {\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n res.flushHeaders();\n}\n\n/**\n * Create an SSE event sender function for a response\n */\nexport function createSSEEventSender(res: express.Response): SSEEventSender {\n return (event: string, data: object) => {\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n };\n}\n","import http from \"http\";\nimport type { AddressInfo, Server } from \"net\";\nimport { Readable } from \"stream\";\nimport { ffsFetch } from \"./fetch\";\n\n/**\n * HTTP proxy for FFmpeg URL handling.\n *\n * Static FFmpeg binaries can have DNS resolution issues on Alpine Linux (musl libc).\n * This proxy lets Node.js handle DNS lookups instead of FFmpeg by proxying HTTP\n * requests through localhost.\n *\n * URL scheme (M3U8-compatible):\n * - Original: https://cdn.example.com/path/to/stream.m3u8\n * - Proxy: http://127.0.0.1:{port}/https://cdn.example.com/path/to/stream.m3u8\n * - Relative: segment-0.ts → http://127.0.0.1:{port}/https://cdn.example.com/path/to/segment-0.ts\n */\nexport class HttpProxy {\n private server: Server | null = null;\n private _port: number | null = null;\n private startPromise: Promise<void> | null = null;\n\n get port(): number | null {\n return this._port;\n }\n\n /**\n * Transform a URL to go through the proxy.\n * @throws Error if proxy not started\n */\n transformUrl(url: string): string {\n if (this._port === null) throw new Error(\"Proxy not started\");\n return `http://127.0.0.1:${this._port}/${url}`;\n }\n\n /**\n * Start the proxy server. Safe to call multiple times.\n */\n async start(): Promise<void> {\n if (this._port !== null) return;\n if (this.startPromise) {\n await this.startPromise;\n return;\n }\n this.startPromise = this.doStart();\n await this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n this.server = http.createServer(async (req, res) => {\n try {\n const originalUrl = this.parseProxyPath(req.url || \"\");\n if (!originalUrl) {\n res.writeHead(400, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Request: invalid proxy path\");\n return;\n }\n\n const response = await ffsFetch(originalUrl, {\n method: req.method as \"GET\" | \"HEAD\" | undefined,\n headers: this.filterHeaders(req.headers),\n bodyTimeout: 0, // No timeout for streaming\n });\n\n // Convert response headers to plain object\n const headers: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n res.writeHead(response.status, headers);\n\n if (response.body) {\n const nodeStream = Readable.fromWeb(response.body);\n nodeStream.pipe(res);\n nodeStream.on(\"error\", (err) => {\n console.error(\"Proxy stream error:\", err);\n res.destroy();\n });\n } else {\n res.end();\n }\n } catch (err) {\n console.error(\"Proxy request error:\", err);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"text/plain\" });\n res.end(\"Bad Gateway\");\n } else {\n res.destroy();\n }\n }\n });\n\n await new Promise<void>((resolve) => {\n this.server!.listen(0, \"127.0.0.1\", () => {\n this._port = (this.server!.address() as AddressInfo).port;\n resolve();\n });\n });\n }\n\n /**\n * Parse the proxy path to extract the original URL.\n * Path format: /{originalUrl}\n */\n private parseProxyPath(path: string): string | null {\n if (!path.startsWith(\"/http://\") && !path.startsWith(\"/https://\")) {\n return null;\n }\n return path.slice(1); // Remove leading /\n }\n\n /**\n * Filter headers to forward to the upstream server.\n * Removes hop-by-hop headers that shouldn't be forwarded.\n */\n private filterHeaders(\n headers: http.IncomingHttpHeaders,\n ): Record<string, string> {\n const skip = new Set([\n \"host\",\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n ]);\n\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (!skip.has(key.toLowerCase()) && typeof value === \"string\") {\n result[key] = value;\n }\n }\n return result;\n }\n\n /**\n * Close the proxy server and reset state.\n */\n close(): void {\n this.server?.close();\n this.server = null;\n this._port = null;\n this.startPromise = null;\n }\n}\n","import express from \"express\";\nimport { Readable, Transform } from \"stream\";\nimport { randomUUID } from \"crypto\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport {\n extractEffieSources,\n extractEffieSourcesWithTypes,\n} from \"@effing/effie\";\nimport type { EffieSourceWithType } from \"@effing/effie\";\nimport type { ServerContext, SSEEventSender, WarmupJob } from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\nimport { proxyRemoteSSE } from \"./orchestrating\";\n\n/**\n * Check if a source should be skipped during warmup.\n * Video/audio sources are passed directly to FFmpeg and don't need caching.\n */\nfunction shouldSkipWarmup(source: EffieSourceWithType): boolean {\n return source.type === \"video\" || source.type === \"audio\";\n}\n\n// Track in-flight fetches to avoid duplicate fetches within the same instance\nconst inFlightFetches = new Map<string, Promise<void>>();\n\n/**\n * POST /warmup - Create a warmup job\n * Stores the source list in cache and returns a job ID for SSE streaming\n */\nexport async function createWarmupJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { metadata?: Record<string, unknown> },\n): Promise<void> {\n try {\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSourcesWithTypes(parseResult.effie);\n const jobId = randomUUID();\n\n const job: WarmupJob = { sources, metadata: options?.metadata };\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(jobId),\n job,\n ctx.transientStore.jobDataTtlMs,\n );\n\n res.json({\n id: jobId,\n url: `${ctx.baseUrl}/warmup/${jobId}`,\n });\n } catch (error) {\n console.error(\"Error creating warmup job:\", error);\n res.status(500).json({ error: \"Failed to create warmup job\" });\n }\n}\n\n/**\n * GET /warmup/:id - Stream warmup progress via SSE\n * Fetches and caches sources, emitting progress events\n */\nexport async function streamWarmupJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n\n const jobStoreKey = storeKeys.warmupJob(jobId);\n const job = await ctx.transientStore.getJson<WarmupJob>(jobStoreKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found\" });\n return;\n }\n\n // Proxy to warmup backend if resolver is configured\n if (ctx.warmupBackendResolver) {\n const backend = ctx.warmupBackendResolver(job.sources, job.metadata);\n if (backend) {\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n try {\n await proxyRemoteSSE(\n `${backend.baseUrl}/warmup/${jobId}`,\n sendEvent,\n \"\",\n res,\n backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n );\n } finally {\n res.end();\n }\n return;\n }\n }\n\n // Local warmup — only allow the warmup job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n try {\n await warmupSources(job.sources, sendEvent, ctx);\n sendEvent(\"complete\", { status: \"ready\" });\n } catch (error) {\n sendEvent(\"error\", { message: String(error) });\n } finally {\n res.end();\n }\n } catch (error) {\n console.error(\"Error in warmup streaming:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Warmup streaming failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * POST /purge - Purge cached sources for an Effie composition\n */\nexport async function purgeCache(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n const parseResult = parseEffieData(req.body, ctx.skipValidation);\n if (\"error\" in parseResult) {\n res.status(400).json(parseResult);\n return;\n }\n\n const sources = extractEffieSources(parseResult.effie);\n\n let purged = 0;\n for (const url of sources) {\n const ck = storeKeys.source(url);\n if (await ctx.transientStore.exists(ck)) {\n await ctx.transientStore.delete(ck);\n purged++;\n }\n }\n\n res.json({ purged, total: sources.length });\n } catch (error) {\n console.error(\"Error purging cache:\", error);\n res.status(500).json({ error: \"Failed to purge cache\" });\n }\n}\n\n/**\n * Warm up sources by fetching and caching them.\n * HTTP(S) video/audio sources are skipped as they are passed directly to FFmpeg.\n */\nexport async function warmupSources(\n sources: EffieSourceWithType[],\n sendEvent: SSEEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const total = sources.length;\n\n sendEvent(\"start\", { total });\n\n let cached = 0;\n let failed = 0;\n let skipped = 0;\n\n // Separate sources that need caching from those that should be skipped\n const sourcesToCache: EffieSourceWithType[] = [];\n for (const source of sources) {\n if (shouldSkipWarmup(source)) {\n skipped++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"skipped\",\n reason: \"http-video-audio-passthrough\",\n cached,\n failed,\n skipped,\n total,\n });\n } else {\n sourcesToCache.push(source);\n }\n }\n\n // Check what's already cached\n const sourceCacheKeys = sourcesToCache.map((s) => storeKeys.source(s.url));\n const existsMap = await ctx.transientStore.existsMany(sourceCacheKeys);\n\n // Report hits immediately\n for (let i = 0; i < sourcesToCache.length; i++) {\n if (existsMap.get(sourceCacheKeys[i])) {\n cached++;\n sendEvent(\"progress\", {\n url: sourcesToCache[i].url,\n status: \"hit\",\n cached,\n failed,\n skipped,\n total,\n });\n }\n }\n\n // Filter to uncached sources\n const uncached = sourcesToCache.filter(\n (_, i) => !existsMap.get(sourceCacheKeys[i]),\n );\n\n if (uncached.length === 0) {\n sendEvent(\"summary\", { cached, failed, skipped, total });\n return;\n }\n\n // Keepalive interval for long-running fetches\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { cached, failed, skipped, total });\n }, 25_000);\n\n // Fetch uncached sources with concurrency limit\n const queue = [...uncached];\n const workers = Array.from(\n { length: Math.min(ctx.warmupConcurrency, queue.length) },\n async () => {\n while (queue.length > 0) {\n const source = queue.shift()!;\n const cacheKey = storeKeys.source(source.url);\n const startTime = Date.now();\n\n try {\n // Check if another worker is already fetching this\n let fetchPromise = inFlightFetches.get(cacheKey);\n if (!fetchPromise) {\n fetchPromise = fetchAndCache(source.url, cacheKey, sendEvent, ctx);\n inFlightFetches.set(cacheKey, fetchPromise);\n }\n\n await fetchPromise;\n inFlightFetches.delete(cacheKey);\n\n cached++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"cached\",\n cached,\n failed,\n skipped,\n total,\n ms: Date.now() - startTime,\n });\n } catch (error) {\n inFlightFetches.delete(cacheKey);\n failed++;\n sendEvent(\"progress\", {\n url: source.url,\n status: \"error\",\n error: String(error),\n cached,\n failed,\n skipped,\n total,\n ms: Date.now() - startTime,\n });\n }\n }\n },\n );\n\n await Promise.all(workers);\n clearInterval(keepalive);\n\n sendEvent(\"summary\", { cached, failed, skipped, total });\n}\n\n/**\n * Fetch a source and cache it, with streaming progress events\n */\nexport async function fetchAndCache(\n url: string,\n cacheKey: string,\n sendEvent: SSEEventSender,\n ctx: ServerContext,\n): Promise<void> {\n const response = await ffsFetch(url, {\n headersTimeout: 10 * 60 * 1000, // 10 minutes\n bodyTimeout: 20 * 60 * 1000, // 20 minutes\n });\n\n if (!response.ok) {\n throw new Error(`${response.status} ${response.statusText}`);\n }\n\n sendEvent(\"downloading\", { url, status: \"started\", bytesReceived: 0 });\n\n // Stream through a progress tracker\n const sourceStream = Readable.fromWeb(\n response.body as import(\"stream/web\").ReadableStream,\n );\n\n let totalBytes = 0;\n let lastEventTime = Date.now();\n const PROGRESS_INTERVAL = 10_000; // 10 seconds\n\n const progressStream = new Transform({\n transform(chunk, _encoding, callback) {\n totalBytes += chunk.length;\n const now = Date.now();\n if (now - lastEventTime >= PROGRESS_INTERVAL) {\n sendEvent(\"downloading\", {\n url,\n status: \"downloading\",\n bytesReceived: totalBytes,\n });\n lastEventTime = now;\n }\n callback(null, chunk);\n },\n });\n\n // Pipe through progress tracker to cache storage with source TTL\n const trackedStream = sourceStream.pipe(progressStream);\n await ctx.transientStore.put(\n cacheKey,\n trackedStream,\n ctx.transientStore.sourceTtlMs,\n );\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport type { Response as UndiciResponse } from \"undici\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport { extractEffieSourcesWithTypes, effieDataSchema } from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type {\n ServerContext,\n SSEEventSender,\n WarmupAndRenderJob,\n RenderJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\nimport { warmupSources } from \"./caching\";\nimport { renderAndUploadInternal } from \"./rendering\";\n\n/**\n * POST /warmup-and-render - Create a combined warmup and render job\n * Returns a job ID and URL for SSE streaming\n */\nexport async function createWarmupAndRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { metadata?: Record<string, unknown> },\n): Promise<void> {\n try {\n // Parse request body\n const body = req.body as {\n effie: unknown;\n scale?: number;\n upload?: UploadOptions;\n };\n\n let rawEffieData: unknown;\n if (typeof body.effie === \"string\") {\n // Effie is a URL to fetch the EffieData from\n const response = await ffsFetch(body.effie);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n }\n rawEffieData = await response.json();\n } else {\n rawEffieData = body.effie;\n }\n\n // Validate/parse effie data\n let effie: EffieData<EffieSources>;\n if (!ctx.skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n res.status(400).json({\n error: \"Invalid effie data\",\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n });\n return;\n }\n effie = result.data;\n } else {\n const data = rawEffieData as EffieData<EffieSources>;\n if (!data?.segments) {\n res.status(400).json({ error: \"Invalid effie data: missing segments\" });\n return;\n }\n effie = data;\n }\n\n const sources = extractEffieSourcesWithTypes(effie);\n const scale = body.scale ?? 1;\n const upload = body.upload;\n\n // Create IDs for warmup and render sub-jobs\n const jobId = randomUUID();\n const warmupJobId = randomUUID();\n const renderJobId = randomUUID();\n\n // Store the combined job\n const job: WarmupAndRenderJob = {\n effie,\n sources,\n scale,\n upload,\n warmupJobId,\n renderJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n await ctx.transientStore.putJson(\n storeKeys.warmupAndRenderJob(jobId),\n job,\n ctx.transientStore.jobDataTtlMs,\n );\n\n // Also store sub-jobs for backend execution\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(warmupJobId),\n { sources, metadata: options?.metadata },\n ctx.transientStore.jobDataTtlMs,\n );\n await ctx.transientStore.putJson(\n storeKeys.renderJob(renderJobId),\n {\n effie,\n scale,\n upload,\n createdAt: Date.now(),\n metadata: options?.metadata,\n } satisfies RenderJob,\n ctx.transientStore.jobDataTtlMs,\n );\n\n res.json({\n id: jobId,\n url: `${ctx.baseUrl}/warmup-and-render/${jobId}`,\n });\n } catch (error) {\n console.error(\"Error creating warmup-and-render job:\", error);\n res.status(500).json({ error: \"Failed to create warmup-and-render job\" });\n }\n}\n\n/**\n * GET /warmup-and-render/:id - Stream warmup and render progress via SSE\n * Orchestrates warmup (local or remote) followed by render (local or remote)\n */\nexport async function streamWarmupAndRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);\n const job =\n await ctx.transientStore.getJson<WarmupAndRenderJob>(jobStoreKey);\n // Only allow the job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found\" });\n return;\n }\n\n // Resolve backends up front\n const warmupBackend = ctx.warmupBackendResolver\n ? ctx.warmupBackendResolver(job.sources, job.metadata)\n : null;\n const renderBackend = ctx.renderBackendResolver\n ? ctx.renderBackendResolver(job.effie, job.metadata)\n : null;\n\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n // Keepalive interval for long-running operations\n let keepalivePhase: \"warmup\" | \"render\" = \"warmup\";\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { phase: keepalivePhase });\n }, 25_000);\n\n try {\n // Phase 1: Warmup\n if (warmupBackend) {\n // Proxy warmup from remote backend\n await proxyRemoteSSE(\n `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,\n sendEvent,\n \"warmup:\",\n res,\n warmupBackend.apiKey\n ? { Authorization: `Bearer ${warmupBackend.apiKey}` }\n : undefined,\n );\n } else {\n // Local warmup execution\n const warmupSender = prefixEventSender(sendEvent, \"warmup:\");\n await warmupSources(job.sources, warmupSender, ctx);\n warmupSender(\"complete\", { status: \"ready\" });\n }\n\n // Phase 2: Render\n keepalivePhase = \"render\";\n\n if (renderBackend) {\n // Proxy render from remote backend\n await proxyRemoteSSE(\n `${renderBackend.baseUrl}/render/${job.renderJobId}`,\n sendEvent,\n \"render:\",\n res,\n renderBackend.apiKey\n ? { Authorization: `Bearer ${renderBackend.apiKey}` }\n : undefined,\n );\n } else {\n // Local render execution\n const renderSender = prefixEventSender(sendEvent, \"render:\");\n\n if (job.upload) {\n // Upload mode: render and upload, emit SSE events\n renderSender(\"started\", { status: \"rendering\" });\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload,\n renderSender,\n ctx,\n );\n renderSender(\"complete\", { status: \"uploaded\", timings });\n } else {\n // Non-upload mode: return URL to existing render job\n const videoUrl = `${ctx.baseUrl}/render/${job.renderJobId}`;\n sendEvent(\"complete\", { status: \"ready\", videoUrl });\n }\n }\n\n // Final complete event (only for upload mode, non-upload already sent complete)\n if (job.upload && !renderBackend) {\n sendEvent(\"complete\", { status: \"done\" });\n }\n } catch (error) {\n sendEvent(\"error\", {\n phase: keepalivePhase,\n message: String(error),\n });\n } finally {\n clearInterval(keepalive);\n res.end();\n }\n } catch (error) {\n console.error(\"Error in warmup-and-render streaming:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Warmup-and-render streaming failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Create a prefixed event sender that adds a prefix to event names\n */\nexport function prefixEventSender(\n sendEvent: SSEEventSender,\n prefix: string,\n): SSEEventSender {\n return (event: string, data: object) => {\n sendEvent(`${prefix}${event}`, data);\n };\n}\n\n/**\n * Proxy SSE events from a remote backend, prefixing event names\n */\nexport async function proxyRemoteSSE(\n url: string,\n sendEvent: SSEEventSender,\n prefix: string,\n res: express.Response,\n headers?: Record<string, string>,\n): Promise<void> {\n const response = await ffsFetch(url, {\n headers: {\n Accept: \"text/event-stream\",\n ...headers,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Remote backend error: ${response.status}`);\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response body from remote backend\");\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n // Check if client disconnected\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n // Parse SSE events from buffer\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n let currentEvent = \"\";\n let currentData = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n currentEvent = line.slice(7);\n } else if (line.startsWith(\"data: \")) {\n currentData = line.slice(6);\n } else if (line === \"\" && currentEvent && currentData) {\n // End of event, forward it with prefix\n try {\n const data = JSON.parse(currentData);\n sendEvent(`${prefix}${currentEvent}`, data);\n } catch {\n // Skip malformed JSON\n }\n currentEvent = \"\";\n currentData = \"\";\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Proxy a binary stream (e.g., video) from a fetch Response to an Express response.\n * Forwards Content-Type and Content-Length headers.\n */\nexport async function proxyBinaryStream(\n response: UndiciResponse,\n res: express.Response,\n): Promise<void> {\n const contentType = response.headers.get(\"content-type\");\n if (contentType) res.set(\"Content-Type\", contentType);\n\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength) res.set(\"Content-Length\", contentLength);\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error(\"No response body\");\n }\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n res.write(value);\n }\n } finally {\n reader.releaseLock();\n res.end();\n }\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport { effieDataSchema } from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type {\n ServerContext,\n SSEEventSender,\n BackendConfig,\n RenderJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n} from \"./shared\";\nimport { proxyBinaryStream } from \"./orchestrating\";\n\n/**\n * POST /render - Create a render job\n * Returns a job ID and URL for streaming the rendered video\n */\nexport async function createRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n options?: { metadata?: Record<string, unknown> },\n): Promise<void> {\n try {\n // Wrapped format has `effie` property,\n // otherwise it's just raw EffieData (which doesn't have an `effie` property)\n const isWrapped = \"effie\" in req.body;\n\n let rawEffieData: unknown;\n let scale: number;\n let upload: UploadOptions | undefined;\n\n if (isWrapped) {\n // Wrapped format: { effie: EffieData | string, scale?, upload? }\n const options = req.body as {\n effie: unknown;\n scale?: number;\n upload?: UploadOptions;\n };\n\n if (typeof options.effie === \"string\") {\n // Effie is a string, so it's a URL to fetch the EffieData from\n const response = await ffsFetch(options.effie);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n }\n rawEffieData = await response.json();\n } else {\n // Effie is an EffieData object\n rawEffieData = options.effie;\n }\n\n scale = options.scale ?? 1;\n upload = options.upload;\n } else {\n // Body is the EffieData, options in query params\n rawEffieData = req.body;\n scale = parseFloat(req.query.scale?.toString() || \"1\");\n }\n\n // Validate/parse effie data (validation can be disabled by setting FFS_SKIP_VALIDATION)\n let effie: EffieData<EffieSources>;\n if (!ctx.skipValidation) {\n const result = effieDataSchema.safeParse(rawEffieData);\n if (!result.success) {\n res.status(400).json({\n error: \"Invalid effie data\",\n issues: result.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n })),\n });\n return;\n }\n effie = result.data;\n } else {\n // Minimal validation when schema validation is disabled\n const data = rawEffieData as EffieData<EffieSources>;\n if (!data?.segments) {\n res.status(400).json({ error: \"Invalid effie data: missing segments\" });\n return;\n }\n effie = data;\n }\n\n // Create render job\n const jobId = randomUUID();\n const job: RenderJob = {\n effie,\n scale,\n upload,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.jobDataTtlMs,\n );\n\n res.json({\n id: jobId,\n url: `${ctx.baseUrl}/render/${jobId}`,\n });\n } catch (error) {\n console.error(\"Error creating render job:\", error);\n res.status(500).json({ error: \"Failed to create render job\" });\n }\n}\n\n/**\n * GET /render/:id - Execute render job\n * Streams video directly (no upload) or SSE progress events (with upload)\n */\nexport async function streamRenderJob(\n req: express.Request,\n res: express.Response,\n ctx: ServerContext,\n): Promise<void> {\n try {\n setupCORSHeaders(res);\n\n const jobId = req.params.id;\n\n const jobStoreKey = storeKeys.renderJob(jobId);\n const job = await ctx.transientStore.getJson<RenderJob>(jobStoreKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found or expired\" });\n return;\n }\n\n // Proxy to render backend if resolver is configured\n if (ctx.renderBackendResolver) {\n const backend = ctx.renderBackendResolver(job.effie, job.metadata);\n if (backend) {\n await proxyRenderFromBackend(res, jobId, backend);\n return;\n }\n }\n\n // Render locally — only allow the render job to run once\n ctx.transientStore.delete(jobStoreKey);\n\n // Dispatch based on upload mode\n if (job.upload) {\n await streamRenderWithUpload(res, job, ctx);\n } else {\n await streamRenderDirect(res, job, ctx);\n }\n } catch (error) {\n console.error(\"Error in render:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Rendering failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Stream video directly to the response (no upload)\n */\nexport async function streamRenderDirect(\n res: express.Response,\n job: RenderJob,\n ctx: ServerContext,\n): Promise<void> {\n const { EffieRenderer } = await import(\"../render\");\n const renderer = new EffieRenderer(job.effie, {\n transientStore: ctx.transientStore,\n httpProxy: ctx.httpProxy,\n });\n const videoStream = await renderer.render(job.scale);\n\n res.on(\"close\", () => {\n videoStream.destroy();\n renderer.close();\n });\n\n res.set(\"Content-Type\", \"video/mp4\");\n res.set(\"Cache-Control\", \"public, immutable, max-age=86400\");\n videoStream.pipe(res);\n}\n\n/**\n * Render and upload, streaming SSE progress events\n */\nexport async function streamRenderWithUpload(\n res: express.Response,\n job: RenderJob,\n ctx: ServerContext,\n): Promise<void> {\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n // Keepalive interval for long-running renders\n const keepalive = setInterval(() => {\n sendEvent(\"keepalive\", { status: \"rendering\" });\n }, 25_000);\n\n try {\n sendEvent(\"started\", { status: \"rendering\" });\n\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload!,\n sendEvent,\n ctx,\n );\n\n sendEvent(\"complete\", { status: \"uploaded\", timings });\n } catch (error) {\n sendEvent(\"error\", { message: String(error) });\n } finally {\n clearInterval(keepalive);\n res.end();\n }\n}\n\n/**\n * Internal render and upload logic\n * Returns timings for the SSE complete event\n */\nexport async function renderAndUploadInternal(\n effie: EffieData<EffieSources>,\n scale: number,\n upload: UploadOptions,\n sendEvent: SSEEventSender,\n ctx: ServerContext,\n): Promise<Record<string, number>> {\n const timings: Record<string, number> = {};\n\n // Fetch and upload cover if coverUrl provided\n if (upload.coverUrl) {\n const fetchCoverStartTime = Date.now();\n const coverFetchResponse = await ffsFetch(effie.cover);\n if (!coverFetchResponse.ok) {\n throw new Error(\n `Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`,\n );\n }\n const coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());\n timings.fetchCoverTime = Date.now() - fetchCoverStartTime;\n\n const uploadCoverStartTime = Date.now();\n const uploadCoverResponse = await ffsFetch(upload.coverUrl, {\n method: \"PUT\",\n body: coverBuffer,\n headers: {\n \"Content-Type\": \"image/png\",\n \"Content-Length\": coverBuffer.length.toString(),\n },\n });\n if (!uploadCoverResponse.ok) {\n throw new Error(\n `Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`,\n );\n }\n timings.uploadCoverTime = Date.now() - uploadCoverStartTime;\n }\n\n // Render effie data to video\n const renderStartTime = Date.now();\n const { EffieRenderer } = await import(\"../render\");\n const renderer = new EffieRenderer(effie, {\n transientStore: ctx.transientStore,\n httpProxy: ctx.httpProxy,\n });\n const videoStream = await renderer.render(scale);\n const chunks: Buffer[] = [];\n for await (const chunk of videoStream) {\n chunks.push(Buffer.from(chunk));\n }\n const videoBuffer = Buffer.concat(chunks);\n timings.renderTime = Date.now() - renderStartTime;\n\n // Update keepalive status for upload phase\n sendEvent(\"keepalive\", { status: \"uploading\" });\n\n // Upload rendered video\n const uploadStartTime = Date.now();\n const uploadResponse = await ffsFetch(upload.videoUrl, {\n method: \"PUT\",\n body: videoBuffer,\n headers: {\n \"Content-Type\": \"video/mp4\",\n \"Content-Length\": videoBuffer.length.toString(),\n },\n });\n if (!uploadResponse.ok) {\n throw new Error(\n `Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`,\n );\n }\n timings.uploadTime = Date.now() - uploadStartTime;\n\n return timings;\n}\n\n/**\n * Proxy render from backend based on Content-Type.\n * SSE (upload mode) uses proxyRemoteSSE, video stream uses proxyBinaryStream.\n */\nasync function proxyRenderFromBackend(\n res: express.Response,\n jobId: string,\n backend: BackendConfig,\n): Promise<void> {\n const backendUrl = `${backend.baseUrl}/render/${jobId}`;\n const response = await ffsFetch(backendUrl, {\n headers: backend.apiKey\n ? { Authorization: `Bearer ${backend.apiKey}` }\n : undefined,\n });\n\n if (!response.ok) {\n res.status(response.status).json({ error: \"Backend render failed\" });\n return;\n }\n\n const contentType = response.headers.get(\"content-type\") || \"\";\n\n if (contentType.includes(\"text/event-stream\")) {\n // Upload mode: proxy SSE events\n setupSSEResponse(res);\n const sendEvent = createSSEEventSender(res);\n\n const reader = response.body?.getReader();\n if (!reader) {\n sendEvent(\"error\", { message: \"No response body from backend\" });\n res.end();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (res.destroyed) {\n reader.cancel();\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\";\n\n let currentEvent = \"\";\n let currentData = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event: \")) {\n currentEvent = line.slice(7);\n } else if (line.startsWith(\"data: \")) {\n currentData = line.slice(6);\n } else if (line === \"\" && currentEvent && currentData) {\n try {\n const data = JSON.parse(currentData);\n sendEvent(currentEvent, data);\n } catch {\n // Skip malformed JSON\n }\n currentEvent = \"\";\n currentData = \"\";\n }\n }\n }\n } finally {\n reader.releaseLock();\n res.end();\n }\n } else {\n // Non-upload mode: proxy binary video stream\n await proxyBinaryStream(response, res);\n }\n}\n"],"mappings":"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","names":["Readable","randomUUID","extractEffieSourcesWithTypes","randomUUID","effieDataSchema","effieDataSchema","options","effieDataSchema","effieDataSchema","randomUUID","extractEffieSourcesWithTypes","randomUUID","sendEvent","Readable"]}
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { T as TransientStore, H as HttpProxy } from '../proxy-
|
|
1
|
+
import { T as TransientStore, H as HttpProxy } from '../proxy-CsZ5h2Ya.js';
|
|
2
2
|
import { EffieSourceWithType, EffieData, EffieSources } from '@effing/effie';
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import { Response } from 'undici';
|
|
@@ -8,14 +8,22 @@ type UploadOptions = {
|
|
|
8
8
|
videoUrl: string;
|
|
9
9
|
coverUrl?: string;
|
|
10
10
|
};
|
|
11
|
+
type BackendConfig = {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
};
|
|
15
|
+
type WarmupBackendResolver = (sources: EffieSourceWithType[], metadata?: Record<string, unknown>) => BackendConfig | null;
|
|
16
|
+
type RenderBackendResolver = (effie: EffieData<EffieSources>, metadata?: Record<string, unknown>) => BackendConfig | null;
|
|
11
17
|
type WarmupJob = {
|
|
12
18
|
sources: EffieSourceWithType[];
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
13
20
|
};
|
|
14
21
|
type RenderJob = {
|
|
15
22
|
effie: EffieData<EffieSources>;
|
|
16
23
|
scale: number;
|
|
17
24
|
upload?: UploadOptions;
|
|
18
25
|
createdAt: number;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
19
27
|
};
|
|
20
28
|
type WarmupAndRenderJob = {
|
|
21
29
|
effie: EffieData<EffieSources>;
|
|
@@ -25,6 +33,7 @@ type WarmupAndRenderJob = {
|
|
|
25
33
|
warmupJobId: string;
|
|
26
34
|
renderJobId: string;
|
|
27
35
|
createdAt: number;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
28
37
|
};
|
|
29
38
|
type ServerContext = {
|
|
30
39
|
transientStore: TransientStore;
|
|
@@ -32,22 +41,26 @@ type ServerContext = {
|
|
|
32
41
|
baseUrl: string;
|
|
33
42
|
skipValidation: boolean;
|
|
34
43
|
warmupConcurrency: number;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
warmupBackendApiKey?: string;
|
|
38
|
-
renderBackendApiKey?: string;
|
|
44
|
+
warmupBackendResolver?: WarmupBackendResolver;
|
|
45
|
+
renderBackendResolver?: RenderBackendResolver;
|
|
39
46
|
};
|
|
40
47
|
type SSEEventSender = (event: string, data: object) => void;
|
|
41
48
|
/**
|
|
42
49
|
* Create the server context with configuration from environment variables
|
|
43
50
|
*/
|
|
44
|
-
declare function createServerContext(
|
|
51
|
+
declare function createServerContext(options?: {
|
|
52
|
+
warmupBackendResolver?: WarmupBackendResolver;
|
|
53
|
+
renderBackendResolver?: RenderBackendResolver;
|
|
54
|
+
httpProxy?: boolean;
|
|
55
|
+
}): Promise<ServerContext>;
|
|
45
56
|
|
|
46
57
|
/**
|
|
47
58
|
* POST /warmup - Create a warmup job
|
|
48
59
|
* Stores the source list in cache and returns a job ID for SSE streaming
|
|
49
60
|
*/
|
|
50
|
-
declare function createWarmupJob(req: express.Request, res: express.Response, ctx: ServerContext
|
|
61
|
+
declare function createWarmupJob(req: express.Request, res: express.Response, ctx: ServerContext, options?: {
|
|
62
|
+
metadata?: Record<string, unknown>;
|
|
63
|
+
}): Promise<void>;
|
|
51
64
|
/**
|
|
52
65
|
* GET /warmup/:id - Stream warmup progress via SSE
|
|
53
66
|
* Fetches and caches sources, emitting progress events
|
|
@@ -62,7 +75,9 @@ declare function purgeCache(req: express.Request, res: express.Response, ctx: Se
|
|
|
62
75
|
* POST /render - Create a render job
|
|
63
76
|
* Returns a job ID and URL for streaming the rendered video
|
|
64
77
|
*/
|
|
65
|
-
declare function createRenderJob(req: express.Request, res: express.Response, ctx: ServerContext
|
|
78
|
+
declare function createRenderJob(req: express.Request, res: express.Response, ctx: ServerContext, options?: {
|
|
79
|
+
metadata?: Record<string, unknown>;
|
|
80
|
+
}): Promise<void>;
|
|
66
81
|
/**
|
|
67
82
|
* GET /render/:id - Execute render job
|
|
68
83
|
* Streams video directly (no upload) or SSE progress events (with upload)
|
|
@@ -73,7 +88,9 @@ declare function streamRenderJob(req: express.Request, res: express.Response, ct
|
|
|
73
88
|
* POST /warmup-and-render - Create a combined warmup and render job
|
|
74
89
|
* Returns a job ID and URL for SSE streaming
|
|
75
90
|
*/
|
|
76
|
-
declare function createWarmupAndRenderJob(req: express.Request, res: express.Response, ctx: ServerContext
|
|
91
|
+
declare function createWarmupAndRenderJob(req: express.Request, res: express.Response, ctx: ServerContext, options?: {
|
|
92
|
+
metadata?: Record<string, unknown>;
|
|
93
|
+
}): Promise<void>;
|
|
77
94
|
/**
|
|
78
95
|
* GET /warmup-and-render/:id - Stream warmup and render progress via SSE
|
|
79
96
|
* Orchestrates warmup (local or remote) followed by render (local or remote)
|
|
@@ -89,4 +106,4 @@ declare function proxyRemoteSSE(url: string, sendEvent: SSEEventSender, prefix:
|
|
|
89
106
|
*/
|
|
90
107
|
declare function proxyBinaryStream(response: Response, res: express.Response): Promise<void>;
|
|
91
108
|
|
|
92
|
-
export { type RenderJob, type SSEEventSender, type ServerContext, type UploadOptions, type WarmupAndRenderJob, type WarmupJob, createRenderJob, createServerContext, createWarmupAndRenderJob, createWarmupJob, proxyBinaryStream, proxyRemoteSSE, purgeCache, streamRenderJob, streamWarmupAndRenderJob, streamWarmupJob };
|
|
109
|
+
export { type BackendConfig, type RenderBackendResolver, type RenderJob, type SSEEventSender, type ServerContext, type UploadOptions, type WarmupAndRenderJob, type WarmupBackendResolver, type WarmupJob, createRenderJob, createServerContext, createWarmupAndRenderJob, createWarmupJob, proxyBinaryStream, proxyRemoteSSE, purgeCache, streamRenderJob, streamWarmupAndRenderJob, streamWarmupJob };
|
package/dist/handlers/index.js
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
streamRenderJob,
|
|
10
10
|
streamWarmupAndRenderJob,
|
|
11
11
|
streamWarmupJob
|
|
12
|
-
} from "../chunk-
|
|
13
|
-
import "../chunk-
|
|
12
|
+
} from "../chunk-WD7MF3GH.js";
|
|
13
|
+
import "../chunk-4N2GLGC5.js";
|
|
14
14
|
export {
|
|
15
15
|
createRenderJob,
|
|
16
16
|
createServerContext,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable } from 'stream';
|
|
2
2
|
import { EffieWebUrl, EffieData, EffieSources } from '@effing/effie';
|
|
3
|
-
import { T as TransientStore, H as HttpProxy } from './proxy-
|
|
3
|
+
import { T as TransientStore, H as HttpProxy } from './proxy-CsZ5h2Ya.js';
|
|
4
4
|
|
|
5
5
|
type EffieRendererOptions = {
|
|
6
6
|
/**
|