@agent-foundry/replay-server 1.0.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/.cursor/project.mdc +694 -0
- package/.dockerignore +11 -0
- package/Dockerfile +156 -0
- package/README.md +628 -0
- package/bundles/.gitkeep +2 -0
- package/dist/cli/render.d.ts +10 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +206 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/renderer/PuppeteerRenderer.d.ts +72 -0
- package/dist/renderer/PuppeteerRenderer.d.ts.map +1 -0
- package/dist/renderer/PuppeteerRenderer.js +392 -0
- package/dist/renderer/PuppeteerRenderer.js.map +1 -0
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +348 -0
- package/dist/server/index.js.map +1 -0
- package/docker-compose.local.yml +29 -0
- package/package.json +35 -0
- package/samples/life-replay-lr-mkcdfzc2-u8cqbs.json +1770 -0
- package/scripts/build-bundle.sh +52 -0
- package/scripts/deploy-aliyun.sh +81 -0
- package/src/cli/render.ts +243 -0
- package/src/index.ts +2 -0
- package/src/renderer/PuppeteerRenderer.ts +512 -0
- package/src/renderer/index.ts +1 -0
- package/src/server/index.ts +407 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
---
|
|
2
|
+
alwaysApply: true
|
|
3
|
+
---
|
|
4
|
+
# Replay Server - Project Documentation
|
|
5
|
+
|
|
6
|
+
## Project Overview
|
|
7
|
+
|
|
8
|
+
Replay Server is a **video rendering service** for AgentFoundry mini-game replays. It uses Puppeteer to load games in a headless browser, injects replay manifests, and captures video frames that are streamed directly to FFmpeg for encoding.
|
|
9
|
+
|
|
10
|
+
**Purpose**: Convert game replay manifests into MP4 video files for sharing and playback.
|
|
11
|
+
|
|
12
|
+
**Tech Stack**:
|
|
13
|
+
- **Node.js 18+** - Runtime
|
|
14
|
+
- **TypeScript 5.3.3** - Type safety
|
|
15
|
+
- **Express 4.18.2** - HTTP API server
|
|
16
|
+
- **Puppeteer 22.0.0** - Headless browser automation
|
|
17
|
+
- **FFmpeg** - Video encoding (via fluent-ffmpeg)
|
|
18
|
+
- **pnpm** - Package manager (monorepo workspace)
|
|
19
|
+
|
|
20
|
+
**Key Features**:
|
|
21
|
+
- Headless browser rendering (zero code duplication with game)
|
|
22
|
+
- Static bundle hosting (no external game server needed)
|
|
23
|
+
- Streaming video encoding (no temporary files)
|
|
24
|
+
- HTTP API for async job processing
|
|
25
|
+
- CLI tool for offline rendering
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
### High-Level Architecture
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
graph TB
|
|
35
|
+
subgraph Client["Client"]
|
|
36
|
+
API["HTTP API<br/>POST /render"]
|
|
37
|
+
CLI["CLI Tool<br/>render.ts"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph Server["Replay Server"]
|
|
41
|
+
Express["Express Server<br/>Port 3001"]
|
|
42
|
+
JobManager["Job Manager<br/>In-Memory Store"]
|
|
43
|
+
Renderer["PuppeteerRenderer"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
subgraph Browser["Headless Browser"]
|
|
47
|
+
Puppeteer["Puppeteer<br/>Chromium"]
|
|
48
|
+
GamePage["Game Page<br/>?mode=replay"]
|
|
49
|
+
Manifest["Manifest Injection<br/>window.__REPLAY_MANIFEST__"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph Encoding["Video Encoding"]
|
|
53
|
+
FFmpeg["FFmpeg Process<br/>libx264/h264_nvenc"]
|
|
54
|
+
VideoFile["Output MP4<br/>H.264"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
subgraph Bundles["Bundle Hosting"]
|
|
58
|
+
StaticServer["Static Server<br/>/bundles/*"]
|
|
59
|
+
GameBundle["Game Bundle<br/>bundles/game-life-restart/"]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
API --> Express
|
|
63
|
+
CLI --> Renderer
|
|
64
|
+
Express --> JobManager
|
|
65
|
+
Express --> Renderer
|
|
66
|
+
Renderer --> Puppeteer
|
|
67
|
+
Puppeteer --> GamePage
|
|
68
|
+
GamePage --> Manifest
|
|
69
|
+
Renderer --> FFmpeg
|
|
70
|
+
FFmpeg --> VideoFile
|
|
71
|
+
Puppeteer --> StaticServer
|
|
72
|
+
StaticServer --> GameBundle
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Rendering Flow
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
sequenceDiagram
|
|
79
|
+
participant Client
|
|
80
|
+
participant Server
|
|
81
|
+
participant Renderer
|
|
82
|
+
participant Browser
|
|
83
|
+
participant Game
|
|
84
|
+
participant FFmpeg
|
|
85
|
+
|
|
86
|
+
Client->>Server: POST /render {manifest, config}
|
|
87
|
+
Server->>Server: Create job (pending)
|
|
88
|
+
Server->>Client: {jobId, status: "pending"}
|
|
89
|
+
|
|
90
|
+
Server->>Renderer: render(manifest, config)
|
|
91
|
+
Renderer->>Browser: Launch Chromium
|
|
92
|
+
Browser->>Game: Navigate to gameUrl?mode=replay
|
|
93
|
+
Game->>Game: Wait for React init
|
|
94
|
+
Renderer->>Game: Inject manifest (window.__REPLAY_MANIFEST__)
|
|
95
|
+
Renderer->>Game: Dispatch 'replay-manifest-loaded' event
|
|
96
|
+
Game->>Game: Enter replay mode
|
|
97
|
+
Game->>Renderer: Set data-replay-ready="true"
|
|
98
|
+
|
|
99
|
+
Renderer->>FFmpeg: Launch FFmpeg process
|
|
100
|
+
loop For each age in timeline
|
|
101
|
+
Renderer->>Game: Dispatch 'replay-next-age' event
|
|
102
|
+
Game->>Game: Advance to next age
|
|
103
|
+
loop For each frame (fps * secondsPerAge)
|
|
104
|
+
Renderer->>Browser: Capture screenshot (JPEG)
|
|
105
|
+
Renderer->>FFmpeg: Write frame to stdin
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Renderer->>FFmpeg: Close stdin
|
|
110
|
+
FFmpeg->>FFmpeg: Encode video
|
|
111
|
+
FFmpeg->>Server: Write MP4 file
|
|
112
|
+
Renderer->>Server: Update job (completed)
|
|
113
|
+
Client->>Server: GET /status/:jobId
|
|
114
|
+
Server->>Client: {status: "completed", outputPath}
|
|
115
|
+
Client->>Server: GET /download/:jobId
|
|
116
|
+
Server->>Client: Video file
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Key Design Decisions
|
|
120
|
+
|
|
121
|
+
#### 1. Puppeteer-Based Rendering
|
|
122
|
+
|
|
123
|
+
**Why**: Zero code duplication. The actual game runs in a headless browser, ensuring the video looks exactly like gameplay.
|
|
124
|
+
|
|
125
|
+
**How**:
|
|
126
|
+
- Game is loaded from URL (local dev) or bundle (production)
|
|
127
|
+
- Replay manifest is injected via `window.__REPLAY_MANIFEST__`
|
|
128
|
+
- Game enters "replay mode" and plays back events
|
|
129
|
+
- Screenshots captured at each frame
|
|
130
|
+
|
|
131
|
+
#### 2. Bundle Hosting
|
|
132
|
+
|
|
133
|
+
**Why**: Eliminates need for external game server. Bundles are self-contained static files.
|
|
134
|
+
|
|
135
|
+
**How**:
|
|
136
|
+
- Game bundles built with `VITE_BASE_PATH=/bundles/<bundleId>/`
|
|
137
|
+
- Served from Express static middleware at `/bundles/<bundleId>/`
|
|
138
|
+
- Manifest includes `bundleId` field
|
|
139
|
+
- Server resolves game URL: `http://localhost:PORT/bundles/<bundleId>/`
|
|
140
|
+
|
|
141
|
+
#### 3. Streaming FFmpeg Encoding
|
|
142
|
+
|
|
143
|
+
**Why**: No temporary files, lower disk I/O, faster encoding.
|
|
144
|
+
|
|
145
|
+
**How**:
|
|
146
|
+
- Screenshots captured as JPEG buffers
|
|
147
|
+
- Frames written directly to FFmpeg stdin
|
|
148
|
+
- FFmpeg configured with `-f image2pipe -vcodec mjpeg -i -`
|
|
149
|
+
- Process handles stdin drain events for backpressure
|
|
150
|
+
|
|
151
|
+
#### 4. Game URL Resolution Priority
|
|
152
|
+
|
|
153
|
+
1. `config.gameUrl` (explicit URL in render request)
|
|
154
|
+
2. `manifest.bundleId` → `/bundles/<bundleId>/` (bundle hosting)
|
|
155
|
+
3. `GAME_URL` environment variable (fallback)
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Core Components
|
|
160
|
+
|
|
161
|
+
### 1. HTTP Server (`src/server/index.ts`)
|
|
162
|
+
|
|
163
|
+
Express-based API server handling render jobs, job status, and bundle management.
|
|
164
|
+
|
|
165
|
+
**Key Responsibilities**:
|
|
166
|
+
- Accept render requests with manifest
|
|
167
|
+
- Manage job lifecycle (pending → processing → completed/failed)
|
|
168
|
+
- Serve static game bundles
|
|
169
|
+
- Provide job status and download endpoints
|
|
170
|
+
|
|
171
|
+
**Job Storage**: In-memory Map (for now, could be Redis/DB in production)
|
|
172
|
+
|
|
173
|
+
**Routes**:
|
|
174
|
+
- `POST /render` - Start render job
|
|
175
|
+
- `GET /jobs` - List all jobs
|
|
176
|
+
- `GET /status/:jobId` - Get job status
|
|
177
|
+
- `GET /download/:jobId` - Download completed video
|
|
178
|
+
- `GET /bundles` - List available bundles
|
|
179
|
+
- `GET /health` - Health check
|
|
180
|
+
- `GET /debug/chrome` - Debug Chromium installation
|
|
181
|
+
|
|
182
|
+
### 2. Puppeteer Renderer (`src/renderer/PuppeteerRenderer.ts`)
|
|
183
|
+
|
|
184
|
+
Core rendering engine that orchestrates browser, game loading, and frame capture.
|
|
185
|
+
|
|
186
|
+
**Key Responsibilities**:
|
|
187
|
+
- Launch and manage headless Chromium instance
|
|
188
|
+
- Navigate to game and inject manifest
|
|
189
|
+
- Wait for game to enter replay mode
|
|
190
|
+
- Capture frames and stream to FFmpeg
|
|
191
|
+
- Handle errors and cleanup
|
|
192
|
+
|
|
193
|
+
**Key Methods**:
|
|
194
|
+
- `render(manifest, config)` - Main rendering entry point
|
|
195
|
+
- `initBrowser(config)` - Launch Chromium with optimized flags
|
|
196
|
+
- `loadGame(gameUrl, manifest)` - Navigate and inject manifest
|
|
197
|
+
- `captureAndEncodeFrames(manifest, config)` - Frame capture loop
|
|
198
|
+
- `launchFFmpeg(encoder, config)` - Start FFmpeg process
|
|
199
|
+
|
|
200
|
+
**Browser Configuration**:
|
|
201
|
+
- Single-process mode (container-friendly)
|
|
202
|
+
- Disabled GPU, sandbox, extensions
|
|
203
|
+
- Optimized for headless rendering
|
|
204
|
+
- Chinese fonts support (Noto CJK, WQY Zenhei)
|
|
205
|
+
|
|
206
|
+
### 3. CLI Tool (`src/cli/render.ts`)
|
|
207
|
+
|
|
208
|
+
Command-line interface for offline video rendering.
|
|
209
|
+
|
|
210
|
+
**Usage**:
|
|
211
|
+
```bash
|
|
212
|
+
pnpm --filter replay-server render -- \
|
|
213
|
+
--manifest ./replay.json \
|
|
214
|
+
--output ./output.mp4 \
|
|
215
|
+
--game-url http://localhost:5173 \
|
|
216
|
+
--width 1080 \
|
|
217
|
+
--height 1920 \
|
|
218
|
+
--fps 30
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Features**:
|
|
222
|
+
- Load manifest from file or URL
|
|
223
|
+
- Progress bar display
|
|
224
|
+
- Configurable video settings
|
|
225
|
+
- Hardware acceleration support
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## API Endpoints
|
|
230
|
+
|
|
231
|
+
### POST /render
|
|
232
|
+
|
|
233
|
+
Start a new render job.
|
|
234
|
+
|
|
235
|
+
**Request Body**:
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"manifest": {
|
|
239
|
+
"schema": "lifeRestart.replay.v1",
|
|
240
|
+
"bundleId": "game-life-restart",
|
|
241
|
+
"gameId": "test-game",
|
|
242
|
+
"timeline": [...],
|
|
243
|
+
"highlights": []
|
|
244
|
+
},
|
|
245
|
+
"config": {
|
|
246
|
+
"width": 1080,
|
|
247
|
+
"height": 1920,
|
|
248
|
+
"fps": 30,
|
|
249
|
+
"secondsPerAge": 2,
|
|
250
|
+
"gameUrl": "http://localhost:5173" // Optional, overrides bundleId
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Alternative** (with manifestUrl):
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"manifestUrl": "https://example.com/replay.json",
|
|
259
|
+
"config": {...}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Response**:
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"jobId": "5eaee18f-48a7-4193-b594-86756ac76ce2",
|
|
267
|
+
"status": "pending",
|
|
268
|
+
"message": "Render job started"
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Status Codes**:
|
|
273
|
+
- `200` - Job created successfully
|
|
274
|
+
- `400` - Invalid manifest or missing required fields
|
|
275
|
+
- `500` - Internal server error
|
|
276
|
+
|
|
277
|
+
### GET /jobs
|
|
278
|
+
|
|
279
|
+
List all render jobs.
|
|
280
|
+
|
|
281
|
+
**Response**:
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"jobs": [
|
|
285
|
+
{
|
|
286
|
+
"jobId": "5eaee18f-48a7-4193-b594-86756ac76ce2",
|
|
287
|
+
"status": "completed",
|
|
288
|
+
"progress": {
|
|
289
|
+
"stage": "complete",
|
|
290
|
+
"current": 1,
|
|
291
|
+
"total": 1,
|
|
292
|
+
"message": "Completed in 45.2s"
|
|
293
|
+
},
|
|
294
|
+
"error": null,
|
|
295
|
+
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
296
|
+
"completedAt": "2024-01-01T00:05:00.000Z"
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
"count": 1
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### GET /status/:jobId
|
|
304
|
+
|
|
305
|
+
Get status of a specific job.
|
|
306
|
+
|
|
307
|
+
**Response**:
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"jobId": "5eaee18f-48a7-4193-b594-86756ac76ce2",
|
|
311
|
+
"status": "processing",
|
|
312
|
+
"progress": {
|
|
313
|
+
"stage": "capture",
|
|
314
|
+
"current": 150,
|
|
315
|
+
"total": 300,
|
|
316
|
+
"message": "Capturing & encoding frame 150/300 (libx264)"
|
|
317
|
+
},
|
|
318
|
+
"error": null,
|
|
319
|
+
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
320
|
+
"completedAt": null
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Status Values**:
|
|
325
|
+
- `pending` - Job created, not started
|
|
326
|
+
- `processing` - Currently rendering
|
|
327
|
+
- `completed` - Video ready for download
|
|
328
|
+
- `failed` - Error occurred (check `error` field)
|
|
329
|
+
|
|
330
|
+
### GET /download/:jobId
|
|
331
|
+
|
|
332
|
+
Download completed video file.
|
|
333
|
+
|
|
334
|
+
**Response**: Binary MP4 file
|
|
335
|
+
|
|
336
|
+
**Status Codes**:
|
|
337
|
+
- `200` - Video file
|
|
338
|
+
- `400` - Job not completed
|
|
339
|
+
- `404` - Job or file not found
|
|
340
|
+
|
|
341
|
+
### GET /bundles
|
|
342
|
+
|
|
343
|
+
List available game bundles.
|
|
344
|
+
|
|
345
|
+
**Response**:
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"bundles": [
|
|
349
|
+
{
|
|
350
|
+
"bundleId": "game-life-restart",
|
|
351
|
+
"path": "/app/packages/replay-server/bundles/game-life-restart",
|
|
352
|
+
"url": "http://localhost:3001/bundles/game-life-restart/",
|
|
353
|
+
"ready": true
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"count": 1,
|
|
357
|
+
"bundlesDir": "/app/packages/replay-server/bundles"
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### GET /health
|
|
362
|
+
|
|
363
|
+
Health check endpoint.
|
|
364
|
+
|
|
365
|
+
**Response**:
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"status": "ok",
|
|
369
|
+
"gameUrl": "http://localhost:5173",
|
|
370
|
+
"bundlesDir": "/app/packages/replay-server/bundles",
|
|
371
|
+
"bundleCount": 1
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### GET /debug/chrome
|
|
376
|
+
|
|
377
|
+
Debug Chromium installation (useful for containerized deployments).
|
|
378
|
+
|
|
379
|
+
**Response**:
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"executablePath": "/usr/bin/chromium",
|
|
383
|
+
"tests": {
|
|
384
|
+
"version": {
|
|
385
|
+
"ok": true,
|
|
386
|
+
"output": "Chromium 120.0.6099.224"
|
|
387
|
+
},
|
|
388
|
+
"dependencies": {
|
|
389
|
+
"ok": true,
|
|
390
|
+
"summary": "All libraries found"
|
|
391
|
+
},
|
|
392
|
+
"chineseFonts": {
|
|
393
|
+
"ok": true,
|
|
394
|
+
"count": 15,
|
|
395
|
+
"sample": ["Noto Sans CJK SC", "WQY Zenhei"]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Game Integration Requirements
|
|
404
|
+
|
|
405
|
+
For a game to work with the replay renderer, it must support replay mode:
|
|
406
|
+
|
|
407
|
+
### 1. Replay Mode Detection
|
|
408
|
+
|
|
409
|
+
The game should check for `?mode=replay` query parameter:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
414
|
+
if (urlParams.get('mode') === 'replay') {
|
|
415
|
+
// Enter replay mode
|
|
416
|
+
}
|
|
417
|
+
}, []);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 2. Manifest Injection
|
|
421
|
+
|
|
422
|
+
The renderer injects the manifest in two ways:
|
|
423
|
+
|
|
424
|
+
**Method 1**: Window property
|
|
425
|
+
```typescript
|
|
426
|
+
if ((window as any).__REPLAY_MANIFEST__) {
|
|
427
|
+
const manifest = (window as any).__REPLAY_MANIFEST__;
|
|
428
|
+
enterReplayMode(manifest);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Method 2**: Custom event
|
|
433
|
+
```typescript
|
|
434
|
+
window.addEventListener('replay-manifest-loaded', (e: CustomEvent) => {
|
|
435
|
+
const manifest = e.detail;
|
|
436
|
+
enterReplayMode(manifest);
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### 3. Replay Control
|
|
441
|
+
|
|
442
|
+
The renderer dispatches `replay-next-age` events to advance through the timeline:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
window.addEventListener('replay-next-age', () => {
|
|
446
|
+
// Advance to next age in replay
|
|
447
|
+
replayNextAge();
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### 4. Ready Indicator
|
|
452
|
+
|
|
453
|
+
The game must set `data-replay-ready="true"` when ready for capture:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
<div data-replay-ready="true">
|
|
457
|
+
{/* game content */}
|
|
458
|
+
</div>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The renderer waits for this attribute before starting frame capture.
|
|
462
|
+
|
|
463
|
+
### 5. React Initialization
|
|
464
|
+
|
|
465
|
+
The renderer waits for React to initialize by checking for:
|
|
466
|
+
- `ion-app` element exists
|
|
467
|
+
- `ion-spinner` element does not exist (loading complete)
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Deployment
|
|
472
|
+
|
|
473
|
+
### Local Development
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# From project root
|
|
477
|
+
pnpm dev:replay-server
|
|
478
|
+
|
|
479
|
+
# Or from package directory
|
|
480
|
+
cd packages/replay-server
|
|
481
|
+
pnpm run dev
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Requirements**:
|
|
485
|
+
- Node.js 18+
|
|
486
|
+
- FFmpeg in PATH
|
|
487
|
+
- Game running at `http://localhost:5173` OR bundle in `bundles/` directory
|
|
488
|
+
|
|
489
|
+
### Docker (Recommended)
|
|
490
|
+
|
|
491
|
+
Multi-stage build that:
|
|
492
|
+
1. Builds game bundle (`game-life-restart`)
|
|
493
|
+
2. Builds replay-server
|
|
494
|
+
3. Creates runtime image with both embedded
|
|
495
|
+
|
|
496
|
+
**Build**:
|
|
497
|
+
```bash
|
|
498
|
+
# From project root
|
|
499
|
+
docker build -f packages/replay-server/Dockerfile -t replay-server:latest .
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Run**:
|
|
503
|
+
```bash
|
|
504
|
+
docker run -d \
|
|
505
|
+
--name replay-server \
|
|
506
|
+
-p 3001:3001 \
|
|
507
|
+
--shm-size=2g \
|
|
508
|
+
replay-server:latest
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Docker Compose**:
|
|
512
|
+
```bash
|
|
513
|
+
cd packages/replay-server
|
|
514
|
+
docker-compose -f docker-compose.local.yml up --build
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Aliyun Function Compute
|
|
518
|
+
|
|
519
|
+
**Build for FC**:
|
|
520
|
+
```bash
|
|
521
|
+
docker buildx build \
|
|
522
|
+
--platform linux/amd64 \
|
|
523
|
+
--provenance=false \
|
|
524
|
+
--sbom=false \
|
|
525
|
+
-t ${ACR_REGISTRY}/${ACR_NAMESPACE}/replay-server:latest \
|
|
526
|
+
-f packages/replay-server/Dockerfile \
|
|
527
|
+
--push \
|
|
528
|
+
.
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Function Configuration**:
|
|
532
|
+
- **Memory**: Minimum 2048 MB (2GB), recommended 4GB
|
|
533
|
+
- **Timeout**: Minimum 300 seconds (5 minutes), recommended 600 seconds
|
|
534
|
+
- **Instance Concurrency**: 1 (required - prevents resource contention)
|
|
535
|
+
- **CPU**: Auto-scaled with memory
|
|
536
|
+
|
|
537
|
+
**Environment Variables**:
|
|
538
|
+
- `PORT=9000` (FC sets this automatically)
|
|
539
|
+
- `BUNDLES_DIR=/app/packages/replay-server/bundles`
|
|
540
|
+
- `OUTPUT_DIR=/tmp/output` (use /tmp for writable directory)
|
|
541
|
+
|
|
542
|
+
**Deploy Script**:
|
|
543
|
+
```bash
|
|
544
|
+
cd packages/replay-server
|
|
545
|
+
ACR_NAMESPACE=your-namespace ./scripts/deploy-aliyun.sh
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Configuration
|
|
551
|
+
|
|
552
|
+
### Environment Variables
|
|
553
|
+
|
|
554
|
+
| Variable | Default | Description |
|
|
555
|
+
|----------|---------|-------------|
|
|
556
|
+
| `PORT` | `3001` | Server port |
|
|
557
|
+
| `GAME_URL` | `http://localhost:5173` | Fallback game URL when no bundleId |
|
|
558
|
+
| `BUNDLES_DIR` | `./bundles` | Directory containing game bundles |
|
|
559
|
+
| `OUTPUT_DIR` | `./output` | Directory for rendered videos |
|
|
560
|
+
| `PUPPETEER_EXECUTABLE_PATH` | `/usr/bin/chromium` | Chromium executable path |
|
|
561
|
+
|
|
562
|
+
### Render Configuration
|
|
563
|
+
|
|
564
|
+
| Option | Default | Description |
|
|
565
|
+
|--------|---------|-------------|
|
|
566
|
+
| `width` | `720` | Video width (pixels) |
|
|
567
|
+
| `height` | `1280` | Video height (pixels) |
|
|
568
|
+
| `fps` | `16` | Frames per second |
|
|
569
|
+
| `secondsPerAge` | `1.2` | Seconds to display each age |
|
|
570
|
+
| `useHardwareAcceleration` | `false` | Use h264_nvenc (GPU) instead of libx264 (CPU) |
|
|
571
|
+
|
|
572
|
+
### Video Output
|
|
573
|
+
|
|
574
|
+
- **Format**: MP4 (H.264)
|
|
575
|
+
- **Resolution**: 720x1280 (portrait, for mobile/Feed)
|
|
576
|
+
- **FPS**: 16 (configurable)
|
|
577
|
+
- **Duration**: ~1.2s per age (configurable)
|
|
578
|
+
- **Encoder**: libx264 (CPU) by default, h264_nvenc (GPU) optional
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Building Game Bundles
|
|
583
|
+
|
|
584
|
+
Before deploying, build game bundles that will be hosted by replay-server.
|
|
585
|
+
|
|
586
|
+
### Using Build Script
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
cd packages/replay-server
|
|
590
|
+
./scripts/build-bundle.sh game-life-restart
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
This will:
|
|
594
|
+
1. Build the game from `repo/game-life-restart`
|
|
595
|
+
2. Copy `dist/` to `bundles/game-life-restart/`
|
|
596
|
+
|
|
597
|
+
### Manual Build
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
# Build the game
|
|
601
|
+
cd repo/game-life-restart
|
|
602
|
+
pnpm install
|
|
603
|
+
VITE_BASE_PATH=/bundles/game-life-restart/ pnpm build
|
|
604
|
+
|
|
605
|
+
# Copy to bundles
|
|
606
|
+
mkdir -p packages/replay-server/bundles/game-life-restart
|
|
607
|
+
cp -r dist/* packages/replay-server/bundles/game-life-restart/
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Docker (Automatic)
|
|
611
|
+
|
|
612
|
+
The Dockerfile includes a multi-stage build that automatically builds and embeds the game bundle. No manual steps required.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## File Structure
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
packages/replay-server/
|
|
620
|
+
├── .cursor/
|
|
621
|
+
│ ├── project.mdc # This file
|
|
622
|
+
│ └── dev.mdc # Development rules
|
|
623
|
+
├── bundles/ # Game bundles (gitignored)
|
|
624
|
+
│ └── game-life-restart/
|
|
625
|
+
├── samples/ # Sample replay manifests
|
|
626
|
+
│ └── life-replay-*.json
|
|
627
|
+
├── scripts/
|
|
628
|
+
│ ├── build-bundle.sh # Build game bundle
|
|
629
|
+
│ └── deploy-aliyun.sh # Deploy to ACR
|
|
630
|
+
├── src/
|
|
631
|
+
│ ├── cli/
|
|
632
|
+
│ │ └── render.ts # CLI tool
|
|
633
|
+
│ ├── renderer/
|
|
634
|
+
│ │ ├── index.ts
|
|
635
|
+
│ │ └── PuppeteerRenderer.ts # Core renderer
|
|
636
|
+
│ ├── server/
|
|
637
|
+
│ │ └── index.ts # Express HTTP server
|
|
638
|
+
│ └── index.ts # Package exports
|
|
639
|
+
├── Dockerfile # Multi-stage Docker build
|
|
640
|
+
├── docker-compose.local.yml
|
|
641
|
+
├── package.json
|
|
642
|
+
├── tsconfig.json
|
|
643
|
+
└── README.md
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Key Files Reference
|
|
649
|
+
|
|
650
|
+
### Server
|
|
651
|
+
- `src/server/index.ts` - Express HTTP API server (407 lines)
|
|
652
|
+
- Handles render jobs, bundle serving, job status
|
|
653
|
+
|
|
654
|
+
### Renderer
|
|
655
|
+
- `src/renderer/PuppeteerRenderer.ts` - Core rendering engine (513 lines)
|
|
656
|
+
- Browser management, game loading, frame capture, FFmpeg integration
|
|
657
|
+
|
|
658
|
+
### CLI
|
|
659
|
+
- `src/cli/render.ts` - Command-line tool (244 lines)
|
|
660
|
+
- Manifest loading, progress display, renderer invocation
|
|
661
|
+
|
|
662
|
+
### Configuration
|
|
663
|
+
- `package.json` - Dependencies and scripts
|
|
664
|
+
- `tsconfig.json` - TypeScript configuration
|
|
665
|
+
- `Dockerfile` - Multi-stage build for production
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Dependencies
|
|
670
|
+
|
|
671
|
+
### Core Dependencies
|
|
672
|
+
- `express` - HTTP server framework
|
|
673
|
+
- `puppeteer` - Headless browser automation
|
|
674
|
+
- `fluent-ffmpeg` - FFmpeg wrapper (note: actual FFmpeg binary required)
|
|
675
|
+
- `cors` - CORS middleware
|
|
676
|
+
- `uuid` - Job ID generation
|
|
677
|
+
|
|
678
|
+
### Type Dependencies
|
|
679
|
+
- `@types/express`
|
|
680
|
+
- `@types/fluent-ffmpeg`
|
|
681
|
+
- `@types/cors`
|
|
682
|
+
- `@types/uuid`
|
|
683
|
+
- `@types/node`
|
|
684
|
+
|
|
685
|
+
### Shared Package
|
|
686
|
+
- `@agent-foundry/replay` - Replay manifest type definitions
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Related Documentation
|
|
691
|
+
|
|
692
|
+
- [README.md](../README.md) - Main documentation with usage examples
|
|
693
|
+
- [../replay-shared/README.md](../replay-shared/README.md) - Replay manifest schema
|
|
694
|
+
- [../../repo/game-life-restart/doc/replay/](../../repo/game-life-restart/doc/replay/) - Game replay integration guide
|