@effing/ffs 0.6.1 → 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.
@@ -2,7 +2,7 @@ import {
2
2
  createTransientStore,
3
3
  ffsFetch,
4
4
  storeKeys
5
- } from "./chunk-4N2GLGC5.js";
5
+ } from "./chunk-UN34ESVZ.js";
6
6
 
7
7
  // src/handlers/shared.ts
8
8
  import "express";
@@ -190,446 +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
- res.set("Cache-Control", "public, immutable, max-age=86400");
326
- videoStream.pipe(res);
327
- }
328
- async function streamRenderWithUpload(res, job, ctx) {
329
- setupSSEResponse(res);
330
- const sendEvent = createSSEEventSender(res);
331
- const keepalive = setInterval(() => {
332
- sendEvent("keepalive", { status: "rendering" });
333
- }, 25e3);
334
- try {
335
- sendEvent("started", { status: "rendering" });
336
- const timings = await renderAndUploadInternal(
337
- job.effie,
338
- job.scale,
339
- job.upload,
340
- sendEvent,
341
- ctx
342
- );
343
- sendEvent("complete", { status: "uploaded", timings });
344
- } catch (error) {
345
- sendEvent("error", { message: String(error) });
346
- } finally {
347
- clearInterval(keepalive);
348
- res.end();
349
- }
350
- }
351
- async function renderAndUploadInternal(effie, scale, upload, sendEvent, ctx) {
352
- const timings = {};
353
- if (upload.coverUrl) {
354
- const fetchCoverStartTime = Date.now();
355
- const coverFetchResponse = await ffsFetch(effie.cover);
356
- if (!coverFetchResponse.ok) {
357
- throw new Error(
358
- `Failed to fetch cover image: ${coverFetchResponse.status} ${coverFetchResponse.statusText}`
359
- );
360
- }
361
- const coverBuffer = Buffer.from(await coverFetchResponse.arrayBuffer());
362
- timings.fetchCoverTime = Date.now() - fetchCoverStartTime;
363
- const uploadCoverStartTime = Date.now();
364
- const uploadCoverResponse = await ffsFetch(upload.coverUrl, {
365
- method: "PUT",
366
- body: coverBuffer,
367
- headers: {
368
- "Content-Type": "image/png",
369
- "Content-Length": coverBuffer.length.toString()
370
- }
371
- });
372
- if (!uploadCoverResponse.ok) {
373
- throw new Error(
374
- `Failed to upload cover: ${uploadCoverResponse.status} ${uploadCoverResponse.statusText}`
375
- );
376
- }
377
- timings.uploadCoverTime = Date.now() - uploadCoverStartTime;
378
- }
379
- const renderStartTime = Date.now();
380
- const { EffieRenderer } = await import("./render-IKGZZOBP.js");
381
- const renderer = new EffieRenderer(effie, {
382
- transientStore: ctx.transientStore,
383
- httpProxy: ctx.httpProxy
384
- });
385
- const videoStream = await renderer.render(scale);
386
- const chunks = [];
387
- for await (const chunk of videoStream) {
388
- chunks.push(Buffer.from(chunk));
389
- }
390
- const videoBuffer = Buffer.concat(chunks);
391
- timings.renderTime = Date.now() - renderStartTime;
392
- sendEvent("keepalive", { status: "uploading" });
393
- const uploadStartTime = Date.now();
394
- const uploadResponse = await ffsFetch(upload.videoUrl, {
395
- method: "PUT",
396
- body: videoBuffer,
397
- headers: {
398
- "Content-Type": "video/mp4",
399
- "Content-Length": videoBuffer.length.toString()
400
- }
401
- });
402
- if (!uploadResponse.ok) {
403
- throw new Error(
404
- `Failed to upload rendered video: ${uploadResponse.status} ${uploadResponse.statusText}`
405
- );
406
- }
407
- timings.uploadTime = Date.now() - uploadStartTime;
408
- return timings;
409
- }
410
- async function proxyRenderFromBackend(res, jobId, backend) {
411
- const backendUrl = `${backend.baseUrl}/render/${jobId}`;
412
- const response = await ffsFetch(backendUrl, {
413
- headers: backend.apiKey ? { Authorization: `Bearer ${backend.apiKey}` } : void 0
414
- });
415
- if (!response.ok) {
416
- res.status(response.status).json({ error: "Backend render failed" });
417
- return;
418
- }
419
- const contentType = response.headers.get("content-type") || "";
420
- if (contentType.includes("text/event-stream")) {
421
- setupSSEResponse(res);
422
- const sendEvent = createSSEEventSender(res);
423
- const reader = response.body?.getReader();
424
- if (!reader) {
425
- sendEvent("error", { message: "No response body from backend" });
426
- res.end();
427
- return;
428
- }
429
- const decoder = new TextDecoder();
430
- let buffer = "";
431
- try {
432
- while (true) {
433
- const { done, value } = await reader.read();
434
- if (done) break;
435
- if (res.destroyed) {
436
- reader.cancel();
437
- break;
438
- }
439
- buffer += decoder.decode(value, { stream: true });
440
- const lines = buffer.split("\n");
441
- buffer = lines.pop() || "";
442
- let currentEvent = "";
443
- let currentData = "";
444
- for (const line of lines) {
445
- if (line.startsWith("event: ")) {
446
- currentEvent = line.slice(7);
447
- } else if (line.startsWith("data: ")) {
448
- currentData = line.slice(6);
449
- } else if (line === "" && currentEvent && currentData) {
450
- try {
451
- const data = JSON.parse(currentData);
452
- sendEvent(currentEvent, data);
453
- } catch {
454
- }
455
- currentEvent = "";
456
- currentData = "";
457
- }
458
- }
459
- }
460
- } finally {
461
- reader.releaseLock();
462
- res.end();
463
- }
464
- } else {
465
- await proxyBinaryStream(response, res);
466
- }
467
- }
468
-
469
- // src/handlers/orchestrating.ts
470
- async function createWarmupAndRenderJob(req, res, ctx, options) {
471
- try {
472
- const body = req.body;
473
- let rawEffieData;
474
- if (typeof body.effie === "string") {
475
- const response = await ffsFetch(body.effie);
476
- if (!response.ok) {
477
- throw new Error(
478
- `Failed to fetch Effie data: ${response.status} ${response.statusText}`
479
- );
480
- }
481
- rawEffieData = await response.json();
482
- } else {
483
- rawEffieData = body.effie;
484
- }
485
- let effie;
486
- if (!ctx.skipValidation) {
487
- const result = effieDataSchema3.safeParse(rawEffieData);
488
- if (!result.success) {
489
- res.status(400).json({
490
- error: "Invalid effie data",
491
- issues: result.error.issues.map((issue) => ({
492
- path: issue.path.join("."),
493
- message: issue.message
494
- }))
495
- });
496
- return;
497
- }
498
- effie = result.data;
499
- } else {
500
- const data = rawEffieData;
501
- if (!data?.segments) {
502
- res.status(400).json({ error: "Invalid effie data: missing segments" });
503
- return;
504
- }
505
- effie = data;
506
- }
507
- const sources = extractEffieSourcesWithTypes(effie);
508
- const scale = body.scale ?? 1;
509
- const upload = body.upload;
510
- const jobId = randomUUID2();
511
- const warmupJobId = randomUUID2();
512
- const renderJobId = randomUUID2();
513
- const job = {
514
- effie,
515
- sources,
516
- scale,
517
- upload,
518
- warmupJobId,
519
- renderJobId,
520
- createdAt: Date.now(),
521
- metadata: options?.metadata
522
- };
523
- await ctx.transientStore.putJson(
524
- storeKeys.warmupAndRenderJob(jobId),
525
- job,
526
- ctx.transientStore.jobDataTtlMs
527
- );
528
- await ctx.transientStore.putJson(
529
- storeKeys.warmupJob(warmupJobId),
530
- { sources, metadata: options?.metadata },
531
- ctx.transientStore.jobDataTtlMs
532
- );
533
- await ctx.transientStore.putJson(
534
- storeKeys.renderJob(renderJobId),
535
- {
536
- effie,
537
- scale,
538
- upload,
539
- createdAt: Date.now(),
540
- metadata: options?.metadata
541
- },
542
- ctx.transientStore.jobDataTtlMs
543
- );
544
- res.json({
545
- id: jobId,
546
- url: `${ctx.baseUrl}/warmup-and-render/${jobId}`
547
- });
548
- } catch (error) {
549
- console.error("Error creating warmup-and-render job:", error);
550
- res.status(500).json({ error: "Failed to create warmup-and-render job" });
551
- }
552
- }
553
- async function streamWarmupAndRenderJob(req, res, ctx) {
554
- try {
555
- setupCORSHeaders(res);
556
- const jobId = req.params.id;
557
- const jobStoreKey = storeKeys.warmupAndRenderJob(jobId);
558
- const job = await ctx.transientStore.getJson(jobStoreKey);
559
- ctx.transientStore.delete(jobStoreKey);
560
- if (!job) {
561
- res.status(404).json({ error: "Job not found" });
562
- return;
563
- }
564
- const warmupBackend = ctx.warmupBackendResolver ? ctx.warmupBackendResolver(job.sources, job.metadata) : null;
565
- const renderBackend = ctx.renderBackendResolver ? ctx.renderBackendResolver(job.effie, job.metadata) : null;
566
- setupSSEResponse(res);
567
- const sendEvent = createSSEEventSender(res);
568
- let keepalivePhase = "warmup";
569
- const keepalive = setInterval(() => {
570
- sendEvent("keepalive", { phase: keepalivePhase });
571
- }, 25e3);
572
- try {
573
- if (warmupBackend) {
574
- await proxyRemoteSSE(
575
- `${warmupBackend.baseUrl}/warmup/${job.warmupJobId}`,
576
- sendEvent,
577
- "warmup:",
578
- res,
579
- warmupBackend.apiKey ? { Authorization: `Bearer ${warmupBackend.apiKey}` } : void 0
580
- );
581
- } else {
582
- const warmupSender = prefixEventSender(sendEvent, "warmup:");
583
- await warmupSources(job.sources, warmupSender, ctx);
584
- warmupSender("complete", { status: "ready" });
585
- }
586
- keepalivePhase = "render";
587
- if (renderBackend) {
588
- await proxyRemoteSSE(
589
- `${renderBackend.baseUrl}/render/${job.renderJobId}`,
590
- sendEvent,
591
- "render:",
592
- res,
593
- renderBackend.apiKey ? { Authorization: `Bearer ${renderBackend.apiKey}` } : void 0
594
- );
595
- } else {
596
- const renderSender = prefixEventSender(sendEvent, "render:");
597
- if (job.upload) {
598
- renderSender("started", { status: "rendering" });
599
- const timings = await renderAndUploadInternal(
600
- job.effie,
601
- job.scale,
602
- job.upload,
603
- renderSender,
604
- ctx
605
- );
606
- renderSender("complete", { status: "uploaded", timings });
607
- } else {
608
- const videoUrl = `${ctx.baseUrl}/render/${job.renderJobId}`;
609
- sendEvent("complete", { status: "ready", videoUrl });
610
- }
611
- }
612
- if (job.upload && !renderBackend) {
613
- sendEvent("complete", { status: "done" });
614
- }
615
- } catch (error) {
616
- sendEvent("error", {
617
- phase: keepalivePhase,
618
- message: String(error)
619
- });
620
- } finally {
621
- clearInterval(keepalive);
622
- res.end();
623
- }
624
- } catch (error) {
625
- console.error("Error in warmup-and-render streaming:", error);
626
- if (!res.headersSent) {
627
- res.status(500).json({ error: "Warmup-and-render streaming failed" });
628
- } else {
629
- res.end();
630
- }
631
- }
632
- }
633
193
  function prefixEventSender(sendEvent, prefix) {
634
194
  return (event, data) => {
635
195
  sendEvent(`${prefix}${event}`, data);
@@ -710,6 +270,13 @@ async function proxyBinaryStream(response, res) {
710
270
  }
711
271
 
712
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";
713
280
  function shouldSkipWarmup(source) {
714
281
  return source.type === "video" || source.type === "audio";
715
282
  }
@@ -721,24 +288,24 @@ async function createWarmupJob(req, res, ctx, options) {
721
288
  res.status(400).json(parseResult);
722
289
  return;
723
290
  }
724
- const sources = extractEffieSourcesWithTypes2(parseResult.effie);
725
- const jobId = randomUUID3();
291
+ const sources = extractEffieSourcesWithTypes(parseResult.effie);
292
+ const jobId = randomUUID();
726
293
  const job = { sources, metadata: options?.metadata };
727
294
  await ctx.transientStore.putJson(
728
295
  storeKeys.warmupJob(jobId),
729
296
  job,
730
- ctx.transientStore.jobDataTtlMs
297
+ ctx.transientStore.ttlMs
731
298
  );
732
299
  res.json({
733
300
  id: jobId,
734
- url: `${ctx.baseUrl}/warmup/${jobId}`
301
+ progressUrl: `${ctx.baseUrl}/warmup/${jobId}/progress`
735
302
  });
736
303
  } catch (error) {
737
304
  console.error("Error creating warmup job:", error);
738
305
  res.status(500).json({ error: "Failed to create warmup job" });
739
306
  }
740
307
  }
