@effing/ffs 0.7.3 → 0.9.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 +35 -2
- package/dist/{chunk-46RJNEOH.js → chunk-G46KSSSR.js} +139 -75
- package/dist/chunk-G46KSSSR.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 +137 -75
- 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/README.md
CHANGED
|
@@ -149,6 +149,8 @@ type RenderOptions = {
|
|
|
149
149
|
};
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
Alternatively, raw `EffieData` can be sent directly as the request body. When using the raw format, `scale` and `purge` can be passed as query parameters: `?scale=0.5&purge=true`.
|
|
153
|
+
|
|
152
154
|
**Response:**
|
|
153
155
|
|
|
154
156
|
```json
|
|
@@ -220,7 +222,7 @@ curl http://localhost:2000/render/550e8400-.../video -o output.mp4
|
|
|
220
222
|
|
|
221
223
|
Creates a standalone warmup job for pre-fetching and caching the sources from an Effie composition.
|
|
222
224
|
|
|
223
|
-
**Request:** Same format as `/render`
|
|
225
|
+
**Request:** Same format as `/render` — wrapped EffieData with `effie` field, or raw `EffieData` directly as the body.
|
|
224
226
|
|
|
225
227
|
**Response:**
|
|
226
228
|
|
|
@@ -273,7 +275,7 @@ events.addEventListener("complete", () => {
|
|
|
273
275
|
|
|
274
276
|
Purges cached sources for a given Effie composition.
|
|
275
277
|
|
|
276
|
-
**Request:** Same format as `/render`
|
|
278
|
+
**Request:** Same format as `/render` — wrapped EffieData with `effie` field, or raw `EffieData` directly as the body.
|
|
277
279
|
|
|
278
280
|
**Response:**
|
|
279
281
|
|
|
@@ -281,6 +283,37 @@ Purges cached sources for a given Effie composition.
|
|
|
281
283
|
{ "purged": 3, "total": 5 }
|
|
282
284
|
```
|
|
283
285
|
|
|
286
|
+
### Error Responses
|
|
287
|
+
|
|
288
|
+
All HTTP error responses share a unified JSON shape:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
type ApiError = {
|
|
292
|
+
error: string; // Human-readable message
|
|
293
|
+
code: ErrorCode; // Machine-readable code
|
|
294
|
+
issues?: Array<{ path: string; message: string }>; // Validation details (Zod failures only)
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Code | Status | Description |
|
|
299
|
+
| ---------------- | ------ | ----------------------------------------- |
|
|
300
|
+
| `UNAUTHORIZED` | 401 | Missing or invalid API key |
|
|
301
|
+
| `INVALID_EFFIE` | 400 | Effie data validation or structural error |
|
|
302
|
+
| `NOT_FOUND` | 404 | Job or video not found |
|
|
303
|
+
| `BACKEND_FAILED` | varies | Remote render backend returned an error |
|
|
304
|
+
| `FETCH_FAILED` | 502 | Failed to fetch remote Effie data URL |
|
|
305
|
+
| `INTERNAL_ERROR` | 500 | Catch-all for unhandled exceptions |
|
|
306
|
+
|
|
307
|
+
For `INVALID_EFFIE` errors caused by schema validation, the `issues` array contains the specific validation failures:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"error": "Invalid effie data",
|
|
312
|
+
"code": "INVALID_EFFIE",
|
|
313
|
+
"issues": [{ "path": "segments.0.layers.0.x", "message": "Required" }]
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
284
317
|
## Backend Separation
|
|
285
318
|
|
|
286
319
|
FFS supports running warmup and render on separate backends via resolver callbacks.
|
|
@@ -131,9 +131,27 @@ var HttpProxy = class {
|
|
|
131
131
|
|
|
132
132
|
// src/handlers/shared.ts
|
|
133
133
|
import { effieDataSchema } from "@effing/effie";
|
|
134
|
+
|
|
135
|
+
// src/handlers/errors.ts
|
|
136
|
+
var ErrorCode = {
|
|
137
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
138
|
+
INVALID_EFFIE: "INVALID_EFFIE",
|
|
139
|
+
NOT_FOUND: "NOT_FOUND",
|
|
140
|
+
BACKEND_FAILED: "BACKEND_FAILED",
|
|
141
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
142
|
+
FETCH_FAILED: "FETCH_FAILED"
|
|
143
|
+
};
|
|
144
|
+
function sendError(res, status, code, message, issues) {
|
|
145
|
+
if (res.headersSent) return;
|
|
146
|
+
const body = { error: message, code };
|
|
147
|
+
if (issues) body.issues = issues;
|
|
148
|
+
res.status(status).json(body);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/handlers/shared.ts
|
|
134
152
|
async function createServerContext(options) {
|
|
135
153
|
const port = process.env.FFS_PORT || process.env.PORT || 2e3;
|
|
136
|
-
const enableHttpProxy = options?.httpProxy ??
|
|
154
|
+
const enableHttpProxy = options?.httpProxy ?? true;
|
|
137
155
|
let httpProxy;
|
|
138
156
|
if (enableHttpProxy) {
|
|
139
157
|
httpProxy = new HttpProxy();
|
|
@@ -157,6 +175,7 @@ function parseEffieData(body, skipValidation) {
|
|
|
157
175
|
if (!result.success) {
|
|
158
176
|
return {
|
|
159
177
|
error: "Invalid effie data",
|
|
178
|
+
code: ErrorCode.INVALID_EFFIE,
|
|
160
179
|
issues: result.error.issues.map((issue) => ({
|
|
161
180
|
path: issue.path.join("."),
|
|
162
181
|
message: issue.message
|
|
@@ -167,7 +186,10 @@ function parseEffieData(body, skipValidation) {
|
|
|
167
186
|
} else {
|
|
168
187
|
const effie = rawEffieData;
|
|
169
188
|
if (!effie?.segments) {
|
|
170
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
error: "Invalid effie data: missing segments",
|
|
191
|
+
code: ErrorCode.INVALID_EFFIE
|
|
192
|
+
};
|
|
171
193
|
}
|
|
172
194
|
return { effie };
|
|
173
195
|
}
|
|
@@ -182,7 +204,7 @@ function setupSSEResponse(res) {
|
|
|
182
204
|
res.setHeader("Connection", "keep-alive");
|
|
183
205
|
res.flushHeaders();
|
|
184
206
|
}
|
|
185
|
-
function
|
|
207
|
+
function createEventSender(res) {
|
|
186
208
|
return (event, data) => {
|
|
187
209
|
res.write(`event: ${event}
|
|
188
210
|
data: ${JSON.stringify(data)}
|
|
@@ -191,9 +213,9 @@ data: ${JSON.stringify(data)}
|
|
|
191
213
|
};
|
|
192
214
|
}
|
|
193
215
|
function prefixEventSender(sendEvent, prefix) {
|
|
194
|
-
return (event, data) => {
|
|
216
|
+
return ((event, data) => {
|
|
195
217
|
sendEvent(`${prefix}${event}`, data);
|
|
196
|
-
};
|
|
218
|
+
});
|
|
197
219
|
}
|
|
198
220
|
async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
199
221
|
const response = await ffsFetch(url, {
|
|
@@ -211,6 +233,8 @@ async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
|
211
233
|
}
|
|
212
234
|
const decoder = new TextDecoder();
|
|
213
235
|
let buffer = "";
|
|
236
|
+
let currentEvent = "";
|
|
237
|
+
let currentData = "";
|
|
214
238
|
try {
|
|
215
239
|
while (true) {
|
|
216
240
|
const { done, value } = await reader.read();
|
|
@@ -222,8 +246,6 @@ async function proxyRemoteSSE(url, sendEvent, prefix, res, headers) {
|
|
|
222
246
|
buffer += decoder.decode(value, { stream: true });
|
|
223
247
|
const lines = buffer.split("\n");
|
|
224
248
|
buffer = lines.pop() || "";
|
|
225
|
-
let currentEvent = "";
|
|
226
|
-
let currentData = "";
|
|
227
249
|
for (const line of lines) {
|
|
228
250
|
if (line.startsWith("event: ")) {
|
|
229
251
|
currentEvent = line.slice(7);
|
|
@@ -302,7 +324,12 @@ async function createWarmupJob(req, res, ctx, options) {
|
|
|
302
324
|
});
|
|
303
325
|
} catch (error) {
|
|
304
326
|
console.error("Error creating warmup job:", error);
|
|
305
|
-
|
|
327
|
+
sendError(
|
|
328
|
+
res,
|
|
329
|
+
500,
|
|
330
|
+
ErrorCode.INTERNAL_ERROR,
|
|
331
|
+
"Failed to create warmup job"
|
|
332
|
+
);
|
|
306
333
|
}
|
|
307
334
|
}
|
|
308
335
|
async function streamWarmupProgress(req, res, ctx) {
|
|
@@ -312,14 +339,14 @@ async function streamWarmupProgress(req, res, ctx) {
|
|
|
312
339
|
const jobStoreKey = storeKeys.warmupJob(jobId);
|
|
313
340
|
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
314
341
|
if (!job) {
|
|
315
|
-
res
|
|
342
|
+
sendError(res, 404, ErrorCode.NOT_FOUND, "Job not found");
|
|
316
343
|
return;
|
|
317
344
|
}
|
|
318
345
|
if (ctx.warmupBackendResolver) {
|
|
319
346
|
const backend = ctx.warmupBackendResolver(job.sources, job.metadata);
|
|
320
347
|
if (backend) {
|
|
321
348
|
setupSSEResponse(res);
|
|
322
|
-
const sendEvent2 =
|
|
349
|
+
const sendEvent2 = createEventSender(res);
|
|
323
350
|
try {
|
|
324
351
|
await proxyRemoteSSE(
|
|
325
352
|
`${backend.baseUrl}/warmup/${jobId}/progress`,
|
|
@@ -336,7 +363,7 @@ async function streamWarmupProgress(req, res, ctx) {
|
|
|
336
363
|
}
|
|
337
364
|
ctx.transientStore.delete(jobStoreKey);
|
|
338
365
|
setupSSEResponse(res);
|
|
339
|
-
const sendEvent =
|
|
366
|
+
const sendEvent = createEventSender(res);
|
|
340
367
|
try {
|
|
341
368
|
await warmupSources(job.sources, sendEvent, ctx);
|
|
342
369
|
sendEvent("complete", { status: "ready" });
|
|
@@ -348,7 +375,7 @@ async function streamWarmupProgress(req, res, ctx) {
|
|
|
348
375
|
} catch (error) {
|
|
349
376
|
console.error("Error in warmup streaming:", error);
|
|
350
377
|
if (!res.headersSent) {
|
|
351
|
-
res
|
|
378
|
+
sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Warmup streaming failed");
|
|
352
379
|
} else {
|
|
353
380
|
res.end();
|
|
354
381
|
}
|
|
@@ -377,7 +404,7 @@ async function purgeCache(req, res, ctx) {
|
|
|
377
404
|
res.json(result);
|
|
378
405
|
} catch (error) {
|
|
379
406
|
console.error("Error purging cache:", error);
|
|
380
|
-
res
|
|
407
|
+
sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Failed to purge cache");
|
|
381
408
|
}
|
|
382
409
|
}
|
|
383
410
|
async function warmupSources(sources, sendEvent, ctx) {
|
|
@@ -520,50 +547,51 @@ import "express";
|
|
|
520
547
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
521
548
|
import {
|
|
522
549
|
extractEffieSourcesWithTypes as extractEffieSourcesWithTypes2,
|
|
523
|
-
extractEffieSources as extractEffieSources2
|
|
524
|
-
effieDataSchema as effieDataSchema2
|
|
550
|
+
extractEffieSources as extractEffieSources2
|
|
525
551
|
} from "@effing/effie";
|
|
526
552
|
async function createRenderJob(req, res, ctx, options) {
|
|
527
553
|
try {
|
|
528
554
|
const body = req.body;
|
|
529
|
-
let rawEffieData;
|
|
530
555
|
if (typeof body.effie === "string") {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
556
|
+
let response;
|
|
557
|
+
try {
|
|
558
|
+
response = await ffsFetch(body.effie);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
sendError(
|
|
561
|
+
res,
|
|
562
|
+
502,
|
|
563
|
+
ErrorCode.FETCH_FAILED,
|
|
564
|
+
`Failed to fetch Effie data: ${error instanceof Error ? error.message : String(error)}`
|
|
535
565
|
);
|
|
536
|
-
}
|
|
537
|
-
rawEffieData = await response.json();
|
|
538
|
-
} else {
|
|
539
|
-
rawEffieData = body.effie;
|
|
540
|
-
}
|
|
541
|
-
let effie;
|
|
542
|
-
if (!ctx.skipValidation) {
|
|
543
|
-
const result = effieDataSchema2.safeParse(rawEffieData);
|
|
544
|
-
if (!result.success) {
|
|
545
|
-
res.status(400).json({
|
|
546
|
-
error: "Invalid effie data",
|
|
547
|
-
issues: result.error.issues.map((issue) => ({
|
|
548
|
-
path: issue.path.join("."),
|
|
549
|
-
message: issue.message
|
|
550
|
-
}))
|
|
551
|
-
});
|
|
552
566
|
return;
|
|
553
567
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
568
|
+
if (!response.ok) {
|
|
569
|
+
sendError(
|
|
570
|
+
res,
|
|
571
|
+
502,
|
|
572
|
+
ErrorCode.FETCH_FAILED,
|
|
573
|
+
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
574
|
+
);
|
|
559
575
|
return;
|
|
560
576
|
}
|
|
561
|
-
effie =
|
|
577
|
+
body.effie = await response.json();
|
|
578
|
+
}
|
|
579
|
+
const parseResult = parseEffieData(body, ctx.skipValidation);
|
|
580
|
+
if ("error" in parseResult) {
|
|
581
|
+
sendError(
|
|
582
|
+
res,
|
|
583
|
+
400,
|
|
584
|
+
parseResult.code,
|
|
585
|
+
parseResult.error,
|
|
586
|
+
parseResult.issues
|
|
587
|
+
);
|
|
588
|
+
return;
|
|
562
589
|
}
|
|
590
|
+
const effie = parseResult.effie;
|
|
563
591
|
const sources = extractEffieSourcesWithTypes2(effie);
|
|
564
|
-
const scale = body.scale ?? 1;
|
|
592
|
+
const scale = body.scale ?? (req.query?.scale ? parseFloat(req.query.scale) : void 0) ?? 1;
|
|
593
|
+
const purge = body.purge ?? (req.query?.purge === "true" ? true : void 0) ?? false;
|
|
565
594
|
const upload = body.upload;
|
|
566
|
-
const purge = body.purge;
|
|
567
595
|
const jobId = randomUUID2();
|
|
568
596
|
const warmupJobId = randomUUID2();
|
|
569
597
|
const job = {
|
|
@@ -592,7 +620,12 @@ async function createRenderJob(req, res, ctx, options) {
|
|
|
592
620
|
});
|
|
593
621
|
} catch (error) {
|
|
594
622
|
console.error("Error creating render job:", error);
|
|
595
|
-
|
|
623
|
+
sendError(
|
|
624
|
+
res,
|
|
625
|
+
500,
|
|
626
|
+
ErrorCode.INTERNAL_ERROR,
|
|
627
|
+
"Failed to create render job"
|
|
628
|
+
);
|
|
596
629
|
}
|
|
597
630
|
}
|
|
598
631
|
async function streamRenderProgress(req, res, ctx) {
|
|
@@ -602,14 +635,15 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
602
635
|
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
603
636
|
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
604
637
|
if (!job) {
|
|
605
|
-
res
|
|
638
|
+
sendError(res, 404, ErrorCode.NOT_FOUND, "Job not found");
|
|
606
639
|
return;
|
|
607
640
|
}
|
|
608
641
|
ctx.transientStore.delete(jobStoreKey);
|
|
609
642
|
const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
610
643
|
const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
|
|
611
644
|
setupSSEResponse(res);
|
|
612
|
-
const sendEvent =
|
|
645
|
+
const sendEvent = createEventSender(res);
|
|
646
|
+
const rawSendEvent = createEventSender(res);
|
|
613
647
|
let keepalivePhase = "warmup";
|
|
614
648
|
const keepalive = setInterval(() => {
|
|
615
649
|
sendEvent("keepalive", { phase: keepalivePhase });
|
|
@@ -626,13 +660,16 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
626
660
|
if (warmupBackend) {
|
|
627
661
|
await proxyRemoteSSE(
|
|
628
662
|
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,
|
|
629
|
-
|
|
663
|
+
rawSendEvent,
|
|
630
664
|
"warmup:",
|
|
631
665
|
res,
|
|
632
666
|
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
633
667
|
);
|
|
634
668
|
} else {
|
|
635
|
-
const warmupSender = prefixEventSender(
|
|
669
|
+
const warmupSender = prefixEventSender(
|
|
670
|
+
rawSendEvent,
|
|
671
|
+
"warmup:"
|
|
672
|
+
);
|
|
636
673
|
await warmupSources(job.sources, warmupSender, ctx);
|
|
637
674
|
warmupSender("complete", { status: "ready" });
|
|
638
675
|
}
|
|
@@ -641,7 +678,8 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
641
678
|
if (renderBackend) {
|
|
642
679
|
const videoJob = {
|
|
643
680
|
effie: job.effie,
|
|
644
|
-
scale: job.scale
|
|
681
|
+
scale: job.scale,
|
|
682
|
+
metadata: job.metadata
|
|
645
683
|
};
|
|
646
684
|
await ctx.transientStore.putJson(
|
|
647
685
|
storeKeys.videoJob(jobId),
|
|
@@ -662,7 +700,10 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
662
700
|
job.upload,
|
|
663
701
|
sendEvent
|
|
664
702
|
);
|
|
665
|
-
sendEvent(
|
|
703
|
+
sendEvent(
|
|
704
|
+
"render:complete",
|
|
705
|
+
timings
|
|
706
|
+
);
|
|
666
707
|
} else {
|
|
667
708
|
const timings = await renderAndUploadInternal(
|
|
668
709
|
job.effie,
|
|
@@ -671,13 +712,17 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
671
712
|
sendEvent,
|
|
672
713
|
ctx
|
|
673
714
|
);
|
|
674
|
-
sendEvent(
|
|
715
|
+
sendEvent(
|
|
716
|
+
"render:complete",
|
|
717
|
+
timings
|
|
718
|
+
);
|
|
675
719
|
}
|
|
676
720
|
sendEvent("complete", { status: "done" });
|
|
677
721
|
} else {
|
|
678
722
|
const videoJob = {
|
|
679
723
|
effie: job.effie,
|
|
680
|
-
scale: job.scale
|
|
724
|
+
scale: job.scale,
|
|
725
|
+
metadata: job.metadata
|
|
681
726
|
};
|
|
682
727
|
await ctx.transientStore.putJson(
|
|
683
728
|
storeKeys.videoJob(jobId),
|
|
@@ -699,7 +744,12 @@ async function streamRenderProgress(req, res, ctx) {
|
|
|
699
744
|
} catch (error) {
|
|
700
745
|
console.error("Error in render progress streaming:", error);
|
|
701
746
|
if (!res.headersSent) {
|
|
702
|
-
|
|
747
|
+
sendError(
|
|
748
|
+
res,
|
|
749
|
+
500,
|
|
750
|
+
ErrorCode.INTERNAL_ERROR,
|
|
751
|
+
"Render progress streaming failed"
|
|
752
|
+
);
|
|
703
753
|
} else {
|
|
704
754
|
res.end();
|
|
705
755
|
}
|
|
@@ -712,18 +762,26 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
712
762
|
const videoJobKey = storeKeys.videoJob(jobId);
|
|
713
763
|
const videoJob = await ctx.transientStore.getJson(videoJobKey);
|
|
714
764
|
if (!videoJob) {
|
|
715
|
-
res
|
|
765
|
+
sendError(res, 404, ErrorCode.NOT_FOUND, "Video not found or expired");
|
|
716
766
|
return;
|
|
717
767
|
}
|
|
718
768
|
if (ctx.renderBackendResolver) {
|
|
719
|
-
const backend = ctx.renderBackendResolver(
|
|
769
|
+
const backend = ctx.renderBackendResolver(
|
|
770
|
+
videoJob.effie,
|
|
771
|
+
videoJob.metadata
|
|
772
|
+
);
|
|
720
773
|
if (backend) {
|
|
721
774
|
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
722
775
|
const response = await ffsFetch(backendUrl, {
|
|
723
776
|
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
724
777
|
});
|
|
725
778
|
if (!response.ok) {
|
|
726
|
-
|
|
779
|
+
sendError(
|
|
780
|
+
res,
|
|
781
|
+
response.status,
|
|
782
|
+
ErrorCode.BACKEND_FAILED,
|
|
783
|
+
"Backend render failed"
|
|
784
|
+
);
|
|
727
785
|
return;
|
|
728
786
|
}
|
|
729
787
|
await proxyBinaryStream(response, res);
|
|
@@ -735,14 +793,14 @@ async function streamRenderVideo(req, res, ctx) {
|
|
|
735
793
|
} catch (error) {
|
|
736
794
|
console.error("Error streaming video:", error);
|
|
737
795
|
if (!res.headersSent) {
|
|
738
|
-
res
|
|
796
|
+
sendError(res, 500, ErrorCode.INTERNAL_ERROR, "Video streaming failed");
|
|
739
797
|
} else {
|
|
740
798
|
res.end();
|
|
741
799
|
}
|
|
742
800
|
}
|
|
743
801
|
}
|
|
744
802
|
async function streamRenderDirect(res, job, ctx) {
|
|
745
|
-
const { EffieRenderer } = await import("./render-
|
|
803
|
+
const { EffieRenderer } = await import("./render-DMHG3YJU.js");
|
|
746
804
|
const renderer = new EffieRenderer(job.effie, {
|
|
747
805
|
transientStore: ctx.transientStore,
|
|
748
806
|
httpProxy: ctx.httpProxy
|
|
@@ -816,29 +874,35 @@ async function uploadRenderedVideo(videoBuffer, effie, upload, sendEvent) {
|
|
|
816
874
|
}
|
|
817
875
|
async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
818
876
|
const renderStartTime = Date.now();
|
|
819
|
-
const { EffieRenderer } = await import("./render-
|
|
877
|
+
const { EffieRenderer } = await import("./render-DMHG3YJU.js");
|
|
820
878
|
const renderer = new EffieRenderer(effie, {
|
|
821
879
|
transientStore: ctx.transientStore,
|
|
822
880
|
httpProxy: ctx.httpProxy
|
|
823
881
|
});
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
882
|
+
try {
|
|
883
|
+
const videoStream = await renderer.render(scale);
|
|
884
|
+
const chunks = [];
|
|
885
|
+
for await (const chunk of videoStream) {
|
|
886
|
+
chunks.push(Buffer.from(chunk));
|
|
887
|
+
}
|
|
888
|
+
const videoBuffer = Buffer.concat(chunks);
|
|
889
|
+
const renderTime = Date.now() - renderStartTime;
|
|
890
|
+
const timings = await uploadRenderedVideo(
|
|
891
|
+
videoBuffer,
|
|
892
|
+
effie,
|
|
893
|
+
upload,
|
|
894
|
+
sendEvent
|
|
895
|
+
);
|
|
896
|
+
timings.renderTime = renderTime;
|
|
897
|
+
return timings;
|
|
898
|
+
} finally {
|
|
899
|
+
renderer.close();
|
|
828
900
|
}
|
|
829
|
-
const videoBuffer = Buffer.concat(chunks);
|
|
830
|
-
const renderTime = Date.now() - renderStartTime;
|
|
831
|
-
const timings = await uploadRenderedVideo(
|
|
832
|
-
videoBuffer,
|
|
833
|
-
effie,
|
|
834
|
-
upload,
|
|
835
|
-
sendEvent
|
|
836
|
-
);
|
|
837
|
-
timings.renderTime = renderTime;
|
|
838
|
-
return timings;
|
|
839
901
|
}
|
|
840
902
|
|
|
841
903
|
export {
|
|
904
|
+
ErrorCode,
|
|
905
|
+
sendError,
|
|
842
906
|
createServerContext,
|
|
843
907
|
proxyRemoteSSE,
|
|
844
908
|
proxyBinaryStream,
|
|
@@ -849,4 +913,4 @@ export {
|
|
|
849
913
|
streamRenderProgress,
|
|
850
914
|
streamRenderVideo
|
|
851
915
|
};
|
|
852
|
-
//# sourceMappingURL=chunk-
|
|
916
|
+
//# sourceMappingURL=chunk-G46KSSSR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/handlers/shared.ts","../src/proxy.ts","../src/handlers/errors.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 { TypedEventSender, EventSender } from \"../sse\";\nexport type { EventSender } from \"../sse\";\nimport type {\n EffieData,\n EffieSources,\n EffieSourceWithType,\n} from \"@effing/effie\";\nimport { effieDataSchema } from \"@effing/effie\";\nimport { ErrorCode } from \"./errors\";\nimport type { ErrorCode as ErrorCodeType } from \"./errors\";\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 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 ParseEffieResult =\n | { effie: EffieData<EffieSources> }\n | {\n error: string;\n code: ErrorCodeType;\n issues?: Array<{ path: string; message: string }>;\n };\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 ?? true;\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 code: ErrorCode.INVALID_EFFIE,\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 {\n error: \"Invalid effie data: missing segments\",\n code: ErrorCode.INVALID_EFFIE,\n };\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 createEventSender(res: express.Response): EventSender;\nexport function createEventSender<TMap extends Record<string, unknown>>(\n res: express.Response,\n): TypedEventSender<TMap>;\nexport function createEventSender(res: express.Response): EventSender {\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<TMap extends Record<string, unknown>>(\n sendEvent: EventSender,\n prefix: string,\n): TypedEventSender<TMap> {\n return ((event: string, data: object) => {\n sendEvent(`${prefix}${event}`, data);\n }) as TypedEventSender<TMap>;\n}\n\n/**\n * Proxy SSE events from a remote backend, prefixing event names\n */\nexport async function proxyRemoteSSE(\n url: string,\n sendEvent: EventSender,\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 let currentEvent = \"\";\n let currentData = \"\";\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 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 type express from \"express\";\n\nexport const ErrorCode = {\n UNAUTHORIZED: \"UNAUTHORIZED\",\n INVALID_EFFIE: \"INVALID_EFFIE\",\n NOT_FOUND: \"NOT_FOUND\",\n BACKEND_FAILED: \"BACKEND_FAILED\",\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\n FETCH_FAILED: \"FETCH_FAILED\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nexport type ApiError = {\n error: string;\n code: ErrorCode;\n issues?: Array<{ path: string; message: string }>;\n};\n\nexport function sendError(\n res: express.Response,\n status: number,\n code: ErrorCode,\n message: string,\n issues?: Array<{ path: string; message: string }>,\n): void {\n if (res.headersSent) return;\n const body: ApiError = { error: message, code };\n if (issues) body.issues = issues;\n res.status(status).json(body);\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 { WarmupEventMap, WarmupEventSender } from \"../sse\";\nimport type { ServerContext, WarmupJob } from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createEventSender,\n} from \"./shared\";\nimport { proxyRemoteSSE } from \"./shared\";\nimport { sendError, ErrorCode } from \"./errors\";\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 sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Failed to create warmup job\",\n );\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 sendError(res, 404, ErrorCode.NOT_FOUND, \"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 = createEventSender(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 = createEventSender<WarmupEventMap>(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 sendError(res, 500, ErrorCode.INTERNAL_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 sendError(res, 500, ErrorCode.INTERNAL_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: WarmupEventSender,\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: WarmupEventSender,\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} from \"@effing/effie\";\nimport type { EffieData, EffieSources } from \"@effing/effie\";\nimport type { RenderEventMap, RenderEventSender, WarmupEventMap } from \"../sse\";\nimport type {\n ServerContext,\n RenderJob,\n VideoJob,\n UploadOptions,\n} from \"./shared\";\nimport {\n parseEffieData,\n setupCORSHeaders,\n setupSSEResponse,\n createEventSender,\n prefixEventSender,\n proxyRemoteSSE,\n proxyBinaryStream,\n} from \"./shared\";\nimport { warmupSources, purgeCachedSources } from \"./caching\";\nimport { sendError, ErrorCode } from \"./errors\";\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 Record<string, unknown>;\n\n // URL handling (wrapped format only): fetch remote EffieData\n if (typeof body.effie === \"string\") {\n let response;\n try {\n response = await ffsFetch(body.effie);\n } catch (error) {\n sendError(\n res,\n 502,\n ErrorCode.FETCH_FAILED,\n `Failed to fetch Effie data: ${error instanceof Error ? error.message : String(error)}`,\n );\n return;\n }\n if (!response.ok) {\n sendError(\n res,\n 502,\n ErrorCode.FETCH_FAILED,\n `Failed to fetch Effie data: ${response.status} ${response.statusText}`,\n );\n return;\n }\n body.effie = await response.json();\n }\n\n // Parse & validate effie data (supports both wrapped and raw formats)\n const parseResult = parseEffieData(body, ctx.skipValidation);\n if (\"error\" in parseResult) {\n sendError(\n res,\n 400,\n parseResult.code,\n parseResult.error,\n parseResult.issues,\n );\n return;\n }\n const effie = parseResult.effie;\n\n const sources = extractEffieSourcesWithTypes(effie);\n const scale =\n (body.scale as number | undefined) ??\n (req.query?.scale ? parseFloat(req.query.scale as string) : undefined) ??\n 1;\n const purge =\n (body.purge as boolean | undefined) ??\n (req.query?.purge === \"true\" ? true : undefined) ??\n false;\n const upload = body.upload as UploadOptions | undefined;\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 sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Failed to create render job\",\n );\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 sendError(res, 404, ErrorCode.NOT_FOUND, \"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 = createEventSender<RenderEventMap>(res);\n const rawSendEvent = createEventSender(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 rawSendEvent,\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<WarmupEventMap>(\n rawSendEvent,\n \"warmup:\",\n );\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 metadata: job.metadata,\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(\n \"render:complete\",\n timings as RenderEventMap[\"render:complete\"],\n );\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(\n \"render:complete\",\n timings as RenderEventMap[\"render:complete\"],\n );\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 metadata: job.metadata,\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 sendError(\n res,\n 500,\n ErrorCode.INTERNAL_ERROR,\n \"Render progress streaming failed\",\n );\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 sendError(res, 404, ErrorCode.NOT_FOUND, \"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(\n videoJob.effie,\n videoJob.metadata,\n );\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 sendError(\n res,\n response.status,\n ErrorCode.BACKEND_FAILED,\n \"Backend render failed\",\n );\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 sendError(res, 500, ErrorCode.INTERNAL_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: RenderEventSender,\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: RenderEventSender,\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 try {\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 } finally {\n renderer.close();\n }\n}\n"],"mappings":"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","names":["Readable","sendEvent","Readable","randomUUID","extractEffieSourcesWithTypes","extractEffieSources","extractEffieSourcesWithTypes","randomUUID","extractEffieSources"]}
|
|
@@ -290,6 +290,9 @@ var FFmpegRunner = class {
|
|
|
290
290
|
transformedStream.pipe(writeStream);
|
|
291
291
|
writeStream.on("finish", next);
|
|
292
292
|
writeStream.on("error", reject);
|
|
293
|
+
} else {
|
|
294
|
+
stream2.resume();
|
|
295
|
+
next();
|
|
293
296
|
}
|
|
294
297
|
});
|
|
295
298
|
extract.on("finish", resolve);
|
|
@@ -936,4 +939,4 @@ export {
|
|
|
936
939
|
FFmpegRunner,
|
|
937
940
|
EffieRenderer
|
|
938
941
|
};
|
|
939
|
-
//# sourceMappingURL=chunk-
|
|
942
|
+
//# sourceMappingURL=chunk-O3UH3WAZ.js.map
|