@effing/ffs 0.6.1 → 0.7.1
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-WD7MF3GH.js → chunk-D3QX3EOA.js} +345 -464
- package/dist/chunk-D3QX3EOA.js.map +1 -0
- package/dist/{chunk-4N2GLGC5.js → chunk-UN34ESVZ.js} +15 -26
- package/dist/chunk-UN34ESVZ.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 +349 -467
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-4N2GLGC5.js.map +0 -1
- package/dist/chunk-WD7MF3GH.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
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Takes an `EffieData` composition and renders it to an MP4 video using FFmpeg. Us
|
|
|
12
12
|
npm install @effing/ffs
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
FFmpeg is bundled via
|
|
15
|
+
FFmpeg is bundled via `@effing/ffmpeg` — no system installation required.
|
|
16
16
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
@@ -44,17 +44,21 @@ npx @effing/ffs
|
|
|
44
44
|
FFS_PORT=8080 npx @effing/ffs
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Rendering is a two-step process
|
|
47
|
+
Rendering is a two-step process: POST to create a job, then connect to the SSE progress stream to track warmup and rendering. The video URL is revealed in the `ready` event.
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
# 1.
|
|
50
|
+
# 1. Create a render job
|
|
51
51
|
curl -X POST http://localhost:2000/render \
|
|
52
52
|
-H "Content-Type: application/json" \
|
|
53
|
-
-d
|
|
54
|
-
# Returns: { "id": "...", "
|
|
53
|
+
-d '{"effie": ...}'
|
|
54
|
+
# Returns: { "id": "...", "progressUrl": "http://localhost:2000/render/.../progress" }
|
|
55
55
|
|
|
56
|
-
# 2.
|
|
57
|
-
curl http://localhost:2000/render
|
|
56
|
+
# 2. Connect to SSE progress stream (or use EventSource in browser)
|
|
57
|
+
curl http://localhost:2000/render/.../progress
|
|
58
|
+
# SSE events: warmup:start, warmup:progress, warmup:complete, ready (with videoUrl)
|
|
59
|
+
|
|
60
|
+
# 3. Fetch the video
|
|
61
|
+
curl http://localhost:2000/render/.../video -o output.mp4
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
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.
|
|
@@ -73,25 +77,13 @@ The server uses an internal HTTP proxy for video/audio URLs to ensure reliable D
|
|
|
73
77
|
| `FFS_TRANSIENT_STORE_ACCESS_KEY` | S3 access key ID |
|
|
74
78
|
| `FFS_TRANSIENT_STORE_SECRET_KEY` | S3 secret access key |
|
|
75
79
|
| `FFS_TRANSIENT_STORE_LOCAL_DIR` | Local storage directory (when not using S3) |
|
|
76
|
-
| `
|
|
77
|
-
| `FFS_JOB_DATA_TTL_MS` | TTL for job data in ms (default: 8 hours) |
|
|
80
|
+
| `FFS_TRANSIENT_STORE_TTL_MS` | TTL for all transient data in ms (default: 60 min) |
|
|
78
81
|
| `FFS_WARMUP_CONCURRENCY` | Concurrent source fetches during warmup (default: 4) |
|
|
79
82
|
|
|
80
83
|
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.
|
|
81
84
|
|
|
82
85
|
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.
|
|
83
86
|
|
|
84
|
-
## Concepts
|
|
85
|
-
|
|
86
|
-
### EffieRenderer
|
|
87
|
-
|
|
88
|
-
The main class that orchestrates video rendering:
|
|
89
|
-
|
|
90
|
-
1. **Builds FFmpeg command** — Constructs complex filter graphs for overlays, transitions, effects
|
|
91
|
-
2. **Fetches sources** — Downloads images, animations, and audio from URLs
|
|
92
|
-
3. **Processes layers** — Applies motion, effects, and timing to each layer
|
|
93
|
-
4. **Outputs video** — Streams H.264/AAC MP4 to stdout or file
|
|
94
|
-
|
|
95
87
|
## API Overview
|
|
96
88
|
|
|
97
89
|
### EffieRenderer
|
|
@@ -141,20 +133,15 @@ When running as an HTTP server, FFS provides endpoints for rendering, cache warm
|
|
|
141
133
|
|
|
142
134
|
### `POST /render`
|
|
143
135
|
|
|
144
|
-
Creates a render job
|
|
145
|
-
|
|
146
|
-
**Raw EffieData**: Body is raw EffieData, options in query params.
|
|
136
|
+
Creates a render job that includes warmup and render phases. Supports optional cache purging.
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
| ----------- | ------------------------- |
|
|
150
|
-
| `scale` | Scale factor (default: 1) |
|
|
151
|
-
|
|
152
|
-
**Wrapped format**: Body contains `effie` plus options.
|
|
138
|
+
**Request:**
|
|
153
139
|
|
|
154
140
|
```typescript
|
|
155
141
|
type RenderOptions = {
|
|
156
142
|
effie: EffieData | string; // EffieData object or URL to fetch from
|
|
157
143
|
scale?: number; // Scale factor (default: 1)
|
|
144
|
+
purge?: boolean; // Purge cached sources before warmup
|
|
158
145
|
upload?: {
|
|
159
146
|
videoUrl: string; // Pre-signed URL to upload rendered video
|
|
160
147
|
coverUrl?: string; // Pre-signed URL to upload cover image
|
|
@@ -162,70 +149,84 @@ type RenderOptions = {
|
|
|
162
149
|
};
|
|
163
150
|
```
|
|
164
151
|
|
|
165
|
-
| Option | Effect |
|
|
166
|
-
| -------------- | ---------------------------------------------- |
|
|
167
|
-
| `effie` as URL | Fetches EffieData from the URL before storing |
|
|
168
|
-
| `upload` | GET will upload and stream SSE progress events |
|
|
169
|
-
|
|
170
152
|
**Response:**
|
|
171
153
|
|
|
172
154
|
```json
|
|
173
155
|
{
|
|
174
156
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
175
|
-
"
|
|
157
|
+
"progressUrl": "http://localhost:2000/render/550e8400-e29b-41d4-a716-446655440000/progress"
|
|
176
158
|
}
|
|
177
159
|
```
|
|
178
160
|
|
|
179
|
-
### `GET /render/:id`
|
|
161
|
+
### `GET /render/:id/progress`
|
|
180
162
|
|
|
181
|
-
|
|
163
|
+
Streams warmup and render progress via SSE. All warmup events are prefixed with `warmup:`, render events with `render:`.
|
|
182
164
|
|
|
183
|
-
**
|
|
165
|
+
**Events:**
|
|
184
166
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
167
|
+
| Event | Phase | Data |
|
|
168
|
+
| -------------------- | ------ | ----------------------------------------------------------------- |
|
|
169
|
+
| `purge:complete` | purge | `{ "purged": 3, "total": 5 }` |
|
|
170
|
+
| `warmup:start` | warmup | `{ "total": 5 }` |
|
|
171
|
+
| `warmup:progress` | warmup | `{ "url": "...", "status": "hit"\|"cached"\|"error", ... }` |
|
|
172
|
+
| `warmup:downloading` | warmup | `{ "url": "...", "status": "downloading", "bytesReceived": ... }` |
|
|
173
|
+
| `warmup:summary` | warmup | `{ "cached": 5, "failed": 0, "skipped": 0, "total": 5 }` |
|
|
174
|
+
| `warmup:complete` | warmup | `{ "status": "ready" }` |
|
|
175
|
+
| `render:started` | render | `{ "status": "rendering" }` |
|
|
176
|
+
| `keepalive` | both | `{ "phase": "warmup" }` or `{ "phase": "render" }` |
|
|
177
|
+
| `render:complete` | render | `{ "status": "uploaded", "timings": {...} }` (upload mode) |
|
|
178
|
+
| `ready` | - | `{ "videoUrl": "..." }` (non-upload mode) |
|
|
179
|
+
| `complete` | - | `{ "status": "done" }` (upload mode) |
|
|
180
|
+
| `error` | any | `{ "phase": "warmup"\|"render", "message": "..." }` |
|
|
188
181
|
|
|
189
|
-
**
|
|
182
|
+
**Without upload** — The `ready` event provides a `videoUrl` pointing to `/render/:id/video`. The actual rendering happens when you fetch that URL:
|
|
190
183
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
184
|
+
```typescript
|
|
185
|
+
const events = new EventSource(progressUrl);
|
|
186
|
+
events.addEventListener("ready", (e) => {
|
|
187
|
+
const { videoUrl } = JSON.parse(e.data);
|
|
188
|
+
// Fetch videoUrl to stream the rendered video
|
|
189
|
+
events.close();
|
|
190
|
+
});
|
|
191
|
+
```
|
|
197
192
|
|
|
198
|
-
**
|
|
193
|
+
**With upload** — Uploads directly and streams progress:
|
|
199
194
|
|
|
200
195
|
```typescript
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
196
|
+
const events = new EventSource(progressUrl);
|
|
197
|
+
events.addEventListener("render:complete", (e) => {
|
|
198
|
+
const { timings } = JSON.parse(e.data);
|
|
199
|
+
console.log("Uploaded!", timings);
|
|
200
|
+
});
|
|
201
|
+
events.addEventListener("complete", () => {
|
|
202
|
+
events.close();
|
|
203
|
+
});
|
|
204
|
+
```
|
|
207
205
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
### `GET /render/:id/video`
|
|
207
|
+
|
|
208
|
+
Streams the rendered MP4 video (non-upload mode only). Returns 404 until the warmup phase completes and the video sub-job is created.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
curl http://localhost:2000/render/550e8400-.../video -o output.mp4
|
|
211
212
|
```
|
|
212
213
|
|
|
213
214
|
### `POST /warmup`
|
|
214
215
|
|
|
215
|
-
Creates a warmup job for pre-fetching and caching the sources from an Effie composition.
|
|
216
|
+
Creates a standalone warmup job for pre-fetching and caching the sources from an Effie composition.
|
|
216
217
|
|
|
217
|
-
**Request:** Same format as `/render` (
|
|
218
|
+
**Request:** Same format as `/render` (wrapped EffieData with `effie` field).
|
|
218
219
|
|
|
219
220
|
**Response:**
|
|
220
221
|
|
|
221
222
|
```json
|
|
222
223
|
{
|
|
223
224
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
224
|
-
"
|
|
225
|
+
"progressUrl": "http://localhost:2000/warmup/550e8400-e29b-41d4-a716-446655440000/progress"
|
|
225
226
|
}
|
|
226
227
|
```
|
|
227
228
|
|
|
228
|
-
### `GET /warmup/:id`
|
|
229
|
+
### `GET /warmup/:id/progress`
|
|
229
230
|
|
|
230
231
|
Runs the cache warmup job and streams the progress via Server-Sent Events (SSE). Connect with `EventSource` for real-time updates.
|
|
231
232
|
|
|
@@ -244,14 +245,14 @@ Runs the cache warmup job and streams the progress via Server-Sent Events (SSE).
|
|
|
244
245
|
|
|
245
246
|
```typescript
|
|
246
247
|
// Create warmup job
|
|
247
|
-
const {
|
|
248
|
+
const { progressUrl } = await fetch("/warmup", {
|
|
248
249
|
method: "POST",
|
|
249
250
|
headers: { "Content-Type": "application/json" },
|
|
250
251
|
body: JSON.stringify({ effie: effieData }),
|
|
251
252
|
}).then((r) => r.json());
|
|
252
253
|
|
|
253
|
-
// Stream progress
|
|
254
|
-
const events = new EventSource(
|
|
254
|
+
// Stream progress
|
|
255
|
+
const events = new EventSource(progressUrl);
|
|
255
256
|
events.addEventListener("complete", () => {
|
|
256
257
|
events.close();
|
|
257
258
|
// Now safe to call /render
|
|
@@ -262,7 +263,7 @@ events.addEventListener("complete", () => {
|
|
|
262
263
|
|
|
263
264
|
Purges cached sources for a given Effie composition.
|
|
264
265
|
|
|
265
|
-
**Request:** Same format as `/render` (
|
|
266
|
+
**Request:** Same format as `/render` (wrapped EffieData with `effie` field).
|
|
266
267
|
|
|
267
268
|
**Response:**
|
|
268
269
|
|
|
@@ -270,73 +271,6 @@ Purges cached sources for a given Effie composition.
|
|
|
270
271
|
{ "purged": 3, "total": 5 }
|
|
271
272
|
```
|
|
272
273
|
|
|
273
|
-
### `POST /warmup-and-render`
|
|
274
|
-
|
|
275
|
-
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.
|
|
276
|
-
|
|
277
|
-
**Request:**
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
type WarmupAndRenderOptions = {
|
|
281
|
-
effie: EffieData | string; // EffieData object or URL to fetch from
|
|
282
|
-
scale?: number; // Scale factor (default: 1)
|
|
283
|
-
upload?: {
|
|
284
|
-
videoUrl: string; // Pre-signed URL to upload rendered video
|
|
285
|
-
coverUrl?: string; // Pre-signed URL to upload cover image
|
|
286
|
-
};
|
|
287
|
-
};
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Response:**
|
|
291
|
-
|
|
292
|
-
```json
|
|
293
|
-
{
|
|
294
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
295
|
-
"url": "http://localhost:2000/warmup-and-render/550e8400-e29b-41d4-a716-446655440000"
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### `GET /warmup-and-render/:id`
|
|
300
|
-
|
|
301
|
-
Executes the combined warmup and render job, streaming progress via SSE. All events are prefixed with `warmup:` or `render:` to indicate the phase.
|
|
302
|
-
|
|
303
|
-
**Events:**
|
|
304
|
-
|
|
305
|
-
| Event | Phase | Data |
|
|
306
|
-
| -------------------- | ------ | ----------------------------------------------------------------- |
|
|
307
|
-
| `warmup:start` | warmup | `{ "total": 5 }` |
|
|
308
|
-
| `warmup:progress` | warmup | `{ "url": "...", "status": "hit"\|"cached"\|"error", ... }` |
|
|
309
|
-
| `warmup:downloading` | warmup | `{ "url": "...", "status": "downloading", "bytesReceived": ... }` |
|
|
310
|
-
| `warmup:summary` | warmup | `{ "cached": 5, "failed": 0, "skipped": 0, "total": 5 }` |
|
|
311
|
-
| `warmup:complete` | warmup | `{ "status": "ready" }` |
|
|
312
|
-
| `render:started` | render | `{ "status": "rendering" }` |
|
|
313
|
-
| `keepalive` | both | `{ "phase": "warmup" }` or `{ "phase": "render" }` |
|
|
314
|
-
| `render:complete` | render | `{ "status": "uploaded", "timings": {...} }` (upload mode) |
|
|
315
|
-
| `complete` | - | `{ "status": "ready", "videoUrl": "..." }` (non-upload mode) |
|
|
316
|
-
| `error` | any | `{ "phase": "warmup"\|"render", "message": "..." }` |
|
|
317
|
-
|
|
318
|
-
**Without upload** — Returns a `videoUrl` pointing to `/render/:id` for streaming:
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
const events = new EventSource(url);
|
|
322
|
-
events.addEventListener("complete", (e) => {
|
|
323
|
-
const { videoUrl } = JSON.parse(e.data);
|
|
324
|
-
// Fetch videoUrl to stream the rendered video
|
|
325
|
-
events.close();
|
|
326
|
-
});
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
**With upload** — Uploads directly and streams progress:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
const events = new EventSource(url);
|
|
333
|
-
events.addEventListener("render:complete", (e) => {
|
|
334
|
-
const { timings } = JSON.parse(e.data);
|
|
335
|
-
console.log("Uploaded!", timings);
|
|
336
|
-
events.close();
|
|
337
|
-
});
|
|
338
|
-
```
|
|
339
|
-
|
|
340
274
|
## Backend Separation
|
|
341
275
|
|
|
342
276
|
FFS supports running warmup and render on separate backends via resolver callbacks.
|
|
@@ -369,8 +303,9 @@ const ctx = await createServerContext({
|
|
|
369
303
|
});
|
|
370
304
|
```
|
|
371
305
|
|
|
372
|
-
The
|
|
373
|
-
|
|
306
|
+
The `warmupBackendResolver` determines where warmup _work_ happens — used by `/warmup/:id/progress` and the warmup phase within `/render/:id/progress`. The `renderBackendResolver` determines where video rendering _work_ happens — used by `/render/:id/video` and the render+upload phase in upload mode.
|
|
307
|
+
|
|
308
|
+
Both resolvers receive optional metadata (passed via handler options). Return `null` to handle locally.
|
|
374
309
|
|
|
375
310
|
### Job metadata
|
|
376
311
|
|
|
@@ -429,33 +364,36 @@ const finalStream = await joinRenderer.render();
|
|
|
429
364
|
|
|
430
365
|
```typescript
|
|
431
366
|
// Create render job
|
|
432
|
-
const {
|
|
367
|
+
const { progressUrl } = await fetch("http://localhost:2000/render", {
|
|
433
368
|
method: "POST",
|
|
434
369
|
headers: { "Content-Type": "application/json" },
|
|
435
370
|
body: JSON.stringify({ effie: effieData, scale: 0.5 }),
|
|
436
371
|
}).then((r) => r.json());
|
|
437
372
|
|
|
438
|
-
//
|
|
439
|
-
const
|
|
373
|
+
// Connect to SSE progress
|
|
374
|
+
const events = new EventSource(progressUrl);
|
|
375
|
+
events.addEventListener("ready", (e) => {
|
|
376
|
+
const { videoUrl } = JSON.parse(e.data);
|
|
377
|
+
// Fetch the video (rendering happens on-demand)
|
|
378
|
+
const video = await fetch(videoUrl).then((r) => r.blob());
|
|
379
|
+
events.close();
|
|
380
|
+
});
|
|
440
381
|
```
|
|
441
382
|
|
|
442
|
-
**
|
|
383
|
+
**Render with cache purge:**
|
|
443
384
|
|
|
444
385
|
```typescript
|
|
445
|
-
const {
|
|
386
|
+
const { progressUrl } = await fetch("http://localhost:2000/render", {
|
|
446
387
|
method: "POST",
|
|
447
388
|
headers: { "Content-Type": "application/json" },
|
|
448
|
-
body: JSON.stringify({
|
|
449
|
-
effie: "https://example.com/composition.json",
|
|
450
|
-
scale: 0.5,
|
|
451
|
-
}),
|
|
389
|
+
body: JSON.stringify({ effie: effieData, scale: 0.5, purge: true }),
|
|
452
390
|
}).then((r) => r.json());
|
|
453
391
|
```
|
|
454
392
|
|
|
455
393
|
**Render and upload to S3 (SSE progress):**
|
|
456
394
|
|
|
457
395
|
```typescript
|
|
458
|
-
const {
|
|
396
|
+
const { progressUrl } = await fetch("http://localhost:2000/render", {
|
|
459
397
|
method: "POST",
|
|
460
398
|
headers: { "Content-Type": "application/json" },
|
|
461
399
|
body: JSON.stringify({
|
|
@@ -468,38 +406,9 @@ const { url } = await fetch("http://localhost:2000/render", {
|
|
|
468
406
|
}).then((r) => r.json());
|
|
469
407
|
|
|
470
408
|
// Connect to SSE for progress
|
|
471
|
-
const events = new EventSource(
|
|
472
|
-
events.addEventListener("complete", (
|
|
473
|
-
|
|
474
|
-
console.log("Uploaded!", timings);
|
|
475
|
-
events.close();
|
|
476
|
-
});
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
**Warmup and render in one stream:**
|
|
480
|
-
|
|
481
|
-
```typescript
|
|
482
|
-
const { url } = await fetch("http://localhost:2000/warmup-and-render", {
|
|
483
|
-
method: "POST",
|
|
484
|
-
headers: { "Content-Type": "application/json" },
|
|
485
|
-
body: JSON.stringify({ effie: effieData, scale: 0.5 }),
|
|
486
|
-
}).then((r) => r.json());
|
|
487
|
-
|
|
488
|
-
// Connect to SSE for combined progress
|
|
489
|
-
const events = new EventSource(url);
|
|
490
|
-
|
|
491
|
-
events.addEventListener("warmup:progress", (e) => {
|
|
492
|
-
const { url, status, cached, total } = JSON.parse(e.data);
|
|
493
|
-
console.log(`Warmup: ${cached}/${total} - ${url} ${status}`);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
events.addEventListener("render:started", () => {
|
|
497
|
-
console.log("Rendering started...");
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
events.addEventListener("complete", (e) => {
|
|
501
|
-
const { videoUrl } = JSON.parse(e.data);
|
|
502
|
-
console.log("Ready! Video at:", videoUrl);
|
|
409
|
+
const events = new EventSource(progressUrl);
|
|
410
|
+
events.addEventListener("complete", () => {
|
|
411
|
+
console.log("Done!");
|
|
503
412
|
events.close();
|
|
504
413
|
});
|
|
505
414
|
```
|
|
@@ -15,14 +15,12 @@ import { pipeline } from "stream/promises";
|
|
|
15
15
|
import path from "path";
|
|
16
16
|
import os from "os";
|
|
17
17
|
import crypto from "crypto";
|
|
18
|
-
var
|
|
19
|
-
var DEFAULT_JOB_DATA_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
18
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
20
19
|
var S3TransientStore = class {
|
|
21
20
|
client;
|
|
22
21
|
bucket;
|
|
23
22
|
prefix;
|
|
24
|
-
|
|
25
|
-
jobDataTtlMs;
|
|
23
|
+
ttlMs;
|
|
26
24
|
constructor(options) {
|
|
27
25
|
this.client = new S3Client({
|
|
28
26
|
endpoint: options.endpoint,
|
|
@@ -35,8 +33,7 @@ var S3TransientStore = class {
|
|
|
35
33
|
});
|
|
36
34
|
this.bucket = options.bucket;
|
|
37
35
|
this.prefix = options.prefix ?? "";
|
|
38
|
-
this.
|
|
39
|
-
this.jobDataTtlMs = options.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
|
|
36
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
40
37
|
}
|
|
41
38
|
getExpires(ttlMs) {
|
|
42
39
|
return new Date(Date.now() + ttlMs);
|
|
@@ -51,7 +48,7 @@ var S3TransientStore = class {
|
|
|
51
48
|
Bucket: this.bucket,
|
|
52
49
|
Key: this.getFullKey(key),
|
|
53
50
|
Body: stream,
|
|
54
|
-
Expires: this.getExpires(ttlMs ?? this.
|
|
51
|
+
Expires: this.getExpires(ttlMs ?? this.ttlMs)
|
|
55
52
|
}
|
|
56
53
|
});
|
|
57
54
|
await upload.done();
|
|
@@ -119,7 +116,7 @@ var S3TransientStore = class {
|
|
|
119
116
|
Key: this.getFullKey(key),
|
|
120
117
|
Body: JSON.stringify(data),
|
|
121
118
|
ContentType: "application/json",
|
|
122
|
-
Expires: this.getExpires(ttlMs ?? this.
|
|
119
|
+
Expires: this.getExpires(ttlMs ?? this.ttlMs)
|
|
123
120
|
})
|
|
124
121
|
);
|
|
125
122
|
}
|
|
@@ -149,15 +146,10 @@ var LocalTransientStore = class {
|
|
|
149
146
|
baseDir;
|
|
150
147
|
initialized = false;
|
|
151
148
|
cleanupInterval;
|
|
152
|
-
|
|
153
|
-
jobDataTtlMs;
|
|
154
|
-
/** For cleanup, use the longer of the two TTLs */
|
|
155
|
-
maxTtlMs;
|
|
149
|
+
ttlMs;
|
|
156
150
|
constructor(options) {
|
|
157
151
|
this.baseDir = options?.baseDir ?? path.join(os.tmpdir(), "ffs-transient");
|
|
158
|
-
this.
|
|
159
|
-
this.jobDataTtlMs = options?.jobDataTtlMs ?? DEFAULT_JOB_DATA_TTL_MS;
|
|
160
|
-
this.maxTtlMs = Math.max(this.sourceTtlMs, this.jobDataTtlMs);
|
|
152
|
+
this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
|
|
161
153
|
this.cleanupInterval = setInterval(() => {
|
|
162
154
|
this.cleanupExpired().catch(console.error);
|
|
163
155
|
}, 3e5);
|
|
@@ -188,7 +180,7 @@ var LocalTransientStore = class {
|
|
|
188
180
|
} else if (entry.isFile()) {
|
|
189
181
|
try {
|
|
190
182
|
const stat = await fs.stat(fullPath);
|
|
191
|
-
if (now - stat.mtimeMs > this.
|
|
183
|
+
if (now - stat.mtimeMs > this.ttlMs) {
|
|
192
184
|
await fs.rm(fullPath, { force: true });
|
|
193
185
|
}
|
|
194
186
|
} catch {
|
|
@@ -272,8 +264,7 @@ var LocalTransientStore = class {
|
|
|
272
264
|
}
|
|
273
265
|
};
|
|
274
266
|
function createTransientStore() {
|
|
275
|
-
const
|
|
276
|
-
const jobDataTtlMs = process.env.FFS_JOB_DATA_TTL_MS ? parseInt(process.env.FFS_JOB_DATA_TTL_MS, 10) : DEFAULT_JOB_DATA_TTL_MS;
|
|
267
|
+
const ttlMs = process.env.FFS_TRANSIENT_STORE_TTL_MS ? parseInt(process.env.FFS_TRANSIENT_STORE_TTL_MS, 10) : DEFAULT_TTL_MS;
|
|
277
268
|
if (process.env.FFS_TRANSIENT_STORE_BUCKET) {
|
|
278
269
|
return new S3TransientStore({
|
|
279
270
|
endpoint: process.env.FFS_TRANSIENT_STORE_ENDPOINT,
|
|
@@ -282,14 +273,12 @@ function createTransientStore() {
|
|
|
282
273
|
prefix: process.env.FFS_TRANSIENT_STORE_PREFIX,
|
|
283
274
|
accessKeyId: process.env.FFS_TRANSIENT_STORE_ACCESS_KEY,
|
|
284
275
|
secretAccessKey: process.env.FFS_TRANSIENT_STORE_SECRET_KEY,
|
|
285
|
-
|
|
286
|
-
jobDataTtlMs
|
|
276
|
+
ttlMs
|
|
287
277
|
});
|
|
288
278
|
}
|
|
289
279
|
return new LocalTransientStore({
|
|
290
280
|
baseDir: process.env.FFS_TRANSIENT_STORE_LOCAL_DIR,
|
|
291
|
-
|
|
292
|
-
jobDataTtlMs
|
|
281
|
+
ttlMs
|
|
293
282
|
});
|
|
294
283
|
}
|
|
295
284
|
function hashUrl(url) {
|
|
@@ -304,14 +293,14 @@ function warmupJobStoreKey(jobId) {
|
|
|
304
293
|
function renderJobStoreKey(jobId) {
|
|
305
294
|
return `jobs/render/${jobId}.json`;
|
|
306
295
|
}
|
|
307
|
-
function
|
|
308
|
-
return `jobs/
|
|
296
|
+
function videoJobStoreKey(jobId) {
|
|
297
|
+
return `jobs/video/${jobId}.json`;
|
|
309
298
|
}
|
|
310
299
|
var storeKeys = {
|
|
311
300
|
source: sourceStoreKey,
|
|
312
301
|
warmupJob: warmupJobStoreKey,
|
|
313
302
|
renderJob: renderJobStoreKey,
|
|
314
|
-
|
|
303
|
+
videoJob: videoJobStoreKey
|
|
315
304
|
};
|
|
316
305
|
|
|
317
306
|
// src/fetch.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ffsFetch,
|
|
3
3
|
storeKeys
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-UN34ESVZ.js";
|
|
5
5
|
|
|
6
6
|
// src/render.ts
|
|
7
7
|
import { Readable } from "stream";
|
|
@@ -936,4 +936,4 @@ export {
|
|
|
936
936
|
FFmpegRunner,
|
|
937
937
|
EffieRenderer
|
|
938
938
|
};
|
|
939
|
-
//# sourceMappingURL=chunk-
|
|
939
|
+
//# sourceMappingURL=chunk-CSKH34HX.js.map
|