@twick/render-server 0.15.20 โ†’ 0.15.22

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/src/renderer.ts DELETED
@@ -1,115 +0,0 @@
1
- import { renderVideo } from "@twick/renderer";
2
-
3
- // Simple in-process concurrency control for render jobs.
4
- // This helps avoid overloading a single server instance.
5
- let activeRenders = 0;
6
- const renderQueue: Array<() => void> = [];
7
-
8
- const getMaxConcurrentRenders = () => {
9
- const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;
10
- const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;
11
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;
12
- };
13
-
14
- async function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {
15
- const maxConcurrent = getMaxConcurrentRenders();
16
-
17
- if (activeRenders >= maxConcurrent) {
18
- await new Promise<void>((resolve) => {
19
- renderQueue.push(resolve);
20
- });
21
- }
22
-
23
- activeRenders++;
24
- try {
25
- return await fn();
26
- } finally {
27
- activeRenders--;
28
- const next = renderQueue.shift();
29
- if (next) {
30
- next();
31
- }
32
- }
33
- }
34
-
35
- /**
36
- * Renders a Twick video with the provided variables and settings.
37
- * Processes project variables, merges settings with defaults, and
38
- * generates a video file using the Twick renderer.
39
- *
40
- * @param variables - Project variables containing input configuration
41
- * @param settings - Optional render settings to override defaults
42
- * @returns Promise resolving to the path of the rendered video file
43
- *
44
- * @example
45
- * ```js
46
- * const videoPath = await renderTwickVideo(
47
- * { input: { properties: { width: 1920, height: 1080 } } },
48
- * { quality: "high", outFile: "my-video.mp4" }
49
- * );
50
- * // videoPath = "./output/my-video.mp4"
51
- * ```
52
- */
53
- const renderTwickVideo = async (variables: any, settings: any) => {
54
- const start = Date.now();
55
- try {
56
- const { input } = variables;
57
- const { properties } = input;
58
-
59
- // Basic safety limits (can be overridden via env)
60
- const maxWidth =
61
- parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? "3840", 10) || 3840;
62
- const maxHeight =
63
- parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? "2160", 10) || 2160;
64
- const maxFps =
65
- parseInt(process.env.TWICK_MAX_RENDER_FPS ?? "60", 10) || 60;
66
-
67
- const width = Math.min(properties.width, maxWidth);
68
- const height = Math.min(properties.height, maxHeight);
69
- const fps = Math.min(properties.fps ?? 30, maxFps);
70
-
71
- // Merge user settings with defaults
72
- const mergedSettings = {
73
- logProgress: true,
74
- outDir: "./output",
75
- outFile: properties.reqesutId ?? `video-${Date.now()}` + ".mp4",
76
- quality: "medium",
77
- projectSettings: {
78
- exporter: {
79
- name: "@twick/core/wasm",
80
- },
81
- size: {
82
- x: width,
83
- y: height,
84
- },
85
- fps,
86
- },
87
- ...settings, // Allow user settings to override defaults
88
- };
89
-
90
- const result = await withRenderSlot(async () => {
91
- const file = await renderVideo({
92
- projectFile: "@twick/visualizer/dist/project.js",
93
- variables,
94
- settings: mergedSettings,
95
- });
96
- return file;
97
- });
98
-
99
- const elapsedMs = Date.now() - start;
100
- console.log(
101
- `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,
102
- result,
103
- );
104
- return result;
105
- } catch (error) {
106
- const elapsedMs = Date.now() - start;
107
- console.error(
108
- `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,
109
- error,
110
- );
111
- throw error;
112
- }
113
- };
114
-
115
- export default renderTwickVideo;
package/src/server.ts DELETED
@@ -1,202 +0,0 @@
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";
6
-
7
- const PORT = process.env.PORT || 5000;
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 (all configurable via env)
14
- const RATE_LIMIT_WINDOW_MS =
15
- parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? "900000", 10) || 15 * 60 * 1000; // default 15 min (ms)
16
- const RATE_LIMIT_MAX_REQUESTS =
17
- parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? "100", 10) || 100; // max requests per window
18
- const RATE_LIMIT_CLEANUP_INTERVAL_MS =
19
- parseInt(process.env.RATE_LIMIT_CLEANUP_INTERVAL_MS ?? "60000", 10) || 60 * 1000; // default 1 min (ms)
20
-
21
- // In-memory store for rate limiting
22
- interface RateLimitEntry {
23
- count: number;
24
- resetTime: number;
25
- }
26
-
27
- const rateLimitStore = new Map<string, RateLimitEntry>();
28
-
29
- // Cleanup expired entries periodically
30
- setInterval(() => {
31
- const now = Date.now();
32
- for (const [key, entry] of rateLimitStore.entries()) {
33
- if (now > entry.resetTime) {
34
- rateLimitStore.delete(key);
35
- }
36
- }
37
- }, RATE_LIMIT_CLEANUP_INTERVAL_MS);
38
-
39
- /**
40
- * Rate limiting middleware for API endpoints.
41
- * Tracks request counts per IP address and enforces rate limits
42
- * to prevent abuse of the render server.
43
- *
44
- * @param req - Express request object
45
- * @param res - Express response object
46
- * @param next - Express next function
47
- *
48
- * @example
49
- * ```js
50
- * app.use('/api', rateLimitMiddleware);
51
- * // Applies rate limiting to all /api routes
52
- * ```
53
- */
54
- const rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
55
- const clientIP = req.ip || req.connection.remoteAddress || 'unknown';
56
- const now = Date.now();
57
-
58
- // Get or create rate limit entry for this IP
59
- let entry = rateLimitStore.get(clientIP);
60
-
61
- if (!entry || now > entry.resetTime) {
62
- // New window or expired entry
63
- entry = {
64
- count: 1,
65
- resetTime: now + RATE_LIMIT_WINDOW_MS
66
- };
67
- rateLimitStore.set(clientIP, entry);
68
- } else {
69
- // Increment count in current window
70
- entry.count++;
71
-
72
- if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
73
- // Rate limit exceeded
74
- const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
75
- res.set('Retry-After', retryAfter.toString());
76
- res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
77
- res.set('X-RateLimit-Remaining', '0');
78
- res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
79
-
80
- return res.status(429).json({
81
- success: false,
82
- error: 'Too many requests',
83
- message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
84
- retryAfter
85
- });
86
- }
87
- }
88
-
89
- // Add rate limit headers
90
- res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
91
- res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
92
- res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
93
-
94
- next();
95
- };
96
-
97
- const nodeApp: import("express").Express = express();
98
-
99
- nodeApp.use(cors());
100
- nodeApp.use(express.json());
101
-
102
- // Serve static files from output directory
103
- nodeApp.use("/output", express.static(path.join(__dirname, "../output")));
104
-
105
- /**
106
- * POST endpoint for video rendering requests.
107
- * Accepts project variables and settings, renders the video,
108
- * and returns a download URL for the completed video.
109
- *
110
- * @param req - Express request object containing variables and settings
111
- * @param res - Express response object
112
- *
113
- * @example
114
- * ```js
115
- * POST /api/render-video
116
- * Body: { variables: {...}, settings: {...} }
117
- * Response: { success: true, downloadUrl: "..." }
118
- * ```
119
- */
120
- nodeApp.post("/api/render-video", async (req, res) => {
121
- const { variables, settings } = req.body;
122
-
123
- try {
124
- const outputPath = await renderTwickVideo(variables, settings);
125
- res.json({
126
- success: true,
127
- downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`,
128
- });
129
- } catch (error) {
130
- console.error("Render error:", error);
131
- res.status(500).json({
132
- success: false,
133
- error: error instanceof Error ? error.message : "Unknown error",
134
- });
135
- }
136
- });
137
-
138
- /**
139
- * GET endpoint for downloading rendered videos.
140
- * Serves video files with rate limiting and security checks
141
- * to prevent path traversal attacks.
142
- *
143
- * @param req - Express request object with filename parameter
144
- * @param res - Express response object
145
- *
146
- * @example
147
- * ```js
148
- * GET /download/video-123.mp4
149
- * // Downloads the specified video file
150
- * ```
151
- */
152
- nodeApp.get("/download/:filename", rateLimitMiddleware, (req, res) => {
153
- const outputDir = path.resolve(__dirname, "../output");
154
- const requestedPath = path.resolve(outputDir, req.params.filename);
155
- if (!requestedPath.startsWith(outputDir + path.sep)) {
156
- // Attempted path traversal or access outside output directory
157
- res.status(403).json({
158
- success: false,
159
- error: "Forbidden",
160
- });
161
- return;
162
- }
163
- res.download(requestedPath, (err) => {
164
- if (err) {
165
- res.status(404).json({
166
- success: false,
167
- error: "File not found",
168
- });
169
- }
170
- });
171
- });
172
-
173
- /**
174
- * Health check endpoint for monitoring server status.
175
- * Returns server status and current timestamp for health monitoring.
176
- *
177
- * @param req - Express request object
178
- * @param res - Express response object
179
- *
180
- * @example
181
- * ```js
182
- * GET /health
183
- * Response: { status: "ok", timestamp: "2024-01-01T00:00:00.000Z" }
184
- * ```
185
- */
186
- nodeApp.get("/health", (req, res) => {
187
- res.json({
188
- status: "ok",
189
- timestamp: new Date().toISOString(),
190
- });
191
- });
192
-
193
- // Export the app for programmatic usage
194
- export default nodeApp;
195
-
196
- // Start the server
197
- nodeApp.listen(PORT, () => {
198
- console.log(`Render server running on port ${PORT}`);
199
- console.log(`Health check: ${BASE_PATH}/health`);
200
- console.log(`API endpoint: ${BASE_PATH}/api/render-video`);
201
- console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 60000} minutes`);
202
- });
@@ -1,228 +0,0 @@
1
- /**
2
- * Test file for rendering a video with an example project
3
- *
4
- * This script demonstrates how to use the renderTwickVideo function
5
- * to create a video from a Twick project configuration.
6
- *
7
- * Run with: tsx src/test-render.ts
8
- */
9
-
10
- import { renderTwickVideo } from "./index.js";
11
- import { fileURLToPath } from "url";
12
- import { dirname, join } from "path";
13
-
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = dirname(__filename);
16
-
17
- /**
18
- * Example project configuration
19
- * This creates a simple video with:
20
- * - A colored background rectangle
21
- * - Animated text elements
22
- * - Audio track
23
- */
24
- const exampleProject = {
25
- input: {
26
- properties: {
27
- width: 1920,
28
- height: 1080,
29
- fps: 30,
30
- },
31
- tracks: [
32
- {
33
- id: "t-background",
34
- type: "element",
35
- name: "background",
36
- elements: [
37
- {
38
- id: "e-bg-1",
39
- trackId: "t-background",
40
- type: "rect",
41
- s: 0,
42
- e: 10,
43
- props: {
44
- width: 1920,
45
- height: 1080,
46
- fill: "#1a1a2e",
47
- },
48
- },
49
- ],
50
- },
51
- {
52
- id: "t-text",
53
- type: "element",
54
- name: "text",
55
- elements: [
56
- {
57
- id: "e-text-1",
58
- trackId: "t-text",
59
- type: "text",
60
- s: 1,
61
- e: 4,
62
- t: "Welcome to Twick!",
63
- props: {
64
- fill: "#ffffff",
65
- fontSize: 72,
66
- fontFamily: "Arial",
67
- fontWeight: "bold",
68
- x: 0,
69
- y: -200,
70
- },
71
- animation: {
72
- name: "fade",
73
- animate: "enter",
74
- duration: 1,
75
- },
76
- textEffect: {
77
- name: "typewriter",
78
- duration: 2,
79
- },
80
- },
81
- {
82
- id: "e-text-2",
83
- trackId: "t-text",
84
- type: "text",
85
- s: 4,
86
- e: 7,
87
- t: "Create Amazing Videos",
88
- props: {
89
- fill: "#4ecdc4",
90
- fontSize: 64,
91
- fontFamily: "Arial",
92
- fontWeight: "bold",
93
- x: 0,
94
- y: 0,
95
- },
96
- animation: {
97
- name: "rise",
98
- animate: "enter",
99
- duration: 1,
100
- },
101
- },
102
- {
103
- id: "e-text-3",
104
- trackId: "t-text",
105
- type: "text",
106
- s: 7,
107
- e: 10,
108
- t: "Programmatically!",
109
- props: {
110
- fill: "#ffd700",
111
- fontSize: 80,
112
- fontFamily: "Arial",
113
- fontWeight: "bold",
114
- x: 0,
115
- y: 200,
116
- },
117
- animation: {
118
- name: "fade",
119
- animate: "enter",
120
- duration: 1,
121
- },
122
- },
123
- ],
124
- },
125
- {
126
- id: "t-shapes",
127
- type: "element",
128
- name: "shapes",
129
- elements: [
130
- {
131
- id: "e-circle-1",
132
- trackId: "t-shapes",
133
- type: "circle",
134
- s: 2,
135
- e: 8,
136
- props: {
137
- width: 200,
138
- height: 200,
139
- fill: "#ff6b6b",
140
- x: -400,
141
- y: 0,
142
- },
143
- animation: {
144
- name: "fade",
145
- animate: "enter",
146
- duration: 1,
147
- },
148
- },
149
- {
150
- id: "e-rect-1",
151
- trackId: "t-shapes",
152
- type: "rect",
153
- s: 5,
154
- e: 10,
155
- props: {
156
- width: 300,
157
- height: 150,
158
- fill: "#4ecdc4",
159
- x: 400,
160
- y: 0,
161
- },
162
- animation: {
163
- name: "rise",
164
- animate: "enter",
165
- duration: 1,
166
- },
167
- },
168
- ],
169
- },
170
- ],
171
- version: 1,
172
- },
173
- };
174
-
175
- /**
176
- * Render settings
177
- */
178
- const renderSettings = {
179
- outFile: `test-video-${Date.now()}.mp4`,
180
- outDir: join(__dirname, "../output"),
181
- quality: "medium",
182
- logProgress: true,
183
- };
184
-
185
- /**
186
- * Main test function
187
- */
188
- async function testVideoRender() {
189
- console.log("๐ŸŽฌ Starting video render test...\n");
190
- console.log("๐Ÿ“‹ Project Configuration:");
191
- console.log(` Resolution: ${exampleProject.input.properties.width}x${exampleProject.input.properties.height}`);
192
- console.log(` FPS: ${exampleProject.input.properties.fps}`);
193
- console.log(` Tracks: ${exampleProject.input.tracks.length}`);
194
- console.log(` Total Elements: ${exampleProject.input.tracks.reduce((sum, track) => sum + track.elements.length, 0)}\n`);
195
-
196
- try {
197
- console.log("๐Ÿ”„ Rendering video...");
198
- const startTime = Date.now();
199
-
200
- const outputPath = await renderTwickVideo(exampleProject, renderSettings);
201
-
202
- const endTime = Date.now();
203
- const duration = ((endTime - startTime) / 1000).toFixed(2);
204
-
205
- console.log("\nโœ… Video rendered successfully!");
206
- console.log(`๐Ÿ“ Output file: ${outputPath}`);
207
- console.log(`โฑ๏ธ Render time: ${duration}s`);
208
- console.log(`\n๐ŸŽ‰ Test completed successfully!`);
209
-
210
- return outputPath;
211
- } catch (error) {
212
- console.error("\nโŒ Render failed:");
213
- console.error(error);
214
- if (error instanceof Error) {
215
- console.error(` Error: ${error.message}`);
216
- if (error.stack) {
217
- console.error(` Stack: ${error.stack}`);
218
- }
219
- }
220
- process.exit(1);
221
- }
222
- }
223
-
224
- // Run the test
225
- testVideoRender().catch((error) => {
226
- console.error("Fatal error:", error);
227
- process.exit(1);
228
- });
package/test.js DELETED
@@ -1,163 +0,0 @@
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);