741
- async function streamWarmupJob(req, res, ctx) {
308
+ async function streamWarmupProgress(req, res, ctx) {
742
309
  try {
743
310
  setupCORSHeaders(res);
744
311
  const jobId = req.params.id;
@@ -755,7 +322,7 @@ async function streamWarmupJob(req, res, ctx) {
755
322
  const sendEvent2 = createSSEEventSender(res);
756
323
  try {
757
324
  await proxyRemoteSSE(
758
- `${backend.baseUrl}/warmup/${jobId}`,
325
+ `${backend.baseUrl}/warmup/${jobId}/progress`,
759
326
  sendEvent2,
760
327
  "",
761
328
  res,
@@ -787,6 +354,17 @@ async function streamWarmupJob(req, res, ctx) {
787
354
  }
788
355
  }
789
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
+ }
790
368
  async function purgeCache(req, res, ctx) {
791
369
  try {
792
370
  const parseResult = parseEffieData(req.body, ctx.skipValidation);
@@ -795,15 +373,8 @@ async function purgeCache(req, res, ctx) {
795
373
  return;
796
374
  }
797
375
  const sources = extractEffieSources(parseResult.effie);
798
- let purged = 0;
799
- for (const url of sources) {
800
- const ck = storeKeys.source(url);
801
- if (await ctx.transientStore.exists(ck)) {
802
- await ctx.transientStore.delete(ck);
803
- purged++;
804
- }
805
- }
806
- res.json({ purged, total: sources.length });
376
+ const result = await purgeCachedSources(sources, ctx.transientStore);
377
+ res.json(result);
807
378
  } catch (error) {
808
379
  console.error("Error purging cache:", error);
809
380
  res.status(500).json({ error: "Failed to purge cache" });
@@ -940,20 +511,302 @@ async function fetchAndCache(url, cacheKey, sendEvent, ctx) {
940
511
  await ctx.transientStore.put(
941
512
  cacheKey,
942
513
  trackedStream,
943
- ctx.transientStore.sourceTtlMs
514
+ ctx.transientStore.ttlMs
944
515
  );
945
516
  }
946
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
+
947
801
  export {
948
802
  createServerContext,
949
- createRenderJob,
950
- streamRenderJob,
951
- createWarmupAndRenderJob,
952
- streamWarmupAndRenderJob,
953
803
  proxyRemoteSSE,
954
804
  proxyBinaryStream,
955
805
  createWarmupJob,
956
- streamWarmupJob,
957
- purgeCache
806
+ streamWarmupProgress,
807
+ purgeCache,
808
+ createRenderJob,
809
+ streamRenderProgress,
810
+ streamRenderVideo
958
811
  };
959
- //# sourceMappingURL=chunk-WD7MF3GH.js.map
812
+ //# sourceMappingURL=chunk-XOQMR7GF.js.map