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