apexify.js 5.1.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/README.md +248 -1105
  3. package/dist/cjs/Canvas/ApexPainter.d.ts +182 -204
  4. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  5. package/dist/cjs/Canvas/ApexPainter.js +482 -1286
  6. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  7. package/dist/cjs/Canvas/extended/CanvasCreator.d.ts +33 -0
  8. package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -0
  9. package/dist/cjs/Canvas/extended/CanvasCreator.js +223 -0
  10. package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -0
  11. package/dist/cjs/Canvas/extended/ChartCreator.d.ts +26 -0
  12. package/dist/cjs/Canvas/extended/ChartCreator.d.ts.map +1 -0
  13. package/dist/cjs/Canvas/extended/ChartCreator.js +50 -0
  14. package/dist/cjs/Canvas/extended/ChartCreator.js.map +1 -0
  15. package/dist/cjs/Canvas/extended/GIFCreator.d.ts +43 -0
  16. package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -0
  17. package/dist/cjs/Canvas/extended/GIFCreator.js +157 -0
  18. package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -0
  19. package/dist/cjs/Canvas/extended/ImageCreator.d.ts +83 -0
  20. package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -0
  21. package/dist/cjs/Canvas/extended/ImageCreator.js +479 -0
  22. package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -0
  23. package/dist/cjs/Canvas/extended/TextCreator.d.ts +35 -0
  24. package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -0
  25. package/dist/cjs/Canvas/extended/TextCreator.js +98 -0
  26. package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -0
  27. package/dist/cjs/Canvas/extended/VideoCreator.d.ts +370 -0
  28. package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -0
  29. package/dist/cjs/Canvas/extended/VideoCreator.js +478 -0
  30. package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -0
  31. package/dist/cjs/Canvas/utils/Background/bg.d.ts +1 -1
  32. package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
  33. package/dist/cjs/Canvas/utils/Background/bg.js +43 -7
  34. package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
  35. package/dist/cjs/Canvas/utils/Charts/barchart.d.ts +230 -0
  36. package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -0
  37. package/dist/cjs/Canvas/utils/Charts/barchart.js +1891 -0
  38. package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -0
  39. package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
  40. package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
  41. package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +368 -0
  42. package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -0
  43. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
  44. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
  45. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
  46. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
  47. package/dist/cjs/Canvas/utils/Charts/index.d.ts +45 -0
  48. package/dist/cjs/Canvas/utils/Charts/index.d.ts.map +1 -0
  49. package/dist/cjs/Canvas/utils/Charts/index.js +17 -0
  50. package/dist/cjs/Canvas/utils/Charts/index.js.map +1 -0
  51. package/dist/cjs/Canvas/utils/Charts/linechart.d.ts +216 -0
  52. package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -0
  53. package/dist/cjs/Canvas/utils/Charts/linechart.js +1761 -0
  54. package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -0
  55. package/dist/cjs/Canvas/utils/Charts/piechart.d.ts +167 -0
  56. package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -0
  57. package/dist/cjs/Canvas/utils/Charts/piechart.js +794 -0
  58. package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -0
  59. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
  60. package/dist/cjs/Canvas/utils/General/batchOperations.js +3 -4
  61. package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
  62. package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
  63. package/dist/cjs/Canvas/utils/General/general functions.js +62 -33
  64. package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
  65. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
  66. package/dist/cjs/Canvas/utils/General/imageStitching.js +3 -6
  67. package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
  68. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
  69. package/dist/cjs/Canvas/utils/Image/imageMasking.js +5 -12
  70. package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
  71. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +4 -4
  72. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  73. package/dist/cjs/Canvas/utils/Image/imageProperties.js +44 -9
  74. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  75. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
  76. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  77. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
  78. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  79. package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts +1 -1
  80. package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
  81. package/dist/cjs/Canvas/utils/Texts/textProperties.js +48 -5
  82. package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
  83. package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts +489 -0
  84. package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
  85. package/dist/cjs/Canvas/utils/Video/videoHelpers.js +1835 -0
  86. package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -0
  87. package/dist/cjs/Canvas/utils/errorUtils.d.ts +15 -0
  88. package/dist/cjs/Canvas/utils/errorUtils.d.ts.map +1 -0
  89. package/dist/cjs/Canvas/utils/errorUtils.js +26 -0
  90. package/dist/cjs/Canvas/utils/errorUtils.js.map +1 -0
  91. package/dist/cjs/Canvas/utils/types.d.ts +17 -178
  92. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  93. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  94. package/dist/cjs/Canvas/utils/utils.d.ts +4 -3
  95. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  96. package/dist/cjs/Canvas/utils/utils.js +40 -6
  97. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  98. package/dist/cjs/index.d.ts +1 -8
  99. package/dist/cjs/index.d.ts.map +1 -1
  100. package/dist/cjs/index.js +14 -45
  101. package/dist/cjs/index.js.map +1 -1
  102. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  103. package/dist/esm/Canvas/ApexPainter.d.ts +182 -204
  104. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  105. package/dist/esm/Canvas/ApexPainter.js +482 -1286
  106. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  107. package/dist/esm/Canvas/extended/CanvasCreator.d.ts +33 -0
  108. package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -0
  109. package/dist/esm/Canvas/extended/CanvasCreator.js +223 -0
  110. package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -0
  111. package/dist/esm/Canvas/extended/ChartCreator.d.ts +26 -0
  112. package/dist/esm/Canvas/extended/ChartCreator.d.ts.map +1 -0
  113. package/dist/esm/Canvas/extended/ChartCreator.js +50 -0
  114. package/dist/esm/Canvas/extended/ChartCreator.js.map +1 -0
  115. package/dist/esm/Canvas/extended/GIFCreator.d.ts +43 -0
  116. package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -0
  117. package/dist/esm/Canvas/extended/GIFCreator.js +157 -0
  118. package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -0
  119. package/dist/esm/Canvas/extended/ImageCreator.d.ts +83 -0
  120. package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -0
  121. package/dist/esm/Canvas/extended/ImageCreator.js +479 -0
  122. package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -0
  123. package/dist/esm/Canvas/extended/TextCreator.d.ts +35 -0
  124. package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -0
  125. package/dist/esm/Canvas/extended/TextCreator.js +98 -0
  126. package/dist/esm/Canvas/extended/TextCreator.js.map +1 -0
  127. package/dist/esm/Canvas/extended/VideoCreator.d.ts +370 -0
  128. package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -0
  129. package/dist/esm/Canvas/extended/VideoCreator.js +478 -0
  130. package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -0
  131. package/dist/esm/Canvas/utils/Background/bg.d.ts +1 -1
  132. package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
  133. package/dist/esm/Canvas/utils/Background/bg.js +43 -7
  134. package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
  135. package/dist/esm/Canvas/utils/Charts/barchart.d.ts +230 -0
  136. package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -0
  137. package/dist/esm/Canvas/utils/Charts/barchart.js +1891 -0
  138. package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -0
  139. package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
  140. package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
  141. package/dist/esm/Canvas/utils/Charts/comparisonchart.js +368 -0
  142. package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -0
  143. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
  144. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
  145. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
  146. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
  147. package/dist/esm/Canvas/utils/Charts/index.d.ts +45 -0
  148. package/dist/esm/Canvas/utils/Charts/index.d.ts.map +1 -0
  149. package/dist/esm/Canvas/utils/Charts/index.js +17 -0
  150. package/dist/esm/Canvas/utils/Charts/index.js.map +1 -0
  151. package/dist/esm/Canvas/utils/Charts/linechart.d.ts +216 -0
  152. package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -0
  153. package/dist/esm/Canvas/utils/Charts/linechart.js +1761 -0
  154. package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -0
  155. package/dist/esm/Canvas/utils/Charts/piechart.d.ts +167 -0
  156. package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -0
  157. package/dist/esm/Canvas/utils/Charts/piechart.js +794 -0
  158. package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -0
  159. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
  160. package/dist/esm/Canvas/utils/General/batchOperations.js +3 -4
  161. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
  162. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  163. package/dist/esm/Canvas/utils/General/general functions.js +62 -33
  164. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  165. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
  166. package/dist/esm/Canvas/utils/General/imageStitching.js +3 -6
  167. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
  168. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
  169. package/dist/esm/Canvas/utils/Image/imageMasking.js +5 -12
  170. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
  171. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +4 -4
  172. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  173. package/dist/esm/Canvas/utils/Image/imageProperties.js +44 -9
  174. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  175. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
  176. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  177. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
  178. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  179. package/dist/esm/Canvas/utils/Texts/textProperties.d.ts +1 -1
  180. package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
  181. package/dist/esm/Canvas/utils/Texts/textProperties.js +48 -5
  182. package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
  183. package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts +489 -0
  184. package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
  185. package/dist/esm/Canvas/utils/Video/videoHelpers.js +1835 -0
  186. package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -0
  187. package/dist/esm/Canvas/utils/errorUtils.d.ts +15 -0
  188. package/dist/esm/Canvas/utils/errorUtils.d.ts.map +1 -0
  189. package/dist/esm/Canvas/utils/errorUtils.js +26 -0
  190. package/dist/esm/Canvas/utils/errorUtils.js.map +1 -0
  191. package/dist/esm/Canvas/utils/types.d.ts +17 -178
  192. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  193. package/dist/esm/Canvas/utils/types.js.map +1 -1
  194. package/dist/esm/Canvas/utils/utils.d.ts +4 -3
  195. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  196. package/dist/esm/Canvas/utils/utils.js +40 -6
  197. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  198. package/dist/esm/index.d.ts +1 -8
  199. package/dist/esm/index.d.ts.map +1 -1
  200. package/dist/esm/index.js +14 -45
  201. package/dist/esm/index.js.map +1 -1
  202. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  203. package/package.json +234 -198
  204. package/dist/cjs/Canvas/utils/Charts/charts.d.ts +0 -13
  205. package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +0 -1
  206. package/dist/cjs/Canvas/utils/Charts/charts.js +0 -466
  207. package/dist/cjs/Canvas/utils/Charts/charts.js.map +0 -1
  208. package/dist/esm/Canvas/utils/Charts/charts.d.ts +0 -13
  209. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +0 -1
  210. package/dist/esm/Canvas/utils/Charts/charts.js +0 -466
  211. package/dist/esm/Canvas/utils/Charts/charts.js.map +0 -1
  212. package/lib/Canvas/ApexPainter.ts +0 -5414
  213. package/lib/Canvas/utils/Background/bg.ts +0 -285
  214. package/lib/Canvas/utils/Charts/charts.ts +0 -548
  215. package/lib/Canvas/utils/Custom/advancedLines.ts +0 -387
  216. package/lib/Canvas/utils/Custom/customLines.ts +0 -206
  217. package/lib/Canvas/utils/General/batchOperations.ts +0 -103
  218. package/lib/Canvas/utils/General/conversion.ts +0 -34
  219. package/lib/Canvas/utils/General/general functions.ts +0 -726
  220. package/lib/Canvas/utils/General/imageCompression.ts +0 -316
  221. package/lib/Canvas/utils/General/imageStitching.ts +0 -252
  222. package/lib/Canvas/utils/Image/imageEffects.ts +0 -175
  223. package/lib/Canvas/utils/Image/imageFilters.ts +0 -356
  224. package/lib/Canvas/utils/Image/imageMasking.ts +0 -335
  225. package/lib/Canvas/utils/Image/imageProperties.ts +0 -587
  226. package/lib/Canvas/utils/Image/professionalImageFilters.ts +0 -391
  227. package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +0 -229
  228. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +0 -455
  229. package/lib/Canvas/utils/Shapes/shapes.ts +0 -528
  230. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +0 -716
  231. package/lib/Canvas/utils/Texts/textPathRenderer.ts +0 -320
  232. package/lib/Canvas/utils/Texts/textProperties.ts +0 -231
  233. package/lib/Canvas/utils/types.ts +0 -983
  234. package/lib/Canvas/utils/utils.ts +0 -135
  235. package/lib/index.ts +0 -81
  236. package/lib/utils.ts +0 -5
