simple-ffmpegjs 0.1.1 → 0.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.
package/src/lib/utils.js CHANGED
@@ -1,3 +1,6 @@
1
+ const { spawn } = require("child_process");
2
+ const { FFmpegError, ExportCancelledError } = require("../core/errors");
3
+
1
4
  const formatBytes = (bytes) => {
2
5
  if (!Number.isFinite(bytes)) return `${bytes}`;
3
6
  const units = ["B", "KB", "MB", "GB", "TB"];
@@ -10,4 +13,200 @@ const formatBytes = (bytes) => {
10
13
  return `${n.toFixed(n < 10 && i > 0 ? 2 : 1)} ${units[i]}`;
11
14
  };
12
15
 
13
- module.exports = { formatBytes };
16
+ /**
17
+ * Parse FFmpeg time string (HH:MM:SS.ms) to seconds
18
+ */
19
+ function parseFFmpegTime(timeStr) {
20
+ if (!timeStr) return 0;
21
+ const parts = timeStr.split(":");
22
+ if (parts.length === 3) {
23
+ const [hours, minutes, seconds] = parts;
24
+ return (
25
+ parseFloat(hours) * 3600 + parseFloat(minutes) * 60 + parseFloat(seconds)
26
+ );
27
+ }
28
+ return parseFloat(timeStr) || 0;
29
+ }
30
+
31
+ /**
32
+ * Parse FFmpeg progress line and extract metrics
33
+ */
34
+ function parseFFmpegProgress(line, totalDuration) {
35
+ const progress = {};
36
+
37
+ // Parse frame= 120
38
+ const frameMatch = line.match(/frame=\s*(\d+)/);
39
+ if (frameMatch) progress.frame = parseInt(frameMatch[1], 10);
40
+
41
+ // Parse fps=45.2
42
+ const fpsMatch = line.match(/fps=\s*([\d.]+)/);
43
+ if (fpsMatch) progress.fps = parseFloat(fpsMatch[1]);
44
+
45
+ // Parse time=00:00:04.00
46
+ const timeMatch = line.match(/time=\s*([\d:.]+)/);
47
+ if (timeMatch) {
48
+ progress.timeProcessed = parseFFmpegTime(timeMatch[1]);
49
+ if (totalDuration > 0) {
50
+ progress.percent = Math.min(
51
+ 100,
52
+ Math.round((progress.timeProcessed / totalDuration) * 100)
53
+ );
54
+ }
55
+ }
56
+
57
+ // Parse speed=1.5x
58
+ const speedMatch = line.match(/speed=\s*([\d.]+)x/);
59
+ if (speedMatch) progress.speed = parseFloat(speedMatch[1]);
60
+
61
+ // Parse bitrate=1234kbits/s
62
+ const bitrateMatch = line.match(/bitrate=\s*([\d.]+)kbits\/s/);
63
+ if (bitrateMatch) progress.bitrate = parseFloat(bitrateMatch[1]);
64
+
65
+ // Parse size= 1234kB
66
+ const sizeMatch = line.match(/size=\s*(\d+)kB/);
67
+ if (sizeMatch) progress.size = parseInt(sizeMatch[1], 10) * 1024;
68
+
69
+ return progress;
70
+ }
71
+
72
+ /**
73
+ * Run FFmpeg command with spawn, supporting progress callbacks and cancellation
74
+ * @param {Object} options
75
+ * @param {string} options.command - The full FFmpeg command string
76
+ * @param {number} options.totalDuration - Expected output duration in seconds (for progress %)
77
+ * @param {Function} options.onProgress - Progress callback
78
+ * @param {AbortSignal} options.signal - AbortSignal for cancellation
79
+ * @returns {Promise<{stdout: string, stderr: string}>}
80
+ */
81
+ function runFFmpeg({ command, totalDuration = 0, onProgress, signal }) {
82
+ return new Promise((resolve, reject) => {
83
+ // Parse command into args (simple split, assumes no quoted args with spaces in values)
84
+ // FFmpeg commands from this library don't have spaces in quoted paths handled this way
85
+ // We need to handle the command string properly
86
+ const args = parseFFmpegCommand(command);
87
+ const ffmpegPath = args.shift(); // Remove 'ffmpeg' from args
88
+
89
+ const proc = spawn(ffmpegPath, args, {
90
+ stdio: ["pipe", "pipe", "pipe"],
91
+ });
92
+
93
+ let stdout = "";
94
+ let stderr = "";
95
+ let cancelled = false;
96
+
97
+ // Handle cancellation
98
+ if (signal) {
99
+ const abortHandler = () => {
100
+ cancelled = true;
101
+ proc.kill("SIGTERM");
102
+ };
103
+
104
+ if (signal.aborted) {
105
+ proc.kill("SIGTERM");
106
+ reject(new ExportCancelledError());
107
+ return;
108
+ }
109
+
110
+ signal.addEventListener("abort", abortHandler, { once: true });
111
+
112
+ proc.on("close", () => {
113
+ signal.removeEventListener("abort", abortHandler);
114
+ });
115
+ }
116
+
117
+ proc.stdout.on("data", (data) => {
118
+ stdout += data.toString();
119
+ });
120
+
121
+ proc.stderr.on("data", (data) => {
122
+ const chunk = data.toString();
123
+ stderr += chunk;
124
+
125
+ // Parse progress from stderr (FFmpeg outputs progress to stderr)
126
+ if (onProgress && typeof onProgress === "function") {
127
+ const progress = parseFFmpegProgress(chunk, totalDuration);
128
+ if (Object.keys(progress).length > 0) {
129
+ onProgress(progress);
130
+ }
131
+ }
132
+ });
133
+
134
+ proc.on("error", (error) => {
135
+ reject(
136
+ new FFmpegError(`FFmpeg process error: ${error.message}`, {
137
+ stderr,
138
+ command,
139
+ })
140
+ );
141
+ });
142
+
143
+ proc.on("close", (code) => {
144
+ if (cancelled) {
145
+ reject(new ExportCancelledError());
146
+ return;
147
+ }
148
+
149
+ if (code !== 0) {
150
+ reject(
151
+ new FFmpegError(`FFmpeg exited with code ${code}`, {
152
+ stderr,
153
+ command,
154
+ exitCode: code,
155
+ })
156
+ );
157
+ return;
158
+ }
159
+
160
+ resolve({ stdout, stderr });
161
+ });
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Parse a command string into an array of arguments
167
+ * Handles quoted strings and escaped characters
168
+ */
169
+ function parseFFmpegCommand(command) {
170
+ const args = [];
171
+ let current = "";
172
+ let inQuote = false;
173
+ let quoteChar = "";
174
+
175
+ for (let i = 0; i < command.length; i++) {
176
+ const char = command[i];
177
+
178
+ if (inQuote) {
179
+ if (char === quoteChar) {
180
+ inQuote = false;
181
+ // Don't add the closing quote to the argument
182
+ } else {
183
+ current += char;
184
+ }
185
+ } else if (char === '"' || char === "'") {
186
+ inQuote = true;
187
+ quoteChar = char;
188
+ // Don't add the opening quote to the argument
189
+ } else if (char === " " || char === "\t") {
190
+ if (current.length > 0) {
191
+ args.push(current);
192
+ current = "";
193
+ }
194
+ } else {
195
+ current += char;
196
+ }
197
+ }
198
+
199
+ if (current.length > 0) {
200
+ args.push(current);
201
+ }
202
+
203
+ return args;
204
+ }
205
+
206
+ module.exports = {
207
+ formatBytes,
208
+ parseFFmpegTime,
209
+ parseFFmpegProgress,
210
+ runFFmpeg,
211
+ parseFFmpegCommand,
212
+ };
package/src/loaders.js CHANGED
@@ -118,12 +118,12 @@ function loadText(project, clipObj) {
118
118
  fontSize: clipObj.fontSize || C.DEFAULT_FONT_SIZE,
119
119
  fontColor: clipObj.fontColor || C.DEFAULT_FONT_COLOR,
120
120
  };
121
- if (typeof clipObj.centerX === "number") clip.centerX = clipObj.centerX;
121
+ if (typeof clipObj.xPercent === "number") clip.xPercent = clipObj.xPercent;
122
122
  else if (typeof clipObj.x === "number") clip.x = clipObj.x;
123
- else clip.centerX = 0;
124
- if (typeof clipObj.centerY === "number") clip.centerY = clipObj.centerY;
123
+ else clip.xPercent = 0.5; // Default to centered
124
+ if (typeof clipObj.yPercent === "number") clip.yPercent = clipObj.yPercent;
125
125
  else if (typeof clipObj.y === "number") clip.y = clipObj.y;
126
- else clip.centerY = 0;
126
+ else clip.yPercent = 0.5; // Default to centered
127
127
  project.textClips.push(clip);
128
128
  }
129
129