@effing/ffs 0.7.3 → 0.8.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/README.md +31 -0
- package/dist/{chunk-46RJNEOH.js → chunk-4WWHUVBG.js} +120 -49
- package/dist/chunk-4WWHUVBG.js.map +1 -0
- package/dist/{chunk-2CCHBAXN.js → chunk-O3UH3WAZ.js} +4 -1
- package/dist/{chunk-2CCHBAXN.js.map → chunk-O3UH3WAZ.js.map} +1 -1
- package/dist/handlers/index.d.ts +26 -3
- package/dist/handlers/index.js +5 -1
- package/dist/index.js +1 -1
- package/dist/{render-S3MCYNKR.js → render-DMHG3YJU.js} +2 -2
- package/dist/{render-OMXRUVX5.js → render-YKC5W4YT.js} +3 -0
- package/dist/server.js +118 -49
- package/dist/server.js.map +1 -1
- package/dist/sse.d.ts +72 -0
- package/dist/sse.js +1 -0
- package/dist/sse.js.map +1 -0
- package/package.json +7 -3
- package/dist/chunk-46RJNEOH.js.map +0 -1
- /package/dist/{render-S3MCYNKR.js.map → render-DMHG3YJU.js.map} +0 -0
package/dist/server.js
CHANGED
|
@@ -136,9 +136,27 @@ var HttpProxy = class {
|
|
|
136
136
|
|
|
137
137
|
// src/handlers/shared.ts
|
|
138
138
|
import { effieDataSchema } from "@effing/effie";
|
|
139
|
+
|
|
140
|
+
// src/handlers/errors.ts
|
|
141
|
+
var ErrorCode = {
|
|
142
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
143
|
+
INVALID_EFFIE: "INVALID_EFFIE",
|
|
144
|
+
NOT_FOUND: "NOT_FOUND",
|
|
145
|
+
BACKEND_FAILED: "BACKEND_FAILED",
|
|
146
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
147
|
+
FETCH_FAILED: "FETCH_FAILED"
|
|
148
|
+
};
|
|
149
|
+
function sendError(res, status, code, message, issues) {
|
|
150
|
+
if (res.headersSent) return;
|
|
151
|
+
const body = { error: message, code };
|
|
152
|
+
if (issues) body.issues = issues;
|
|
153
|
+
res.status(status).json(body);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/handlers/shared.ts
|
|
139
157
|
async function createServerContext(options) {
|
|
140
158
|
const port2 = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
141
|
-
const enableHttpProxy = options?.httpProxy ??
|
|
159
|
+
const enableHttpProxy = options?.httpProxy ?? true;
|
|
142
160
|
let httpProxy;
|
|
143
161
|
if (enableHttpProxy) {
|
|
144
162
|
httpProxy = new HttpProxy();
|
|
@@ -162,6 +180,7 @@ function parseEffieData(body, skipValidation) {
|
|
|
162
180
|
if (!result.success) {
|
|
163
181
|
return {
|
|
164
182
|
error: "Invalid effie data",
|
|
183
|
+
code: ErrorCode.INVALID_EFFIE,
|
|
165
184
|
issues: result.error.issues.map((issue) => ({
|
|
166
185
|
path: issue.path.join("."),
|
|
167
186
|
message: issue.message
|
|
@@ -172,7 +191,10 @@ function parseEffieData(body, skipValidation) {
|
|
|
172
191
|
} else {
|
|
173
192
|
const effie = rawEffieData;
|
|
174
193
|
if (!effie?.segments) {
|
|
175
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
error: "Invalid effie data: missing segments",
|
|
196
|
+
code: ErrorCode.INVALID_EFFIE
|
|
197
|
+
};
|
|
176
198
|
}
|
|
177
199
|
return { effie };
|
|
178
200
|
}
|
|
@@ -187,7 +209,7 @@ function setupSSEResponse(res) {
|
|
|
187
209
|
res.setHeader("Connection", "keep-alive");
|
|
188
210
|
res.flushHeaders();
|
|
189
211
|
}
|
|
190
|
-
function
|
|
212
|
+
function createEventSender(res) {
|
|
191
213
|
return (event, data) => {
|
|
192
214
|
res.write(`event: ${event}
|
|
193
215
|
data: ${JSON.stringify(data)}
|
|
@@ -196,9 +218,9 @@ data: ${JSON.stringify(data)}
|
|
|
196
218
|
};
|
|
197
219
|
}
|
|
198
220
|
function prefixEventSender(sendEvent, prefix) {
|
|
199
|
-
return (event, data) => {
|
|
221
|
+
return ((event, data) => {
|
|
200
222
|
sendEvent(`${prefix}${event}`, data);
|
|
201
|
-
};
|
|
223
|
+
});
|
|
202
224
|
}
|
|
203
225
|
async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
204
226
|
const response = await ffsFetch(url, {
|
|
@@ -216,6 +238,8 @@ async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
|
216
238
|
}
|
|
217
239
|
const decoder = new TextDecoder();
|
|
218
240
|
let buffer = "";
|
|
241
|
+
let currentEvent = "";
|
|
242
|
+
let currentData = "";
|
|
219
243
|
try {
|
|
220
244
|
while (true) {
|
|
221
245
|
const { done, value } = await reader.read();
|
|
@@ -227,8 +251,6 @@ async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
|
227
251
|
buffer += decoder.decode(value, { stream: true });
|
|
228
252
|
const lines = buffer.split("\n");
|
|
229
253
|
buffer = lines.pop() || "";
|
|
230
|
-
let currentEvent = "";
|
|
231
|
-
let currentData = "";
|
|
232
254
|
for (const line of lines) {
|
|
233
255
|
if (line.startsWith("event: ")) {
|
|
234
256
|
currentEvent = line.slice(7);
|
|
@@ -307,7 +329,12 @@ async function createWarmupJob(req, res, ctx2, options) {
|
|
|
307
329
|
});
|
|
308
330
|
} catch (error) {
|
|
309
331
|
console.error("Error creating warmup job:", error);
|
|
310
|
-
|
|
332
|
+
sendError(
|
|
333
|
+
res,
|
|
334
|
+
500,
|
|
335
|
+
ErrorCode.INTERNAL_ERROR,
|
|
336
|
+
"Failed to create warmup job"
|
|
337
|
+
);
|
|
311
338
|
}
|
|
312
339
|
}
|
|
313
340
|
async function streamWarmupProgress(req, res, ctx2) {
|
|
@@ -317,14 +344,14 @@ async function streamWarmupProgress(req, res, ctx2) {
|
|
|
317
344
|
const jobStoreKey = storeKeys.warmupJob(jobId);
|
|
318
345
|
const job = await ctx2.transientStore.getJson(jobStoreKey);
|
|
319
346
|
if (!job) {
|
|
320
|
-
res
|
|
347
|
+
sendError(res, 404, ErrorCode.NOT_FOUND, "Job not found");
|
|
321
348
|
return;
|
|
322
349
|
}
|
|
323
350
|
if (ctx2.warmupBackendResolver) {
|
|
324
351
|
const backend = ctx2.warmupBackendResolver(job.sources, job.metadata);
|
|
325
352
|
if (backend) {
|
|
326
353
|
setupSSEResponse(res);
|
|
327
|
-
const sendEvent2 =
|
|
354
|
+
const sendEvent2 = createEventSender(res);
|
|
328
355
|
try {
|
|
329
356
|
await proxyRemoteSSE(
|
|
330
357
|
`${backend.baseUrl}/warmup/${jobId}/progress`,
|
|
@@ -341,7 +368,7 @@ async function streamWarmupProgress(req, res, ctx2) {
|
|
|
341
368
|
}
|
|
342
369
|
ctx2.transientStore.delete(jobStoreKey);
|
|
343
370
|
setupSSEResponse(res);
|
|
344
|
-
const sendEvent =
|
|
371
|
+
const sendEvent = createEventSender(res);
|
|
345
372
|
try {
|
|
346
373
|
await warmupSources(job.sources, sendEvent, ctx2);
|
|
347
374
|
sendEvent("complete", { status: "ready" });
|
|
@@ -353,7 +380,7 @@ async function streamWarmupProgress(req, res, ctx2) {
|
|
|
353
380
|
} catch (error) {
|
|
354
381
|
console.error("Error in warmup streaming:", error);
|
|
355
382
|
if (!res.headersSent) {
|
|
356
|
-
res
|
|
383
|
+
sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Warmup streaming failed");
|
|
357
384
|
} else {
|
|
358
385
|
res.end();
|
|
359
386
|
}
|
|
@@ -382,7 +409,7 @@ async function purgeCache(req, res, ctx2) {
|
|
|
382
409
|
res.json(result);
|
|
383
410
|
} catch (error) {
|
|
384
411
|
console.error("Error purging cache:", error);
|
|
385
|
-
res
|
|
412
|
+
sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Failed to purge cache");
|
|
386
413
|
}
|
|
387
414
|
}
|
|
388
415
|
async function warmupSources(sources, sendEvent, ctx2) {
|
|
@@ -547,20 +574,28 @@ async function createRenderJob(req, res, ctx2, options) {
|
|
|
547
574
|
if (!ctx2.skipValidation) {
|
|
548
575
|
const result = effieDataSchema2.safeParse(rawEffieData);
|
|
549
576
|
if (!result.success) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
577
|
+
sendError(
|
|
578
|
+
res,
|
|
579
|
+
400,
|
|
580
|
+
ErrorCode.INVALID_EFFIE,
|
|
581
|
+
"Invalid effie data",
|
|
582
|
+
result.error.issues.map((issue) => ({
|
|
553
583
|
path: issue.path.join("."),
|
|
554
584
|
message: issue.message
|
|
555
585
|
}))
|
|
556
|
-
|
|
586
|
+
);
|
|
557
587
|
return;
|
|
558
588
|
}
|
|
559
589
|
effie = result.data;
|
|
560
590
|
} else {
|
|
561
591
|
const data = rawEffieData;
|
|
562
592
|
if (!data?.segments) {
|
|
563
|
-
|
|
593
|
+
sendError(
|
|
594
|
+
res,
|
|
595
|
+
400,
|
|
596
|
+
ErrorCode.INVALID_EFFIE,
|
|
597
|
+
"Invalid effie data: missing segments"
|
|
598
|
+
);
|
|
564
599
|
return;
|
|
565
600
|
}
|
|
566
601
|
effie = data;
|
|
@@ -597,7 +632,12 @@ async function createRenderJob(req, res, ctx2, options) {
|
|
|
597
632
|
});
|
|
598
633
|
} catch (error) {
|
|
599
634
|
console.error("Error creating render job:", error);
|
|
600
|
-
|
|
635
|
+
sendError(
|
|
636
|
+
res,
|
|
637
|
+
500,
|
|
638
|
+
ErrorCode.INTERNAL_ERROR,
|
|
639
|
+
"Failed to create render job"
|
|
640
|
+
);
|
|
601
641
|
}
|
|
602
642
|
}
|
|
603
643
|
async function streamRenderProgress(req, res, ctx2) {
|
|
@@ -607,14 +647,15 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
607
647
|
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
608
648
|
const job = await ctx2.transientStore.getJson(jobStoreKey);
|
|
609
649
|
if (!job) {
|
|
610
|
-
res
|
|
650
|
+
sendError(res, 404, ErrorCode.NOT_FOUND, "Job not found");
|
|
611
651
|
return;
|
|
612
652
|
}
|
|
613
653
|
ctx2.transientStore.delete(jobStoreKey);
|
|
614
654
|
const warmupBackend = ctx2.warmupBackendResolver ? ctx2.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
615
655
|
const renderBackend = ctx2.renderBackendResolver ? ctx2.renderBackendResolver(job.effie, job.metadata) : null;
|
|
616
656
|
setupSSEResponse(res);
|
|
617
|
-
const sendEvent =
|
|
657
|
+
const sendEvent = createEventSender(res);
|
|
658
|
+
const rawSendEvent = createEventSender(res);
|
|
618
659
|
let keepalivePhase = "warmup";
|
|
619
660
|
const keepalive = setInterval(() => {
|
|
620
661
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
@@ -631,13 +672,16 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
631
672
|
if (warmupBackend) {
|
|
632
673
|
await proxyRemoteSSE(
|
|
633
674
|
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,
|
|
634
|
-
|
|
675
|
+
rawSendEvent,
|
|
635
676
|
"warmup:",
|
|
636
677
|
res,
|
|
637
678
|
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
638
679
|
);
|
|
639
680
|
} else {
|
|
640
|
-
const warmupSender = prefixEventSender(
|
|
681
|
+
const warmupSender = prefixEventSender(
|
|
682
|
+
rawSendEvent,
|
|
683
|
+
"warmup:"
|
|
684
|
+
);
|
|
641
685
|
await warmupSources(job.sources, warmupSender, ctx2);
|
|
642
686
|
warmupSender("complete", { status: "ready" });
|
|
643
687
|
}
|
|
@@ -646,7 +690,8 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
646
690
|
if (renderBackend) {
|
|
647
691
|
const videoJob = {
|
|
648
692
|
effie: job.effie,
|
|
649
|
-
scale: job.scale
|
|
693
|
+
scale: job.scale,
|
|
694
|
+
metadata: job.metadata
|
|
650
695
|
};
|
|
651
696
|
await ctx2.transientStore.putJson(
|
|
652
697
|
storeKeys.videoJob(jobId),
|
|
@@ -667,7 +712,10 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
667
712
|
job.upload,
|
|
668
713
|
sendEvent
|
|
669
714
|
);
|
|
670
|
-
sendEvent(
|
|
715
|
+
sendEvent(
|
|
716
|
+
"render:complete",
|
|
717
|
+
timings
|
|
718
|
+
);
|
|
671
719
|
} else {
|
|
672
720
|
const timings = await renderAndUploadInternal(
|
|
673
721
|
job.effie,
|
|
@@ -676,13 +724,17 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
676
724
|
sendEvent,
|
|
677
725
|
ctx2
|
|
678
726
|
);
|
|
679
|
-
sendEvent(
|
|
727
|
+
sendEvent(
|
|
728
|
+
"render:complete",
|
|
729
|
+
timings
|
|
730
|
+
);
|
|
680
731
|
}
|
|
681
732
|
sendEvent("complete", { status: "done" });
|
|
682
733
|
} else {
|
|
683
734
|
const videoJob = {
|
|
684
735
|
effie: job.effie,
|
|
685
|
-
scale: job.scale
|
|
736
|
+
scale: job.scale,
|
|
737
|
+
metadata: job.metadata
|
|
686
738
|
};
|
|
687
739
|
await ctx2.transientStore.putJson(
|
|
688
740
|
storeKeys.videoJob(jobId),
|
|
@@ -704,7 +756,12 @@ async function streamRenderProgress(req, res, ctx2) {
|
|
|
704
756
|
} catch (error) {
|
|
705
757
|
console.error("Error in render progress streaming:", error);
|
|
706
758
|
if (!res.headersSent) {
|
|
707
|
-
|
|
759
|
+
sendError(
|
|
760
|
+
res,
|
|
761
|
+
500,
|
|
762
|
+
ErrorCode.INTERNAL_ERROR,
|
|
763
|
+
"Render progress streaming failed"
|
|
764
|
+
);
|
|
708
765
|
} else {
|
|
709
766
|
res.end();
|
|
710
767
|
}
|
|
@@ -717,18 +774,26 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
717
774
|
const videoJobKey = storeKeys.videoJob(jobId);
|
|
718
775
|
const videoJob = await ctx2.transientStore.getJson(videoJobKey);
|
|
719
776
|
if (!videoJob) {
|
|
720
|
-
res
|
|
777
|
+
sendError(res, 404, ErrorCode.NOT_FOUND, "Video not found or expired");
|
|
721
778
|
return;
|
|
722
779
|
}
|
|
723
780
|
if (ctx2.renderBackendResolver) {
|
|
724
|
-
const backend = ctx2.renderBackendResolver(
|
|
781
|
+
const backend = ctx2.renderBackendResolver(
|
|
782
|
+
videoJob.effie,
|
|
783
|
+
videoJob.metadata
|
|
784
|
+
);
|
|
725
785
|
if (backend) {
|
|
726
786
|
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
727
787
|
const response = await ffsFetch(backendUrl, {
|
|
728
788
|
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
729
789
|
});
|
|
730
790
|
if (!response.ok) {
|
|
731
|
-
|
|
791
|
+
sendError(
|
|
792
|
+
res,
|
|
793
|
+
response.status,
|
|
794
|
+
ErrorCode.BACKEND_FAILED,
|
|
795
|
+
"Backend render failed"
|
|
796
|
+
);
|
|
732
797
|
return;
|
|
733
798
|
}
|
|
734
799
|
await proxyBinaryStream(response, res);
|
|
@@ -740,14 +805,14 @@ async function streamRenderVideo(req, res, ctx2) {
|
|
|
740
805
|
} catch (error) {
|
|
741
806
|
console.error("Error streaming video:", error);
|
|
742
807
|
if (!res.headersSent) {
|
|
743
|
-
res
|
|
808
|
+
sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Video streaming failed");
|
|
744
809
|
} else {
|
|
745
810
|
res.end();
|
|
746
811
|
}
|
|
747
812
|
}
|
|
748
813
|
}
|
|
749
814
|
async function streamRenderDirect(res, job, ctx2) {
|
|
750
|
-
const { EffieRenderer } = await import("./render-
|
|
815
|
+
const { EffieRenderer } = await import("./render-YKC5W4YT.js");
|
|
751
816
|
const renderer = new EffieRenderer(job.effie, {
|
|
752
817
|
transientStore: ctx2.transientStore,
|
|
753
818
|
httpProxy: ctx2.httpProxy
|
|
@@ -821,26 +886,30 @@ async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
|
|
|
821
886
|
}
|
|
822
887
|
async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx2) {
|
|
823
888
|
const renderStartTime = Date.now();
|
|
824
|
-
const { EffieRenderer } = await import("./render-
|
|
889
|
+
const { EffieRenderer } = await import("./render-YKC5W4YT.js");
|
|
825
890
|
const renderer = new EffieRenderer(effie, {
|
|
826
891
|
transientStore: ctx2.transientStore,
|
|
827
892
|
httpProxy: ctx2.httpProxy
|
|
828
893
|
});
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
894
|
+
try {
|
|
895
|
+
const videoStream = await renderer.render(scale);
|
|
896
|
+
const chunks = [];
|
|
897
|
+
for await (const chunk of videoStream) {
|
|
898
|
+
chunks.push(Buffer.from(chunk));
|
|
899
|
+
}
|
|
900
|
+
const videoBuffer = Buffer.concat(chunks);
|
|
901
|
+
const renderTime = Date.now() - renderStartTime;
|
|
902
|
+
const timings = await uploadRenderedVideo(
|
|
903
|
+
videoBuffer,
|
|
904
|
+
effie,
|
|
905
|
+
upload,
|
|
906
|
+
sendEvent
|
|
907
|
+
);
|
|
908
|
+
timings.renderTime = renderTime;
|
|
909
|
+
return timings;
|
|
910
|
+
} finally {
|
|
911
|
+
renderer.close();
|
|
833
912
|
}
|
|
834
|
-
const videoBuffer = Buffer.concat(chunks);
|
|
835
|
-
const renderTime = Date.now() - renderStartTime;
|
|
836
|
-
const timings = await uploadRenderedVideo(
|
|
837
|
-
videoBuffer,
|
|
838
|
-
effie,
|
|
839
|
-
upload,
|
|
840
|
-
sendEvent
|
|
841
|
-
);
|
|
842
|
-
timings.renderTime = renderTime;
|
|
843
|
-
return timings;
|
|
844
913
|
}
|
|
845
914
|
|
|
846
915
|
// src/server.ts
|
|
@@ -856,7 +925,7 @@ function validateAuth(req, res) {
|
|
|
856
925
|
if (!apiKey) return true;
|
|
857
926
|
const authHeader = req.headers.authorization;
|
|
858
927
|
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
|
|
859
|
-
res
|
|
928
|
+
sendError(res, 401, ErrorCode.UNAUTHORIZED, "Unauthorized");
|
|
860
929
|
return false;
|
|
861
930
|
}
|
|
862
931
|
return true;
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import express from \"express\";\nimport bodyParser from \"body-parser\";\nimport {\n createServerContext,\n createWarmupJob,\n streamWarmupProgress,\n purgeCache,\n createRenderJob,\n streamRenderProgress,\n streamRenderVideo,\n} from \"./handlers\";\n\nconst app: express.Express = express();\napp.disable(\"x-powered-by\");\napp.use(bodyParser.json({ limit: \"50mb\" })); // Support large JSON requests\n\nconst ctx = await createServerContext();\nif (ctx.httpProxy) {\n console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);\n}\n\nfunction validateAuth(req: express.Request, res: express.Response): boolean {\n const apiKey = process.env.FFS_API_KEY;\n if (!apiKey) return true; // No auth required if api key not set\n\n const authHeader = req.headers.authorization;\n if (!authHeader || authHeader !== `Bearer ${apiKey}`) {\n res
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import express from \"express\";\nimport bodyParser from \"body-parser\";\nimport {\n createServerContext,\n createWarmupJob,\n streamWarmupProgress,\n purgeCache,\n createRenderJob,\n streamRenderProgress,\n streamRenderVideo,\n sendError,\n ErrorCode,\n} from \"./handlers\";\n\nconst app: express.Express = express();\napp.disable(\"x-powered-by\");\napp.use(bodyParser.json({ limit: \"50mb\" })); // Support large JSON requests\n\nconst ctx = await createServerContext();\nif (ctx.httpProxy) {\n console.log(`FFS HTTP proxy listening on port ${ctx.httpProxy.port}`);\n}\n\nfunction validateAuth(req: express.Request, res: express.Response): boolean {\n const apiKey = process.env.FFS_API_KEY;\n if (!apiKey) return true; // No auth required if api key not set\n\n const authHeader = req.headers.authorization;\n if (!authHeader || authHeader !== `Bearer ${apiKey}`) {\n sendError(res, 401, ErrorCode.UNAUTHORIZED, \"Unauthorized\");\n return false;\n }\n return true;\n}\n\n// Routes with auth (POST endpoints)\napp.post(\"/warmup\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createWarmupJob(req, res, ctx);\n});\napp.post(\"/purge\", (req, res) => {\n if (!validateAuth(req, res)) return;\n purgeCache(req, res, ctx);\n});\napp.post(\"/render\", (req, res) => {\n if (!validateAuth(req, res)) return;\n createRenderJob(req, res, ctx);\n});\n\n// Routes without auth (GET endpoints use job ID as capability token)\napp.get(\"/warmup/:id/progress\", (req, res) =>\n streamWarmupProgress(req, res, ctx),\n);\napp.get(\"/render/:id/progress\", (req, res) =>\n streamRenderProgress(req, res, ctx),\n);\napp.get(\"/render/:id/video\", (req, res) => streamRenderVideo(req, res, ctx));\n\n// Server lifecycle\nconst port = process.env.FFS_PORT || process.env.PORT || 2000; // ffmpeg was conceived in the year 2000\nconst server = app.listen(port, () => {\n console.log(`FFS server listening on port ${port}`);\n});\n\nfunction shutdown() {\n console.log(\"Shutting down FFS server...\");\n ctx.httpProxy?.close();\n ctx.transientStore.close();\n server.close(() => {\n console.log(\"FFS server stopped\");\n process.exit(0);\n });\n}\n\nprocess.on(\"SIGTERM\", shutdown);\nprocess.on(\"SIGINT\", shutdown);\n\nexport { app };\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,aAAa;AACpB,OAAO,gBAAgB;AAavB,IAAM,MAAuB,QAAQ;AACrC,IAAI,QAAQ,cAAc;AAC1B,IAAI,IAAI,WAAW,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC;AAE1C,IAAM,MAAM,MAAM,oBAAoB;AACtC,IAAI,IAAI,WAAW;AACjB,UAAQ,IAAI,oCAAoC,IAAI,UAAU,IAAI,EAAE;AACtE;AAEA,SAAS,aAAa,KAAsB,KAAgC;AAC1E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,aAAa,IAAI,QAAQ;AAC/B,MAAI,CAAC,cAAc,eAAe,UAAU,MAAM,IAAI;AACpD,cAAU,KAAK,KAAK,UAAU,cAAc,cAAc;AAC1D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AACD,IAAI,KAAK,UAAU,CAAC,KAAK,QAAQ;AAC/B,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,aAAW,KAAK,KAAK,GAAG;AAC1B,CAAC;AACD,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AAChC,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG;AAC7B,kBAAgB,KAAK,KAAK,GAAG;AAC/B,CAAC;AAGD,IAAI;AAAA,EAAI;AAAA,EAAwB,CAAC,KAAK,QACpC,qBAAqB,KAAK,KAAK,GAAG;AACpC;AACA,IAAI;AAAA,EAAI;AAAA,EAAwB,CAAC,KAAK,QACpC,qBAAqB,KAAK,KAAK,GAAG;AACpC;AACA,IAAI,IAAI,qBAAqB,CAAC,KAAK,QAAQ,kBAAkB,KAAK,KAAK,GAAG,CAAC;AAG3E,IAAM,OAAO,QAAQ,IAAI,YAAY,QAAQ,IAAI,QAAQ;AACzD,IAAM,SAAS,IAAI,OAAO,MAAM,MAAM;AACpC,UAAQ,IAAI,gCAAgC,IAAI,EAAE;AACpD,CAAC;AAED,SAAS,WAAW;AAClB,UAAQ,IAAI,6BAA6B;AACzC,MAAI,WAAW,MAAM;AACrB,MAAI,eAAe,MAAM;AACzB,SAAO,MAAM,MAAM;AACjB,YAAQ,IAAI,oBAAoB;AAChC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;AAEA,QAAQ,GAAG,WAAW,QAAQ;AAC9B,QAAQ,GAAG,UAAU,QAAQ;","names":[]}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
type WarmupEventMap = {
|
|
2
|
+
start: {
|
|
3
|
+
total: number;
|
|
4
|
+
};
|
|
5
|
+
progress: {
|
|
6
|
+
url: string;
|
|
7
|
+
status: "skipped" | "hit" | "cached" | "error";
|
|
8
|
+
cached: number;
|
|
9
|
+
failed: number;
|
|
10
|
+
skipped: number;
|
|
11
|
+
total: number;
|
|
12
|
+
ms?: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
};
|
|
16
|
+
downloading: {
|
|
17
|
+
url: string;
|
|
18
|
+
status: "started" | "downloading";
|
|
19
|
+
bytesReceived: number;
|
|
20
|
+
};
|
|
21
|
+
keepalive: {
|
|
22
|
+
cached: number;
|
|
23
|
+
failed: number;
|
|
24
|
+
skipped: number;
|
|
25
|
+
total: number;
|
|
26
|
+
};
|
|
27
|
+
summary: {
|
|
28
|
+
cached: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
skipped: number;
|
|
31
|
+
total: number;
|
|
32
|
+
};
|
|
33
|
+
complete: {
|
|
34
|
+
status: "ready";
|
|
35
|
+
};
|
|
36
|
+
error: {
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
type RenderEventMap = {
|
|
41
|
+
keepalive: {
|
|
42
|
+
phase: "warmup" | "render";
|
|
43
|
+
} | {
|
|
44
|
+
status: "uploading";
|
|
45
|
+
};
|
|
46
|
+
"purge:complete": {
|
|
47
|
+
purged: number;
|
|
48
|
+
total: number;
|
|
49
|
+
};
|
|
50
|
+
"render:complete": {
|
|
51
|
+
renderTime?: number;
|
|
52
|
+
uploadTime: number;
|
|
53
|
+
uploadCoverTime?: number;
|
|
54
|
+
fetchCoverTime?: number;
|
|
55
|
+
};
|
|
56
|
+
complete: {
|
|
57
|
+
status: "done";
|
|
58
|
+
};
|
|
59
|
+
ready: {
|
|
60
|
+
videoUrl: string;
|
|
61
|
+
};
|
|
62
|
+
error: {
|
|
63
|
+
phase: "warmup" | "render";
|
|
64
|
+
message: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
type TypedEventSender<TMap extends Record<string, unknown>> = <K extends keyof TMap & string>(event: K, data: TMap[K]) => void;
|
|
68
|
+
type WarmupEventSender = TypedEventSender<WarmupEventMap>;
|
|
69
|
+
type RenderEventSender = TypedEventSender<RenderEventMap>;
|
|
70
|
+
type EventSender = (event: string, data: object) => void;
|
|
71
|
+
|
|
72
|
+
export type { EventSender, RenderEventMap, RenderEventSender, TypedEventSender, WarmupEventMap, WarmupEventSender };
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effing/ffs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "FFmpeg-based effie rendering service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"./handlers": {
|
|
16
16
|
"types": "./dist/handlers/index.d.ts",
|
|
17
17
|
"import": "./dist/handlers/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./sse": {
|
|
20
|
+
"types": "./dist/sse.d.ts",
|
|
21
|
+
"import": "./dist/sse.js"
|
|
18
22
|
}
|
|
19
23
|
},
|
|
20
24
|
"bin": {
|
|
@@ -32,10 +36,10 @@
|
|
|
32
36
|
"tar-stream": "^3.1.7",
|
|
33
37
|
"undici": "^7.3.0",
|
|
34
38
|
"zod": "^3.25.76",
|
|
35
|
-
"@effing/effie": "0.
|
|
39
|
+
"@effing/effie": "0.8.0"
|
|
36
40
|
},
|
|
37
41
|
"optionalDependencies": {
|
|
38
|
-
"@effing/ffmpeg": "0.
|
|
42
|
+
"@effing/ffmpeg": "0.8.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@types/body-parser": "^1.19.5",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handlers/shared.ts","../src/proxy.ts","../src/handlers/caching.ts","../src/handlers/rendering.ts"],"sourcesContent":["import express from \"express\";\nimport type { Response as UndiciResponse } from \"undici\";\nimport type { TransientStore } from \"../storage\";\nimport { createTransientStore } from \"../storage\";\nimport { HttpProxy } from \"../proxy\";\nimport { ffsFetch } from \"../fetch\";\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 sources: EffieSourceWithType[];\n scale: number;\n upload?: UploadOptions;\n purge?: boolean;\n warmupJobId: string;\n createdAt: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type VideoJob = {\n effie: EffieData<EffieSources>;\n scale: number;\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\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 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 type { TransientStore } from \"../storage\";\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 \"./shared\";\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.ttlMs,\n );\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/warmup/${jobId}/progress`,\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/progress - Stream warmup progress via SSE\n * Fetches and caches sources, emitting progress events\n */\nexport async function streamWarmupProgress(\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}/progress`,\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 * Purge cached sources by URL list.\n * Returns the number purged and total.\n */\nexport async function purgeCachedSources(\n urls: string[],\n store: TransientStore,\n): Promise<{ purged: number; total: number }> {\n let purged = 0;\n for (const url of urls) {\n const ck = storeKeys.source(url);\n if (await store.exists(ck)) {\n await store.delete(ck);\n purged++;\n }\n }\n return { purged, total: urls.length };\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 const result = await purgeCachedSources(sources, ctx.transientStore);\n\n res.json(result);\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.ttlMs,\n );\n}\n","import express from \"express\";\nimport { randomUUID } from \"crypto\";\nimport { storeKeys } from \"../storage\";\nimport { ffsFetch } from \"../fetch\";\nimport {\n extractEffieSourcesWithTypes,\n extractEffieSources,\n effieDataSchema,\n} from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type {\n ServerContext,\n SSEEventSender,\n RenderJob,\n VideoJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n setupCORSHeaders,\n setupSSEResponse,\n createSSEEventSender,\n prefixEventSender,\n proxyRemoteSSE,\n proxyBinaryStream,\n} from \"./shared\";\nimport { warmupSources, purgeCachedSources } from \"./caching\";\n\n/**\n * POST /render - Create a render job (warmup + render, optional purge)\n * Returns a job ID and progress URL for SSE streaming\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 // Parse request body\n const body = req.body as {\n effie: unknown;\n scale?: number;\n upload?: UploadOptions;\n purge?: boolean;\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 const purge = body.purge;\n\n // Create IDs\n const jobId = randomUUID();\n const warmupJobId = randomUUID();\n\n // Store the render job\n const job: RenderJob = {\n effie,\n sources,\n scale,\n upload,\n purge,\n warmupJobId,\n createdAt: Date.now(),\n metadata: options?.metadata,\n };\n\n await ctx.transientStore.putJson(\n storeKeys.renderJob(jobId),\n job,\n ctx.transientStore.ttlMs,\n );\n\n // Store warmup sub-job for backend execution\n await ctx.transientStore.putJson(\n storeKeys.warmupJob(warmupJobId),\n { sources, metadata: options?.metadata },\n ctx.transientStore.ttlMs,\n );\n\n res.json({\n id: jobId,\n progressUrl: `${ctx.baseUrl}/render/${jobId}/progress`,\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/progress - Stream render progress via SSE\n * Orchestrates warmup (local or remote) followed by render (local or remote)\n */\nexport async function streamRenderProgress(\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.renderJob(jobId);\n const job = await ctx.transientStore.getJson<RenderJob>(jobStoreKey);\n\n if (!job) {\n res.status(404).json({ error: \"Job not found\" });\n return;\n }\n\n // Only allow the job to run once\n ctx.transientStore.delete(jobStoreKey);\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 0: Purge (if requested)\n if (job.purge) {\n const sourceUrls = extractEffieSources(job.effie);\n const purgeResult = await purgeCachedSources(\n sourceUrls,\n ctx.transientStore,\n );\n sendEvent(\"purge:complete\", purgeResult);\n }\n\n // Phase 1: Warmup\n if (warmupBackend) {\n // Proxy warmup from remote backend\n await proxyRemoteSSE(\n `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,\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 (job.upload) {\n if (renderBackend) {\n // Upload + backend: store VideoJob for backend to render,\n // fetch binary video from backend, upload locally.\n const videoJob: VideoJob = {\n effie: job.effie,\n scale: job.scale,\n };\n await ctx.transientStore.putJson(\n storeKeys.videoJob(jobId),\n videoJob,\n ctx.transientStore.ttlMs,\n );\n\n const backendUrl = `${renderBackend.baseUrl}/render/${jobId}/video`;\n const response = await ffsFetch(backendUrl, {\n headers: renderBackend.apiKey\n ? { Authorization: `Bearer ${renderBackend.apiKey}` }\n : undefined,\n });\n if (!response.ok) {\n throw new Error(`Backend render failed: ${response.status}`);\n }\n const videoBuffer = Buffer.from(await response.arrayBuffer());\n\n const timings = await uploadRenderedVideo(\n videoBuffer,\n job.effie,\n job.upload,\n sendEvent,\n );\n sendEvent(\"render:complete\", timings);\n } else {\n // Upload + no backend: render and upload locally (no VideoJob stored)\n const timings = await renderAndUploadInternal(\n job.effie,\n job.scale,\n job.upload,\n sendEvent,\n ctx,\n );\n sendEvent(\"render:complete\", timings);\n }\n sendEvent(\"complete\", { status: \"done\" });\n } else {\n // Non-upload mode: store VideoJob for on-demand fetch via /render/:id/video\n const videoJob: VideoJob = {\n effie: job.effie,\n scale: job.scale,\n };\n await ctx.transientStore.putJson(\n storeKeys.videoJob(jobId),\n videoJob,\n ctx.transientStore.ttlMs,\n );\n const videoUrl = `${ctx.baseUrl}/render/${jobId}/video`;\n sendEvent(\"ready\", { videoUrl });\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 render progress streaming:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Render progress streaming failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * GET /render/:id/video - Stream rendered video\n * Reads the video sub-job from the store, deletes it (one-time use), and streams the MP4.\n */\nexport async function streamRenderVideo(\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 videoJobKey = storeKeys.videoJob(jobId);\n const videoJob = await ctx.transientStore.getJson<VideoJob>(videoJobKey);\n\n if (!videoJob) {\n res.status(404).json({ error: \"Video not found or expired\" });\n return;\n }\n\n // Proxy to render backend if resolver is configured\n // Don't delete — the backend reads/deletes the VideoJob from shared store\n if (ctx.renderBackendResolver) {\n const backend = ctx.renderBackendResolver(videoJob.effie);\n if (backend) {\n const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;\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 await proxyBinaryStream(response, res);\n return;\n }\n }\n\n // Local render — safe to delete the video job (one-time use)\n ctx.transientStore.delete(videoJobKey);\n\n // Render locally\n await streamRenderDirect(res, videoJob, ctx);\n } catch (error) {\n console.error(\"Error streaming video:\", error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Video streaming failed\" });\n } else {\n res.end();\n }\n }\n}\n\n/**\n * Stream video directly to the response (no upload)\n */\nasync function streamRenderDirect(\n res: express.Response,\n job: VideoJob,\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 * Upload a rendered video buffer (and optional cover) to presigned URLs.\n * Shared between local render+upload and backend render+upload flows.\n */\nasync function uploadRenderedVideo(\n videoBuffer: Buffer,\n effie: EffieData<EffieSources>,\n upload: UploadOptions,\n sendEvent: SSEEventSender,\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 let coverBuffer: Buffer;\n if (effie.cover.startsWith(\"data:\")) {\n const commaIndex = effie.cover.indexOf(\",\");\n if (commaIndex === -1) {\n throw new Error(\"Invalid cover data URL\");\n }\n const meta = effie.cover.slice(5, commaIndex); // after \"data:\"\n const isBase64 = meta.endsWith(\";base64\");\n const data = effie.cover.slice(commaIndex + 1);\n coverBuffer = isBase64\n ? Buffer.from(data, \"base64\")\n : Buffer.from(decodeURIComponent(data));\n } else {\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 coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());\n }\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 // 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 * 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 // Render effie data to video buffer\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 const renderTime = Date.now() - renderStartTime;\n\n // Upload video (and cover)\n const timings = await uploadRenderedVideo(\n videoBuffer,\n effie,\n upload,\n sendEvent,\n );\n timings.renderTime = renderTime;\n\n return timings;\n}\n"],"mappings":"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","names":["Readable","sendEvent","Readable","randomUUID","extractEffieSourcesWithTypes","extractEffieSources","effieDataSchema","effieDataSchema","extractEffieSourcesWithTypes","randomUUID","extractEffieSources"]}
|
|
File without changes
|