@effing/ffs 0.1.2 → 0.3.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 CHANGED
@@ -57,24 +57,29 @@ curl -X POST http://localhost:2000/render \
57
57
  curl http://localhost:2000/render/123e4567-e89b-12d3-a456-426614174000 -o output.mp4
58
58
  ```
59
59
 
60
+ The server uses an internal HTTP proxy for video/audio URLs to ensure reliable DNS resolution in containerized environments (e.g., Alpine Linux). This is why you might see another server running on a random port.
61
+
60
62
  #### Environment Variables
61
63
 
62
- | Variable | Description |
63
- | ----------------------- | --------------------------------------------- |
64
- | `FFS_PORT` | Server port (default: 2000) |
65
- | `FFS_BASE_URL` | Base URL for returned URLs |
66
- | `FFS_API_KEY` | API key for authentication (optional) |
67
- | `FFS_CACHE_BUCKET` | S3 bucket for cache (enables S3 mode) |
68
- | `FFS_CACHE_ENDPOINT` | S3-compatible endpoint (for e.g. R2 or MinIO) |
69
- | `FFS_CACHE_REGION` | AWS region (default: "auto") |
70
- | `FFS_CACHE_PREFIX` | Key prefix for cached objects |
71
- | `FFS_CACHE_ACCESS_KEY` | S3 access key ID |
72
- | `FFS_CACHE_SECRET_KEY` | S3 secret access key |
73
- | `FFS_CACHE_LOCAL_DIR` | Local cache directory (when not using S3) |
74
- | `FFS_CACHE_TTL_MS` | Cache TTL in milliseconds (default: 60 min) |
75
- | `FFS_CACHE_CONCURRENCY` | Concurrent fetches during warmup (default: 4) |
76
-
77
- When `FFS_CACHE_BUCKET` is not set, FFS uses the local filesystem for caching (default: system temp directory). Local cache files are automatically cleaned up after the TTL expires.
64
+ | Variable | Description |
65
+ | -------------------------------- | ---------------------------------------------------- |
66
+ | `FFS_PORT` | Server port (default: 2000) |
67
+ | `FFS_BASE_URL` | Base URL for returned URLs |
68
+ | `FFS_API_KEY` | API key for authentication (optional) |
69
+ | `FFS_TRANSIENT_STORE_BUCKET` | S3 bucket for transient store (enables S3 mode) |
70
+ | `FFS_TRANSIENT_STORE_ENDPOINT` | S3-compatible endpoint (for e.g. R2 or MinIO) |
71
+ | `FFS_TRANSIENT_STORE_REGION` | AWS region (default: "auto") |
72
+ | `FFS_TRANSIENT_STORE_PREFIX` | Key prefix for stored objects |
73
+ | `FFS_TRANSIENT_STORE_ACCESS_KEY` | S3 access key ID |
74
+ | `FFS_TRANSIENT_STORE_SECRET_KEY` | S3 secret access key |
75
+ | `FFS_TRANSIENT_STORE_LOCAL_DIR` | Local storage directory (when not using S3) |
76
+ | `FFS_SOURCE_CACHE_TTL_MS` | TTL for cached sources in ms (default: 60 min) |
77
+ | `FFS_JOB_METADATA_TTL_MS` | TTL for job metadata in ms (default: 8 hours) |
78
+ | `FFS_WARMUP_CONCURRENCY` | Concurrent source fetches during warmup (default: 4) |
79
+ | `FFS_WARMUP_BACKEND_BASE_URL` | Separate backend for warmup (see Backend Separation) |
80
+ | `FFS_RENDER_BACKEND_BASE_URL` | Separate backend for render (see Backend Separation) |
81
+
82
+ When `FFS_TRANSIENT_STORE_BUCKET` is not set, FFS uses the local filesystem for storage (default: system temp directory). Local files are automatically cleaned up after the TTL expires.
78
83
 
79
84
  For S3 storage, the TTL is set as the `Expires` header on objects. Note that this is metadata only. To enable automatic deletion, configure [S3 lifecycle rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) on your bucket to delete expired objects.
80
85
 
@@ -267,6 +272,95 @@ Purges cached sources for a given Effie composition.
267
272
  { "purged": 3, "total": 5 }
268
273
  ```
269
274
 
