@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,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build a game and copy to bundles directory
|
|
3
|
+
# Usage: ./build-bundle.sh game-life-restart
|
|
4
|
+
#
|
|
5
|
+
# This script:
|
|
6
|
+
# 1. Builds the specified game from repo/
|
|
7
|
+
# 2. Copies the dist/ output to bundles/<game-name>/
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
GAME_NAME=$1
|
|
12
|
+
|
|
13
|
+
if [ -z "$GAME_NAME" ]; then
|
|
14
|
+
echo "Usage: $0 <game-name>"
|
|
15
|
+
echo "Example: $0 game-life-restart"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Get the repo root (3 levels up from this script)
|
|
20
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
21
|
+
REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd)
|
|
22
|
+
GAME_DIR="$REPO_ROOT/repo/$GAME_NAME"
|
|
23
|
+
BUNDLES_DIR="$SCRIPT_DIR/../bundles"
|
|
24
|
+
|
|
25
|
+
# Verify game exists
|
|
26
|
+
if [ ! -d "$GAME_DIR" ]; then
|
|
27
|
+
echo "Error: Game directory not found: $GAME_DIR"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "📦 Building $GAME_NAME..."
|
|
32
|
+
echo " Game directory: $GAME_DIR"
|
|
33
|
+
echo " Bundles directory: $BUNDLES_DIR"
|
|
34
|
+
|
|
35
|
+
# Build the game with base path for bundle serving
|
|
36
|
+
BASE_PATH="/bundles/$GAME_NAME/"
|
|
37
|
+
echo "📦 Building with base path: $BASE_PATH"
|
|
38
|
+
cd "$GAME_DIR"
|
|
39
|
+
pnpm install
|
|
40
|
+
VITE_BASE_PATH="$BASE_PATH" pnpm build
|
|
41
|
+
|
|
42
|
+
# Create bundle directory
|
|
43
|
+
mkdir -p "$BUNDLES_DIR/$GAME_NAME"
|
|
44
|
+
|
|
45
|
+
# Copy build output
|
|
46
|
+
echo "📋 Copying build output to bundles..."
|
|
47
|
+
cp -r dist/* "$BUNDLES_DIR/$GAME_NAME/"
|
|
48
|
+
|
|
49
|
+
echo "✅ Bundle created: $BUNDLES_DIR/$GAME_NAME"
|
|
50
|
+
echo ""
|
|
51
|
+
echo "To use this bundle, set bundleId in your manifest:"
|
|
52
|
+
echo ' { "bundleId": "'$GAME_NAME'" }'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Deploy replay-server to Aliyun Container Registry (ACR)
|
|
3
|
+
#
|
|
4
|
+
# Prerequisites:
|
|
5
|
+
# 1. Docker installed and running
|
|
6
|
+
# 2. Logged into ACR: docker login registry.cn-beijing.aliyuncs.com
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./deploy-aliyun.sh
|
|
10
|
+
# ./deploy-aliyun.sh <custom-tag>
|
|
11
|
+
#
|
|
12
|
+
# Environment variables (optional):
|
|
13
|
+
# ACR_REGISTRY - ACR registry URL (default: registry.cn-beijing.aliyuncs.com)
|
|
14
|
+
# ACR_NAMESPACE - Your ACR namespace (required)
|
|
15
|
+
# IMAGE_NAME - Image name (default: replay-server)
|
|
16
|
+
|
|
17
|
+
set -e
|
|
18
|
+
|
|
19
|
+
# Configuration
|
|
20
|
+
ACR_REGISTRY="${ACR_REGISTRY:-registry.cn-beijing.aliyuncs.com}"
|
|
21
|
+
ACR_NAMESPACE="${ACR_NAMESPACE:-}"
|
|
22
|
+
IMAGE_NAME="${IMAGE_NAME:-replay-server}"
|
|
23
|
+
VERSION="${1:-$(date +%Y%m%d-%H%M%S)}"
|
|
24
|
+
|
|
25
|
+
# Validate namespace
|
|
26
|
+
if [ -z "$ACR_NAMESPACE" ]; then
|
|
27
|
+
echo "Error: ACR_NAMESPACE environment variable is required"
|
|
28
|
+
echo ""
|
|
29
|
+
echo "Usage:"
|
|
30
|
+
echo " ACR_NAMESPACE=your-namespace ./deploy-aliyun.sh"
|
|
31
|
+
echo ""
|
|
32
|
+
echo "Or set it in your environment:"
|
|
33
|
+
echo " export ACR_NAMESPACE=your-namespace"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Get repo root
|
|
38
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
39
|
+
REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd)
|
|
40
|
+
|
|
41
|
+
echo "🚀 Deploying replay-server to Aliyun ACR"
|
|
42
|
+
echo " Registry: $ACR_REGISTRY"
|
|
43
|
+
echo " Namespace: $ACR_NAMESPACE"
|
|
44
|
+
echo " Image: $IMAGE_NAME"
|
|
45
|
+
echo " Version: $VERSION"
|
|
46
|
+
echo ""
|
|
47
|
+
|
|
48
|
+
# Build from project root
|
|
49
|
+
cd "$REPO_ROOT"
|
|
50
|
+
|
|
51
|
+
FULL_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/${IMAGE_NAME}"
|
|
52
|
+
|
|
53
|
+
echo "📦 Building Docker image..."
|
|
54
|
+
docker build \
|
|
55
|
+
-f packages/replay-server/Dockerfile \
|
|
56
|
+
-t "${FULL_IMAGE}:${VERSION}" \
|
|
57
|
+
-t "${FULL_IMAGE}:latest" \
|
|
58
|
+
.
|
|
59
|
+
|
|
60
|
+
echo ""
|
|
61
|
+
echo "📤 Pushing to ACR..."
|
|
62
|
+
docker push "${FULL_IMAGE}:${VERSION}"
|
|
63
|
+
docker push "${FULL_IMAGE}:latest"
|
|
64
|
+
|
|
65
|
+
echo ""
|
|
66
|
+
echo "✅ Deployment complete!"
|
|
67
|
+
echo ""
|
|
68
|
+
echo "Image URLs:"
|
|
69
|
+
echo " ${FULL_IMAGE}:${VERSION}"
|
|
70
|
+
echo " ${FULL_IMAGE}:latest"
|
|
71
|
+
echo ""
|
|
72
|
+
echo "Next steps:"
|
|
73
|
+
echo " 1. Go to Aliyun Function Compute console"
|
|
74
|
+
echo " 2. Create/update function with image: ${FULL_IMAGE}:${VERSION}"
|
|
75
|
+
echo " 3. Set environment variables:"
|
|
76
|
+
echo " - PORT=9000"
|
|
77
|
+
echo " - OUTPUT_DIR=/tmp/output"
|
|
78
|
+
echo " 4. Configure function settings:"
|
|
79
|
+
echo " - Memory: 4096 MB"
|
|
80
|
+
echo " - Timeout: 600 seconds"
|
|
81
|
+
echo " - Instance Concurrency: 1"
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI tool for offline video rendering
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx tsx src/cli/render.ts --manifest ./replay.json --output ./output.mp4
|
|
7
|
+
* npx tsx src/cli/render.ts --manifest-url https://... --game-url http://localhost:5173
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { PuppeteerRenderer, RenderProgress } from '../renderer/PuppeteerRenderer.js';
|
|
13
|
+
import type { ReplayManifestV1 } from '@agent-foundry/replay';
|
|
14
|
+
|
|
15
|
+
interface CliArgs {
|
|
16
|
+
manifest?: string;
|
|
17
|
+
manifestUrl?: string;
|
|
18
|
+
output: string;
|
|
19
|
+
gameUrl: string;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
fps: number;
|
|
23
|
+
secondsPerAge: number;
|
|
24
|
+
keepTemp: boolean;
|
|
25
|
+
useHardwareAcceleration: boolean;
|
|
26
|
+
help: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(): CliArgs {
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const result: CliArgs = {
|
|
32
|
+
output: './output.mp4',
|
|
33
|
+
gameUrl: 'http://localhost:5173',
|
|
34
|
+
width: 720,
|
|
35
|
+
height: 1280,
|
|
36
|
+
fps: 16,
|
|
37
|
+
secondsPerAge: 1.0,
|
|
38
|
+
keepTemp: false,
|
|
39
|
+
useHardwareAcceleration: false,
|
|
40
|
+
help: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
const next = args[i + 1];
|
|
46
|
+
|
|
47
|
+
switch (arg) {
|
|
48
|
+
case '--manifest':
|
|
49
|
+
case '-m':
|
|
50
|
+
result.manifest = next;
|
|
51
|
+
i++;
|
|
52
|
+
break;
|
|
53
|
+
case '--manifest-url':
|
|
54
|
+
result.manifestUrl = next;
|
|
55
|
+
i++;
|
|
56
|
+
break;
|
|
57
|
+
case '--output':
|
|
58
|
+
case '-o':
|
|
59
|
+
result.output = next;
|
|
60
|
+
i++;
|
|
61
|
+
break;
|
|
62
|
+
case '--game-url':
|
|
63
|
+
case '-g':
|
|
64
|
+
result.gameUrl = next;
|
|
65
|
+
i++;
|
|
66
|
+
break;
|
|
67
|
+
case '--width':
|
|
68
|
+
result.width = parseInt(next, 10);
|
|
69
|
+
i++;
|
|
70
|
+
break;
|
|
71
|
+
case '--height':
|
|
72
|
+
result.height = parseInt(next, 10);
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
case '--fps':
|
|
76
|
+
result.fps = parseInt(next, 10);
|
|
77
|
+
i++;
|
|
78
|
+
break;
|
|
79
|
+
case '--seconds-per-age':
|
|
80
|
+
result.secondsPerAge = parseFloat(next);
|
|
81
|
+
i++;
|
|
82
|
+
break;
|
|
83
|
+
case '--keep-temp':
|
|
84
|
+
result.keepTemp = true;
|
|
85
|
+
break;
|
|
86
|
+
case '--use-hardware-acceleration':
|
|
87
|
+
case '--nvenc':
|
|
88
|
+
result.useHardwareAcceleration = true;
|
|
89
|
+
break;
|
|
90
|
+
case '--help':
|
|
91
|
+
case '-h':
|
|
92
|
+
result.help = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printHelp(): void {
|
|
101
|
+
console.log(`
|
|
102
|
+
🎬 LifeRestart Replay Renderer CLI
|
|
103
|
+
|
|
104
|
+
Usage:
|
|
105
|
+
npx tsx src/cli/render.ts [options]
|
|
106
|
+
|
|
107
|
+
Options:
|
|
108
|
+
-m, --manifest <path> Path to replay manifest JSON file
|
|
109
|
+
--manifest-url <url> URL to fetch replay manifest from
|
|
110
|
+
-o, --output <path> Output video path (default: ./output.mp4)
|
|
111
|
+
-g, --game-url <url> Game URL (default: http://localhost:5173)
|
|
112
|
+
--width <px> Video width (default: 720)
|
|
113
|
+
--height <px> Video height (default: 1280)
|
|
114
|
+
--fps <n> Frames per second (default: 16)
|
|
115
|
+
--seconds-per-age <n> Seconds to display each age (default: 1.0)
|
|
116
|
+
--use-hardware-acceleration Enable NVENC hardware acceleration (default: CPU encoding)
|
|
117
|
+
--nvenc Alias for --use-hardware-acceleration
|
|
118
|
+
--keep-temp Keep temporary screenshot files (deprecated)
|
|
119
|
+
-h, --help Show this help message
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
# Render from local manifest file
|
|
123
|
+
npx tsx src/cli/render.ts -m ./replay.json -o ./video.mp4
|
|
124
|
+
|
|
125
|
+
# Render from URL
|
|
126
|
+
npx tsx src/cli/render.ts --manifest-url https://example.com/replay.json
|
|
127
|
+
|
|
128
|
+
# Custom video settings
|
|
129
|
+
npx tsx src/cli/render.ts -m ./replay.json --width 720 --height 1280 --fps 60
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function loadManifest(args: CliArgs): Promise<ReplayManifestV1> {
|
|
134
|
+
if (args.manifest) {
|
|
135
|
+
const manifestPath = path.resolve(args.manifest);
|
|
136
|
+
if (!fs.existsSync(manifestPath)) {
|
|
137
|
+
throw new Error(`Manifest file not found: ${manifestPath}`);
|
|
138
|
+
}
|
|
139
|
+
const content = await fs.promises.readFile(manifestPath, 'utf-8');
|
|
140
|
+
return JSON.parse(content);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (args.manifestUrl) {
|
|
144
|
+
const response = await fetch(args.manifestUrl);
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Failed to fetch manifest: ${response.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
return await response.json();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error('Either --manifest or --manifest-url is required');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatProgress(progress: RenderProgress): string {
|
|
155
|
+
const percent = progress.total > 0
|
|
156
|
+
? Math.round((progress.current / progress.total) * 100)
|
|
157
|
+
: 0;
|
|
158
|
+
|
|
159
|
+
const bar = '█'.repeat(Math.floor(percent / 5)) + '░'.repeat(20 - Math.floor(percent / 5));
|
|
160
|
+
|
|
161
|
+
return `[${bar}] ${percent}% - ${progress.message}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main(): Promise<void> {
|
|
165
|
+
const args = parseArgs();
|
|
166
|
+
|
|
167
|
+
if (args.help) {
|
|
168
|
+
printHelp();
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!args.manifest && !args.manifestUrl) {
|
|
173
|
+
console.error('Error: Either --manifest or --manifest-url is required');
|
|
174
|
+
console.error('Run with --help for usage information');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('🎬 LifeRestart Replay Renderer');
|
|
179
|
+
console.log('━'.repeat(50));
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Load manifest
|
|
183
|
+
console.log('📄 Loading manifest...');
|
|
184
|
+
const manifest = await loadManifest(args);
|
|
185
|
+
console.log(` Game ID: ${manifest.gameId}`);
|
|
186
|
+
console.log(` Timeline: ${manifest.timeline.length} ages`);
|
|
187
|
+
console.log(` Highlights: ${manifest.highlights.length}`);
|
|
188
|
+
|
|
189
|
+
// Estimate duration
|
|
190
|
+
const estimatedDuration = manifest.timeline.length * args.secondsPerAge;
|
|
191
|
+
console.log(` Estimated video duration: ${estimatedDuration}s`);
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
// Create output directory
|
|
195
|
+
const outputDir = path.dirname(path.resolve(args.output));
|
|
196
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
// Start rendering
|
|
199
|
+
console.log('🚀 Starting render...');
|
|
200
|
+
console.log(` Game URL: ${args.gameUrl}`);
|
|
201
|
+
console.log(` Output: ${args.output}`);
|
|
202
|
+
console.log(` Resolution: ${args.width}x${args.height} @ ${args.fps}fps`);
|
|
203
|
+
console.log(` Encoder: ${args.useHardwareAcceleration ? 'h264_nvenc (Hardware)' : 'libx264 (CPU)'}`);
|
|
204
|
+
console.log('');
|
|
205
|
+
|
|
206
|
+
const renderer = new PuppeteerRenderer();
|
|
207
|
+
|
|
208
|
+
let lastLine = '';
|
|
209
|
+
const result = await renderer.render(manifest, {
|
|
210
|
+
gameUrl: args.gameUrl,
|
|
211
|
+
outputPath: path.resolve(args.output),
|
|
212
|
+
width: args.width,
|
|
213
|
+
height: args.height,
|
|
214
|
+
fps: args.fps,
|
|
215
|
+
secondsPerAge: args.secondsPerAge,
|
|
216
|
+
useHardwareAcceleration: args.useHardwareAcceleration,
|
|
217
|
+
keepTempFiles: args.keepTemp,
|
|
218
|
+
onProgress: (progress) => {
|
|
219
|
+
const line = formatProgress(progress);
|
|
220
|
+
if (line !== lastLine) {
|
|
221
|
+
process.stdout.write(`\r${line}`);
|
|
222
|
+
lastLine = line;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
console.log('\n');
|
|
228
|
+
|
|
229
|
+
if (result.success) {
|
|
230
|
+
console.log('✅ Render completed successfully!');
|
|
231
|
+
console.log(` Output: ${result.outputPath}`);
|
|
232
|
+
console.log(` Duration: ${result.duration?.toFixed(1)}s`);
|
|
233
|
+
} else {
|
|
234
|
+
console.error('❌ Render failed:', result.error);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('❌ Error:', error instanceof Error ? error.message : error);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
package/src/index.ts
ADDED