@@ -0,0 +1,1835 @@
1
+ "use strict";
2
+ /**
3
+ * Video helper functions for ApexPainter
4
+ * This file contains all video processing helper methods that were previously in ApexPainter.ts
5
+ * to keep the main class cleaner and more maintainable.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.VideoHelpers = void 0;
12
+ const canvas_1 = require("@napi-rs/canvas");
13
+ const child_process_1 = require("child_process");
14
+ const util_1 = require("util");
15
+ const axios_1 = __importDefault(require("axios"));
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const errorUtils_1 = require("../errorUtils");
19
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
20
+ /**
21
+ * Helper function to resolve video source (Buffer, URL, or local path) to a file path
22
+ * Downloads URLs and writes Buffers to temp files as needed
23
+ */
24
+ async function resolveVideoSource(videoSource, frameDir, timestamp) {
25
+ if (Buffer.isBuffer(videoSource)) {
26
+ // Buffer: write to temp file
27
+ const videoPath = path_1.default.join(frameDir, `temp-video-${timestamp}.mp4`);
28
+ fs_1.default.writeFileSync(videoPath, videoSource);
29
+ return { videoPath, shouldCleanup: true };
30
+ }
31
+ else if (typeof videoSource === 'string' && /^https?:\/\//i.test(videoSource)) {
32
+ // HTTP/HTTPS URL: download and write to temp file
33
+ const response = await (0, axios_1.default)({
34
+ method: 'get',
35
+ url: videoSource,
36
+ responseType: 'arraybuffer'
37
+ });
38
+ const videoPath = path_1.default.join(frameDir, `temp-video-${timestamp}.mp4`);
39
+ fs_1.default.writeFileSync(videoPath, Buffer.from(response.data));
40
+ return { videoPath, shouldCleanup: true };
41
+ }
42
+ else {
43
+ // Local file path: resolve and validate
44
+ let resolvedPath = videoSource;
45
+ if (!path_1.default.isAbsolute(resolvedPath)) {
46
+ resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
47
+ }
48
+ if (!fs_1.default.existsSync(resolvedPath)) {
49
+ throw new Error(`Video file not found: ${videoSource}`);
50
+ }
51
+ return { videoPath: resolvedPath, shouldCleanup: false };
52
+ }
53
+ }
54
+ /**
55
+ * Video helper functions class
56
+ * All video processing helper methods are contained here
57
+ */
58
+ class VideoHelpers {
59
+ deps;
60
+ constructor(dependencies) {
61
+ this.deps = dependencies;
62
+ }
63
+ /**
64
+ * Helper to execute FFmpeg with progress tracking
65
+ * @private
66
+ */
67
+ async executeFFmpegWithProgress(command, options, onProgress) {
68
+ return new Promise((resolve, reject) => {
69
+ const childProcess = (0, child_process_1.exec)(command, options, (error) => {
70
+ if (error)
71
+ reject(error);
72
+ else
73
+ resolve();
74
+ });
75
+ if (onProgress) {
76
+ let stderrBuffer = '';
77
+ childProcess.stderr?.on('data', (data) => {
78
+ stderrBuffer += data.toString();
79
+ // Parse FFmpeg progress output
80
+ const timeMatch = stderrBuffer.match(/time=(\d+):(\d+):(\d+\.\d+)/);
81
+ const durationMatch = stderrBuffer.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
82
+ if (timeMatch && durationMatch) {
83
+ const currentTime = parseFloat(timeMatch[1]) * 3600 + parseFloat(timeMatch[2]) * 60 + parseFloat(timeMatch[3]);
84
+ const totalDuration = parseFloat(durationMatch[1]) * 3600 + parseFloat(durationMatch[2]) * 60 + parseFloat(durationMatch[3]);
85
+ const percent = Math.min(100, (currentTime / totalDuration) * 100);
86
+ const speedMatch = stderrBuffer.match(/speed=\s*([\d.]+)x/);
87
+ const speed = speedMatch ? parseFloat(speedMatch[1]) : 1;
88
+ onProgress({ percent, time: currentTime, speed });
89
+ }
90
+ });
91
+ }
92
+ });
93
+ }
94
+ /**
95
+ * Generate video thumbnail (grid of frames)
96
+ */
97
+ async generateVideoThumbnail(videoSource, options, videoInfo) {
98
+ const count = options.count || 9;
99
+ const grid = options.grid || { cols: 3, rows: 3 };
100
+ const frameWidth = options.width || 320;
101
+ const frameHeight = options.height || 180;
102
+ const outputFormat = options.outputFormat || 'jpg';
103
+ const quality = options.quality || 2;
104
+ if (!videoInfo) {
105
+ videoInfo = await this.deps.getVideoInfo(videoSource, true);
106
+ }
107
+ const duration = videoInfo.duration;
108
+ const interval = duration / (count + 1); // Distribute frames evenly
109
+ // Extract frames
110
+ const frames = [];
111
+ for (let i = 1; i <= count; i++) {
112
+ const time = interval * i;
113
+ const frame = await this.deps.extractVideoFrame(videoSource, 0, time, outputFormat, quality);
114
+ if (frame) {
115
+ frames.push(frame);
116
+ }
117
+ }
118
+ // Create thumbnail canvas
119
+ const thumbnailWidth = frameWidth * grid.cols;
120
+ const thumbnailHeight = frameHeight * grid.rows;
121
+ const canvas = (0, canvas_1.createCanvas)(thumbnailWidth, thumbnailHeight);
122
+ const ctx = (0, errorUtils_1.getCanvasContext)(canvas);
123
+ // Draw frames in grid
124
+ for (let i = 0; i < frames.length; i++) {
125
+ const row = Math.floor(i / grid.cols);
126
+ const col = i % grid.cols;
127
+ const x = col * frameWidth;
128
+ const y = row * frameHeight;
129
+ const frameImage = await (0, canvas_1.loadImage)(frames[i]);
130
+ ctx.drawImage(frameImage, x, y, frameWidth, frameHeight);
131
+ }
132
+ return {
133
+ buffer: canvas.toBuffer('image/png'),
134
+ canvas: { width: thumbnailWidth, height: thumbnailHeight }
135
+ };
136
+ }
137
+ /**
138
+ * Convert video format
139
+ */
140
+ async convertVideo(videoSource, options) {
141
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
142
+ if (!fs_1.default.existsSync(frameDir)) {
143
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
144
+ }
145
+ const timestamp = Date.now();
146
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
147
+ const format = options.format || 'mp4';
148
+ const qualityPresets = {
149
+ low: '-crf 28',
150
+ medium: '-crf 23',
151
+ high: '-crf 18',
152
+ ultra: '-crf 15'
153
+ };
154
+ const qualityFlag = options.bitrate
155
+ ? `-b:v ${options.bitrate}k`
156
+ : qualityPresets[options.quality || 'medium'];
157
+ const fpsFlag = options.fps ? `-r ${options.fps}` : '';
158
+ const resolutionFlag = options.resolution
159
+ ? `-vf scale=${options.resolution.width}:${options.resolution.height}`
160
+ : '';
161
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
162
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
163
+ const command = `ffmpeg -i "${escapedVideoPath}" ${qualityFlag} ${fpsFlag} ${resolutionFlag} -y "${escapedOutputPath}"`;
164
+ try {
165
+ await execAsync(command, {
166
+ timeout: 300000, // 5 minute timeout
167
+ maxBuffer: 10 * 1024 * 1024
168
+ });
169
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
170
+ fs_1.default.unlinkSync(videoPath);
171
+ }
172
+ return { outputPath: options.outputPath, success: true };
173
+ }
174
+ catch (error) {
175
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
176
+ fs_1.default.unlinkSync(videoPath);
177
+ }
178
+ throw error;
179
+ }
180
+ }
181
+ /**
182
+ * Trim/Cut video
183
+ */
184
+ async trimVideo(videoSource, options) {
185
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
186
+ if (!fs_1.default.existsSync(frameDir)) {
187
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
188
+ }
189
+ const timestamp = Date.now();
190
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
191
+ const duration = options.endTime - options.startTime;
192
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
193
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
194
+ const command = `ffmpeg -i "${escapedVideoPath}" -ss ${options.startTime} -t ${duration} -c copy -y "${escapedOutputPath}"`;
195
+ try {
196
+ await execAsync(command, {
197
+ timeout: 300000,
198
+ maxBuffer: 10 * 1024 * 1024
199
+ });
200
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
201
+ fs_1.default.unlinkSync(videoPath);
202
+ }
203
+ return { outputPath: options.outputPath, success: true };
204
+ }
205
+ catch (error) {
206
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
207
+ fs_1.default.unlinkSync(videoPath);
208
+ }
209
+ throw error;
210
+ }
211
+ }
212
+ /**
213
+ * Extract audio from video
214
+ */
215
+ async extractAudio(videoSource, options) {
216
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
217
+ if (!fs_1.default.existsSync(frameDir)) {
218
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
219
+ }
220
+ const timestamp = Date.now();
221
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
222
+ // Check if video has audio stream
223
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
224
+ try {
225
+ const { stdout } = await execAsync(`ffprobe -v error -select_streams a:0 -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "${escapedVideoPath}"`, { timeout: 10000, maxBuffer: 1024 * 1024 });
226
+ const hasAudio = stdout.toString().trim() === 'audio';
227
+ if (!hasAudio) {
228
+ throw new Error('Video does not contain an audio stream. Cannot extract audio.');
229
+ }
230
+ }
231
+ catch (error) {
232
+ if (error instanceof Error && error.message.includes('Video does not contain')) {
233
+ throw error;
234
+ }
235
+ // If ffprobe fails, assume no audio
236
+ throw new Error('Video does not contain an audio stream. Cannot extract audio.');
237
+ }
238
+ const format = options.format || 'mp3';
239
+ const bitrate = options.bitrate || 128;
240
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
241
+ const command = `ffmpeg -i "${escapedVideoPath}" -vn -acodec ${format === 'mp3' ? 'libmp3lame' : format === 'wav' ? 'pcm_s16le' : format === 'aac' ? 'aac' : 'libvorbis'} -ab ${bitrate}k -y "${escapedOutputPath}"`;
242
+ try {
243
+ await execAsync(command, {
244
+ timeout: 300000,
245
+ maxBuffer: 10 * 1024 * 1024
246
+ });
247
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
248
+ fs_1.default.unlinkSync(videoPath);
249
+ }
250
+ return { outputPath: options.outputPath, success: true };
251
+ }
252
+ catch (error) {
253
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
254
+ fs_1.default.unlinkSync(videoPath);
255
+ }
256
+ throw error;
257
+ }
258
+ }
259
+ /**
260
+ * Add watermark to video
261
+ */
262
+ async addWatermarkToVideo(videoSource, options) {
263
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
264
+ if (!fs_1.default.existsSync(frameDir)) {
265
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
266
+ }
267
+ const timestamp = Date.now();
268
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
269
+ let watermarkPath = options.watermarkPath;
270
+ if (!/^https?:\/\//i.test(watermarkPath)) {
271
+ watermarkPath = path_1.default.join(process.cwd(), watermarkPath);
272
+ }
273
+ if (!fs_1.default.existsSync(watermarkPath)) {
274
+ throw new Error(`Watermark file not found: ${options.watermarkPath}`);
275
+ }
276
+ const position = options.position || 'bottom-right';
277
+ const opacity = options.opacity || 0.5;
278
+ const size = options.size ? `scale=${options.size.width}:${options.size.height}` : '';
279
+ const positionMap = {
280
+ 'top-left': '10:10',
281
+ 'top-right': 'W-w-10:10',
282
+ 'bottom-left': '10:H-h-10',
283
+ 'bottom-right': 'W-w-10:H-h-10',
284
+ 'center': '(W-w)/2:(H-h)/2'
285
+ };
286
+ const overlay = positionMap[position];
287
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
288
+ const escapedWatermarkPath = watermarkPath.replace(/"/g, '\\"');
289
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
290
+ const filter = `[1:v]${size ? size + ',' : ''}format=rgba,colorchannelmixer=aa=${opacity}[wm];[0:v][wm]overlay=${overlay}`;
291
+ const command = `ffmpeg -i "${escapedVideoPath}" -i "${escapedWatermarkPath}" -filter_complex "${filter}" -y "${escapedOutputPath}"`;
292
+ try {
293
+ await execAsync(command, {
294
+ timeout: 300000,
295
+ maxBuffer: 10 * 1024 * 1024
296
+ });
297
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
298
+ fs_1.default.unlinkSync(videoPath);
299
+ }
300
+ return { outputPath: options.outputPath, success: true };
301
+ }
302
+ catch (error) {
303
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
304
+ fs_1.default.unlinkSync(videoPath);
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+ /**
310
+ * Change video speed
311
+ */
312
+ async changeVideoSpeed(videoSource, options) {
313
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
314
+ if (!fs_1.default.existsSync(frameDir)) {
315
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
316
+ }
317
+ const timestamp = Date.now();
318
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
319
+ // Check if video has audio stream
320
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
321
+ let hasAudio = false;
322
+ try {
323
+ const { stdout } = await execAsync(`ffprobe -v error -select_streams a:0 -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "${escapedVideoPath}"`, { timeout: 10000, maxBuffer: 1024 * 1024 });
324
+ hasAudio = stdout.toString().trim() === 'audio';
325
+ }
326
+ catch {
327
+ hasAudio = false;
328
+ }
329
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
330
+ let command;
331
+ if (hasAudio) {
332
+ // Video has audio - process both video and audio
333
+ // For speeds > 2.0, we need to chain atempo filters (atempo max is 2.0)
334
+ if (options.speed > 2.0) {
335
+ const atempoCount = Math.ceil(Math.log2(options.speed));
336
+ const atempoValue = Math.pow(2, Math.log2(options.speed) / atempoCount);
337
+ const atempoFilters = Array(atempoCount).fill(atempoValue).map(v => `atempo=${v}`).join(',');
338
+ command = `ffmpeg -i "${escapedVideoPath}" -filter_complex "[0:v]setpts=${1 / options.speed}*PTS[v];[0:a]${atempoFilters}[a]" -map "[v]" -map "[a]" -y "${escapedOutputPath}"`;
339
+ }
340
+ else if (options.speed < 0.5) {
341
+ // For speeds < 0.5, we need to chain atempo filters
342
+ const atempoCount = Math.ceil(Math.log2(1 / options.speed));
343
+ const atempoValue = Math.pow(0.5, Math.log2(1 / options.speed) / atempoCount);
344
+ const atempoFilters = Array(atempoCount).fill(atempoValue).map(v => `atempo=${v}`).join(',');
345
+ command = `ffmpeg -i "${escapedVideoPath}" -filter_complex "[0:v]setpts=${1 / options.speed}*PTS[v];[0:a]${atempoFilters}[a]" -map "[v]" -map "[a]" -y "${escapedOutputPath}"`;
346
+ }
347
+ else {
348
+ // Normal speed range (0.5 to 2.0)
349
+ command = `ffmpeg -i "${escapedVideoPath}" -filter_complex "[0:v]setpts=${1 / options.speed}*PTS[v];[0:a]atempo=${options.speed}[a]" -map "[v]" -map "[a]" -y "${escapedOutputPath}"`;
350
+ }
351
+ }
352
+ else {
353
+ // No audio - only process video
354
+ command = `ffmpeg -i "${escapedVideoPath}" -filter_complex "[0:v]setpts=${1 / options.speed}*PTS[v]" -map "[v]" -y "${escapedOutputPath}"`;
355
+ }
356
+ try {
357
+ await execAsync(command, {
358
+ timeout: 300000,
359
+ maxBuffer: 10 * 1024 * 1024
360
+ });
361
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
362
+ fs_1.default.unlinkSync(videoPath);
363
+ }
364
+ return { outputPath: options.outputPath, success: true };
365
+ }
366
+ catch (error) {
367
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
368
+ fs_1.default.unlinkSync(videoPath);
369
+ }
370
+ throw error;
371
+ }
372
+ }
373
+ /**
374
+ * Generate video preview (multiple frames)
375
+ */
376
+ async generateVideoPreview(videoSource, options, videoInfo) {
377
+ const count = options.count || 10;
378
+ const outputDir = options.outputDirectory || path_1.default.join(process.cwd(), 'video-preview');
379
+ const outputFormat = options.outputFormat || 'png';
380
+ const quality = options.quality || 2;
381
+ if (!fs_1.default.existsSync(outputDir)) {
382
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
383
+ }
384
+ if (!videoInfo) {
385
+ videoInfo = await this.deps.getVideoInfo(videoSource, true);
386
+ }
387
+ const duration = videoInfo.duration;
388
+ const interval = duration / (count + 1);
389
+ const frames = [];
390
+ for (let i = 1; i <= count; i++) {
391
+ const time = interval * i;
392
+ const frameBuffer = await this.deps.extractVideoFrame(videoSource, 0, time, outputFormat, quality);
393
+ if (frameBuffer) {
394
+ const framePath = path_1.default.join(outputDir, `preview-${String(i).padStart(3, '0')}.${outputFormat}`);
395
+ fs_1.default.writeFileSync(framePath, frameBuffer);
396
+ frames.push({
397
+ source: framePath,
398
+ frameNumber: i,
399
+ time: time
400
+ });
401
+ }
402
+ }
403
+ return frames;
404
+ }
405
+ /**
406
+ * Apply video effects/filters
407
+ */
408
+ async applyVideoEffects(videoSource, options) {
409
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
410
+ if (!fs_1.default.existsSync(frameDir)) {
411
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
412
+ }
413
+ const timestamp = Date.now();
414
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
415
+ // Build filter chain
416
+ const filters = [];
417
+ for (const filter of options.filters) {
418
+ switch (filter.type) {
419
+ case 'blur':
420
+ filters.push(`boxblur=${filter.intensity || 5}`);
421
+ break;
422
+ case 'brightness':
423
+ filters.push(`eq=brightness=${((filter.value || 0) / 100).toFixed(2)}`);
424
+ break;
425
+ case 'contrast':
426
+ filters.push(`eq=contrast=${1 + ((filter.value || 0) / 100)}`);
427
+ break;
428
+ case 'saturation':
429
+ filters.push(`eq=saturation=${1 + ((filter.value || 0) / 100)}`);
430
+ break;
431
+ case 'grayscale':
432
+ filters.push('hue=s=0');
433
+ break;
434
+ case 'sepia':
435
+ filters.push('colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131');
436
+ break;
437
+ case 'invert':
438
+ filters.push('negate');
439
+ break;
440
+ case 'sharpen':
441
+ filters.push(`unsharp=5:5:${filter.intensity || 1.0}:5:5:0.0`);
442
+ break;
443
+ case 'noise':
444
+ filters.push(`noise=alls=${filter.intensity || 20}:allf=t+u`);
445
+ break;
446
+ }
447
+ }
448
+ const filterChain = filters.length > 0 ? `-vf "${filters.join(',')}"` : '';
449
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
450
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
451
+ const command = `ffmpeg -i "${escapedVideoPath}" ${filterChain} -y "${escapedOutputPath}"`;
452
+ try {
453
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
454
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
455
+ fs_1.default.unlinkSync(videoPath);
456
+ }
457
+ return { outputPath: options.outputPath, success: true };
458
+ }
459
+ catch (error) {
460
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
461
+ fs_1.default.unlinkSync(videoPath);
462
+ }
463
+ throw error;
464
+ }
465
+ }
466
+ /**
467
+ * Merge/Concatenate videos
468
+ */
469
+ async mergeVideos(options) {
470
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
471
+ if (!fs_1.default.existsSync(frameDir)) {
472
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
473
+ }
474
+ const timestamp = Date.now();
475
+ const videoPaths = [];
476
+ const shouldCleanup = [];
477
+ // Prepare all video files
478
+ for (let i = 0; i < options.videos.length; i++) {
479
+ const video = options.videos[i];
480
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(video, frameDir, timestamp + i);
481
+ videoPaths.push(videoPath);
482
+ shouldCleanup.push(shouldCleanupVideo);
483
+ }
484
+ const mode = options.mode || 'sequential';
485
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
486
+ let command;
487
+ if (mode === 'sequential') {
488
+ // Create concat file
489
+ const concatFile = path_1.default.join(frameDir, `concat-${timestamp}.txt`);
490
+ const concatContent = videoPaths.map(vp => `file '${vp.replace(/'/g, "\\'")}'`).join('\n');
491
+ fs_1.default.writeFileSync(concatFile, concatContent);
492
+ command = `ffmpeg -f concat -safe 0 -i "${concatFile.replace(/"/g, '\\"')}" -c copy -y "${escapedOutputPath}"`;
493
+ }
494
+ else if (mode === 'side-by-side') {
495
+ const escapedPaths = videoPaths.map(vp => vp.replace(/"/g, '\\"'));
496
+ command = `ffmpeg -i "${escapedPaths[0]}" -i "${escapedPaths[1] || escapedPaths[0]}" -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" -y "${escapedOutputPath}"`;
497
+ }
498
+ else if (mode === 'grid') {
499
+ const grid = options.grid || { cols: 2, rows: 2 };
500
+ const escapedPaths = videoPaths.map(vp => vp.replace(/"/g, '\\"'));
501
+ // Simplified grid - would need more complex filter for full grid
502
+ command = `ffmpeg -i "${escapedPaths[0]}" -i "${escapedPaths[1] || escapedPaths[0]}" -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" -y "${escapedOutputPath}"`;
503
+ }
504
+ else {
505
+ throw new Error(`Unknown merge mode: ${mode}`);
506
+ }
507
+ try {
508
+ await execAsync(command, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
509
+ // Cleanup
510
+ for (let i = 0; i < videoPaths.length; i++) {
511
+ if (shouldCleanup[i] && fs_1.default.existsSync(videoPaths[i])) {
512
+ fs_1.default.unlinkSync(videoPaths[i]);
513
+ }
514
+ }
515
+ return { outputPath: options.outputPath, success: true };
516
+ }
517
+ catch (error) {
518
+ // Cleanup on error
519
+ for (let i = 0; i < videoPaths.length; i++) {
520
+ if (shouldCleanup[i] && fs_1.default.existsSync(videoPaths[i])) {
521
+ fs_1.default.unlinkSync(videoPaths[i]);
522
+ }
523
+ }
524
+ throw error;
525
+ }
526
+ }
527
+ /**
528
+ * Replace segment in video with segment from another video
529
+ */
530
+ async replaceVideoSegment(mainVideoSource, options) {
531
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
532
+ if (!fs_1.default.existsSync(frameDir)) {
533
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
534
+ }
535
+ const timestamp = Date.now();
536
+ const tempFiles = [];
537
+ let shouldCleanupMain = false;
538
+ let shouldCleanupReplacement = false;
539
+ // Prepare main video
540
+ const { videoPath: mainVideoPath, shouldCleanup: shouldCleanupMainValue } = await resolveVideoSource(mainVideoSource, frameDir, timestamp);
541
+ shouldCleanupMain = shouldCleanupMainValue;
542
+ if (shouldCleanupMain) {
543
+ tempFiles.push(mainVideoPath);
544
+ }
545
+ // Validate that either replacementVideo or replacementFrames is provided
546
+ if (!options.replacementVideo && !options.replacementFrames) {
547
+ throw new Error('Either replacementVideo or replacementFrames must be provided');
548
+ }
549
+ if (options.replacementVideo && options.replacementFrames) {
550
+ throw new Error('Cannot specify both replacementVideo and replacementFrames');
551
+ }
552
+ // Get main video info to validate times
553
+ const mainVideoInfo = await this.deps.getVideoInfo(mainVideoPath, true);
554
+ if (!mainVideoInfo) {
555
+ throw new Error('Failed to get main video information');
556
+ }
557
+ if (options.targetStartTime < 0 || options.targetEndTime > mainVideoInfo.duration) {
558
+ throw new Error(`Target time range (${options.targetStartTime}-${options.targetEndTime}s) is outside video duration (${mainVideoInfo.duration}s)`);
559
+ }
560
+ if (options.targetStartTime >= options.targetEndTime) {
561
+ throw new Error('targetStartTime must be less than targetEndTime');
562
+ }
563
+ const targetDuration = options.targetEndTime - options.targetStartTime;
564
+ const escapedMainPath = mainVideoPath.replace(/"/g, '\\"');
565
+ try {
566
+ // Step 1: Extract part before the segment to replace
567
+ const part1Path = path_1.default.join(frameDir, `part1-${timestamp}.mp4`);
568
+ tempFiles.push(part1Path);
569
+ if (options.targetStartTime > 0) {
570
+ const escapedPart1 = part1Path.replace(/"/g, '\\"');
571
+ const part1Command = `ffmpeg -i "${escapedMainPath}" -t ${options.targetStartTime} -c copy -y "${escapedPart1}"`;
572
+ await execAsync(part1Command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
573
+ }
574
+ // Step 2: Create replacement segment (from video or frames)
575
+ const replacementSegmentPath = path_1.default.join(frameDir, `replacement-segment-${timestamp}.mp4`);
576
+ tempFiles.push(replacementSegmentPath);
577
+ if (options.replacementVideo) {
578
+ // Extract replacement segment from replacement video
579
+ const { videoPath: replacementVideoPath, shouldCleanup: shouldCleanupReplacementValue } = await resolveVideoSource(options.replacementVideo, frameDir, timestamp + 1000);
580
+ shouldCleanupReplacement = shouldCleanupReplacementValue;
581
+ if (shouldCleanupReplacement) {
582
+ tempFiles.push(replacementVideoPath);
583
+ }
584
+ const replacementStartTime = options.replacementStartTime || 0;
585
+ const replacementDuration = options.replacementDuration || targetDuration;
586
+ const escapedReplacementPath = replacementVideoPath.replace(/"/g, '\\"');
587
+ const escapedSegment = replacementSegmentPath.replace(/"/g, '\\"');
588
+ const segmentCommand = `ffmpeg -i "${escapedReplacementPath}" -ss ${replacementStartTime} -t ${replacementDuration} -c copy -y "${escapedSegment}"`;
589
+ await execAsync(segmentCommand, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
590
+ }
591
+ else if (options.replacementFrames) {
592
+ // Create video from frames - will be available after we add createVideoFromFrames
593
+ const replacementFps = options.replacementFps || 30;
594
+ await this.createVideoFromFrames({
595
+ frames: options.replacementFrames,
596
+ outputPath: replacementSegmentPath,
597
+ fps: replacementFps,
598
+ format: 'mp4',
599
+ quality: 'high'
600
+ });
601
+ }
602
+ // Step 3: Extract part after the segment to replace
603
+ const part3Path = path_1.default.join(frameDir, `part3-${timestamp}.mp4`);
604
+ tempFiles.push(part3Path);
605
+ const remainingDuration = mainVideoInfo.duration - options.targetEndTime;
606
+ if (remainingDuration > 0) {
607
+ const escapedPart3 = part3Path.replace(/"/g, '\\"');
608
+ const part3Command = `ffmpeg -i "${escapedMainPath}" -ss ${options.targetEndTime} -t ${remainingDuration} -c copy -y "${escapedPart3}"`;
609
+ await execAsync(part3Command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
610
+ }
611
+ // Step 4: Create concat file and merge all parts
612
+ const concatFile = path_1.default.join(frameDir, `concat-${timestamp}.txt`);
613
+ tempFiles.push(concatFile);
614
+ const concatParts = [];
615
+ // Add part 1 if it exists and has content
616
+ if (options.targetStartTime > 0 && fs_1.default.existsSync(part1Path) && fs_1.default.statSync(part1Path).size > 0) {
617
+ concatParts.push(part1Path.replace(/\\/g, '/').replace(/'/g, "\\'"));
618
+ }
619
+ // Add replacement segment
620
+ if (fs_1.default.existsSync(replacementSegmentPath) && fs_1.default.statSync(replacementSegmentPath).size > 0) {
621
+ concatParts.push(replacementSegmentPath.replace(/\\/g, '/').replace(/'/g, "\\'"));
622
+ }
623
+ // Add part 3 if it exists and has content
624
+ if (remainingDuration > 0 && fs_1.default.existsSync(part3Path) && fs_1.default.statSync(part3Path).size > 0) {
625
+ concatParts.push(part3Path.replace(/\\/g, '/').replace(/'/g, "\\'"));
626
+ }
627
+ if (concatParts.length === 0) {
628
+ throw new Error('No valid video segments to concatenate');
629
+ }
630
+ const concatContent = concatParts.map(p => `file '${p}'`).join('\n');
631
+ fs_1.default.writeFileSync(concatFile, concatContent);
632
+ // Step 5: Concatenate all parts
633
+ const escapedConcatFile = concatFile.replace(/"/g, '\\"');
634
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
635
+ const concatCommand = `ffmpeg -f concat -safe 0 -i "${escapedConcatFile}" -c copy -y "${escapedOutputPath}"`;
636
+ await execAsync(concatCommand, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
637
+ // Cleanup temp files
638
+ for (const tempFile of tempFiles) {
639
+ if (fs_1.default.existsSync(tempFile)) {
640
+ try {
641
+ fs_1.default.unlinkSync(tempFile);
642
+ }
643
+ catch {
644
+ // Ignore cleanup errors
645
+ }
646
+ }
647
+ }
648
+ return { outputPath: options.outputPath, success: true };
649
+ }
650
+ catch (error) {
651
+ // Cleanup temp files on error
652
+ for (const tempFile of tempFiles) {
653
+ if (fs_1.default.existsSync(tempFile)) {
654
+ try {
655
+ fs_1.default.unlinkSync(tempFile);
656
+ }
657
+ catch {
658
+ // Ignore cleanup errors
659
+ }
660
+ }
661
+ }
662
+ throw error;
663
+ }
664
+ }
665
+ /**
666
+ * Rotate/Flip video
667
+ */
668
+ async rotateVideo(videoSource, options) {
669
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
670
+ if (!fs_1.default.existsSync(frameDir)) {
671
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
672
+ }
673
+ const timestamp = Date.now();
674
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
675
+ const filters = [];
676
+ if (options.angle) {
677
+ const rotationMap = {
678
+ 90: 'transpose=1',
679
+ 180: 'transpose=1,transpose=1',
680
+ 270: 'transpose=2'
681
+ };
682
+ filters.push(rotationMap[options.angle]);
683
+ }
684
+ if (options.flip) {
685
+ if (options.flip === 'horizontal') {
686
+ filters.push('hflip');
687
+ }
688
+ else if (options.flip === 'vertical') {
689
+ filters.push('vflip');
690
+ }
691
+ else if (options.flip === 'both') {
692
+ filters.push('hflip', 'vflip');
693
+ }
694
+ }
695
+ const filterChain = filters.length > 0 ? `-vf "${filters.join(',')}"` : '';
696
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
697
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
698
+ const command = `ffmpeg -i "${escapedVideoPath}" ${filterChain} -y "${escapedOutputPath}"`;
699
+ try {
700
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
701
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
702
+ fs_1.default.unlinkSync(videoPath);
703
+ }
704
+ return { outputPath: options.outputPath, success: true };
705
+ }
706
+ catch (error) {
707
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
708
+ fs_1.default.unlinkSync(videoPath);
709
+ }
710
+ throw error;
711
+ }
712
+ }
713
+ /**
714
+ * Crop video
715
+ */
716
+ async cropVideo(videoSource, options) {
717
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
718
+ if (!fs_1.default.existsSync(frameDir)) {
719
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
720
+ }
721
+ const timestamp = Date.now();
722
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
723
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
724
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
725
+ const command = `ffmpeg -i "${escapedVideoPath}" -vf "crop=${options.width}:${options.height}:${options.x}:${options.y}" -y "${escapedOutputPath}"`;
726
+ try {
727
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
728
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
729
+ fs_1.default.unlinkSync(videoPath);
730
+ }
731
+ return { outputPath: options.outputPath, success: true };
732
+ }
733
+ catch (error) {
734
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
735
+ fs_1.default.unlinkSync(videoPath);
736
+ }
737
+ throw error;
738
+ }
739
+ }
740
+ /**
741
+ * Compress/Optimize video
742
+ */
743
+ async compressVideo(videoSource, options) {
744
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
745
+ if (!fs_1.default.existsSync(frameDir)) {
746
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
747
+ }
748
+ const timestamp = Date.now();
749
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
750
+ // Get original size
751
+ let originalSize = 0;
752
+ if (Buffer.isBuffer(videoSource)) {
753
+ originalSize = videoSource.length;
754
+ }
755
+ else {
756
+ originalSize = fs_1.default.statSync(videoPath).size;
757
+ }
758
+ const qualityPresets = {
759
+ low: '-crf 32 -preset fast',
760
+ medium: '-crf 28 -preset medium',
761
+ high: '-crf 23 -preset slow',
762
+ ultra: '-crf 18 -preset veryslow'
763
+ };
764
+ let qualityFlag = qualityPresets[options.quality || 'medium'];
765
+ if (options.maxBitrate) {
766
+ qualityFlag = `-b:v ${options.maxBitrate}k -maxrate ${options.maxBitrate}k -bufsize ${options.maxBitrate * 2}k`;
767
+ }
768
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
769
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
770
+ const command = `ffmpeg -i "${escapedVideoPath}" ${qualityFlag} -y "${escapedOutputPath}"`;
771
+ try {
772
+ await execAsync(command, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
773
+ const compressedSize = fs_1.default.existsSync(options.outputPath) ? fs_1.default.statSync(options.outputPath).size : 0;
774
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
775
+ fs_1.default.unlinkSync(videoPath);
776
+ }
777
+ return {
778
+ outputPath: options.outputPath,
779
+ success: true,
780
+ originalSize,
781
+ compressedSize
782
+ };
783
+ }
784
+ catch (error) {
785
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
786
+ fs_1.default.unlinkSync(videoPath);
787
+ }
788
+ throw error;
789
+ }
790
+ }
791
+ /**
792
+ * Add text overlay to video
793
+ */
794
+ async addTextToVideo(videoSource, options) {
795
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
796
+ if (!fs_1.default.existsSync(frameDir)) {
797
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
798
+ }
799
+ const timestamp = Date.now();
800
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
801
+ const position = options.position || 'bottom-center';
802
+ const fontSize = options.fontSize || 24;
803
+ const fontColor = options.fontColor || 'white';
804
+ const bgColor = options.backgroundColor || 'black@0.5';
805
+ const positionMap = {
806
+ 'top-left': `x=10:y=10`,
807
+ 'top-center': `x=(w-text_w)/2:y=10`,
808
+ 'top-right': `x=w-text_w-10:y=10`,
809
+ 'center': `x=(w-text_w)/2:y=(h-text_h)/2`,
810
+ 'bottom-left': `x=10:y=h-text_h-10`,
811
+ 'bottom-center': `x=(w-text_w)/2:y=h-text_h-10`,
812
+ 'bottom-right': `x=w-text_w-10:y=h-text_h-10`
813
+ };
814
+ const pos = positionMap[position];
815
+ const textEscaped = options.text.replace(/:/g, '\\:').replace(/'/g, "\\'");
816
+ const timeFilter = options.startTime !== undefined && options.endTime !== undefined
817
+ ? `:enable='between(t,${options.startTime},${options.endTime})'`
818
+ : '';
819
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
820
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
821
+ const command = `ffmpeg -i "${escapedVideoPath}" -vf "drawtext=text='${textEscaped}':fontsize=${fontSize}:fontcolor=${fontColor}:box=1:boxcolor=${bgColor}:${pos}${timeFilter}" -y "${escapedOutputPath}"`;
822
+ try {
823
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
824
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
825
+ fs_1.default.unlinkSync(videoPath);
826
+ }
827
+ return { outputPath: options.outputPath, success: true };
828
+ }
829
+ catch (error) {
830
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
831
+ fs_1.default.unlinkSync(videoPath);
832
+ }
833
+ throw error;
834
+ }
835
+ }
836
+ /**
837
+ * Create video from frames/images
838
+ */
839
+ async createVideoFromFrames(options) {
840
+ if (!options.frames || options.frames.length === 0) {
841
+ throw new Error('createFromFrames: At least one frame is required');
842
+ }
843
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
844
+ if (!fs_1.default.existsSync(frameDir)) {
845
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
846
+ }
847
+ const timestamp = Date.now();
848
+ const fps = options.fps || 30;
849
+ const format = options.format || 'mp4';
850
+ const qualityPresets = {
851
+ low: '-crf 28',
852
+ medium: '-crf 23',
853
+ high: '-crf 18',
854
+ ultra: '-crf 15'
855
+ };
856
+ const qualityFlag = options.bitrate
857
+ ? `-b:v ${options.bitrate}k`
858
+ : qualityPresets[options.quality || 'medium'];
859
+ // Process frames: save buffers to temp files, resolve paths
860
+ const framePaths = [];
861
+ const tempFiles = [];
862
+ const frameSequenceDir = path_1.default.join(frameDir, `frames-${timestamp}`);
863
+ try {
864
+ // Get first frame dimensions if resolution not specified
865
+ let frameWidth;
866
+ let frameHeight;
867
+ if (options.resolution) {
868
+ frameWidth = options.resolution.width;
869
+ frameHeight = options.resolution.height;
870
+ }
871
+ else {
872
+ // Load first frame to get dimensions
873
+ const firstFrame = options.frames[0];
874
+ let firstFramePath;
875
+ if (Buffer.isBuffer(firstFrame)) {
876
+ firstFramePath = path_1.default.join(frameDir, `frame-${timestamp}-0.png`);
877
+ fs_1.default.writeFileSync(firstFramePath, firstFrame);
878
+ tempFiles.push(firstFramePath);
879
+ }
880
+ else {
881
+ let resolvedPath = firstFrame;
882
+ if (!/^https?:\/\//i.test(resolvedPath)) {
883
+ resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
884
+ }
885
+ if (!fs_1.default.existsSync(resolvedPath)) {
886
+ throw new Error(`Frame file not found: ${firstFrame}`);
887
+ }
888
+ firstFramePath = resolvedPath;
889
+ }
890
+ // Get dimensions using ffprobe or loadImage
891
+ try {
892
+ const { loadImage } = await import('@napi-rs/canvas');
893
+ const img = await loadImage(firstFramePath);
894
+ frameWidth = img.width;
895
+ frameHeight = img.height;
896
+ }
897
+ catch {
898
+ // Fallback: try to get from ffprobe
899
+ const escapedPath = firstFramePath.replace(/"/g, '\\"');
900
+ try {
901
+ const { stdout } = await execAsync(`ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of default=noprint_wrappers=1:nokey=1 "${escapedPath}"`, { timeout: 10000, maxBuffer: 1024 * 1024 });
902
+ const [w, h] = stdout.toString().trim().split('\n').map(Number);
903
+ if (w && h) {
904
+ frameWidth = w;
905
+ frameHeight = h;
906
+ }
907
+ }
908
+ catch {
909
+ throw new Error('Could not determine frame dimensions. Please specify resolution.');
910
+ }
911
+ }
912
+ }
913
+ // Process all frames - save all to temp directory with sequential naming for reliable pattern matching
914
+ if (!fs_1.default.existsSync(frameSequenceDir)) {
915
+ fs_1.default.mkdirSync(frameSequenceDir, { recursive: true });
916
+ }
917
+ for (let i = 0; i < options.frames.length; i++) {
918
+ const frame = options.frames[i];
919
+ let frameBuffer;
920
+ if (Buffer.isBuffer(frame)) {
921
+ frameBuffer = frame;
922
+ }
923
+ else {
924
+ let resolvedPath = frame;
925
+ if (!/^https?:\/\//i.test(resolvedPath)) {
926
+ resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
927
+ }
928
+ if (!fs_1.default.existsSync(resolvedPath)) {
929
+ throw new Error(`Frame file not found: ${frame}`);
930
+ }
931
+ frameBuffer = fs_1.default.readFileSync(resolvedPath);
932
+ }
933
+ // Save with sequential naming (frame-000000.png, frame-000001.png, etc.)
934
+ const frameNumber = i.toString().padStart(6, '0');
935
+ const framePath = path_1.default.join(frameSequenceDir, `frame-${frameNumber}.png`);
936
+ fs_1.default.writeFileSync(framePath, frameBuffer);
937
+ tempFiles.push(framePath);
938
+ framePaths.push(framePath);
939
+ }
940
+ // Use image2 pattern input for reliable frame sequence
941
+ const patternPath = path_1.default.join(frameSequenceDir, 'frame-%06d.png').replace(/\\/g, '/');
942
+ const escapedPattern = patternPath.replace(/"/g, '\\"');
943
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
944
+ const resolutionFlag = frameWidth && frameHeight
945
+ ? `-vf scale=${frameWidth}:${frameHeight}:force_original_aspect_ratio=decrease,pad=${frameWidth}:${frameHeight}:(ow-iw)/2:(oh-ih)/2`
946
+ : '';
947
+ // Use image2 demuxer with pattern for frame sequence
948
+ const command = `ffmpeg -framerate ${fps} -i "${escapedPattern}" ${resolutionFlag} ${qualityFlag} -pix_fmt yuv420p -y "${escapedOutputPath}"`;
949
+ await execAsync(command, {
950
+ timeout: 600000, // 10 minute timeout for large frame sequences
951
+ maxBuffer: 10 * 1024 * 1024
952
+ });
953
+ // Cleanup temp files and directory
954
+ for (const tempFile of tempFiles) {
955
+ if (fs_1.default.existsSync(tempFile)) {
956
+ try {
957
+ fs_1.default.unlinkSync(tempFile);
958
+ }
959
+ catch {
960
+ // Ignore cleanup errors
961
+ }
962
+ }
963
+ }
964
+ // Remove frame sequence directory
965
+ if (fs_1.default.existsSync(frameSequenceDir)) {
966
+ try {
967
+ fs_1.default.rmSync(frameSequenceDir, { recursive: true, force: true });
968
+ }
969
+ catch {
970
+ // Ignore cleanup errors
971
+ }
972
+ }
973
+ return { outputPath: options.outputPath, success: true };
974
+ }
975
+ catch (error) {
976
+ // Cleanup temp files on error
977
+ for (const tempFile of tempFiles) {
978
+ if (fs_1.default.existsSync(tempFile)) {
979
+ try {
980
+ fs_1.default.unlinkSync(tempFile);
981
+ }
982
+ catch {
983
+ // Ignore cleanup errors
984
+ }
985
+ }
986
+ }
987
+ // Remove frame sequence directory on error
988
+ if (fs_1.default.existsSync(frameSequenceDir)) {
989
+ try {
990
+ fs_1.default.rmSync(frameSequenceDir, { recursive: true, force: true });
991
+ }
992
+ catch {
993
+ // Ignore cleanup errors
994
+ }
995
+ }
996
+ throw error;
997
+ }
998
+ }
999
+ /**
1000
+ * Add fade effects to video
1001
+ */
1002
+ async addFadeToVideo(videoSource, options) {
1003
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1004
+ if (!fs_1.default.existsSync(frameDir)) {
1005
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1006
+ }
1007
+ const timestamp = Date.now();
1008
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1009
+ const videoInfo = await this.deps.getVideoInfo(videoPath, true);
1010
+ const duration = videoInfo?.duration || 0;
1011
+ const filters = [];
1012
+ if (options.fadeIn) {
1013
+ filters.push(`fade=t=in:st=0:d=${options.fadeIn}`);
1014
+ }
1015
+ if (options.fadeOut && duration > options.fadeOut) {
1016
+ filters.push(`fade=t=out:st=${duration - options.fadeOut}:d=${options.fadeOut}`);
1017
+ }
1018
+ const filterChain = filters.length > 0 ? `-vf "${filters.join(',')}"` : '';
1019
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1020
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1021
+ const command = `ffmpeg -i "${escapedVideoPath}" ${filterChain} -y "${escapedOutputPath}"`;
1022
+ try {
1023
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1024
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1025
+ fs_1.default.unlinkSync(videoPath);
1026
+ }
1027
+ return { outputPath: options.outputPath, success: true };
1028
+ }
1029
+ catch (error) {
1030
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1031
+ fs_1.default.unlinkSync(videoPath);
1032
+ }
1033
+ throw error;
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Reverse video playback
1038
+ */
1039
+ async reverseVideo(videoSource, options) {
1040
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1041
+ if (!fs_1.default.existsSync(frameDir)) {
1042
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1043
+ }
1044
+ const timestamp = Date.now();
1045
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1046
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1047
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1048
+ const command = `ffmpeg -i "${escapedVideoPath}" -vf reverse -af areverse -y "${escapedOutputPath}"`;
1049
+ try {
1050
+ await execAsync(command, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
1051
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1052
+ fs_1.default.unlinkSync(videoPath);
1053
+ }
1054
+ return { outputPath: options.outputPath, success: true };
1055
+ }
1056
+ catch (error) {
1057
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1058
+ fs_1.default.unlinkSync(videoPath);
1059
+ }
1060
+ throw error;
1061
+ }
1062
+ }
1063
+ /**
1064
+ * Create seamless video loop
1065
+ */
1066
+ async createVideoLoop(videoSource, options) {
1067
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1068
+ if (!fs_1.default.existsSync(frameDir)) {
1069
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1070
+ }
1071
+ const timestamp = Date.now();
1072
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1073
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1074
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1075
+ // Create loop by concatenating video with itself
1076
+ const concatFile = path_1.default.join(frameDir, `loop-${timestamp}.txt`);
1077
+ const concatContent = `file '${videoPath.replace(/'/g, "\\'")}'\nfile '${videoPath.replace(/'/g, "\\'")}'`;
1078
+ fs_1.default.writeFileSync(concatFile, concatContent);
1079
+ const command = `ffmpeg -f concat -safe 0 -i "${concatFile.replace(/"/g, '\\"')}" -c copy -y "${escapedOutputPath}"`;
1080
+ try {
1081
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1082
+ if (fs_1.default.existsSync(concatFile)) {
1083
+ fs_1.default.unlinkSync(concatFile);
1084
+ }
1085
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1086
+ fs_1.default.unlinkSync(videoPath);
1087
+ }
1088
+ return { outputPath: options.outputPath, success: true };
1089
+ }
1090
+ catch (error) {
1091
+ if (fs_1.default.existsSync(concatFile)) {
1092
+ fs_1.default.unlinkSync(concatFile);
1093
+ }
1094
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1095
+ fs_1.default.unlinkSync(videoPath);
1096
+ }
1097
+ throw error;
1098
+ }
1099
+ }
1100
+ /**
1101
+ * Batch process multiple videos
1102
+ */
1103
+ async batchProcessVideos(options) {
1104
+ if (!fs_1.default.existsSync(options.outputDirectory)) {
1105
+ fs_1.default.mkdirSync(options.outputDirectory, { recursive: true });
1106
+ }
1107
+ const results = [];
1108
+ for (let i = 0; i < options.videos.length; i++) {
1109
+ const video = options.videos[i];
1110
+ const outputPath = path_1.default.join(options.outputDirectory, `batch-${i + 1}.mp4`);
1111
+ try {
1112
+ // Process each video with its operations
1113
+ await this.deps.createVideo({
1114
+ source: video.source,
1115
+ ...video.operations
1116
+ });
1117
+ results.push({
1118
+ source: typeof video.source === 'string' ? video.source : 'buffer',
1119
+ output: outputPath,
1120
+ success: true
1121
+ });
1122
+ }
1123
+ catch (error) {
1124
+ results.push({
1125
+ source: typeof video.source === 'string' ? video.source : 'buffer',
1126
+ output: outputPath,
1127
+ success: false
1128
+ });
1129
+ }
1130
+ }
1131
+ return results;
1132
+ }
1133
+ /**
1134
+ * Detect scene changes in video
1135
+ */
1136
+ async detectVideoScenes(videoSource, options) {
1137
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1138
+ if (!fs_1.default.existsSync(frameDir)) {
1139
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1140
+ }
1141
+ const timestamp = Date.now();
1142
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1143
+ const threshold = options.threshold || 0.3;
1144
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1145
+ const sceneFile = path_1.default.join(frameDir, `scenes-${timestamp}.txt`);
1146
+ // Use FFmpeg's scene detection
1147
+ const command = `ffmpeg -i "${escapedVideoPath}" -vf "select='gt(scene,${threshold})',showinfo" -f null - 2>&1 | grep "pts_time" | awk '{print $6}' | sed 's/time=//'`;
1148
+ try {
1149
+ const { stdout } = await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1150
+ const times = stdout.toString().trim().split('\n').filter(t => t).map(parseFloat);
1151
+ const scenes = times.map((time, index) => ({ time, scene: index + 1 }));
1152
+ if (options.outputPath && scenes.length > 0) {
1153
+ fs_1.default.writeFileSync(options.outputPath, JSON.stringify(scenes, null, 2));
1154
+ }
1155
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1156
+ fs_1.default.unlinkSync(videoPath);
1157
+ }
1158
+ if (fs_1.default.existsSync(sceneFile)) {
1159
+ fs_1.default.unlinkSync(sceneFile);
1160
+ }
1161
+ return scenes;
1162
+ }
1163
+ catch (error) {
1164
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1165
+ fs_1.default.unlinkSync(videoPath);
1166
+ }
1167
+ if (fs_1.default.existsSync(sceneFile)) {
1168
+ fs_1.default.unlinkSync(sceneFile);
1169
+ }
1170
+ // Return empty array if detection fails
1171
+ return [];
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Stabilize video (reduce shake)
1176
+ */
1177
+ async stabilizeVideo(videoSource, options) {
1178
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1179
+ if (!fs_1.default.existsSync(frameDir)) {
1180
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1181
+ }
1182
+ const timestamp = Date.now();
1183
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1184
+ const smoothing = options.smoothing || 10;
1185
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1186
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1187
+ // Two-pass stabilization
1188
+ const transformsFile = path_1.default.join(frameDir, `transforms-${timestamp}.trf`);
1189
+ // Pass 1: Analyze
1190
+ const analyzeCommand = `ffmpeg -i "${escapedVideoPath}" -vf vidstabdetect=shakiness=5:accuracy=15:result="${transformsFile.replace(/"/g, '\\"')}" -f null -`;
1191
+ // Pass 2: Transform
1192
+ const transformCommand = `ffmpeg -i "${escapedVideoPath}" -vf vidstabtransform=smoothing=${smoothing}:input="${transformsFile.replace(/"/g, '\\"')}" -y "${escapedOutputPath}"`;
1193
+ try {
1194
+ await execAsync(analyzeCommand, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
1195
+ await execAsync(transformCommand, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
1196
+ if (fs_1.default.existsSync(transformsFile)) {
1197
+ fs_1.default.unlinkSync(transformsFile);
1198
+ }
1199
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1200
+ fs_1.default.unlinkSync(videoPath);
1201
+ }
1202
+ return { outputPath: options.outputPath, success: true };
1203
+ }
1204
+ catch (error) {
1205
+ // Fallback to simple deshake if vidstab is not available
1206
+ const simpleCommand = `ffmpeg -i "${escapedVideoPath}" -vf "hqdn3d=4:3:6:4.5" -y "${escapedOutputPath}"`;
1207
+ try {
1208
+ await execAsync(simpleCommand, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1209
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1210
+ fs_1.default.unlinkSync(videoPath);
1211
+ }
1212
+ return { outputPath: options.outputPath, success: true };
1213
+ }
1214
+ catch (fallbackError) {
1215
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1216
+ fs_1.default.unlinkSync(videoPath);
1217
+ }
1218
+ throw error;
1219
+ }
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Color correct video
1224
+ */
1225
+ async colorCorrectVideo(videoSource, options) {
1226
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1227
+ if (!fs_1.default.existsSync(frameDir)) {
1228
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1229
+ }
1230
+ const timestamp = Date.now();
1231
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1232
+ const filters = [];
1233
+ if (options.brightness !== undefined) {
1234
+ filters.push(`eq=brightness=${(options.brightness / 100).toFixed(2)}`);
1235
+ }
1236
+ if (options.contrast !== undefined) {
1237
+ filters.push(`eq=contrast=${1 + (options.contrast / 100)}`);
1238
+ }
1239
+ if (options.saturation !== undefined) {
1240
+ filters.push(`eq=saturation=${1 + (options.saturation / 100)}`);
1241
+ }
1242
+ if (options.hue !== undefined) {
1243
+ filters.push(`hue=h=${options.hue}`);
1244
+ }
1245
+ if (options.temperature !== undefined) {
1246
+ // Temperature adjustment using colorbalance
1247
+ const temp = options.temperature;
1248
+ if (temp > 0) {
1249
+ filters.push(`colorbalance=rs=${temp / 100}:gs=-${temp / 200}:bs=-${temp / 100}`);
1250
+ }
1251
+ else {
1252
+ filters.push(`colorbalance=rs=${temp / 100}:gs=${-temp / 200}:bs=${-temp / 100}`);
1253
+ }
1254
+ }
1255
+ const filterChain = filters.length > 0 ? `-vf "${filters.join(',')}"` : '';
1256
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1257
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1258
+ const command = `ffmpeg -i "${escapedVideoPath}" ${filterChain} -y "${escapedOutputPath}"`;
1259
+ try {
1260
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1261
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1262
+ fs_1.default.unlinkSync(videoPath);
1263
+ }
1264
+ return { outputPath: options.outputPath, success: true };
1265
+ }
1266
+ catch (error) {
1267
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1268
+ fs_1.default.unlinkSync(videoPath);
1269
+ }
1270
+ throw error;
1271
+ }
1272
+ }
1273
+ /**
1274
+ * Add picture-in-picture
1275
+ */
1276
+ async addPictureInPicture(videoSource, options) {
1277
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1278
+ if (!fs_1.default.existsSync(frameDir)) {
1279
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1280
+ }
1281
+ const timestamp = Date.now();
1282
+ // Handle main video
1283
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1284
+ // Handle overlay video
1285
+ const { videoPath: overlayPath, shouldCleanup: shouldCleanupOverlay } = await resolveVideoSource(options.overlayVideo, frameDir, timestamp + 1000);
1286
+ const position = options.position || 'bottom-right';
1287
+ const size = options.size || { width: 320, height: 180 };
1288
+ const opacity = options.opacity || 1.0;
1289
+ const positionMap = {
1290
+ 'top-left': '10:10',
1291
+ 'top-right': 'W-w-10:10',
1292
+ 'bottom-left': '10:H-h-10',
1293
+ 'bottom-right': 'W-w-10:H-h-10',
1294
+ 'center': '(W-w)/2:(H-h)/2'
1295
+ };
1296
+ const overlay = positionMap[position];
1297
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1298
+ const escapedOverlayPath = overlayPath.replace(/"/g, '\\"');
1299
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1300
+ const filter = `[1:v]scale=${size.width}:${size.height},format=rgba,colorchannelmixer=aa=${opacity}[overlay];[0:v][overlay]overlay=${overlay}`;
1301
+ const command = `ffmpeg -i "${escapedVideoPath}" -i "${escapedOverlayPath}" -filter_complex "${filter}" -y "${escapedOutputPath}"`;
1302
+ try {
1303
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1304
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1305
+ fs_1.default.unlinkSync(videoPath);
1306
+ }
1307
+ if (shouldCleanupOverlay && fs_1.default.existsSync(overlayPath)) {
1308
+ fs_1.default.unlinkSync(overlayPath);
1309
+ }
1310
+ return { outputPath: options.outputPath, success: true };
1311
+ }
1312
+ catch (error) {
1313
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1314
+ fs_1.default.unlinkSync(videoPath);
1315
+ }
1316
+ if (shouldCleanupOverlay && fs_1.default.existsSync(overlayPath)) {
1317
+ fs_1.default.unlinkSync(overlayPath);
1318
+ }
1319
+ throw error;
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Create split screen video
1324
+ */
1325
+ async createSplitScreen(options) {
1326
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1327
+ if (!fs_1.default.existsSync(frameDir)) {
1328
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1329
+ }
1330
+ const timestamp = Date.now();
1331
+ const videoPaths = [];
1332
+ const shouldCleanup = [];
1333
+ // Prepare all video files
1334
+ for (let i = 0; i < options.videos.length; i++) {
1335
+ const video = options.videos[i];
1336
+ if (Buffer.isBuffer(video)) {
1337
+ const tempPath = path_1.default.join(frameDir, `temp-video-${timestamp}-${i}.mp4`);
1338
+ fs_1.default.writeFileSync(tempPath, video);
1339
+ videoPaths.push(tempPath);
1340
+ shouldCleanup.push(true);
1341
+ }
1342
+ else {
1343
+ let resolvedPath = video;
1344
+ if (!/^https?:\/\//i.test(resolvedPath)) {
1345
+ resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
1346
+ }
1347
+ if (!fs_1.default.existsSync(resolvedPath)) {
1348
+ throw new Error(`Video file not found: ${video}`);
1349
+ }
1350
+ videoPaths.push(resolvedPath);
1351
+ shouldCleanup.push(false);
1352
+ }
1353
+ }
1354
+ const layout = options.layout || 'side-by-side';
1355
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1356
+ const escapedPaths = videoPaths.map(vp => vp.replace(/"/g, '\\"'));
1357
+ let command;
1358
+ if (layout === 'side-by-side' && videoPaths.length >= 2) {
1359
+ command = `ffmpeg -i "${escapedPaths[0]}" -i "${escapedPaths[1]}" -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" -y "${escapedOutputPath}"`;
1360
+ }
1361
+ else if (layout === 'top-bottom' && videoPaths.length >= 2) {
1362
+ command = `ffmpeg -i "${escapedPaths[0]}" -i "${escapedPaths[1]}" -filter_complex "[0:v][1:v]vstack=inputs=2[v]" -map "[v]" -y "${escapedOutputPath}"`;
1363
+ }
1364
+ else if (layout === 'grid' && videoPaths.length >= 4) {
1365
+ const grid = options.grid || { cols: 2, rows: 2 };
1366
+ // Simplified 2x2 grid
1367
+ command = `ffmpeg -i "${escapedPaths[0]}" -i "${escapedPaths[1]}" -i "${escapedPaths[2]}" -i "${escapedPaths[3]}" -filter_complex "[0:v][1:v]hstack=inputs=2[top];[2:v][3:v]hstack=inputs=2[bottom];[top][bottom]vstack=inputs=2[v]" -map "[v]" -y "${escapedOutputPath}"`;
1368
+ }
1369
+ else {
1370
+ throw new Error(`Invalid layout or insufficient videos for ${layout}`);
1371
+ }
1372
+ try {
1373
+ await execAsync(command, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
1374
+ // Cleanup
1375
+ for (let i = 0; i < videoPaths.length; i++) {
1376
+ if (shouldCleanup[i] && fs_1.default.existsSync(videoPaths[i])) {
1377
+ fs_1.default.unlinkSync(videoPaths[i]);
1378
+ }
1379
+ }
1380
+ return { outputPath: options.outputPath, success: true };
1381
+ }
1382
+ catch (error) {
1383
+ // Cleanup on error
1384
+ for (let i = 0; i < videoPaths.length; i++) {
1385
+ if (shouldCleanup[i] && fs_1.default.existsSync(videoPaths[i])) {
1386
+ fs_1.default.unlinkSync(videoPaths[i]);
1387
+ }
1388
+ }
1389
+ throw error;
1390
+ }
1391
+ }
1392
+ /**
1393
+ * Create time-lapse video
1394
+ */
1395
+ async createTimeLapseVideo(videoSource, options) {
1396
+ const speed = options.speed || 10;
1397
+ // Time-lapse is essentially speeding up the video
1398
+ return await this.changeVideoSpeed(videoSource, { speed, outputPath: options.outputPath });
1399
+ }
1400
+ /**
1401
+ * Mute video (remove audio) - supports full mute or partial mute with time ranges
1402
+ */
1403
+ async muteVideo(videoSource, options) {
1404
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1405
+ if (!fs_1.default.existsSync(frameDir)) {
1406
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1407
+ }
1408
+ const timestamp = Date.now();
1409
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1410
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1411
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1412
+ let command;
1413
+ if (options.ranges && options.ranges.length > 0) {
1414
+ // Partial mute - mute specific time ranges
1415
+ const volumeFilters = options.ranges.map(range => `volume=enable='between(t,${range.start},${range.end})':volume=0`).join(',');
1416
+ command = `ffmpeg -i "${escapedVideoPath}" -af "${volumeFilters}" -y "${escapedOutputPath}"`;
1417
+ }
1418
+ else {
1419
+ // Full mute - remove audio track
1420
+ command = `ffmpeg -i "${escapedVideoPath}" -an -y "${escapedOutputPath}"`;
1421
+ }
1422
+ try {
1423
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1424
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1425
+ fs_1.default.unlinkSync(videoPath);
1426
+ }
1427
+ return { outputPath: options.outputPath, success: true };
1428
+ }
1429
+ catch (error) {
1430
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1431
+ fs_1.default.unlinkSync(videoPath);
1432
+ }
1433
+ throw error;
1434
+ }
1435
+ }
1436
+ /**
1437
+ * Adjust video volume
1438
+ */
1439
+ async adjustVideoVolume(videoSource, options) {
1440
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1441
+ if (!fs_1.default.existsSync(frameDir)) {
1442
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1443
+ }
1444
+ const timestamp = Date.now();
1445
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1446
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1447
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1448
+ let command;
1449
+ if (options.ranges && options.ranges.length > 0) {
1450
+ // Volume adjustment for specific time ranges
1451
+ const volumeFilters = options.ranges.map(range => {
1452
+ const volumeMultiplier = range.volume / 100;
1453
+ return `volume=enable='between(t,${range.start},${range.end})':volume=${volumeMultiplier}`;
1454
+ }).join(',');
1455
+ command = `ffmpeg -i "${escapedVideoPath}" -af "${volumeFilters}" -y "${escapedOutputPath}"`;
1456
+ }
1457
+ else {
1458
+ // Global volume adjustment
1459
+ const volumeMultiplier = (options.volume || 100) / 100;
1460
+ command = `ffmpeg -i "${escapedVideoPath}" -af "volume=${volumeMultiplier}" -y "${escapedOutputPath}"`;
1461
+ }
1462
+ try {
1463
+ await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1464
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1465
+ fs_1.default.unlinkSync(videoPath);
1466
+ }
1467
+ return { outputPath: options.outputPath, success: true };
1468
+ }
1469
+ catch (error) {
1470
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1471
+ fs_1.default.unlinkSync(videoPath);
1472
+ }
1473
+ throw error;
1474
+ }
1475
+ }
1476
+ /**
1477
+ * Freeze video frame at specific time
1478
+ */
1479
+ async freezeVideoFrame(videoSource, options, onProgress) {
1480
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1481
+ if (!fs_1.default.existsSync(frameDir)) {
1482
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1483
+ }
1484
+ const timestamp = Date.now();
1485
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1486
+ // Get video info
1487
+ const videoInfo = await this.deps.getVideoInfo(videoPath, true);
1488
+ if (!videoInfo) {
1489
+ throw new Error('Failed to get video information');
1490
+ }
1491
+ if (options.time < 0 || options.time > videoInfo.duration) {
1492
+ throw new Error(`Freeze time (${options.time}s) is outside video duration (${videoInfo.duration}s)`);
1493
+ }
1494
+ try {
1495
+ // Extract frame at freeze time
1496
+ const freezeFramePath = path_1.default.join(frameDir, `freeze-frame-${timestamp}.png`);
1497
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1498
+ const escapedFramePath = freezeFramePath.replace(/"/g, '\\"');
1499
+ await execAsync(`ffmpeg -i "${escapedVideoPath}" -ss ${options.time} -vframes 1 -y "${escapedFramePath}"`, { timeout: 30000, maxBuffer: 10 * 1024 * 1024 });
1500
+ // Create video parts: before freeze, freeze frame (repeated), after freeze
1501
+ const part1Path = path_1.default.join(frameDir, `part1-${timestamp}.mp4`);
1502
+ const freezePartPath = path_1.default.join(frameDir, `freeze-part-${timestamp}.mp4`);
1503
+ const part3Path = path_1.default.join(frameDir, `part3-${timestamp}.mp4`);
1504
+ const concatFile = path_1.default.join(frameDir, `concat-${timestamp}.txt`);
1505
+ const escapedPart1 = part1Path.replace(/"/g, '\\"');
1506
+ const escapedFreeze = freezePartPath.replace(/"/g, '\\"');
1507
+ const escapedPart3 = part3Path.replace(/"/g, '\\"');
1508
+ const escapedOutput = options.outputPath.replace(/"/g, '\\"');
1509
+ // Part 1: Before freeze
1510
+ if (options.time > 0) {
1511
+ await execAsync(`ffmpeg -i "${escapedVideoPath}" -t ${options.time} -c copy -y "${escapedPart1}"`, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1512
+ }
1513
+ // Freeze part: Repeat frame for duration
1514
+ await execAsync(`ffmpeg -loop 1 -i "${escapedFramePath}" -t ${options.duration} -c:v libx264 -pix_fmt yuv420p -y "${escapedFreeze}"`, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1515
+ // Part 3: After freeze
1516
+ const remainingDuration = videoInfo.duration - options.time;
1517
+ if (remainingDuration > 0) {
1518
+ await execAsync(`ffmpeg -i "${escapedVideoPath}" -ss ${options.time} -c copy -y "${escapedPart3}"`, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
1519
+ }
1520
+ // Create concat file
1521
+ const concatParts = [];
1522
+ if (options.time > 0 && fs_1.default.existsSync(part1Path)) {
1523
+ concatParts.push(part1Path.replace(/\\/g, '/').replace(/'/g, "\\'"));
1524
+ }
1525
+ if (fs_1.default.existsSync(freezePartPath)) {
1526
+ concatParts.push(freezePartPath.replace(/\\/g, '/').replace(/'/g, "\\'"));
1527
+ }
1528
+ if (remainingDuration > 0 && fs_1.default.existsSync(part3Path)) {
1529
+ concatParts.push(part3Path.replace(/\\/g, '/').replace(/'/g, "\\'"));
1530
+ }
1531
+ const concatContent = concatParts.map(p => `file '${p}'`).join('\n');
1532
+ fs_1.default.writeFileSync(concatFile, concatContent);
1533
+ // Concatenate
1534
+ const escapedConcat = concatFile.replace(/"/g, '\\"');
1535
+ await this.executeFFmpegWithProgress(`ffmpeg -f concat -safe 0 -i "${escapedConcat}" -c copy -y "${escapedOutput}"`, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 }, onProgress);
1536
+ // Cleanup
1537
+ const tempFiles = [freezeFramePath, part1Path, freezePartPath, part3Path, concatFile];
1538
+ for (const file of tempFiles) {
1539
+ if (fs_1.default.existsSync(file)) {
1540
+ try {
1541
+ fs_1.default.unlinkSync(file);
1542
+ }
1543
+ catch { }
1544
+ }
1545
+ }
1546
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1547
+ fs_1.default.unlinkSync(videoPath);
1548
+ }
1549
+ return { outputPath: options.outputPath, success: true };
1550
+ }
1551
+ catch (error) {
1552
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1553
+ fs_1.default.unlinkSync(videoPath);
1554
+ }
1555
+ throw error;
1556
+ }
1557
+ }
1558
+ /**
1559
+ * Export video with preset settings
1560
+ */
1561
+ async exportVideoPreset(videoSource, options, onProgress) {
1562
+ const presets = {
1563
+ youtube: { resolution: { width: 1920, height: 1080 }, fps: 30, bitrate: 8000, format: 'mp4' },
1564
+ instagram: { resolution: { width: 1080, height: 1080 }, fps: 30, bitrate: 3500, format: 'mp4' },
1565
+ tiktok: { resolution: { width: 1080, height: 1920 }, fps: 30, bitrate: 4000, format: 'mp4' },
1566
+ twitter: { resolution: { width: 1280, height: 720 }, fps: 30, bitrate: 5000, format: 'mp4' },
1567
+ facebook: { resolution: { width: 1280, height: 720 }, fps: 30, bitrate: 4000, format: 'mp4' },
1568
+ '4k': { resolution: { width: 3840, height: 2160 }, fps: 30, bitrate: 50000, format: 'mp4' },
1569
+ '1080p': { resolution: { width: 1920, height: 1080 }, fps: 30, bitrate: 8000, format: 'mp4' },
1570
+ '720p': { resolution: { width: 1280, height: 720 }, fps: 30, bitrate: 5000, format: 'mp4' },
1571
+ mobile: { resolution: { width: 720, height: 1280 }, fps: 30, bitrate: 2500, format: 'mp4' },
1572
+ web: { resolution: { width: 1280, height: 720 }, fps: 30, bitrate: 3000, format: 'webm' }
1573
+ };
1574
+ const preset = presets[options.preset.toLowerCase()];
1575
+ if (!preset) {
1576
+ throw new Error(`Unknown export preset: ${options.preset}`);
1577
+ }
1578
+ return await this.convertVideo(videoSource, {
1579
+ outputPath: options.outputPath,
1580
+ format: preset.format,
1581
+ quality: 'high',
1582
+ bitrate: preset.bitrate,
1583
+ fps: preset.fps,
1584
+ resolution: preset.resolution
1585
+ });
1586
+ }
1587
+ /**
1588
+ * Normalize audio levels
1589
+ */
1590
+ async normalizeVideoAudio(videoSource, options, onProgress) {
1591
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1592
+ if (!fs_1.default.existsSync(frameDir)) {
1593
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1594
+ }
1595
+ const timestamp = Date.now();
1596
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1597
+ const method = options.method || 'lufs';
1598
+ const targetLevel = options.targetLevel || (method === 'lufs' ? -23 : -1);
1599
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1600
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1601
+ let command;
1602
+ if (method === 'lufs') {
1603
+ // Use loudnorm for LUFS (broadcast standard)
1604
+ command = `ffmpeg -i "${escapedVideoPath}" -af "loudnorm=I=${targetLevel}:TP=-1.5:LRA=11" -c:v copy -y "${escapedOutputPath}"`;
1605
+ }
1606
+ else if (method === 'peak') {
1607
+ // Peak normalization
1608
+ command = `ffmpeg -i "${escapedVideoPath}" -af "volume=${targetLevel}dB" -c:v copy -y "${escapedOutputPath}"`;
1609
+ }
1610
+ else {
1611
+ // RMS normalization
1612
+ command = `ffmpeg -i "${escapedVideoPath}" -af "volume=${targetLevel}dB" -c:v copy -y "${escapedOutputPath}"`;
1613
+ }
1614
+ try {
1615
+ await this.executeFFmpegWithProgress(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 }, onProgress);
1616
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1617
+ fs_1.default.unlinkSync(videoPath);
1618
+ }
1619
+ return { outputPath: options.outputPath, success: true };
1620
+ }
1621
+ catch (error) {
1622
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1623
+ fs_1.default.unlinkSync(videoPath);
1624
+ }
1625
+ throw error;
1626
+ }
1627
+ }
1628
+ /**
1629
+ * Apply LUT (Look-Up Table) to video
1630
+ */
1631
+ async applyLUTToVideo(videoSource, options, onProgress) {
1632
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1633
+ if (!fs_1.default.existsSync(frameDir)) {
1634
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1635
+ }
1636
+ const timestamp = Date.now();
1637
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1638
+ // Resolve LUT path
1639
+ let lutPath = options.lutPath;
1640
+ if (!/^https?:\/\//i.test(lutPath)) {
1641
+ lutPath = path_1.default.join(process.cwd(), lutPath);
1642
+ }
1643
+ if (!fs_1.default.existsSync(lutPath)) {
1644
+ throw new Error(`LUT file not found: ${options.lutPath}`);
1645
+ }
1646
+ const intensity = options.intensity ?? 1.0;
1647
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1648
+ const escapedLutPath = lutPath.replace(/"/g, '\\"');
1649
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1650
+ // Apply LUT with intensity blending
1651
+ const command = `ffmpeg -i "${escapedVideoPath}" -vf "lut3d='${escapedLutPath}',format=yuv420p" -c:v libx264 -crf 18 -y "${escapedOutputPath}"`;
1652
+ try {
1653
+ await this.executeFFmpegWithProgress(command, { timeout: 600000, maxBuffer: 10 * 1024 * 1024 }, onProgress);
1654
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1655
+ fs_1.default.unlinkSync(videoPath);
1656
+ }
1657
+ return { outputPath: options.outputPath, success: true };
1658
+ }
1659
+ catch (error) {
1660
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1661
+ fs_1.default.unlinkSync(videoPath);
1662
+ }
1663
+ throw error;
1664
+ }
1665
+ }
1666
+ /**
1667
+ * Add video transition between two videos
1668
+ */
1669
+ async addVideoTransition(videoSource, options, onProgress) {
1670
+ if (!options.secondVideo && options.type !== 'fade') {
1671
+ throw new Error('addTransition: secondVideo is required for transition types other than fade');
1672
+ }
1673
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1674
+ if (!fs_1.default.existsSync(frameDir)) {
1675
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1676
+ }
1677
+ const timestamp = Date.now();
1678
+ // Prepare first video
1679
+ const { videoPath: video1Path, shouldCleanup: shouldCleanup1 } = await resolveVideoSource(videoSource, frameDir, timestamp);
1680
+ // Prepare second video (if provided)
1681
+ let video2Path;
1682
+ let shouldCleanup2 = false;
1683
+ if (options.secondVideo) {
1684
+ const { videoPath: video2PathResolved, shouldCleanup: shouldCleanup2Value } = await resolveVideoSource(options.secondVideo, frameDir, timestamp + 1000);
1685
+ video2Path = video2PathResolved;
1686
+ shouldCleanup2 = shouldCleanup2Value;
1687
+ }
1688
+ // Get video info
1689
+ const video1Info = await this.deps.getVideoInfo(video1Path, true);
1690
+ if (!video1Info) {
1691
+ throw new Error('Failed to get video information');
1692
+ }
1693
+ // Ensure videos have same resolution
1694
+ const width = video1Info.width;
1695
+ const height = video1Info.height;
1696
+ const escapedVideo1 = video1Path.replace(/"/g, '\\"');
1697
+ const escapedOutput = options.outputPath.replace(/"/g, '\\"');
1698
+ // Handle fade transition (can work on single video)
1699
+ if (options.type === 'fade' && !options.secondVideo) {
1700
+ // Fade in/out on single video
1701
+ const fadeType = options.direction === 'out' ? 'fade=t=out' : 'fade=t=in';
1702
+ const command = `ffmpeg -i "${escapedVideo1}" -vf "${fadeType}:st=0:d=${options.duration}" -c:a copy -y "${escapedOutput}"`;
1703
+ try {
1704
+ await this.executeFFmpegWithProgress(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 }, onProgress);
1705
+ if (shouldCleanup1 && fs_1.default.existsSync(video1Path)) {
1706
+ fs_1.default.unlinkSync(video1Path);
1707
+ }
1708
+ return { outputPath: options.outputPath, success: true };
1709
+ }
1710
+ catch (error) {
1711
+ if (shouldCleanup1 && fs_1.default.existsSync(video1Path)) {
1712
+ fs_1.default.unlinkSync(video1Path);
1713
+ }
1714
+ throw error;
1715
+ }
1716
+ }
1717
+ if (!video2Path) {
1718
+ throw new Error('Second video is required for this transition type');
1719
+ }
1720
+ const video2Info = await this.deps.getVideoInfo(video2Path, true);
1721
+ if (!video2Info) {
1722
+ throw new Error('Failed to get second video information');
1723
+ }
1724
+ const finalWidth = Math.max(width, video2Info.width);
1725
+ const finalHeight = Math.max(height, video2Info.height);
1726
+ const escapedVideo2 = video2Path.replace(/"/g, '\\"');
1727
+ // Map transition types to FFmpeg xfade types
1728
+ const xfadeTypes = {
1729
+ fade: 'fade',
1730
+ wipe: 'wipeleft',
1731
+ slide: 'slideleft',
1732
+ zoom: 'zoom',
1733
+ rotate: 'rotate',
1734
+ dissolve: 'fade',
1735
+ blur: 'fade',
1736
+ circle: 'circleopen',
1737
+ pixelize: 'pixelize'
1738
+ };
1739
+ let xfadeType = xfadeTypes[options.type] || 'fade';
1740
+ // Handle direction modifiers
1741
+ if (options.direction) {
1742
+ const dirMap = {
1743
+ wipe: { left: 'wipeleft', right: 'wiperight', up: 'wipeup', down: 'wipedown' },
1744
+ slide: { left: 'slideleft', right: 'slideright', up: 'slideup', down: 'slidedown' },
1745
+ zoom: { in: 'zoomin', out: 'zoomout' }
1746
+ };
1747
+ if (dirMap[options.type] && dirMap[options.type][options.direction]) {
1748
+ xfadeType = dirMap[options.type][options.direction];
1749
+ }
1750
+ }
1751
+ const transitionOffset = video1Info.duration - options.duration;
1752
+ // Build FFmpeg command with xfade filter
1753
+ const command = `ffmpeg -i "${escapedVideo1}" -i "${escapedVideo2}" -filter_complex "[0:v]scale=${finalWidth}:${finalHeight}[v0];[1:v]scale=${finalWidth}:${finalHeight}[v1];[v0][v1]xfade=transition=${xfadeType}:duration=${options.duration}:offset=${transitionOffset}[v]" -map "[v]" -c:v libx264 -crf 18 -pix_fmt yuv420p -c:a copy -y "${escapedOutput}"`;
1754
+ try {
1755
+ await this.executeFFmpegWithProgress(command, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 }, onProgress);
1756
+ if (shouldCleanup1 && fs_1.default.existsSync(video1Path)) {
1757
+ fs_1.default.unlinkSync(video1Path);
1758
+ }
1759
+ if (shouldCleanup2 && fs_1.default.existsSync(video2Path)) {
1760
+ fs_1.default.unlinkSync(video2Path);
1761
+ }
1762
+ return { outputPath: options.outputPath, success: true };
1763
+ }
1764
+ catch (error) {
1765
+ if (shouldCleanup1 && fs_1.default.existsSync(video1Path)) {
1766
+ fs_1.default.unlinkSync(video1Path);
1767
+ }
1768
+ if (shouldCleanup2 && fs_1.default.existsSync(video2Path)) {
1769
+ fs_1.default.unlinkSync(video2Path);
1770
+ }
1771
+ throw error;
1772
+ }
1773
+ }
1774
+ /**
1775
+ * Add animated text to video
1776
+ */
1777
+ async addAnimatedTextToVideo(videoSource, options, onProgress) {
1778
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1779
+ if (!fs_1.default.existsSync(frameDir)) {
1780
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1781
+ }
1782
+ const timestamp = Date.now();
1783
+ const { videoPath, shouldCleanup: shouldCleanupVideo } = await resolveVideoSource(videoSource, frameDir, timestamp);
1784
+ const fontSize = options.fontSize || 24;
1785
+ const fontColor = options.fontColor || 'white';
1786
+ const bgColor = options.backgroundColor || 'black@0.5';
1787
+ const animation = options.animation || 'none';
1788
+ const duration = options.endTime - options.startTime;
1789
+ // Build position string
1790
+ let positionStr;
1791
+ if (typeof options.position === 'string') {
1792
+ const positionMap = {
1793
+ 'top-left': 'x=10:y=10',
1794
+ 'top-center': 'x=(w-text_w)/2:y=10',
1795
+ 'top-right': 'x=w-text_w-10:y=10',
1796
+ 'center': 'x=(w-text_w)/2:y=(h-text_h)/2',
1797
+ 'bottom-left': 'x=10:y=h-text_h-10',
1798
+ 'bottom-center': 'x=(w-text_w)/2:y=h-text_h-10',
1799
+ 'bottom-right': 'x=w-text_w-10:y=h-text_h-10'
1800
+ };
1801
+ positionStr = positionMap[options.position] || positionMap['bottom-center'];
1802
+ }
1803
+ else {
1804
+ positionStr = `x=${options.position?.x || 10}:y=${options.position?.y || 10}`;
1805
+ }
1806
+ // Build animation filter
1807
+ let animationFilter = '';
1808
+ if (animation === 'fade') {
1809
+ animationFilter = `:alpha='if(lt(t,${options.startTime}),0,if(lt(t,${options.startTime}+1), (t-${options.startTime})/1, if(lt(t,${options.endTime}-1), 1, if(lt(t,${options.endTime}), (${options.endTime}-t)/1, 0))))'`;
1810
+ }
1811
+ else if (animation === 'slide') {
1812
+ animationFilter = `:x='if(lt(t,${options.startTime}),-text_w,if(lt(t,${options.startTime}+1), (w-text_w)*(t-${options.startTime})/1, if(lt(t,${options.endTime}-1), w-text_w-10, if(lt(t,${options.endTime}), w-text_w-10+(w)*(t-${options.endTime})/1, w))))'`;
1813
+ }
1814
+ const textEscaped = options.text.replace(/:/g, '\\:').replace(/'/g, "\\'");
1815
+ const escapedVideoPath = videoPath.replace(/"/g, '\\"');
1816
+ const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
1817
+ const drawtextFilter = `drawtext=text='${textEscaped}':fontsize=${fontSize}:fontcolor=${fontColor}:box=1:boxcolor=${bgColor}:${positionStr}${animationFilter}:enable='between(t,${options.startTime},${options.endTime})'`;
1818
+ const command = `ffmpeg -i "${escapedVideoPath}" -vf "${drawtextFilter}" -y "${escapedOutputPath}"`;
1819
+ try {
1820
+ await this.executeFFmpegWithProgress(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 }, onProgress);
1821
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1822
+ fs_1.default.unlinkSync(videoPath);
1823
+ }
1824
+ return { outputPath: options.outputPath, success: true };
1825
+ }
1826
+ catch (error) {
1827
+ if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
1828
+ fs_1.default.unlinkSync(videoPath);
1829
+ }
1830
+ throw error;
1831
+ }
1832
+ }
1833
+ }
1834
+ exports.VideoHelpers = VideoHelpers;
1835
+ //# sourceMappingURL=videoHelpers.js.map