275
+ ### `POST /warmup-and-render`
276
+
277
+ Creates a combined warmup and render job that runs both phases in a single SSE stream. This is useful when you want to warmup sources and render in one request.
278
+
279
+ **Request:**
280
+
281
+ ```typescript
282
+ type WarmupAndRenderOptions = {
283
+ effie: EffieData | string; // EffieData object or URL to fetch from
284
+ scale?: number; // Scale factor (default: 1)
285
+ upload?: {
286
+ videoUrl: string; // Pre-signed URL to upload rendered video
287
+ coverUrl?: string; // Pre-signed URL to upload cover image
288
+ };
289
+ };
290
+ ```
291
+
292
+ **Response:**
293
+
294
+ ```json
295
+ {
296
+ "id": "550e8400-e29b-41d4-a716-446655440000",
297
+ "url": "http://localhost:2000/warmup-and-render/550e8400-e29b-41d4-a716-446655440000"
298
+ }
299
+ ```
300
+
301
+ ### `GET /warmup-and-render/:id`
302
+
303
+ Executes the combined warmup and render job, streaming progress via SSE. All events are prefixed with `warmup:` or `render:` to indicate the phase.
304
+
305
+ **Events:**
306
+
307
+ | Event | Phase | Data |
308
+ | -------------------- | ------ | ----------------------------------------------------------------- |
309
+ | `warmup:start` | warmup | `{ "total": 5 }` |
310
+ | `warmup:progress` | warmup | `{ "url": "...", "status": "hit"\|"cached"\|"error", ... }` |
311
+ | `warmup:downloading` | warmup | `{ "url": "...", "status": "downloading", "bytesReceived": ... }` |
312
+ | `warmup:summary` | warmup | `{ "cached": 5, "failed": 0, "skipped": 0, "total": 5 }` |
313
+ | `warmup:complete` | warmup | `{ "status": "ready" }` |
314
+ | `render:started` | render | `{ "status": "rendering" }` |
315
+ | `keepalive` | both | `{ "phase": "warmup" }` or `{ "phase": "render" }` |
316
+ | `render:complete` | render | `{ "status": "uploaded", "timings": {...} }` (upload mode) |
317
+ | `complete` | - | `{ "status": "ready", "videoUrl": "..." }` (non-upload mode) |
318
+ | `error` | any | `{ "phase": "warmup"\|"render", "message": "..." }` |
319
+
320
+ **Without upload** — Returns a `videoUrl` pointing to `/render/:id` for streaming:
321
+
322
+ ```typescript
323
+ const events = new EventSource(url);
324
+ events.addEventListener("complete", (e) => {
325
+ const { videoUrl } = JSON.parse(e.data);
326
+ // Fetch videoUrl to stream the rendered video
327
+ events.close();
328
+ });
329
+ ```
330
+
331
+ **With upload** — Uploads directly and streams progress:
332
+
333
+ ```typescript
334
+ const events = new EventSource(url);
335
+ events.addEventListener("render:complete", (e) => {
336
+ const { timings } = JSON.parse(e.data);
337
+ console.log("Uploaded!", timings);
338
+ events.close();
339
+ });
340
+ ```
341
+
342
+ ## Backend Separation
343
+
344
+ FFS supports running warmup and render on separate backends, useful for scaling or resource isolation. When backend URLs are configured, the cache storage must be shared between services (e.g., using S3).
345
+
346
+ **Environment variables:**
347
+
348
+ - `FFS_WARMUP_BACKEND_BASE_URL` — Base URL for warmup backend (e.g., `https://warmup.your.app`)
349
+ - `FFS_RENDER_BACKEND_BASE_URL` — Base URL for render backend (e.g., `https://render.your.app`)
350
+
351
+ **Behavior when set:**
352
+
353
+ | Endpoint | Effect |
354
+ | ---------------------------- | ---------------------------------------------------- |
355
+ | `POST /warmup` | Returns URL pointing to local server (orchestrator) |
356
+ | `GET /warmup/:id` | Proxies SSE from warmup backend |
357
+ | `POST /render` | Returns URL pointing to local server (orchestrator) |
358
+ | `GET /render/:id` | Proxies from render backend (SSE or video stream) |
359
+ | `POST /warmup-and-render` | Returns URL pointing to local server (orchestrator) |
360
+ | `GET /warmup-and-render/:id` | Proxies SSE from warmup backend, then render backend |
361
+
362
+ All GET endpoints proxy requests to the configured backend, keeping backend URLs hidden from clients. This ensures compatibility with EventSource (which doesn't follow redirects) and simplifies CORS configuration since only the orchestrator needs to be publicly accessible.
363
+
270
364
  ## Examples
271
365
 
272
366
  ### Scale Factor for Previews
@@ -362,6 +456,34 @@ events.addEventListener("complete", (e) => {
362
456
  });
363
457
  ```
364
458
 
459
+ **Warmup and render in one stream:**
460
+
461
+ ```typescript
462
+ const { url } = await fetch("http://localhost:2000/warmup-and-render", {
463
+ method: "POST",
464
+ headers: { "Content-Type": "application/json" },
465
+ body: JSON.stringify({ effie: effieData, scale: 0.5 }),
466
+ }).then((r) => r.json());
467
+
468
+ // Connect to SSE for combined progress
469
+ const events = new EventSource(url);
470
+
471
+ events.addEventListener("warmup:progress", (e) => {
472
+ const { url, status, cached, total } = JSON.parse(e.data);
473
+ console.log(`Warmup: ${cached}/${total} - ${url} ${status}`);
474
+ });
475
+
476
+ events.addEventListener("render:started", () => {
477
+ console.log("Rendering started...");
478
+ });
479
+
480
+ events.addEventListener("complete", (e) => {
481
+ const { videoUrl } = JSON.parse(e.data);
482
+ console.log("Ready! Video at:", videoUrl);
483
+ events.close();
484
+ });
485
+ ```
486
+
365
487
  ## Related Packages
366
488
 
367
489
  - [`@effing/effie`](../effie) — Define video compositions