@twick/render-server 0.14.2
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 +116 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/renderer.d.ts +3 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +37 -0
- package/dist/renderer.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +121 -0
- package/dist/server.js.map +1 -0
- package/package.json +41 -0
- package/package.json.bak +41 -0
- package/src/index.ts +5 -0
- package/src/renderer.ts +38 -0
- package/src/server.ts +145 -0
- package/test.js +163 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @twick/render-server
|
|
2
|
+
|
|
3
|
+
A simple Node.js server for rendering videos using Twick.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install @twick/render-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Starting the server
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Development mode
|
|
17
|
+
pnpm run dev
|
|
18
|
+
|
|
19
|
+
# Production mode
|
|
20
|
+
pnpm run build
|
|
21
|
+
pnpm start
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The server will start on port 3001 by default. You can change this by setting the `PORT` environment variable.
|
|
25
|
+
|
|
26
|
+
### API Endpoints
|
|
27
|
+
|
|
28
|
+
#### POST /api/render-video
|
|
29
|
+
|
|
30
|
+
Renders a video using Twick.
|
|
31
|
+
|
|
32
|
+
**Request Body:**
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"variables": {
|
|
36
|
+
"input": {
|
|
37
|
+
"properties": {
|
|
38
|
+
"width": 720,
|
|
39
|
+
"height": 1280
|
|
40
|
+
},
|
|
41
|
+
"tracks": [
|
|
42
|
+
{
|
|
43
|
+
"id": "t-track-1",
|
|
44
|
+
"type": "element",
|
|
45
|
+
"elements": [
|
|
46
|
+
{
|
|
47
|
+
"id": "e-244f8d5a3baa",
|
|
48
|
+
"trackId": "t-track-1",
|
|
49
|
+
"type": "rect",
|
|
50
|
+
"s": 0,
|
|
51
|
+
"e": 5,
|
|
52
|
+
"props": {
|
|
53
|
+
"width": 720,
|
|
54
|
+
"height": 1280,
|
|
55
|
+
"fill": "#fff000"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"name": "element"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"settings": {
|
|
65
|
+
"outFile": "my-video.mp4",
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Response:**
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"success": true,
|
|
74
|
+
"downloadUrl": "http://localhost:3001/download/output/my-video.mp4"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### GET /download/:outFile
|
|
79
|
+
|
|
80
|
+
Downloads a rendered video file.
|
|
81
|
+
|
|
82
|
+
#### GET /health
|
|
83
|
+
|
|
84
|
+
Health check endpoint.
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
The server uses the following environment variables:
|
|
89
|
+
|
|
90
|
+
- `PORT`: Server port (default: 3001)
|
|
91
|
+
- `NODE_ENV`: Environment (development/production)
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Install dependencies
|
|
97
|
+
pnpm install
|
|
98
|
+
|
|
99
|
+
# Start development server
|
|
100
|
+
pnpm run dev
|
|
101
|
+
|
|
102
|
+
# Build for production
|
|
103
|
+
pnpm run build
|
|
104
|
+
|
|
105
|
+
# Start production server
|
|
106
|
+
pnpm start
|
|
107
|
+
|
|
108
|
+
# Test
|
|
109
|
+
node test.js
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Note:** This server will work on Linux and macOS only. Windows is not supported.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGzD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzD,8CAA8C;AAC9C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,gBAAgB,GAAU,WAAW,GAAG,EAAE,UAAU,GAAG,oBAiC5D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { renderVideo } from "@twick/renderer";
|
|
2
|
+
const renderTwickVideo = async (variables, settings) => {
|
|
3
|
+
try {
|
|
4
|
+
const { input } = variables;
|
|
5
|
+
const { properties } = input;
|
|
6
|
+
// Merge user settings with defaults
|
|
7
|
+
const mergedSettings = {
|
|
8
|
+
logProgress: true,
|
|
9
|
+
outDir: "./output",
|
|
10
|
+
outFile: properties.reqesutId ?? `video-${Date.now()}` + ".mp4",
|
|
11
|
+
quality: "medium",
|
|
12
|
+
projectSettings: {
|
|
13
|
+
exporter: {
|
|
14
|
+
name: "@twick/core/wasm",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
x: properties.width,
|
|
18
|
+
y: properties.height,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
...settings, // Allow user settings to override defaults
|
|
22
|
+
};
|
|
23
|
+
const file = await renderVideo({
|
|
24
|
+
projectFile: "@twick/visualizer/dist/project.js",
|
|
25
|
+
variables: variables,
|
|
26
|
+
settings: mergedSettings,
|
|
27
|
+
});
|
|
28
|
+
console.log("Successfully rendered: ", file);
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("Render error:", error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export default renderTwickVideo;
|
|
37
|
+
//# sourceMappingURL=renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,gBAAgB,GAAG,KAAK,EAAE,SAAc,EAAE,QAAa,EAAE,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;QAC5B,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;QAC7B,oCAAoC;QACpC,MAAM,cAAc,GAAG;YACrB,WAAW,EAAE,IAAI;YACjB,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,UAAU,CAAC,SAAS,IAAI,SAAS,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,MAAM;YAC/D,OAAO,EAAE,QAAQ;YACjB,eAAe,EAAE;gBACf,QAAQ,EAAE;oBACR,IAAI,EAAE,kBAAkB;iBACzB;gBACD,IAAI,EAAE;oBACJ,CAAC,EAAE,UAAU,CAAC,KAAK;oBACnB,CAAC,EAAE,UAAU,CAAC,MAAM;iBACrB;aACF;YACD,GAAG,QAAQ,EAAE,2CAA2C;SACzD,CAAC;QAEF,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC;YAC7B,WAAW,EAAE,mCAAmC;YAChD,SAAS,EAAE,SAAS;YACpB,QAAQ,EAAE,cAAc;SACzB,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;QACtC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA+EA,QAAA,MAAM,OAAO,EAAE,OAAO,SAAS,EAAE,OAAmB,CAAC;AAyDrD,eAAe,OAAO,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import renderTwickVideo from "./renderer.js";
|
|
6
|
+
const PORT = process.env.PORT || 3001;
|
|
7
|
+
const BASE_PATH = `http://localhost:${PORT}`;
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
// Rate limiting configuration
|
|
11
|
+
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
12
|
+
const RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window
|
|
13
|
+
const RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute
|
|
14
|
+
const rateLimitStore = new Map();
|
|
15
|
+
// Cleanup expired entries periodically
|
|
16
|
+
setInterval(() => {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
19
|
+
if (now > entry.resetTime) {
|
|
20
|
+
rateLimitStore.delete(key);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}, RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
24
|
+
// Rate limiting middleware
|
|
25
|
+
const rateLimitMiddleware = (req, res, next) => {
|
|
26
|
+
const clientIP = req.ip || req.connection.remoteAddress || 'unknown';
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
// Get or create rate limit entry for this IP
|
|
29
|
+
let entry = rateLimitStore.get(clientIP);
|
|
30
|
+
if (!entry || now > entry.resetTime) {
|
|
31
|
+
// New window or expired entry
|
|
32
|
+
entry = {
|
|
33
|
+
count: 1,
|
|
34
|
+
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
35
|
+
};
|
|
36
|
+
rateLimitStore.set(clientIP, entry);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Increment count in current window
|
|
40
|
+
entry.count++;
|
|
41
|
+
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
|
|
42
|
+
// Rate limit exceeded
|
|
43
|
+
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
|
|
44
|
+
res.set('Retry-After', retryAfter.toString());
|
|
45
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
46
|
+
res.set('X-RateLimit-Remaining', '0');
|
|
47
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
48
|
+
return res.status(429).json({
|
|
49
|
+
success: false,
|
|
50
|
+
error: 'Too many requests',
|
|
51
|
+
message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
|
|
52
|
+
retryAfter
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Add rate limit headers
|
|
57
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
58
|
+
res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
|
|
59
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
60
|
+
next();
|
|
61
|
+
};
|
|
62
|
+
const nodeApp = express();
|
|
63
|
+
nodeApp.use(cors());
|
|
64
|
+
nodeApp.use(express.json());
|
|
65
|
+
// Serve static files from output directory
|
|
66
|
+
nodeApp.use("/output", express.static(path.join(__dirname, "../output")));
|
|
67
|
+
nodeApp.post("/api/render-video", async (req, res) => {
|
|
68
|
+
const { variables, settings } = req.body;
|
|
69
|
+
try {
|
|
70
|
+
const outputPath = await renderTwickVideo(variables, settings);
|
|
71
|
+
res.json({
|
|
72
|
+
success: true,
|
|
73
|
+
downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error("Render error:", error);
|
|
78
|
+
res.status(500).json({
|
|
79
|
+
success: false,
|
|
80
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Apply rate limiting to download endpoint
|
|
85
|
+
nodeApp.get("/download/:filename", rateLimitMiddleware, (req, res) => {
|
|
86
|
+
const outputDir = path.resolve(__dirname, "../output");
|
|
87
|
+
const requestedPath = path.resolve(outputDir, req.params.filename);
|
|
88
|
+
if (!requestedPath.startsWith(outputDir + path.sep)) {
|
|
89
|
+
// Attempted path traversal or access outside output directory
|
|
90
|
+
res.status(403).json({
|
|
91
|
+
success: false,
|
|
92
|
+
error: "Forbidden",
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
res.download(requestedPath, (err) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
res.status(404).json({
|
|
99
|
+
success: false,
|
|
100
|
+
error: "File not found",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
// Health check endpoint
|
|
106
|
+
nodeApp.get("/health", (req, res) => {
|
|
107
|
+
res.json({
|
|
108
|
+
status: "ok",
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
// Export the app for programmatic usage
|
|
113
|
+
export default nodeApp;
|
|
114
|
+
// Start the server
|
|
115
|
+
nodeApp.listen(PORT, () => {
|
|
116
|
+
console.log(`Render server running on port ${PORT}`);
|
|
117
|
+
console.log(`Health check: ${BASE_PATH}/health`);
|
|
118
|
+
console.log(`API endpoint: ${BASE_PATH}/api/render-video`);
|
|
119
|
+
console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 60000} minutes`);
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twick/render-server",
|
|
3
|
+
"version": "0.14.2",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsx watch src/server.ts",
|
|
10
|
+
"start": "node dist/server.js",
|
|
11
|
+
"clean": "rimraf dist"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@twick/2d": "0.14.2",
|
|
18
|
+
"@twick/core": "0.14.2",
|
|
19
|
+
"@twick/ffmpeg": "0.14.2",
|
|
20
|
+
"@twick/renderer": "0.14.2",
|
|
21
|
+
"@twick/ui": "0.14.2",
|
|
22
|
+
"@twick/visualizer": "0.14.2",
|
|
23
|
+
"@types/express-rate-limit": "^6.0.2",
|
|
24
|
+
"cors": "^2.8.5",
|
|
25
|
+
"express": "^4.18.2",
|
|
26
|
+
"express-rate-limit": "^8.0.1",
|
|
27
|
+
"node-fetch": "^3.3.2",
|
|
28
|
+
"path": "^0.12.7"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/cors": "^2.8.17",
|
|
32
|
+
"@types/express": "^4.17.21",
|
|
33
|
+
"@types/node": "^20.10.0",
|
|
34
|
+
"rimraf": "^5.0.5",
|
|
35
|
+
"tsx": "^4.7.0",
|
|
36
|
+
"typescript": "5.4.2"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twick/render-server",
|
|
3
|
+
"version": "0.14.2",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsx watch src/server.ts",
|
|
10
|
+
"start": "node dist/server.js",
|
|
11
|
+
"clean": "rimraf dist"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@twick/2d": "0.14.2",
|
|
18
|
+
"@twick/core": "0.14.2",
|
|
19
|
+
"@twick/ffmpeg": "0.14.2",
|
|
20
|
+
"@twick/renderer": "0.14.2",
|
|
21
|
+
"@twick/ui": "0.14.2",
|
|
22
|
+
"@twick/visualizer": "0.14.2",
|
|
23
|
+
"@types/express-rate-limit": "^6.0.2",
|
|
24
|
+
"cors": "^2.8.5",
|
|
25
|
+
"express": "^4.18.2",
|
|
26
|
+
"express-rate-limit": "^8.0.1",
|
|
27
|
+
"node-fetch": "^3.3.2",
|
|
28
|
+
"path": "^0.12.7"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/cors": "^2.8.17",
|
|
32
|
+
"@types/express": "^4.17.21",
|
|
33
|
+
"@types/node": "^20.10.0",
|
|
34
|
+
"rimraf": "^5.0.5",
|
|
35
|
+
"tsx": "^4.7.0",
|
|
36
|
+
"typescript": "5.4.2"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { renderVideo } from "@twick/renderer";
|
|
2
|
+
|
|
3
|
+
const renderTwickVideo = async (variables: any, settings: any) => {
|
|
4
|
+
try {
|
|
5
|
+
const { input } = variables;
|
|
6
|
+
const { properties } = input;
|
|
7
|
+
// Merge user settings with defaults
|
|
8
|
+
const mergedSettings = {
|
|
9
|
+
logProgress: true,
|
|
10
|
+
outDir: "./output",
|
|
11
|
+
outFile: properties.reqesutId ?? `video-${Date.now()}` + ".mp4",
|
|
12
|
+
quality: "medium",
|
|
13
|
+
projectSettings: {
|
|
14
|
+
exporter: {
|
|
15
|
+
name: "@twick/core/wasm",
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
x: properties.width,
|
|
19
|
+
y: properties.height,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
...settings, // Allow user settings to override defaults
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const file = await renderVideo({
|
|
26
|
+
projectFile: "@twick/visualizer/dist/project.js",
|
|
27
|
+
variables: variables,
|
|
28
|
+
settings: mergedSettings,
|
|
29
|
+
});
|
|
30
|
+
console.log("Successfully rendered: ", file);
|
|
31
|
+
return file;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("Render error:", error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default renderTwickVideo;
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import renderTwickVideo from "./renderer.js";
|
|
6
|
+
|
|
7
|
+
const PORT = process.env.PORT || 3001;
|
|
8
|
+
const BASE_PATH = `http://localhost:${PORT}`;
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// Rate limiting configuration
|
|
14
|
+
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
15
|
+
const RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window
|
|
16
|
+
const RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute
|
|
17
|
+
|
|
18
|
+
// In-memory store for rate limiting
|
|
19
|
+
interface RateLimitEntry {
|
|
20
|
+
count: number;
|
|
21
|
+
resetTime: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rateLimitStore = new Map<string, RateLimitEntry>();
|
|
25
|
+
|
|
26
|
+
// Cleanup expired entries periodically
|
|
27
|
+
setInterval(() => {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
30
|
+
if (now > entry.resetTime) {
|
|
31
|
+
rateLimitStore.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}, RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
|
35
|
+
|
|
36
|
+
// Rate limiting middleware
|
|
37
|
+
const rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
38
|
+
const clientIP = req.ip || req.connection.remoteAddress || 'unknown';
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
|
|
41
|
+
// Get or create rate limit entry for this IP
|
|
42
|
+
let entry = rateLimitStore.get(clientIP);
|
|
43
|
+
|
|
44
|
+
if (!entry || now > entry.resetTime) {
|
|
45
|
+
// New window or expired entry
|
|
46
|
+
entry = {
|
|
47
|
+
count: 1,
|
|
48
|
+
resetTime: now + RATE_LIMIT_WINDOW_MS
|
|
49
|
+
};
|
|
50
|
+
rateLimitStore.set(clientIP, entry);
|
|
51
|
+
} else {
|
|
52
|
+
// Increment count in current window
|
|
53
|
+
entry.count++;
|
|
54
|
+
|
|
55
|
+
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
|
|
56
|
+
// Rate limit exceeded
|
|
57
|
+
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
|
|
58
|
+
res.set('Retry-After', retryAfter.toString());
|
|
59
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
60
|
+
res.set('X-RateLimit-Remaining', '0');
|
|
61
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
62
|
+
|
|
63
|
+
return res.status(429).json({
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'Too many requests',
|
|
66
|
+
message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
|
|
67
|
+
retryAfter
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add rate limit headers
|
|
73
|
+
res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
|
|
74
|
+
res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
|
|
75
|
+
res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
|
|
76
|
+
|
|
77
|
+
next();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const nodeApp: import("express").Express = express();
|
|
81
|
+
|
|
82
|
+
nodeApp.use(cors());
|
|
83
|
+
nodeApp.use(express.json());
|
|
84
|
+
|
|
85
|
+
// Serve static files from output directory
|
|
86
|
+
nodeApp.use("/output", express.static(path.join(__dirname, "../output")));
|
|
87
|
+
|
|
88
|
+
nodeApp.post("/api/render-video", async (req, res) => {
|
|
89
|
+
const { variables, settings } = req.body;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const outputPath = await renderTwickVideo(variables, settings);
|
|
93
|
+
res.json({
|
|
94
|
+
success: true,
|
|
95
|
+
downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`,
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Render error:", error);
|
|
99
|
+
res.status(500).json({
|
|
100
|
+
success: false,
|
|
101
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Apply rate limiting to download endpoint
|
|
107
|
+
nodeApp.get("/download/:filename", rateLimitMiddleware, (req, res) => {
|
|
108
|
+
const outputDir = path.resolve(__dirname, "../output");
|
|
109
|
+
const requestedPath = path.resolve(outputDir, req.params.filename);
|
|
110
|
+
if (!requestedPath.startsWith(outputDir + path.sep)) {
|
|
111
|
+
// Attempted path traversal or access outside output directory
|
|
112
|
+
res.status(403).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: "Forbidden",
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
res.download(requestedPath, (err) => {
|
|
119
|
+
if (err) {
|
|
120
|
+
res.status(404).json({
|
|
121
|
+
success: false,
|
|
122
|
+
error: "File not found",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Health check endpoint
|
|
129
|
+
nodeApp.get("/health", (req, res) => {
|
|
130
|
+
res.json({
|
|
131
|
+
status: "ok",
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Export the app for programmatic usage
|
|
137
|
+
export default nodeApp;
|
|
138
|
+
|
|
139
|
+
// Start the server
|
|
140
|
+
nodeApp.listen(PORT, () => {
|
|
141
|
+
console.log(`Render server running on port ${PORT}`);
|
|
142
|
+
console.log(`Health check: ${BASE_PATH}/health`);
|
|
143
|
+
console.log(`API endpoint: ${BASE_PATH}/api/render-video`);
|
|
144
|
+
console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 60000} minutes`);
|
|
145
|
+
});
|
package/test.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Simple test script for the render server
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
|
|
4
|
+
const API_URL = 'http://localhost:3001';
|
|
5
|
+
|
|
6
|
+
const sample = {
|
|
7
|
+
"input": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"width": 720,
|
|
10
|
+
"height": 1280
|
|
11
|
+
},
|
|
12
|
+
"tracks": [
|
|
13
|
+
{
|
|
14
|
+
"id": "t-track-1",
|
|
15
|
+
"type": "element",
|
|
16
|
+
"elements": [
|
|
17
|
+
{
|
|
18
|
+
"id": "e-244f8d5a3baa",
|
|
19
|
+
"trackId": "t-track-1",
|
|
20
|
+
"type": "rect",
|
|
21
|
+
"s": 0,
|
|
22
|
+
"e": 5,
|
|
23
|
+
"props": {
|
|
24
|
+
"width": 720,
|
|
25
|
+
"height": 1280,
|
|
26
|
+
"fill": "#fff000"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"name": "element"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "t-track-2",
|
|
34
|
+
"type": "element",
|
|
35
|
+
"elements": [
|
|
36
|
+
{
|
|
37
|
+
"id": "e-244f8d5a3bba",
|
|
38
|
+
"trackId": "t-track-2",
|
|
39
|
+
"type": "text",
|
|
40
|
+
"s": 0,
|
|
41
|
+
"e": 1,
|
|
42
|
+
"props": {
|
|
43
|
+
"text": "Hello Guys!",
|
|
44
|
+
"fontSize": 100,
|
|
45
|
+
"fill": "#FF0000"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "e-244f8d5a3bbb",
|
|
50
|
+
"trackId": "t-track-2",
|
|
51
|
+
"type": "text",
|
|
52
|
+
"s": 1,
|
|
53
|
+
"e": 4,
|
|
54
|
+
"props": {
|
|
55
|
+
"text": "Welcome to the world of Twick!",
|
|
56
|
+
"fontSize": 100,
|
|
57
|
+
"fill": "#FF0000",
|
|
58
|
+
"maxWidth": 500,
|
|
59
|
+
"textAlign": "center",
|
|
60
|
+
"textWrap": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "e-244f8d5a3bbc",
|
|
65
|
+
"trackId": "t-track-2",
|
|
66
|
+
"type": "text",
|
|
67
|
+
"s": 4,
|
|
68
|
+
"e": 5,
|
|
69
|
+
"props": {
|
|
70
|
+
"text": "Thank You !",
|
|
71
|
+
"fontSize": 100,
|
|
72
|
+
"fill": "#FF0000"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"name": "element"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "t-track-3",
|
|
80
|
+
"type": "element",
|
|
81
|
+
"elements": [
|
|
82
|
+
{
|
|
83
|
+
"id": "e-244f8d5aabaa",
|
|
84
|
+
"trackId": "t-track-3",
|
|
85
|
+
"type": "audio",
|
|
86
|
+
"s": 0,
|
|
87
|
+
"e": 5,
|
|
88
|
+
"props": {
|
|
89
|
+
"src": "https://cdn.pixabay.com/audio/2024/09/15/audio_e00d39651a.mp3",
|
|
90
|
+
"play": true,
|
|
91
|
+
"volume": 1
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"name": "audio"
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"version": 1
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async function testHealth() {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`${API_URL}/health`);
|
|
106
|
+
const result = await response.json();
|
|
107
|
+
console.log('โ
Health check passed:', result);
|
|
108
|
+
return true;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('โ Health check failed:', error.message);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function testRender() {
|
|
116
|
+
try {
|
|
117
|
+
console.log('๐ Testing video render...');
|
|
118
|
+
|
|
119
|
+
const response = await fetch(`${API_URL}/api/render-video`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
variables: sample,
|
|
126
|
+
settings: {
|
|
127
|
+
outFile: `test-${Date.now()}.mp4`,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = await response.json();
|
|
133
|
+
|
|
134
|
+
if (result.success) {
|
|
135
|
+
console.log('โ
Render test passed!');
|
|
136
|
+
console.log('๐ Output:', result.downloadUrl);
|
|
137
|
+
return true;
|
|
138
|
+
} else {
|
|
139
|
+
console.error('โ Render test failed:', result.error);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('โ Render test failed:', error.message);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function runTests() {
|
|
149
|
+
console.log('๐งช Running render server tests...\n');
|
|
150
|
+
|
|
151
|
+
const healthPassed = await testHealth();
|
|
152
|
+
console.log('');
|
|
153
|
+
|
|
154
|
+
if (healthPassed) {
|
|
155
|
+
await testRender();
|
|
156
|
+
} else {
|
|
157
|
+
console.log('โ Skipping render test due to health check failure');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('\n๐ Tests completed');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
runTests().catch(console.error);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src",
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"noEmit": false
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|