@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 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
@@ -0,0 +1,3 @@
1
+ export { default as renderTwickVideo } from "./renderer";
2
+ export { default as createServer } from "./server";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,5 @@
1
+ // Export the renderer function
2
+ export { default as renderTwickVideo } from "./renderer";
3
+ // Re-export the server for programmatic usage
4
+ export { default as createServer } from "./server";
5
+ //# sourceMappingURL=index.js.map
@@ -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,3 @@
1
+ declare const renderTwickVideo: (variables: any, settings: any) => Promise<string>;
2
+ export default renderTwickVideo;
3
+ //# sourceMappingURL=renderer.d.ts.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,3 @@
1
+ declare const nodeApp: import("express").Express;
2
+ export default nodeApp;
3
+ //# sourceMappingURL=server.d.ts.map
@@ -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
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ // Export the renderer function
2
+ export { default as renderTwickVideo } from "./renderer";
3
+
4
+ // Re-export the server for programmatic usage
5
+ export { default as createServer } from "./server";
@@ -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
+ }