@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/dist/index.js CHANGED
@@ -69,7 +69,7 @@ var renderTwickVideo = async (variables, settings) => {
69
69
  quality: "medium",
70
70
  projectSettings: {
71
71
  exporter: {
72
- name: "@twick/core/wasm"
72
+ name: "@twick/core/wasm-effects"
73
73
  },
74
74
  size: {
75
75
  x: width,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/renderer.ts"],"sourcesContent":["// Export the renderer function\nexport { default as renderTwickVideo } from \"./renderer\";\n\n","import { renderVideo } from \"@twick/renderer\";\n\n// Simple in-process concurrency control for render jobs.\n// This helps avoid overloading a single server instance.\nlet activeRenders = 0;\nconst renderQueue: Array<() => void> = [];\n\nconst getMaxConcurrentRenders = () => {\n const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;\n const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;\n};\n\nasync function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {\n const maxConcurrent = getMaxConcurrentRenders();\n\n if (activeRenders >= maxConcurrent) {\n await new Promise<void>((resolve) => {\n renderQueue.push(resolve);\n });\n }\n\n activeRenders++;\n try {\n return await fn();\n } finally {\n activeRenders--;\n const next = renderQueue.shift();\n if (next) {\n next();\n }\n }\n}\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n const start = Date.now();\n try {\n const { input } = variables;\n const { properties } = input;\n\n // Basic safety limits (can be overridden via env)\n const maxWidth =\n parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? \"3840\", 10) || 3840;\n const maxHeight =\n parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? \"2160\", 10) || 2160;\n const maxFps =\n parseInt(process.env.TWICK_MAX_RENDER_FPS ?? \"60\", 10) || 60;\n\n const width = Math.min(properties.width, maxWidth);\n const height = Math.min(properties.height, maxHeight);\n const fps = Math.min(properties.fps ?? 30, maxFps);\n\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm\",\n },\n size: {\n x: width,\n y: height,\n },\n fps,\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const result = await withRenderSlot(async () => {\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables,\n settings: mergedSettings,\n });\n return file;\n });\n\n const elapsedMs = Date.now() - start;\n console.log(\n `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,\n result,\n );\n return result;\n } catch (error) {\n const elapsedMs = Date.now() - start;\n console.error(\n `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,\n error,\n );\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,sBAA4B;AAI5B,IAAI,gBAAgB;AACpB,IAAM,cAAiC,CAAC;AAExC,IAAM,0BAA0B,MAAM;AACpC,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,SAAS,UAAU,SAAS,SAAS,EAAE,IAAI;AACjD,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,eAAe,eAAkB,IAAkC;AACjE,QAAM,gBAAgB,wBAAwB;AAE9C,MAAI,iBAAiB,eAAe;AAClC,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,kBAAY,KAAK,OAAO;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA;AACA,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA;AACA,UAAM,OAAO,YAAY,MAAM;AAC/B,QAAI,MAAM;AACR,WAAK;AAAA,IACP;AAAA,EACF;AACF;AAoBA,IAAM,mBAAmB,OAAO,WAAgB,aAAkB;AAChE,QAAM,QAAQ,KAAK,IAAI;AACvB,MAAI;AACF,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,IAAI;AAGvB,UAAM,WACJ,SAAS,QAAQ,IAAI,0BAA0B,QAAQ,EAAE,KAAK;AAChE,UAAM,YACJ,SAAS,QAAQ,IAAI,2BAA2B,QAAQ,EAAE,KAAK;AACjE,UAAM,SACJ,SAAS,QAAQ,IAAI,wBAAwB,MAAM,EAAE,KAAK;AAE5D,UAAM,QAAQ,KAAK,IAAI,WAAW,OAAO,QAAQ;AACjD,UAAM,SAAS,KAAK,IAAI,WAAW,QAAQ,SAAS;AACpD,UAAM,MAAM,KAAK,IAAI,WAAW,OAAO,IAAI,MAAM;AAGjD,UAAM,iBAAiB;AAAA,MACrB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,SAAS,WAAW,aAAa,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,SAAS;AAAA,MACT,iBAAiB;AAAA,QACf,UAAU;AAAA,UACR,MAAM;AAAA,QACR;AAAA,QACA,MAAM;AAAA,UACJ,GAAG;AAAA,UACH,GAAG;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,MACA,GAAG;AAAA;AAAA,IACL;AAEA,UAAM,SAAS,MAAM,eAAe,YAAY;AAC9C,YAAM,OAAO,UAAM,6BAAY;AAAA,QAC7B,aAAa;AAAA,QACb;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AAED,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,sCAAsC,SAAS,cAAc,aAAa;AAAA,MAC1E;AAAA,IACF;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,qCAAqC,SAAS,cAAc,aAAa;AAAA,MACzE;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAEA,IAAO,mBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/renderer.ts"],"sourcesContent":["// Export the renderer function\nexport { default as renderTwickVideo } from \"./renderer\";\n\n","import { renderVideo } from \"@twick/renderer\";\n\n// Simple in-process concurrency control for render jobs.\n// This helps avoid overloading a single server instance.\nlet activeRenders = 0;\nconst renderQueue: Array<() => void> = [];\n\nconst getMaxConcurrentRenders = () => {\n const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;\n const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;\n};\n\nasync function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {\n const maxConcurrent = getMaxConcurrentRenders();\n\n if (activeRenders >= maxConcurrent) {\n await new Promise<void>((resolve) => {\n renderQueue.push(resolve);\n });\n }\n\n activeRenders++;\n try {\n return await fn();\n } finally {\n activeRenders--;\n const next = renderQueue.shift();\n if (next) {\n next();\n }\n }\n}\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n const start = Date.now();\n try {\n const { input } = variables;\n const { properties } = input;\n\n // Basic safety limits (can be overridden via env)\n const maxWidth =\n parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? \"3840\", 10) || 3840;\n const maxHeight =\n parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? \"2160\", 10) || 2160;\n const maxFps =\n parseInt(process.env.TWICK_MAX_RENDER_FPS ?? \"60\", 10) || 60;\n\n const width = Math.min(properties.width, maxWidth);\n const height = Math.min(properties.height, maxHeight);\n const fps = Math.min(properties.fps ?? 30, maxFps);\n\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm-effects\",\n },\n size: {\n x: width,\n y: height,\n },\n fps,\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const result = await withRenderSlot(async () => {\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables,\n settings: mergedSettings,\n });\n return file;\n });\n\n const elapsedMs = Date.now() - start;\n console.log(\n `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,\n result,\n );\n return result;\n } catch (error) {\n const elapsedMs = Date.now() - start;\n console.error(\n `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,\n error,\n );\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,sBAA4B;AAI5B,IAAI,gBAAgB;AACpB,IAAM,cAAiC,CAAC;AAExC,IAAM,0BAA0B,MAAM;AACpC,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,SAAS,UAAU,SAAS,SAAS,EAAE,IAAI;AACjD,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,eAAe,eAAkB,IAAkC;AACjE,QAAM,gBAAgB,wBAAwB;AAE9C,MAAI,iBAAiB,eAAe;AAClC,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,kBAAY,KAAK,OAAO;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA;AACA,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA;AACA,UAAM,OAAO,YAAY,MAAM;AAC/B,QAAI,MAAM;AACR,WAAK;AAAA,IACP;AAAA,EACF;AACF;AAoBA,IAAM,mBAAmB,OAAO,WAAgB,aAAkB;AAChE,QAAM,QAAQ,KAAK,IAAI;AACvB,MAAI;AACF,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,IAAI;AAGvB,UAAM,WACJ,SAAS,QAAQ,IAAI,0BAA0B,QAAQ,EAAE,KAAK;AAChE,UAAM,YACJ,SAAS,QAAQ,IAAI,2BAA2B,QAAQ,EAAE,KAAK;AACjE,UAAM,SACJ,SAAS,QAAQ,IAAI,wBAAwB,MAAM,EAAE,KAAK;AAE5D,UAAM,QAAQ,KAAK,IAAI,WAAW,OAAO,QAAQ;AACjD,UAAM,SAAS,KAAK,IAAI,WAAW,QAAQ,SAAS;AACpD,UAAM,MAAM,KAAK,IAAI,WAAW,OAAO,IAAI,MAAM;AAGjD,UAAM,iBAAiB;AAAA,MACrB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,SAAS,WAAW,aAAa,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,SAAS;AAAA,MACT,iBAAiB;AAAA,QACf,UAAU;AAAA,UACR,MAAM;AAAA,QACR;AAAA,QACA,MAAM;AAAA,UACJ,GAAG;AAAA,UACH,GAAG;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,MACA,GAAG;AAAA;AAAA,IACL;AAEA,UAAM,SAAS,MAAM,eAAe,YAAY;AAC9C,YAAM,OAAO,UAAM,6BAAY;AAAA,QAC7B,aAAa;AAAA,QACb;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AAED,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,sCAAsC,SAAS,cAAc,aAAa;AAAA,MAC1E;AAAA,IACF;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,qCAAqC,SAAS,cAAc,aAAa;AAAA,MACzE;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAEA,IAAO,mBAAQ;","names":[]}
package/dist/index.mjs CHANGED
@@ -43,7 +43,7 @@ var renderTwickVideo = async (variables, settings) => {
43
43
  quality: "medium",
44
44
  projectSettings: {
45
45
  exporter: {
46
- name: "@twick/core/wasm"
46
+ name: "@twick/core/wasm-effects"
47
47
  },
48
48
  size: {
49
49
  x: width,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/renderer.ts"],"sourcesContent":["import { renderVideo } from \"@twick/renderer\";\n\n// Simple in-process concurrency control for render jobs.\n// This helps avoid overloading a single server instance.\nlet activeRenders = 0;\nconst renderQueue: Array<() => void> = [];\n\nconst getMaxConcurrentRenders = () => {\n const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;\n const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;\n};\n\nasync function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {\n const maxConcurrent = getMaxConcurrentRenders();\n\n if (activeRenders >= maxConcurrent) {\n await new Promise<void>((resolve) => {\n renderQueue.push(resolve);\n });\n }\n\n activeRenders++;\n try {\n return await fn();\n } finally {\n activeRenders--;\n const next = renderQueue.shift();\n if (next) {\n next();\n }\n }\n}\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n const start = Date.now();\n try {\n const { input } = variables;\n const { properties } = input;\n\n // Basic safety limits (can be overridden via env)\n const maxWidth =\n parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? \"3840\", 10) || 3840;\n const maxHeight =\n parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? \"2160\", 10) || 2160;\n const maxFps =\n parseInt(process.env.TWICK_MAX_RENDER_FPS ?? \"60\", 10) || 60;\n\n const width = Math.min(properties.width, maxWidth);\n const height = Math.min(properties.height, maxHeight);\n const fps = Math.min(properties.fps ?? 30, maxFps);\n\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm\",\n },\n size: {\n x: width,\n y: height,\n },\n fps,\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const result = await withRenderSlot(async () => {\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables,\n settings: mergedSettings,\n });\n return file;\n });\n\n const elapsedMs = Date.now() - start;\n console.log(\n `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,\n result,\n );\n return result;\n } catch (error) {\n const elapsedMs = Date.now() - start;\n console.error(\n `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,\n error,\n );\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":";AAAA,SAAS,mBAAmB;AAI5B,IAAI,gBAAgB;AACpB,IAAM,cAAiC,CAAC;AAExC,IAAM,0BAA0B,MAAM;AACpC,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,SAAS,UAAU,SAAS,SAAS,EAAE,IAAI;AACjD,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,eAAe,eAAkB,IAAkC;AACjE,QAAM,gBAAgB,wBAAwB;AAE9C,MAAI,iBAAiB,eAAe;AAClC,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,kBAAY,KAAK,OAAO;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA;AACA,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA;AACA,UAAM,OAAO,YAAY,MAAM;AAC/B,QAAI,MAAM;AACR,WAAK;AAAA,IACP;AAAA,EACF;AACF;AAoBA,IAAM,mBAAmB,OAAO,WAAgB,aAAkB;AAChE,QAAM,QAAQ,KAAK,IAAI;AACvB,MAAI;AACF,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,IAAI;AAGvB,UAAM,WACJ,SAAS,QAAQ,IAAI,0BAA0B,QAAQ,EAAE,KAAK;AAChE,UAAM,YACJ,SAAS,QAAQ,IAAI,2BAA2B,QAAQ,EAAE,KAAK;AACjE,UAAM,SACJ,SAAS,QAAQ,IAAI,wBAAwB,MAAM,EAAE,KAAK;AAE5D,UAAM,QAAQ,KAAK,IAAI,WAAW,OAAO,QAAQ;AACjD,UAAM,SAAS,KAAK,IAAI,WAAW,QAAQ,SAAS;AACpD,UAAM,MAAM,KAAK,IAAI,WAAW,OAAO,IAAI,MAAM;AAGjD,UAAM,iBAAiB;AAAA,MACrB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,SAAS,WAAW,aAAa,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,SAAS;AAAA,MACT,iBAAiB;AAAA,QACf,UAAU;AAAA,UACR,MAAM;AAAA,QACR;AAAA,QACA,MAAM;AAAA,UACJ,GAAG;AAAA,UACH,GAAG;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,MACA,GAAG;AAAA;AAAA,IACL;AAEA,UAAM,SAAS,MAAM,eAAe,YAAY;AAC9C,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,aAAa;AAAA,QACb;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AAED,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,sCAAsC,SAAS,cAAc,aAAa;AAAA,MAC1E;AAAA,IACF;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,qCAAqC,SAAS,cAAc,aAAa;AAAA,MACzE;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAEA,IAAO,mBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/renderer.ts"],"sourcesContent":["import { renderVideo } from \"@twick/renderer\";\n\n// Simple in-process concurrency control for render jobs.\n// This helps avoid overloading a single server instance.\nlet activeRenders = 0;\nconst renderQueue: Array<() => void> = [];\n\nconst getMaxConcurrentRenders = () => {\n const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;\n const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;\n};\n\nasync function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {\n const maxConcurrent = getMaxConcurrentRenders();\n\n if (activeRenders >= maxConcurrent) {\n await new Promise<void>((resolve) => {\n renderQueue.push(resolve);\n });\n }\n\n activeRenders++;\n try {\n return await fn();\n } finally {\n activeRenders--;\n const next = renderQueue.shift();\n if (next) {\n next();\n }\n }\n}\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n const start = Date.now();\n try {\n const { input } = variables;\n const { properties } = input;\n\n // Basic safety limits (can be overridden via env)\n const maxWidth =\n parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? \"3840\", 10) || 3840;\n const maxHeight =\n parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? \"2160\", 10) || 2160;\n const maxFps =\n parseInt(process.env.TWICK_MAX_RENDER_FPS ?? \"60\", 10) || 60;\n\n const width = Math.min(properties.width, maxWidth);\n const height = Math.min(properties.height, maxHeight);\n const fps = Math.min(properties.fps ?? 30, maxFps);\n\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm-effects\",\n },\n size: {\n x: width,\n y: height,\n },\n fps,\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const result = await withRenderSlot(async () => {\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables,\n settings: mergedSettings,\n });\n return file;\n });\n\n const elapsedMs = Date.now() - start;\n console.log(\n `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,\n result,\n );\n return result;\n } catch (error) {\n const elapsedMs = Date.now() - start;\n console.error(\n `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,\n error,\n );\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":";AAAA,SAAS,mBAAmB;AAI5B,IAAI,gBAAgB;AACpB,IAAM,cAAiC,CAAC;AAExC,IAAM,0BAA0B,MAAM;AACpC,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,SAAS,UAAU,SAAS,SAAS,EAAE,IAAI;AACjD,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,eAAe,eAAkB,IAAkC;AACjE,QAAM,gBAAgB,wBAAwB;AAE9C,MAAI,iBAAiB,eAAe;AAClC,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,kBAAY,KAAK,OAAO;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA;AACA,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA;AACA,UAAM,OAAO,YAAY,MAAM;AAC/B,QAAI,MAAM;AACR,WAAK;AAAA,IACP;AAAA,EACF;AACF;AAoBA,IAAM,mBAAmB,OAAO,WAAgB,aAAkB;AAChE,QAAM,QAAQ,KAAK,IAAI;AACvB,MAAI;AACF,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,IAAI;AAGvB,UAAM,WACJ,SAAS,QAAQ,IAAI,0BAA0B,QAAQ,EAAE,KAAK;AAChE,UAAM,YACJ,SAAS,QAAQ,IAAI,2BAA2B,QAAQ,EAAE,KAAK;AACjE,UAAM,SACJ,SAAS,QAAQ,IAAI,wBAAwB,MAAM,EAAE,KAAK;AAE5D,UAAM,QAAQ,KAAK,IAAI,WAAW,OAAO,QAAQ;AACjD,UAAM,SAAS,KAAK,IAAI,WAAW,QAAQ,SAAS;AACpD,UAAM,MAAM,KAAK,IAAI,WAAW,OAAO,IAAI,MAAM;AAGjD,UAAM,iBAAiB;AAAA,MACrB,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,SAAS,WAAW,aAAa,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,SAAS;AAAA,MACT,iBAAiB;AAAA,QACf,UAAU;AAAA,UACR,MAAM;AAAA,QACR;AAAA,QACA,MAAM;AAAA,UACJ,GAAG;AAAA,UACH,GAAG;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,MACA,GAAG;AAAA;AAAA,IACL;AAEA,UAAM,SAAS,MAAM,eAAe,YAAY;AAC9C,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,aAAa;AAAA,QACb;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AAED,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,sCAAsC,SAAS,cAAc,aAAa;AAAA,MAC1E;AAAA,IACF;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,YAAQ;AAAA,MACN,qCAAqC,SAAS,cAAc,aAAa;AAAA,MACzE;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAEA,IAAO,mBAAQ;","names":[]}
package/dist/server.mjs CHANGED
@@ -49,7 +49,7 @@ var renderTwickVideo = async (variables, settings) => {
49
49
  quality: "medium",
50
50
  projectSettings: {
51
51
  exporter: {
52
- name: "@twick/core/wasm"
52
+ name: "@twick/core/wasm-effects"
53
53
  },
54
54
  size: {
55
55
  x: width,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/renderer.ts"],"sourcesContent":["import express from \"express\";\nimport cors from \"cors\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport renderTwickVideo from \"./renderer\";\n\nconst PORT = process.env.PORT || 5000;\nconst BASE_PATH = `http://localhost:${PORT}`;\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Rate limiting configuration (all configurable via env)\nconst RATE_LIMIT_WINDOW_MS =\n parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? \"900000\", 10) || 15 * 60 * 1000; // default 15 min (ms)\nconst RATE_LIMIT_MAX_REQUESTS =\n parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? \"100\", 10) || 100; // max requests per window\nconst RATE_LIMIT_CLEANUP_INTERVAL_MS =\n parseInt(process.env.RATE_LIMIT_CLEANUP_INTERVAL_MS ?? \"60000\", 10) || 60 * 1000; // default 1 min (ms)\n\n// In-memory store for rate limiting\ninterface RateLimitEntry {\n count: number;\n resetTime: number;\n}\n\nconst rateLimitStore = new Map<string, RateLimitEntry>();\n\n// Cleanup expired entries periodically\nsetInterval(() => {\n const now = Date.now();\n for (const [key, entry] of rateLimitStore.entries()) {\n if (now > entry.resetTime) {\n rateLimitStore.delete(key);\n }\n }\n}, RATE_LIMIT_CLEANUP_INTERVAL_MS);\n\n/**\n * Rate limiting middleware for API endpoints.\n * Tracks request counts per IP address and enforces rate limits\n * to prevent abuse of the render server.\n *\n * @param req - Express request object\n * @param res - Express response object\n * @param next - Express next function\n * \n * @example\n * ```js\n * app.use('/api', rateLimitMiddleware);\n * // Applies rate limiting to all /api routes\n * ```\n */\nconst rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {\n const clientIP = req.ip || req.connection.remoteAddress || 'unknown';\n const now = Date.now();\n \n // Get or create rate limit entry for this IP\n let entry = rateLimitStore.get(clientIP);\n \n if (!entry || now > entry.resetTime) {\n // New window or expired entry\n entry = {\n count: 1,\n resetTime: now + RATE_LIMIT_WINDOW_MS\n };\n rateLimitStore.set(clientIP, entry);\n } else {\n // Increment count in current window\n entry.count++;\n \n if (entry.count > RATE_LIMIT_MAX_REQUESTS) {\n // Rate limit exceeded\n const retryAfter = Math.ceil((entry.resetTime - now) / 1000);\n res.set('Retry-After', retryAfter.toString());\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', '0');\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n return res.status(429).json({\n success: false,\n error: 'Too many requests',\n message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,\n retryAfter\n });\n }\n }\n \n // Add rate limit headers\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n next();\n};\n\nconst nodeApp: import(\"express\").Express = express();\n\nnodeApp.use(cors());\nnodeApp.use(express.json());\n\n// Serve static files from output directory\nnodeApp.use(\"/output\", express.static(path.join(__dirname, \"../output\")));\n\n/**\n * POST endpoint for video rendering requests.\n * Accepts project variables and settings, renders the video,\n * and returns a download URL for the completed video.\n *\n * @param req - Express request object containing variables and settings\n * @param res - Express response object\n * \n * @example\n * ```js\n * POST /api/render-video\n * Body: { variables: {...}, settings: {...} }\n * Response: { success: true, downloadUrl: \"...\" }\n * ```\n */\nnodeApp.post(\"/api/render-video\", async (req, res) => {\n const { variables, settings } = req.body;\n\n try {\n const outputPath = await renderTwickVideo(variables, settings);\n res.json({\n success: true,\n downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`,\n });\n } catch (error) {\n console.error(\"Render error:\", error);\n res.status(500).json({\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n});\n\n/**\n * GET endpoint for downloading rendered videos.\n * Serves video files with rate limiting and security checks\n * to prevent path traversal attacks.\n *\n * @param req - Express request object with filename parameter\n * @param res - Express response object\n * \n * @example\n * ```js\n * GET /download/video-123.mp4\n * // Downloads the specified video file\n * ```\n */\nnodeApp.get(\"/download/:filename\", rateLimitMiddleware, (req, res) => {\n const outputDir = path.resolve(__dirname, \"../output\");\n const requestedPath = path.resolve(outputDir, req.params.filename);\n if (!requestedPath.startsWith(outputDir + path.sep)) {\n // Attempted path traversal or access outside output directory\n res.status(403).json({\n success: false,\n error: \"Forbidden\",\n });\n return;\n }\n res.download(requestedPath, (err) => {\n if (err) {\n res.status(404).json({\n success: false,\n error: \"File not found\",\n });\n }\n });\n});\n\n/**\n * Health check endpoint for monitoring server status.\n * Returns server status and current timestamp for health monitoring.\n *\n * @param req - Express request object\n * @param res - Express response object\n * \n * @example\n * ```js\n * GET /health\n * Response: { status: \"ok\", timestamp: \"2024-01-01T00:00:00.000Z\" }\n * ```\n */\nnodeApp.get(\"/health\", (req, res) => {\n res.json({\n status: \"ok\",\n timestamp: new Date().toISOString(),\n });\n});\n\n// Export the app for programmatic usage\nexport default nodeApp;\n\n// Start the server\nnodeApp.listen(PORT, () => {\n console.log(`Render server running on port ${PORT}`);\n console.log(`Health check: ${BASE_PATH}/health`);\n console.log(`API endpoint: ${BASE_PATH}/api/render-video`);\n console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 60000} minutes`);\n});\n","import { renderVideo } from \"@twick/renderer\";\n\n// Simple in-process concurrency control for render jobs.\n// This helps avoid overloading a single server instance.\nlet activeRenders = 0;\nconst renderQueue: Array<() => void> = [];\n\nconst getMaxConcurrentRenders = () => {\n const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;\n const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;\n};\n\nasync function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {\n const maxConcurrent = getMaxConcurrentRenders();\n\n if (activeRenders >= maxConcurrent) {\n await new Promise<void>((resolve) => {\n renderQueue.push(resolve);\n });\n }\n\n activeRenders++;\n try {\n return await fn();\n } finally {\n activeRenders--;\n const next = renderQueue.shift();\n if (next) {\n next();\n }\n }\n}\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n const start = Date.now();\n try {\n const { input } = variables;\n const { properties } = input;\n\n // Basic safety limits (can be overridden via env)\n const maxWidth =\n parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? \"3840\", 10) || 3840;\n const maxHeight =\n parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? \"2160\", 10) || 2160;\n const maxFps =\n parseInt(process.env.TWICK_MAX_RENDER_FPS ?? \"60\", 10) || 60;\n\n const width = Math.min(properties.width, maxWidth);\n const height = Math.min(properties.height, maxHeight);\n const fps = Math.min(properties.fps ?? 30, maxFps);\n\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm\",\n },\n size: {\n x: width,\n y: height,\n },\n fps,\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const result = await withRenderSlot(async () => {\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables,\n settings: mergedSettings,\n });\n return file;\n });\n\n const elapsedMs = Date.now() - start;\n console.log(\n `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,\n result,\n );\n return result;\n } catch (error) {\n const elapsedMs = Date.now() - start;\n console.error(\n `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,\n error,\n );\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts","../src/renderer.ts"],"sourcesContent":["import express from \"express\";\nimport cors from \"cors\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport renderTwickVideo from \"./renderer\";\n\nconst PORT = process.env.PORT || 5000;\nconst BASE_PATH = `http://localhost:${PORT}`;\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Rate limiting configuration (all configurable via env)\nconst RATE_LIMIT_WINDOW_MS =\n parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? \"900000\", 10) || 15 * 60 * 1000; // default 15 min (ms)\nconst RATE_LIMIT_MAX_REQUESTS =\n parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? \"100\", 10) || 100; // max requests per window\nconst RATE_LIMIT_CLEANUP_INTERVAL_MS =\n parseInt(process.env.RATE_LIMIT_CLEANUP_INTERVAL_MS ?? \"60000\", 10) || 60 * 1000; // default 1 min (ms)\n\n// In-memory store for rate limiting\ninterface RateLimitEntry {\n count: number;\n resetTime: number;\n}\n\nconst rateLimitStore = new Map<string, RateLimitEntry>();\n\n// Cleanup expired entries periodically\nsetInterval(() => {\n const now = Date.now();\n for (const [key, entry] of rateLimitStore.entries()) {\n if (now > entry.resetTime) {\n rateLimitStore.delete(key);\n }\n }\n}, RATE_LIMIT_CLEANUP_INTERVAL_MS);\n\n/**\n * Rate limiting middleware for API endpoints.\n * Tracks request counts per IP address and enforces rate limits\n * to prevent abuse of the render server.\n *\n * @param req - Express request object\n * @param res - Express response object\n * @param next - Express next function\n * \n * @example\n * ```js\n * app.use('/api', rateLimitMiddleware);\n * // Applies rate limiting to all /api routes\n * ```\n */\nconst rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {\n const clientIP = req.ip || req.connection.remoteAddress || 'unknown';\n const now = Date.now();\n \n // Get or create rate limit entry for this IP\n let entry = rateLimitStore.get(clientIP);\n \n if (!entry || now > entry.resetTime) {\n // New window or expired entry\n entry = {\n count: 1,\n resetTime: now + RATE_LIMIT_WINDOW_MS\n };\n rateLimitStore.set(clientIP, entry);\n } else {\n // Increment count in current window\n entry.count++;\n \n if (entry.count > RATE_LIMIT_MAX_REQUESTS) {\n // Rate limit exceeded\n const retryAfter = Math.ceil((entry.resetTime - now) / 1000);\n res.set('Retry-After', retryAfter.toString());\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', '0');\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n return res.status(429).json({\n success: false,\n error: 'Too many requests',\n message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,\n retryAfter\n });\n }\n }\n \n // Add rate limit headers\n res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());\n res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());\n res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());\n \n next();\n};\n\nconst nodeApp: import(\"express\").Express = express();\n\nnodeApp.use(cors());\nnodeApp.use(express.json());\n\n// Serve static files from output directory\nnodeApp.use(\"/output\", express.static(path.join(__dirname, \"../output\")));\n\n/**\n * POST endpoint for video rendering requests.\n * Accepts project variables and settings, renders the video,\n * and returns a download URL for the completed video.\n *\n * @param req - Express request object containing variables and settings\n * @param res - Express response object\n * \n * @example\n * ```js\n * POST /api/render-video\n * Body: { variables: {...}, settings: {...} }\n * Response: { success: true, downloadUrl: \"...\" }\n * ```\n */\nnodeApp.post(\"/api/render-video\", async (req, res) => {\n const { variables, settings } = req.body;\n\n try {\n const outputPath = await renderTwickVideo(variables, settings);\n res.json({\n success: true,\n downloadUrl: `${BASE_PATH}/download/${path.basename(outputPath)}`,\n });\n } catch (error) {\n console.error(\"Render error:\", error);\n res.status(500).json({\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n});\n\n/**\n * GET endpoint for downloading rendered videos.\n * Serves video files with rate limiting and security checks\n * to prevent path traversal attacks.\n *\n * @param req - Express request object with filename parameter\n * @param res - Express response object\n * \n * @example\n * ```js\n * GET /download/video-123.mp4\n * // Downloads the specified video file\n * ```\n */\nnodeApp.get(\"/download/:filename\", rateLimitMiddleware, (req, res) => {\n const outputDir = path.resolve(__dirname, \"../output\");\n const requestedPath = path.resolve(outputDir, req.params.filename);\n if (!requestedPath.startsWith(outputDir + path.sep)) {\n // Attempted path traversal or access outside output directory\n res.status(403).json({\n success: false,\n error: \"Forbidden\",\n });\n return;\n }\n res.download(requestedPath, (err) => {\n if (err) {\n res.status(404).json({\n success: false,\n error: \"File not found\",\n });\n }\n });\n});\n\n/**\n * Health check endpoint for monitoring server status.\n * Returns server status and current timestamp for health monitoring.\n *\n * @param req - Express request object\n * @param res - Express response object\n * \n * @example\n * ```js\n * GET /health\n * Response: { status: \"ok\", timestamp: \"2024-01-01T00:00:00.000Z\" }\n * ```\n */\nnodeApp.get(\"/health\", (req, res) => {\n res.json({\n status: \"ok\",\n timestamp: new Date().toISOString(),\n });\n});\n\n// Export the app for programmatic usage\nexport default nodeApp;\n\n// Start the server\nnodeApp.listen(PORT, () => {\n console.log(`Render server running on port ${PORT}`);\n console.log(`Health check: ${BASE_PATH}/health`);\n console.log(`API endpoint: ${BASE_PATH}/api/render-video`);\n console.log(`Download endpoint rate limited: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 60000} minutes`);\n});\n","import { renderVideo } from \"@twick/renderer\";\n\n// Simple in-process concurrency control for render jobs.\n// This helps avoid overloading a single server instance.\nlet activeRenders = 0;\nconst renderQueue: Array<() => void> = [];\n\nconst getMaxConcurrentRenders = () => {\n const fromEnv = process.env.TWICK_MAX_CONCURRENT_RENDERS;\n const parsed = fromEnv ? parseInt(fromEnv, 10) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 2;\n};\n\nasync function withRenderSlot<T>(fn: () => Promise<T>): Promise<T> {\n const maxConcurrent = getMaxConcurrentRenders();\n\n if (activeRenders >= maxConcurrent) {\n await new Promise<void>((resolve) => {\n renderQueue.push(resolve);\n });\n }\n\n activeRenders++;\n try {\n return await fn();\n } finally {\n activeRenders--;\n const next = renderQueue.shift();\n if (next) {\n next();\n }\n }\n}\n\n/**\n * Renders a Twick video with the provided variables and settings.\n * Processes project variables, merges settings with defaults, and\n * generates a video file using the Twick renderer.\n *\n * @param variables - Project variables containing input configuration\n * @param settings - Optional render settings to override defaults\n * @returns Promise resolving to the path of the rendered video file\n * \n * @example\n * ```js\n * const videoPath = await renderTwickVideo(\n * { input: { properties: { width: 1920, height: 1080 } } },\n * { quality: \"high\", outFile: \"my-video.mp4\" }\n * );\n * // videoPath = \"./output/my-video.mp4\"\n * ```\n */\nconst renderTwickVideo = async (variables: any, settings: any) => {\n const start = Date.now();\n try {\n const { input } = variables;\n const { properties } = input;\n\n // Basic safety limits (can be overridden via env)\n const maxWidth =\n parseInt(process.env.TWICK_MAX_RENDER_WIDTH ?? \"3840\", 10) || 3840;\n const maxHeight =\n parseInt(process.env.TWICK_MAX_RENDER_HEIGHT ?? \"2160\", 10) || 2160;\n const maxFps =\n parseInt(process.env.TWICK_MAX_RENDER_FPS ?? \"60\", 10) || 60;\n\n const width = Math.min(properties.width, maxWidth);\n const height = Math.min(properties.height, maxHeight);\n const fps = Math.min(properties.fps ?? 30, maxFps);\n\n // Merge user settings with defaults\n const mergedSettings = {\n logProgress: true,\n outDir: \"./output\",\n outFile: properties.reqesutId ?? `video-${Date.now()}` + \".mp4\",\n quality: \"medium\",\n projectSettings: {\n exporter: {\n name: \"@twick/core/wasm-effects\",\n },\n size: {\n x: width,\n y: height,\n },\n fps,\n },\n ...settings, // Allow user settings to override defaults\n };\n\n const result = await withRenderSlot(async () => {\n const file = await renderVideo({\n projectFile: \"@twick/visualizer/dist/project.js\",\n variables,\n settings: mergedSettings,\n });\n return file;\n });\n\n const elapsedMs = Date.now() - start;\n console.log(\n `[RenderServer] Render completed in ${elapsedMs}ms (active=${activeRenders}) ->`,\n result,\n );\n return result;\n } catch (error) {\n const elapsedMs = Date.now() - start;\n console.error(\n `[RenderServer] Render error after ${elapsedMs}ms (active=${activeRenders}):`,\n error,\n );\n throw error;\n }\n};\n\nexport default renderTwickVideo;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twick/render-server",
3
- "version": "0.15.20",
3
+ "version": "0.15.22",
4
4
  "license": "https://github.com/ncounterspecialist/twick/blob/main/LICENSE.md",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -26,12 +26,12 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@twick/2d": "^0.15.20",
30
- "@twick/core": "^0.15.20",
31
- "@twick/ffmpeg": "^0.15.20",
32
- "@twick/renderer": "^0.15.20",
33
- "@twick/ui": "^0.15.20",
34
- "@twick/visualizer": "0.15.20",
29
+ "@twick/2d": "^0.15.22",
30
+ "@twick/core": "^0.15.22",
31
+ "@twick/ffmpeg": "^0.15.22",
32
+ "@twick/renderer": "^0.15.22",
33
+ "@twick/ui": "^0.15.22",
34
+ "@twick/visualizer": "0.15.22",
35
35
  "cors": "^2.8.5",
36
36
  "express": "^4.18.2",
37
37
  "express-rate-limit": "^8.0.1",
@@ -49,5 +49,8 @@
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=20.0.0"
52
- }
52
+ },
53
+ "files": [
54
+ "dist"
55
+ ]
53
56
  }
package/.dockerignore DELETED
@@ -1,13 +0,0 @@
1
- node_modules
2
- npm-debug.log
3
- Dockerfile
4
- .dockerignore
5
- *.md
6
- dist
7
- .git
8
- .gitignore
9
- .env*
10
- *.log
11
- coverage
12
- .nyc_output
13
- test.js
package/Dockerfile DELETED
@@ -1,39 +0,0 @@
1
- FROM ghcr.io/puppeteer/puppeteer:latest AS builder
2
- WORKDIR /app
3
-
4
- COPY package*.json ./
5
- RUN npm install
6
-
7
- # Copy only source and config (avoid overwriting node_modules from context)
8
- COPY src ./src
9
- COPY tsup.config.ts tsconfig.json ./
10
- RUN npm run build
11
- # Ensure server entry point exists (tsup outputs ESM as .mjs)
12
- RUN test -f /app/dist/server.mjs || test -f /app/dist/server.js || (echo "Missing dist/server.mjs or server.js. Contents of /app/dist:" && ls -la /app/dist 2>/dev/null || true && exit 1)
13
-
14
- FROM ghcr.io/puppeteer/puppeteer:latest
15
-
16
- WORKDIR /app
17
-
18
- # Switch to root to install system packages (needed for ffmpeg)
19
- USER root
20
-
21
- RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
22
-
23
- # Drop back to the non-root Puppeteer user for runtime
24
- USER pptruser
25
-
26
- # Tell @twick/ffmpeg to use the system binaries
27
- ENV FFMPEG_PATH=/usr/bin/ffmpeg
28
- ENV FFPROBE_PATH=/usr/bin/ffprobe
29
-
30
- COPY package*.json ./
31
- RUN npm install --only=production
32
-
33
- COPY --from=builder /app/dist ./dist
34
-
35
- EXPOSE 5000
36
-
37
- # Start the built-in render server on port 5000
38
- CMD ["node", "dist/server.mjs"]
39
-
@@ -1,22 +0,0 @@
1
- # @twick/render-server — configurable via .env
2
- # Copy .env.example to .env and adjust values.
3
-
4
- services:
5
- render-server:
6
- build:
7
- context: .
8
- dockerfile: Dockerfile
9
- image: twick-render-server:latest
10
- container_name: twick-render-server
11
- restart: unless-stopped
12
-
13
- environment:
14
- PORT: "${PORT:-5000}"
15
- NODE_ENV: "${NODE_ENV:-production}"
16
- # Rate limiting: window in ms, max requests per window, cleanup interval in ms
17
- RATE_LIMIT_WINDOW_MS: "${RATE_LIMIT_WINDOW_MS:-900000}"
18
- RATE_LIMIT_MAX_REQUESTS: "${RATE_LIMIT_MAX_REQUESTS:-100}"
19
- RATE_LIMIT_CLEANUP_INTERVAL_MS: "${RATE_LIMIT_CLEANUP_INTERVAL_MS:-60000}"
20
-
21
- ports:
22
- - "${PORT:-5000}:${PORT:-5000}"
package/package.json.bak DELETED
@@ -1,53 +0,0 @@
1
- {
2
- "name": "@twick/render-server",
3
- "version": "0.15.20",
4
- "license": "https://github.com/ncounterspecialist/twick/blob/main/LICENSE.md",
5
- "main": "./dist/index.cjs",
6
- "module": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js",
12
- "require": "./dist/index.cjs"
13
- }
14
- },
15
- "bin": {
16
- "twick-render-server": "./dist/cli.js"
17
- },
18
- "scripts": {
19
- "build": "tsup",
20
- "dev": "tsx watch src/server.ts",
21
- "start": "node dist/server.js",
22
- "test:render": "tsx src/test-render.ts",
23
- "clean": "rimraf dist"
24
- },
25
- "publishConfig": {
26
- "access": "public"
27
- },
28
- "dependencies": {
29
- "@twick/2d": "^0.15.20",
30
- "@twick/core": "^0.15.20",
31
- "@twick/ffmpeg": "^0.15.20",
32
- "@twick/renderer": "^0.15.20",
33
- "@twick/ui": "^0.15.20",
34
- "@twick/visualizer": "0.15.20",
35
- "cors": "^2.8.5",
36
- "express": "^4.18.2",
37
- "express-rate-limit": "^8.0.1",
38
- "node-fetch": "^3.3.2",
39
- "path": "^0.12.7"
40
- },
41
- "devDependencies": {
42
- "@types/cors": "^2.8.17",
43
- "@types/express": "^4.17.21",
44
- "@types/node": "^20.10.0",
45
- "rimraf": "^5.0.5",
46
- "tsup": "^8.0.0",
47
- "tsx": "^4.7.0",
48
- "typescript": "5.4.2"
49
- },
50
- "engines": {
51
- "node": ">=20.0.0"
52
- }
53
- }
package/src/cli.ts DELETED
@@ -1,326 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "fs";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- const SERVER_TEMPLATE = `import express from "express";
11
- import cors from "cors";
12
- import path from "path";
13
- import { fileURLToPath } from "url";
14
- import { renderTwickVideo } from "@twick/render-server";
15
-
16
- const PORT = process.env.PORT || 3001;
17
- const BASE_PATH = \`http://localhost:\${PORT}\`;
18
-
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = path.dirname(__filename);
21
-
22
- // Rate limiting configuration
23
- const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
24
- const RATE_LIMIT_MAX_REQUESTS = 100; // Maximum requests per window
25
- const RATE_LIMIT_CLEANUP_INTERVAL_MS = 60 * 1000; // Cleanup every minute
26
-
27
- // In-memory store for rate limiting
28
- interface RateLimitEntry {
29
- count: number;
30
- resetTime: number;
31
- }
32
-
33
- const rateLimitStore = new Map<string, RateLimitEntry>();
34
-
35
- // Cleanup expired entries periodically
36
- setInterval(() => {
37
- const now = Date.now();
38
- for (const [key, entry] of rateLimitStore.entries()) {
39
- if (now > entry.resetTime) {
40
- rateLimitStore.delete(key);
41
- }
42
- }
43
- }, RATE_LIMIT_CLEANUP_INTERVAL_MS);
44
-
45
- /**
46
- * Rate limiting middleware for API endpoints.
47
- * Tracks request counts per IP address and enforces rate limits
48
- * to prevent abuse of the render server.
49
- *
50
- * @param req - Express request object
51
- * @param res - Express response object
52
- * @param next - Express next function
53
- *
54
- * @example
55
- * \`\`\`js
56
- * app.use('/api', rateLimitMiddleware);
57
- * // Applies rate limiting to all /api routes
58
- * \`\`\`
59
- */
60
- const rateLimitMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
61
- const clientIP = req.ip || req.connection.remoteAddress || 'unknown';
62
- const now = Date.now();
63
-
64
- // Get or create rate limit entry for this IP
65
- let entry = rateLimitStore.get(clientIP);
66
-
67
- if (!entry || now > entry.resetTime) {
68
- // New window or expired entry
69
- entry = {
70
- count: 1,
71
- resetTime: now + RATE_LIMIT_WINDOW_MS
72
- };
73
- rateLimitStore.set(clientIP, entry);
74
- } else {
75
- // Increment count in current window
76
- entry.count++;
77
-
78
- if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
79
- // Rate limit exceeded
80
- const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
81
- res.set('Retry-After', retryAfter.toString());
82
- res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
83
- res.set('X-RateLimit-Remaining', '0');
84
- res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
85
-
86
- return res.status(429).json({
87
- success: false,
88
- error: 'Too many requests',
89
- message: \`Rate limit exceeded. Try again in \${retryAfter} seconds.\`,
90
- retryAfter
91
- });
92
- }
93
- }
94
-
95
- // Add rate limit headers
96
- res.set('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString());
97
- res.set('X-RateLimit-Remaining', (RATE_LIMIT_MAX_REQUESTS - entry.count).toString());
98
- res.set('X-RateLimit-Reset', new Date(entry.resetTime).toISOString());
99
-
100
- next();
101
- };
102
-
103
- const app = express();
104
-
105
- app.use(cors());
106
- app.use(express.json());
107
-
108
- // Serve static files from output directory
109
- app.use("/output", express.static(path.join(__dirname, "output")));
110
-
111
- /**
112
- * POST endpoint for video rendering requests.
113
- * Accepts project variables and settings, renders the video,
114
- * and returns a download URL for the completed video.
115
- *
116
- * @param req - Express request object containing variables and settings
117
- * @param res - Express response object
118
- *
119
- * @example
120
- * \`\`\`js
121
- * POST /api/render-video
122
- * Body: { variables: {...}, settings: {...} }
123
- * Response: { success: true, downloadUrl: "..." }
124
- * \`\`\`
125
- */
126
- app.post("/api/render-video", async (req, res) => {
127
- const { variables, settings } = req.body;
128
-
129
- try {
130
- const outputPath = await renderTwickVideo(variables, settings);
131
- res.json({
132
- success: true,
133
- downloadUrl: \`\${BASE_PATH}/download/\${path.basename(outputPath)}\`,
134
- });
135
- } catch (error) {
136
- console.error("Render error:", error);
137
- res.status(500).json({
138
- success: false,
139
- error: error instanceof Error ? error.message : "Unknown error",
140
- });
141
- }
142
- });
143
-
144
- /**
145
- * GET endpoint for downloading rendered videos.
146
- * Serves video files with rate limiting and security checks
147
- * to prevent path traversal attacks.
148
- *
149
- * @param req - Express request object with filename parameter
150
- * @param res - Express response object
151
- *
152
- * @example
153
- * \`\`\`js
154
- * GET /download/video-123.mp4
155
- * // Downloads the specified video file
156
- * \`\`\`
157
- */
158
- app.get("/download/:filename", rateLimitMiddleware, (req, res) => {
159
- const outputDir = path.resolve(__dirname, "output");
160
- const requestedPath = path.resolve(outputDir, req.params.filename);
161
- if (!requestedPath.startsWith(outputDir + path.sep)) {
162
- // Attempted path traversal or access outside output directory
163
- res.status(403).json({
164
- success: false,
165
- error: "Forbidden",
166
- });
167
- return;
168
- }
169
- res.download(requestedPath, (err) => {
170
- if (err) {
171
- res.status(404).json({
172
- success: false,
173
- error: "File not found",
174
- });
175
- }
176
- });
177
- });
178
-
179
- /**
180
- * Health check endpoint for monitoring server status.
181
- * Returns server status and current timestamp for health monitoring.
182
- *
183
- * @param req - Express request object
184
- * @param res - Express response object
185
- *
186
- * @example
187
- * \`\`\`js
188
- * GET /health
189
- * Response: { status: "ok", timestamp: "2024-01-01T00:00:00.000Z" }
190
- * \`\`\`
191
- */
192
- app.get("/health", (req, res) => {
193
- res.json({
194
- status: "ok",
195
- timestamp: new Date().toISOString(),
196
- });
197
- });
198
-
199
- // Start the server
200
- app.listen(PORT, () => {
201
- console.log(\`Render server running on port \${PORT}\`);
202
- console.log(\`Health check: \${BASE_PATH}/health\`);
203
- console.log(\`API endpoint: \${BASE_PATH}/api/render-video\`);
204
- console.log(\`Download endpoint rate limited: \${RATE_LIMIT_MAX_REQUESTS} requests per \${RATE_LIMIT_WINDOW_MS / 60000} minutes\`);
205
- });
206
- `;
207
-
208
- const PACKAGE_JSON_TEMPLATE = `{
209
- "name": "twick-render-server",
210
- "version": "1.0.0",
211
- "type": "module",
212
- "scripts": {
213
- "dev": "tsx watch server.ts",
214
- "build": "tsc",
215
- "start": "node dist/server.js"
216
- },
217
- "dependencies": {
218
- "@twick/render-server": "^0.14.11",
219
- "cors": "^2.8.5",
220
- "express": "^4.18.2"
221
- },
222
- "devDependencies": {
223
- "@types/cors": "^2.8.17",
224
- "@types/express": "^4.17.21",
225
- "@types/node": "^20.10.0",
226
- "tsx": "^4.7.0",
227
- "typescript": "5.4.2"
228
- }
229
- }
230
- `;
231
-
232
- const TSCONFIG_TEMPLATE = `{
233
- "compilerOptions": {
234
- "target": "ES2022",
235
- "module": "ESNext",
236
- "moduleResolution": "bundler",
237
- "allowSyntheticDefaultImports": true,
238
- "esModuleInterop": true,
239
- "allowJs": true,
240
- "strict": true,
241
- "skipLibCheck": true,
242
- "forceConsistentCasingInFileNames": true,
243
- "outDir": "./dist",
244
- "rootDir": ".",
245
- "declaration": true,
246
- "declarationMap": true,
247
- "sourceMap": true,
248
- "resolveJsonModule": true,
249
- "noEmit": false
250
- },
251
- "include": ["server.ts"],
252
- "exclude": ["node_modules", "dist"]
253
- }
254
- `;
255
-
256
- const GITIGNORE_TEMPLATE = `node_modules/
257
- dist/
258
- output/
259
- .env
260
- *.log
261
- `;
262
-
263
- function scaffoldServer() {
264
- const currentDir = process.cwd();
265
- const serverDir = path.join(currentDir, "twick-render-server");
266
-
267
- // Check if directory already exists
268
- if (fs.existsSync(serverDir)) {
269
- console.error(`Error: Directory "${serverDir}" already exists.`);
270
- console.error("Please remove it or choose a different location.");
271
- process.exit(1);
272
- }
273
-
274
- // Create server directory
275
- fs.mkdirSync(serverDir, { recursive: true });
276
- fs.mkdirSync(path.join(serverDir, "output"), { recursive: true });
277
-
278
- // Write server.ts
279
- fs.writeFileSync(
280
- path.join(serverDir, "server.ts"),
281
- SERVER_TEMPLATE,
282
- "utf-8"
283
- );
284
-
285
- // Write package.json
286
- fs.writeFileSync(
287
- path.join(serverDir, "package.json"),
288
- PACKAGE_JSON_TEMPLATE,
289
- "utf-8"
290
- );
291
-
292
- // Write tsconfig.json
293
- fs.writeFileSync(
294
- path.join(serverDir, "tsconfig.json"),
295
- TSCONFIG_TEMPLATE,
296
- "utf-8"
297
- );
298
-
299
- // Write .gitignore
300
- fs.writeFileSync(
301
- path.join(serverDir, ".gitignore"),
302
- GITIGNORE_TEMPLATE,
303
- "utf-8"
304
- );
305
-
306
- console.log(`āœ… Successfully scaffolded Twick render server!`);
307
- console.log(`\nšŸ“ Server created at: ${serverDir}`);
308
- console.log(`\nšŸ“ Next steps:`);
309
- console.log(` 1. cd ${path.basename(serverDir)}`);
310
- console.log(` 2. npm install`);
311
- console.log(` 3. npm run dev (for development)`);
312
- console.log(` 4. npm run build && npm start (for production)`);
313
- }
314
-
315
- // Parse command line arguments
316
- const args = process.argv.slice(2);
317
- const command = args[0];
318
-
319
- if (command === "init") {
320
- scaffoldServer();
321
- } else {
322
- console.log("Usage: npx twick-render-server init");
323
- console.log("\nThis command scaffolds a new Twick render server in the current directory.");
324
- process.exit(1);
325
- }
326
-
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- // Export the renderer function
2
- export { default as renderTwickVideo } from "./renderer";
3
-