@effing/ffs 0.6.0 → 0.7.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 +85 -176
- package/dist/{chunk-PERB3C4S.js → chunk-AUH73KG2.js} +14 -25
- package/dist/{chunk-O7Z6DV2I.js → chunk-CSKH34HX.js} +2 -2
- package/dist/{chunk-4N2GLGC5.js → chunk-UN34ESVZ.js} +15 -26
- package/dist/chunk-UN34ESVZ.js.map +1 -0
- package/dist/{chunk-7KHGAMSG.js → chunk-XOQMR7GF.js} +317 -463
- package/dist/chunk-XOQMR7GF.js.map +1 -0
- package/dist/handlers/index.d.ts +25 -36
- package/dist/handlers/index.js +8 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{proxy-CsZ5h2Ya.d.ts → proxy-BNr00n_4.d.ts} +2 -4
- package/dist/render-6HNZT5UH.js +8 -0
- package/dist/{render-MUKKTCF6.js → render-E3U44GOC.js} +1 -1
- package/dist/server.js +321 -466
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-4N2GLGC5.js.map +0 -1
- package/dist/chunk-7KHGAMSG.js.map +0 -1
- package/dist/render-IKGZZOBP.js +0 -8
- /package/dist/{chunk-O7Z6DV2I.js.map → chunk-CSKH34HX.js.map} +0 -0
- /package/dist/{render-IKGZZOBP.js.map → render-6HNZT5UH.js.map} +0 -0
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
createTransientStore,
|
|
3
3
|
ffsFetch,
|
|
4
4
|
storeKeys
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-UN34ESVZ.js";
|
|
6
6
|
|
|
7
7
|
// src/handlers/shared.ts
|
|
8
8
|
import "express";
|
|
@@ -190,445 +190,6 @@ data: ${JSON.stringify(data)}
|
|
|
190
190
|
`);
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
-
|
|
194
|
-
// src/handlers/caching.ts
|
|
195
|
-
import "express";
|
|
196
|
-
import { Readable as Readable2, Transform } from "stream";
|
|
197
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
198
|
-
import {
|
|
199
|
-
extractEffieSources,
|
|
200
|
-
extractEffieSourcesWithTypes as extractEffieSourcesWithTypes2
|
|
201
|
-
} from "@effing/effie";
|
|
202
|
-
|
|
203
|
-
// src/handlers/orchestrating.ts
|
|
204
|
-
import "express";
|
|
205
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
206
|
-
import { extractEffieSourcesWithTypes, effieDataSchema as effieDataSchema3 } from "@effing/effie";
|
|
207
|
-
|
|
208
|
-
// src/handlers/rendering.ts
|
|
209
|
-
import "express";
|
|
210
|
-
import { randomUUID } from "crypto";
|
|
211
|
-
import { effieDataSchema as effieDataSchema2 } from "@effing/effie";
|
|
212
|
-
async function createRenderJob(req, res, ctx, options) {
|
|
213
|
-
try {
|
|
214
|
-
const isWrapped = "effie" in req.body;
|
|
215
|
-
let rawEffieData;
|
|
216
|
-
let scale;
|
|
217
|
-
let upload;
|
|
218
|
-
if (isWrapped) {
|
|
219
|
-
const options2 = req.body;
|
|
220
|
-
if (typeof options2.effie === "string") {
|
|
221
|
-
const response = await ffsFetch(options2.effie);
|
|
222
|
-
if (!response.ok) {
|
|
223
|
-
throw new Error(
|
|
224
|
-
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
rawEffieData = await response.json();
|
|
228
|
-
} else {
|
|
229
|
-
rawEffieData = options2.effie;
|
|
230
|
-
}
|
|
231
|
-
scale = options2.scale ?? 1;
|
|
232
|
-
upload = options2.upload;
|
|
233
|
-
} else {
|
|
234
|
-
rawEffieData = req.body;
|
|
235
|
-
scale = parseFloat(req.query.scale?.toString() || "1");
|
|
236
|
-
}
|
|
237
|
-
let effie;
|
|
238
|
-
if (!ctx.skipValidation) {
|
|
239
|
-
const result = effieDataSchema2.safeParse(rawEffieData);
|
|
240
|
-
if (!result.success) {
|
|
241
|
-
res.status(400).json({
|
|
242
|
-
error: "Invalid effie data",
|
|
243
|
-
issues: result.error.issues.map((issue) => ({
|
|
244
|
-
path: issue.path.join("."),
|
|
245
|
-
message: issue.message
|
|
246
|
-
}))
|
|
247
|
-
});
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
effie = result.data;
|
|
251
|
-
} else {
|
|
252
|
-
const data = rawEffieData;
|
|
253
|
-
if (!data?.segments) {
|
|
254
|
-
res.status(400).json({ error: "Invalid effie data: missing segments" });
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
effie = data;
|
|
258
|
-
}
|
|
259
|
-
const jobId = randomUUID();
|
|
260
|
-
const job = {
|
|
261
|
-
effie,
|
|
262
|
-
scale,
|
|
263
|
-
upload,
|
|
264
|
-
createdAt: Date.now(),
|
|
265
|
-
metadata: options?.metadata
|
|
266
|
-
};
|
|
267
|
-
await ctx.transientStore.putJson(
|
|
268
|
-
storeKeys.renderJob(jobId),
|
|
269
|
-
job,
|
|
270
|
-
ctx.transientStore.jobDataTtlMs
|
|
271
|
-
);
|
|
272
|
-
res.json({
|
|
273
|
-
id: jobId,
|
|
274
|
-
url: `${ctx.baseUrl}/render/${jobId}`
|
|
275
|
-
});
|
|
276
|
-
} catch (error) {
|
|
277
|
-
console.error("Error creating render job:", error);
|
|
278
|
-
res.status(500).json({ error: "Failed to create render job" });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
async function streamRenderJob(req, res, ctx) {
|
|
282
|
-
try {
|
|
283
|
-
setupCORSHeaders(res);
|
|
284
|
-
const jobId = req.params.id;
|
|
285
|
-
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
286
|
-
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
287
|
-
if (!job) {
|
|
288
|
-
res.status(404).json({ error: "Job not found or expired" });
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (ctx.renderBackendResolver) {
|
|
292
|
-
const backend = ctx.renderBackendResolver(job.effie, job.metadata);
|
|
293
|
-
if (backend) {
|
|
294
|
-
await proxyRenderFromBackend(res, jobId, backend);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
ctx.transientStore.delete(jobStoreKey);
|
|
299
|
-
if (job.upload) {
|
|
300
|
-
await streamRenderWithUpload(res, job, ctx);
|
|
301
|
-
} else {
|
|
302
|
-
await streamRenderDirect(res, job, ctx);
|
|
303
|
-
}
|
|
304
|
-
} catch (error) {
|
|
305
|
-
console.error("Error in render:", error);
|
|
306
|
-
if (!res.headersSent) {
|
|
307
|
-
res.status(500).json({ error: "Rendering failed" });
|
|
308
|
-
} else {
|
|
309
|
-
res.end();
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async function streamRenderDirect(res, job, ctx) {
|
|
314
|
-
const { EffieRenderer } = await import("./render-IKGZZOBP.js");
|
|
315
|
-
const renderer = new EffieRenderer(job.effie, {
|
|
316
|
-
transientStore: ctx.transientStore,
|
|
317
|
-
httpProxy: ctx.httpProxy
|
|
318
|
-
});
|
|
319
|
-
const videoStream = await renderer.render(job.scale);
|
|
320
|
-
res.on("close", () => {
|
|
321
|
-
videoStream.destroy();
|
|
322
|
-
renderer.close();
|
|
323
|
-
});
|
|
324
|
-
res.set("Content-Type", "video/mp4");
|
|
325
|
-
videoStream.pipe(res);
|
|
326
|
-
}
|
|
327
|
-
async function streamRenderWithUpload(res, job, ctx) {
|
|
328
|
-
setupSSEResponse(res);
|
|
329
|
-
const sendEvent = createSSEEventSender(res);
|
|
330
|
-
const keepalive = setInterval(() => {
|
|
331
|
-
sendEvent("keepalive", { status: "rendering" });
|
|
332
|
-
}, 25e3);
|
|
333
|
-
try {
|
|
334
|
-
sendEvent("started", { status: "rendering" });
|
|
335
|
-
const timings = await renderAndUploadInternal(
|
|
336
|
-
job.effie,
|
|
337
|
-
job.scale,
|
|
338
|
-
job.upload,
|
|
339
|
-
sendEvent,
|
|
340
|
-
ctx
|
|
341
|
-
);
|
|
342
|
-
sendEvent("complete", { status: "uploaded", timings });
|
|
343
|
-
} catch (error) {
|
|
344
|
-
sendEvent("error", { message: String(error) });
|
|
345
|
-
} finally {
|
|
346
|
-
clearInterval(keepalive);
|
|
347
|
-
res.end();
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
351
|
-
const timings = {};
|
|
352
|
-
if (upload.coverUrl) {
|
|
353
|
-
const fetchCoverStartTime = Date.now();
|
|
354
|
-
const coverFetchResponse = await ffsFetch(effie.cover);
|
|
355
|
-
if (!coverFetchResponse.ok) {
|
|
356
|
-
throw new Error(
|
|
357
|
-
`Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
const coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());
|
|
361
|
-
timings.fetchCoverTime = Date.now() - fetchCoverStartTime;
|
|
362
|
-
const uploadCoverStartTime = Date.now();
|
|
363
|
-
const uploadCoverResponse = await ffsFetch(upload.coverUrl, {
|
|
364
|
-
method: "PUT",
|
|
365
|
-
body: coverBuffer,
|
|
366
|
-
headers: {
|
|
367
|
-
"Content-Type": "image/png",
|
|
368
|
-
"Content-Length": coverBuffer.length.toString()
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
if (!uploadCoverResponse.ok) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
`Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
|
|
377
|
-
}
|
|
378
|
-
const renderStartTime = Date.now();
|
|
379
|
-
const { EffieRenderer } = await import("./render-IKGZZOBP.js");
|
|
380
|
-
const renderer = new EffieRenderer(effie, {
|
|
381
|
-
transientStore: ctx.transientStore,
|
|
382
|
-
httpProxy: ctx.httpProxy
|
|
383
|
-
});
|
|
384
|
-
const videoStream = await renderer.render(scale);
|
|
385
|
-
const chunks = [];
|
|
386
|
-
for await (const chunk of videoStream) {
|
|
387
|
-
chunks.push(Buffer.from(chunk));
|
|
388
|
-
}
|
|
389
|
-
const videoBuffer = Buffer.concat(chunks);
|
|
390
|
-
timings.renderTime = Date.now() - renderStartTime;
|
|
391
|
-
sendEvent("keepalive", { status: "uploading" });
|
|
392
|
-
const uploadStartTime = Date.now();
|
|
393
|
-
const uploadResponse = await ffsFetch(upload.videoUrl, {
|
|
394
|
-
method: "PUT",
|
|
395
|
-
body: videoBuffer,
|
|
396
|
-
headers: {
|
|
397
|
-
"Content-Type": "video/mp4",
|
|
398
|
-
"Content-Length": videoBuffer.length.toString()
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
if (!uploadResponse.ok) {
|
|
402
|
-
throw new Error(
|
|
403
|
-
`Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
timings.uploadTime = Date.now() - uploadStartTime;
|
|
407
|
-
return timings;
|
|
408
|
-
}
|
|
409
|
-
async function proxyRenderFromBackend(res, jobId, backend) {
|
|
410
|
-
const backendUrl = `${backend.baseUrl}/render/${jobId}`;
|
|
411
|
-
const response = await ffsFetch(backendUrl, {
|
|
412
|
-
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
413
|
-
});
|
|
414
|
-
if (!response.ok) {
|
|
415
|
-
res.status(response.status).json({ error: "Backend render failed" });
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
const contentType = response.headers.get("content-type") || "";
|
|
419
|
-
if (contentType.includes("text/event-stream")) {
|
|
420
|
-
setupSSEResponse(res);
|
|
421
|
-
const sendEvent = createSSEEventSender(res);
|
|
422
|
-
const reader = response.body?.getReader();
|
|
423
|
-
if (!reader) {
|
|
424
|
-
sendEvent("error", { message: "No response body from backend" });
|
|
425
|
-
res.end();
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
const decoder = new TextDecoder();
|
|
429
|
-
let buffer = "";
|
|
430
|
-
try {
|
|
431
|
-
while (true) {
|
|
432
|
-
const { done, value } = await reader.read();
|
|
433
|
-
if (done) break;
|
|
434
|
-
if (res.destroyed) {
|
|
435
|
-
reader.cancel();
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
buffer += decoder.decode(value, { stream: true });
|
|
439
|
-
const lines = buffer.split("\n");
|
|
440
|
-
buffer = lines.pop() || "";
|
|
441
|
-
let currentEvent = "";
|
|
442
|
-
let currentData = "";
|
|
443
|
-
for (const line of lines) {
|
|
444
|
-
if (line.startsWith("event: ")) {
|
|
445
|
-
currentEvent = line.slice(7);
|
|
446
|
-
} else if (line.startsWith("data: ")) {
|
|
447
|
-
currentData = line.slice(6);
|
|
448
|
-
} else if (line === "" && currentEvent && currentData) {
|
|
449
|
-
try {
|
|
450
|
-
const data = JSON.parse(currentData);
|
|
451
|
-
sendEvent(currentEvent, data);
|
|
452
|
-
} catch {
|
|
453
|
-
}
|
|
454
|
-
currentEvent = "";
|
|
455
|
-
currentData = "";
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
} finally {
|
|
460
|
-
reader.releaseLock();
|
|
461
|
-
res.end();
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
await proxyBinaryStream(response, res);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// src/handlers/orchestrating.ts
|
|
469
|
-
async function createWarmupAndRenderJob(req, res, ctx, options) {
|
|
470
|
-
try {
|
|
471
|
-
const body = req.body;
|
|
472
|
-
let rawEffieData;
|
|
473
|
-
if (typeof body.effie === "string") {
|
|
474
|
-
const response = await ffsFetch(body.effie);
|
|
475
|
-
if (!response.ok) {
|
|
476
|
-
throw new Error(
|
|
477
|
-
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
rawEffieData = await response.json();
|
|
481
|
-
} else {
|
|
482
|
-
rawEffieData = body.effie;
|
|
483
|
-
}
|
|
484
|
-
let effie;
|
|
485
|
-
if (!ctx.skipValidation) {
|
|
486
|
-
const result = effieDataSchema3.safeParse(rawEffieData);
|
|
487
|
-
if (!result.success) {
|
|
488
|
-
res.status(400).json({
|
|
489
|
-
error: "Invalid effie data",
|
|
490
|
-
issues: result.error.issues.map((issue) => ({
|
|
491
|
-
path: issue.path.join("."),
|
|
492
|
-
message: issue.message
|
|
493
|
-
}))
|
|
494
|
-
});
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
effie = result.data;
|
|
498
|
-
} else {
|
|
499
|
-
const data = rawEffieData;
|
|
500
|
-
if (!data?.segments) {
|
|
501
|
-
res.status(400).json({ error: "Invalid effie data: missing segments" });
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
effie = data;
|
|
505
|
-
}
|
|
506
|
-
const sources = extractEffieSourcesWithTypes(effie);
|
|
507
|
-
const scale = body.scale ?? 1;
|
|
508
|
-
const upload = body.upload;
|
|
509
|
-
const jobId = randomUUID2();
|
|
510
|
-
const warmupJobId = randomUUID2();
|
|
511
|
-
const renderJobId = randomUUID2();
|
|
512
|
-
const job = {
|
|
513
|
-
effie,
|
|
514
|
-
sources,
|
|
515
|
-
scale,
|
|
516
|
-
upload,
|
|
517
|
-
warmupJobId,
|
|
518
|
-
renderJobId,
|
|
519
|
-
createdAt: Date.now(),
|
|
520
|
-
metadata: options?.metadata
|
|
521
|
-
};
|
|
522
|
-
await ctx.transientStore.putJson(
|
|
523
|
-
storeKeys.warmupAndRenderJob(jobId),
|
|
524
|
-
job,
|
|
525
|
-
ctx.transientStore.jobDataTtlMs
|
|
526
|
-
);
|
|
527
|
-
await ctx.transientStore.putJson(
|
|
528
|
-
storeKeys.warmupJob(warmupJobId),
|
|
529
|
-
{ sources, metadata: options?.metadata },
|
|
530
|
-
ctx.transientStore.jobDataTtlMs
|
|
531
|
-
);
|
|
532
|
-
await ctx.transientStore.putJson(
|
|
533
|
-
storeKeys.renderJob(renderJobId),
|
|
534
|
-
{
|
|
535
|
-
effie,
|
|
536
|
-
scale,
|
|
537
|
-
upload,
|
|
538
|
-
createdAt: Date.now(),
|
|
539
|
-
metadata: options?.metadata
|
|
540
|
-
},
|
|
541
|
-
ctx.transientStore.jobDataTtlMs
|
|
542
|
-
);
|
|
543
|
-
res.json({
|
|
544
|
-
id: jobId,
|
|
545
|
-
url: `${ctx.baseUrl}/warmup-and-render/${jobId}`
|
|
546
|
-
});
|
|
547
|
-
} catch (error) {
|
|
548
|
-
console.error("Error creating warmup-and-render job:", error);
|
|
549
|
-
res.status(500).json({ error: "Failed to create warmup-and-render job" });
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
async function streamWarmupAndRenderJob(req, res, ctx) {
|
|
553
|
-
try {
|
|
554
|
-
setupCORSHeaders(res);
|
|
555
|
-
const jobId = req.params.id;
|
|
556
|
-
const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);
|
|
557
|
-
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
558
|
-
ctx.transientStore.delete(jobStoreKey);
|
|
559
|
-
if (!job) {
|
|
560
|
-
res.status(404).json({ error: "Job not found" });
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
564
|
-
const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
|
|
565
|
-
setupSSEResponse(res);
|
|
566
|
-
const sendEvent = createSSEEventSender(res);
|
|
567
|
-
let keepalivePhase = "warmup";
|
|
568
|
-
const keepalive = setInterval(() => {
|
|
569
|
-
sendEvent("keepalive", { phase: keepalivePhase });
|
|
570
|
-
}, 25e3);
|
|
571
|
-
try {
|
|
572
|
-
if (warmupBackend) {
|
|
573
|
-
await proxyRemoteSSE(
|
|
574
|
-
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,
|
|
575
|
-
sendEvent,
|
|
576
|
-
"warmup:",
|
|
577
|
-
res,
|
|
578
|
-
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
579
|
-
);
|
|
580
|
-
} else {
|
|
581
|
-
const warmupSender = prefixEventSender(sendEvent, "warmup:");
|
|
582
|
-
await warmupSources(job.sources, warmupSender, ctx);
|
|
583
|
-
warmupSender("complete", { status: "ready" });
|
|
584
|
-
}
|
|
585
|
-
keepalivePhase = "render";
|
|
586
|
-
if (renderBackend) {
|
|
587
|
-
await proxyRemoteSSE(
|
|
588
|
-
`${renderBackend.baseUrl}/render/${job.renderJobId}`,
|
|
589
|
-
sendEvent,
|
|
590
|
-
"render:",
|
|
591
|
-
res,
|
|
592
|
-
renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
|
|
593
|
-
);
|
|
594
|
-
} else {
|
|
595
|
-
const renderSender = prefixEventSender(sendEvent, "render:");
|
|
596
|
-
if (job.upload) {
|
|
597
|
-
renderSender("started", { status: "rendering" });
|
|
598
|
-
const timings = await renderAndUploadInternal(
|
|
599
|
-
job.effie,
|
|
600
|
-
job.scale,
|
|
601
|
-
job.upload,
|
|
602
|
-
renderSender,
|
|
603
|
-
ctx
|
|
604
|
-
);
|
|
605
|
-
renderSender("complete", { status: "uploaded", timings });
|
|
606
|
-
} else {
|
|
607
|
-
const videoUrl = `${ctx.baseUrl}/render/${job.renderJobId}`;
|
|
608
|
-
sendEvent("complete", { status: "ready", videoUrl });
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (job.upload && !renderBackend) {
|
|
612
|
-
sendEvent("complete", { status: "done" });
|
|
613
|
-
}
|
|
614
|
-
} catch (error) {
|
|
615
|
-
sendEvent("error", {
|
|
616
|
-
phase: keepalivePhase,
|
|
617
|
-
message: String(error)
|
|
618
|
-
});
|
|
619
|
-
} finally {
|
|
620
|
-
clearInterval(keepalive);
|
|
621
|
-
res.end();
|
|
622
|
-
}
|
|
623
|
-
} catch (error) {
|
|
624
|
-
console.error("Error in warmup-and-render streaming:", error);
|
|
625
|
-
if (!res.headersSent) {
|
|
626
|
-
res.status(500).json({ error: "Warmup-and-render streaming failed" });
|
|
627
|
-
} else {
|
|
628
|
-
res.end();
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
193
|
function prefixEventSender(sendEvent, prefix) {
|
|
633
194
|
return (event, data) => {
|
|
634
195
|
sendEvent(`${prefix}${event}`, data);
|
|
@@ -709,6 +270,13 @@ async function proxyBinaryStream(response, res) {
|
|
|
709
270
|
}
|
|
710
271
|
|
|
711
272
|
// src/handlers/caching.ts
|
|
273
|
+
import "express";
|
|
274
|
+
import { Readable as Readable2, Transform } from "stream";
|
|
275
|
+
import { randomUUID } from "crypto";
|
|
276
|
+
import {
|
|
277
|
+
extractEffieSources,
|
|
278
|
+
extractEffieSourcesWithTypes
|
|
279
|
+
} from "@effing/effie";
|
|
712
280
|
function shouldSkipWarmup(source) {
|
|
713
281
|
return source.type === "video" || source.type === "audio";
|
|
714
282
|
}
|
|
@@ -720,24 +288,24 @@ async function createWarmupJob(req, res, ctx, options) {
|
|
|
720
288
|
res.status(400).json(parseResult);
|
|
721
289
|
return;
|
|
722
290
|
}
|
|
723
|
-
const sources =
|
|
724
|
-
const jobId =
|
|
291
|
+
const sources = extractEffieSourcesWithTypes(parseResult.effie);
|
|
292
|
+
const jobId = randomUUID();
|
|
725
293
|
const job = { sources, metadata: options?.metadata };
|
|
726
294
|
await ctx.transientStore.putJson(
|
|
727
295
|
storeKeys.warmupJob(jobId),
|
|
728
296
|
job,
|
|
729
|
-
ctx.transientStore.
|
|
297
|
+
ctx.transientStore.ttlMs
|
|
730
298
|
);
|
|
731
299
|
res.json({
|
|
732
300
|
id: jobId,
|
|
733
|
-
|
|
301
|
+
progressUrl: `${ctx.baseUrl}/warmup/${jobId}/progress`
|
|
734
302
|
});
|
|
735
303
|
} catch (error) {
|
|
736
304
|
console.error("Error creating warmup job:", error);
|
|
737
305
|
res.status(500).json({ error: "Failed to create warmup job" });
|
|
738
306
|
}
|
|
739
307
|
}
|
|
740
|
-
async function
|
|
308
|
+
async function streamWarmupProgress(req, res, ctx) {
|
|
741
309
|
try {
|
|
742
310
|
setupCORSHeaders(res);
|
|
743
311
|
const jobId = req.params.id;
|
|
@@ -754,7 +322,7 @@ async function streamWarmupJob(req, res, ctx) {
|
|
|
754
322
|
const sendEvent2 = createSSEEventSender(res);
|
|
755
323
|
try {
|
|
756
324
|
await proxyRemoteSSE(
|
|
757
|
-
`${backend.baseUrl}/warmup/${jobId}`,
|
|
325
|
+
`${backend.baseUrl}/warmup/${jobId}/progress`,
|
|
758
326
|
sendEvent2,
|
|
759
327
|
"",
|
|
760
328
|
res,
|
|
@@ -786,6 +354,17 @@ async function streamWarmupJob(req, res, ctx) {
|
|
|
786
354
|
}
|
|
787
355
|
}
|
|
788
356
|
}
|
|
357
|
+
async function purgeCachedSources(urls, store) {
|
|
358
|
+
let purged = 0;
|
|
359
|
+
for (const url of urls) {
|
|
360
|
+
const ck = storeKeys.source(url);
|
|
361
|
+
if (await store.exists(ck)) {
|
|
362
|
+
await store.delete(ck);
|
|
363
|
+
purged++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { purged, total: urls.length };
|
|
367
|
+
}
|
|
789
368
|
async function purgeCache(req, res, ctx) {
|
|
790
369
|
try {
|
|
791
370
|
const parseResult = parseEffieData(req.body, ctx.skipValidation);
|
|
@@ -794,15 +373,8 @@ async function purgeCache(req, res, ctx) {
|
|
|
794
373
|
return;
|
|
795
374
|
}
|
|
796
375
|
const sources = extractEffieSources(parseResult.effie);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const ck = storeKeys.source(url);
|
|
800
|
-
if (await ctx.transientStore.exists(ck)) {
|
|
801
|
-
await ctx.transientStore.delete(ck);
|
|
802
|
-
purged++;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
res.json({ purged, total: sources.length });
|
|
376
|
+
const result = await purgeCachedSources(sources, ctx.transientStore);
|
|
377
|
+
res.json(result);
|
|
806
378
|
} catch (error) {
|
|
807
379
|
console.error("Error purging cache:", error);
|
|
808
380
|
res.status(500).json({ error: "Failed to purge cache" });
|
|
@@ -939,20 +511,302 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx) {
|
|
|
939
511
|
await ctx.transientStore.put(
|
|
940
512
|
cacheKey,
|
|
941
513
|
trackedStream,
|
|
942
|
-
ctx.transientStore.
|
|
514
|
+
ctx.transientStore.ttlMs
|
|
943
515
|
);
|
|
944
516
|
}
|
|
945
517
|
|
|
518
|
+
// src/handlers/rendering.ts
|
|
519
|
+
import "express";
|
|
520
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
521
|
+
import {
|
|
522
|
+
extractEffieSourcesWithTypes as extractEffieSourcesWithTypes2,
|
|
523
|
+
extractEffieSources as extractEffieSources2,
|
|
524
|
+
effieDataSchema as effieDataSchema2
|
|
525
|
+
} from "@effing/effie";
|
|
526
|
+
async function createRenderJob(req, res, ctx, options) {
|
|
527
|
+
try {
|
|
528
|
+
const body = req.body;
|
|
529
|
+
let rawEffieData;
|
|
530
|
+
if (typeof body.effie === "string") {
|
|
531
|
+
const response = await ffsFetch(body.effie);
|
|
532
|
+
if (!response.ok) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Failed to fetch Effie data: ${response.status} ${response.statusText}`
|
|
535
|
+
);
|
|
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
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
effie = result.data;
|
|
555
|
+
} else {
|
|
556
|
+
const data = rawEffieData;
|
|
557
|
+
if (!data?.segments) {
|
|
558
|
+
res.status(400).json({ error: "Invalid effie data: missing segments" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
effie = data;
|
|
562
|
+
}
|
|
563
|
+
const sources = extractEffieSourcesWithTypes2(effie);
|
|
564
|
+
const scale = body.scale ?? 1;
|
|
565
|
+
const upload = body.upload;
|
|
566
|
+
const purge = body.purge;
|
|
567
|
+
const jobId = randomUUID2();
|
|
568
|
+
const warmupJobId = randomUUID2();
|
|
569
|
+
const job = {
|
|
570
|
+
effie,
|
|
571
|
+
sources,
|
|
572
|
+
scale,
|
|
573
|
+
upload,
|
|
574
|
+
purge,
|
|
575
|
+
warmupJobId,
|
|
576
|
+
createdAt: Date.now(),
|
|
577
|
+
metadata: options?.metadata
|
|
578
|
+
};
|
|
579
|
+
await ctx.transientStore.putJson(
|
|
580
|
+
storeKeys.renderJob(jobId),
|
|
581
|
+
job,
|
|
582
|
+
ctx.transientStore.ttlMs
|
|
583
|
+
);
|
|
584
|
+
await ctx.transientStore.putJson(
|
|
585
|
+
storeKeys.warmupJob(warmupJobId),
|
|
586
|
+
{ sources, metadata: options?.metadata },
|
|
587
|
+
ctx.transientStore.ttlMs
|
|
588
|
+
);
|
|
589
|
+
res.json({
|
|
590
|
+
id: jobId,
|
|
591
|
+
progressUrl: `${ctx.baseUrl}/render/${jobId}/progress`
|
|
592
|
+
});
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error("Error creating render job:", error);
|
|
595
|
+
res.status(500).json({ error: "Failed to create render job" });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function streamRenderProgress(req, res, ctx) {
|
|
599
|
+
try {
|
|
600
|
+
setupCORSHeaders(res);
|
|
601
|
+
const jobId = req.params.id;
|
|
602
|
+
const jobStoreKey = storeKeys.renderJob(jobId);
|
|
603
|
+
const job = await ctx.transientStore.getJson(jobStoreKey);
|
|
604
|
+
ctx.transientStore.delete(jobStoreKey);
|
|
605
|
+
if (!job) {
|
|
606
|
+
res.status(404).json({ error: "Job not found" });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
|
|
610
|
+
const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
|
|
611
|
+
setupSSEResponse(res);
|
|
612
|
+
const sendEvent = createSSEEventSender(res);
|
|
613
|
+
let keepalivePhase = "warmup";
|
|
614
|
+
const keepalive = setInterval(() => {
|
|
615
|
+
sendEvent("keepalive", { phase: keepalivePhase });
|
|
616
|
+
}, 25e3);
|
|
617
|
+
try {
|
|
618
|
+
if (job.purge) {
|
|
619
|
+
const sourceUrls = extractEffieSources2(job.effie);
|
|
620
|
+
const purgeResult = await purgeCachedSources(
|
|
621
|
+
sourceUrls,
|
|
622
|
+
ctx.transientStore
|
|
623
|
+
);
|
|
624
|
+
sendEvent("purge:complete", purgeResult);
|
|
625
|
+
}
|
|
626
|
+
if (warmupBackend) {
|
|
627
|
+
await proxyRemoteSSE(
|
|
628
|
+
`${warmupBackend.baseUrl}/warmup/${job.warmupJobId}/progress`,
|
|
629
|
+
sendEvent,
|
|
630
|
+
"warmup:",
|
|
631
|
+
res,
|
|
632
|
+
warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
|
|
633
|
+
);
|
|
634
|
+
} else {
|
|
635
|
+
const warmupSender = prefixEventSender(sendEvent, "warmup:");
|
|
636
|
+
await warmupSources(job.sources, warmupSender, ctx);
|
|
637
|
+
warmupSender("complete", { status: "ready" });
|
|
638
|
+
}
|
|
639
|
+
keepalivePhase = "render";
|
|
640
|
+
if (renderBackend) {
|
|
641
|
+
await proxyRemoteSSE(
|
|
642
|
+
`${renderBackend.baseUrl}/render/${jobId}/progress`,
|
|
643
|
+
sendEvent,
|
|
644
|
+
"render:",
|
|
645
|
+
res,
|
|
646
|
+
renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
|
|
647
|
+
);
|
|
648
|
+
} else {
|
|
649
|
+
if (job.upload) {
|
|
650
|
+
const renderSender = prefixEventSender(sendEvent, "render:");
|
|
651
|
+
renderSender("started", { status: "rendering" });
|
|
652
|
+
const timings = await renderAndUploadInternal(
|
|
653
|
+
job.effie,
|
|
654
|
+
job.scale,
|
|
655
|
+
job.upload,
|
|
656
|
+
renderSender,
|
|
657
|
+
ctx
|
|
658
|
+
);
|
|
659
|
+
renderSender("complete", { status: "uploaded", timings });
|
|
660
|
+
sendEvent("complete", { status: "done" });
|
|
661
|
+
} else {
|
|
662
|
+
const videoJob = { effie: job.effie, scale: job.scale };
|
|
663
|
+
await ctx.transientStore.putJson(
|
|
664
|
+
storeKeys.videoJob(jobId),
|
|
665
|
+
videoJob,
|
|
666
|
+
ctx.transientStore.ttlMs
|
|
667
|
+
);
|
|
668
|
+
const videoUrl = `${ctx.baseUrl}/render/${jobId}/video`;
|
|
669
|
+
sendEvent("ready", { videoUrl });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (error) {
|
|
673
|
+
sendEvent("error", {
|
|
674
|
+
phase: keepalivePhase,
|
|
675
|
+
message: String(error)
|
|
676
|
+
});
|
|
677
|
+
} finally {
|
|
678
|
+
clearInterval(keepalive);
|
|
679
|
+
res.end();
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
console.error("Error in render progress streaming:", error);
|
|
683
|
+
if (!res.headersSent) {
|
|
684
|
+
res.status(500).json({ error: "Render progress streaming failed" });
|
|
685
|
+
} else {
|
|
686
|
+
res.end();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function streamRenderVideo(req, res, ctx) {
|
|
691
|
+
try {
|
|
692
|
+
setupCORSHeaders(res);
|
|
693
|
+
const jobId = req.params.id;
|
|
694
|
+
const videoJobKey = storeKeys.videoJob(jobId);
|
|
695
|
+
const videoJob = await ctx.transientStore.getJson(videoJobKey);
|
|
696
|
+
if (!videoJob) {
|
|
697
|
+
res.status(404).json({ error: "Video not found or expired" });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
ctx.transientStore.delete(videoJobKey);
|
|
701
|
+
if (ctx.renderBackendResolver) {
|
|
702
|
+
const backend = ctx.renderBackendResolver(videoJob.effie);
|
|
703
|
+
if (backend) {
|
|
704
|
+
const backendUrl = `${backend.baseUrl}/render/${jobId}/video`;
|
|
705
|
+
const response = await ffsFetch(backendUrl, {
|
|
706
|
+
headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
|
|
707
|
+
});
|
|
708
|
+
if (!response.ok) {
|
|
709
|
+
res.status(response.status).json({ error: "Backend render failed" });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
await proxyBinaryStream(response, res);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
await streamRenderDirect(res, videoJob, ctx);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error("Error streaming video:", error);
|
|
719
|
+
if (!res.headersSent) {
|
|
720
|
+
res.status(500).json({ error: "Video streaming failed" });
|
|
721
|
+
} else {
|
|
722
|
+
res.end();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function streamRenderDirect(res, job, ctx) {
|
|
727
|
+
const { EffieRenderer } = await import("./render-6HNZT5UH.js");
|
|
728
|
+
const renderer = new EffieRenderer(job.effie, {
|
|
729
|
+
transientStore: ctx.transientStore,
|
|
730
|
+
httpProxy: ctx.httpProxy
|
|
731
|
+
});
|
|
732
|
+
const videoStream = await renderer.render(job.scale);
|
|
733
|
+
res.on("close", () => {
|
|
734
|
+
videoStream.destroy();
|
|
735
|
+
renderer.close();
|
|
736
|
+
});
|
|
737
|
+
res.set("Content-Type", "video/mp4");
|
|
738
|
+
res.set("Cache-Control", "public, immutable, max-age=86400");
|
|
739
|
+
videoStream.pipe(res);
|
|
740
|
+
}
|
|
741
|
+
async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
|
|
742
|
+
const timings = {};
|
|
743
|
+
if (upload.coverUrl) {
|
|
744
|
+
const fetchCoverStartTime = Date.now();
|
|
745
|
+
const coverFetchResponse = await ffsFetch(effie.cover);
|
|
746
|
+
if (!coverFetchResponse.ok) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());
|
|
752
|
+
timings.fetchCoverTime = Date.now() - fetchCoverStartTime;
|
|
753
|
+
const uploadCoverStartTime = Date.now();
|
|
754
|
+
const uploadCoverResponse = await ffsFetch(upload.coverUrl, {
|
|
755
|
+
method: "PUT",
|
|
756
|
+
body: coverBuffer,
|
|
757
|
+
headers: {
|
|
758
|
+
"Content-Type": "image/png",
|
|
759
|
+
"Content-Length": coverBuffer.length.toString()
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
if (!uploadCoverResponse.ok) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
`Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
|
|
768
|
+
}
|
|
769
|
+
const renderStartTime = Date.now();
|
|
770
|
+
const { EffieRenderer } = await import("./render-6HNZT5UH.js");
|
|
771
|
+
const renderer = new EffieRenderer(effie, {
|
|
772
|
+
transientStore: ctx.transientStore,
|
|
773
|
+
httpProxy: ctx.httpProxy
|
|
774
|
+
});
|
|
775
|
+
const videoStream = await renderer.render(scale);
|
|
776
|
+
const chunks = [];
|
|
777
|
+
for await (const chunk of videoStream) {
|
|
778
|
+
chunks.push(Buffer.from(chunk));
|
|
779
|
+
}
|
|
780
|
+
const videoBuffer = Buffer.concat(chunks);
|
|
781
|
+
timings.renderTime = Date.now() - renderStartTime;
|
|
782
|
+
sendEvent("keepalive", { status: "uploading" });
|
|
783
|
+
const uploadStartTime = Date.now();
|
|
784
|
+
const uploadResponse = await ffsFetch(upload.videoUrl, {
|
|
785
|
+
method: "PUT",
|
|
786
|
+
body: videoBuffer,
|
|
787
|
+
headers: {
|
|
788
|
+
"Content-Type": "video/mp4",
|
|
789
|
+
"Content-Length": videoBuffer.length.toString()
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
if (!uploadResponse.ok) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
timings.uploadTime = Date.now() - uploadStartTime;
|
|
798
|
+
return timings;
|
|
799
|
+
}
|
|
800
|
+
|
|
946
801
|
export {
|
|
947
802
|
createServerContext,
|
|
948
|
-
createRenderJob,
|
|
949
|
-
streamRenderJob,
|
|
950
|
-
createWarmupAndRenderJob,
|
|
951
|
-
streamWarmupAndRenderJob,
|
|
952
803
|
proxyRemoteSSE,
|
|
953
804
|
proxyBinaryStream,
|
|
954
805
|
createWarmupJob,
|
|
955
|
-
|
|
956
|
-
purgeCache
|
|
806
|
+
streamWarmupProgress,
|
|
807
|
+
purgeCache,
|
|
808
|
+
createRenderJob,
|
|
809
|
+
streamRenderProgress,
|
|
810
|
+
streamRenderVideo
|
|
957
811
|
};
|
|
958
|
-
//# sourceMappingURL=chunk-
|
|
812
|
+
//# sourceMappingURL=chunk-XOQMR7GF.js.map
|