node-mac-recorder 2.22.15 → 2.22.16

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/binding.gyp CHANGED
@@ -11,6 +11,7 @@
11
11
  "src/audio_recorder.mm",
12
12
  "src/audio_mixer.mm",
13
13
  "src/cursor_tracker.mm",
14
+ "src/text_input_ax_snapshot.mm",
14
15
  "src/window_selector.mm"
15
16
  ],
16
17
  "include_dirs": [
@@ -7,6 +7,7 @@
7
7
  "src/electron_safe/screen_capture_electron.mm",
8
8
  "src/electron_safe/audio_capture_electron.mm",
9
9
  "src/electron_safe/cursor_tracker_electron.mm",
10
+ "src/text_input_ax_snapshot.mm",
10
11
  "src/electron_safe/window_selector_electron.mm"
11
12
  ],
12
13
  "include_dirs": [
@@ -1,6 +1,7 @@
1
1
  const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
+ const cursorCapturePolling = require("./lib/cursorCapture/polling");
4
5
 
5
6
  // Electron-safe native module loading
6
7
  let electronSafeNativeBinding;
@@ -41,6 +42,17 @@ class ElectronSafeMacRecorder extends EventEmitter {
41
42
  this.recordingTimer = null;
42
43
  this.recordingStartTime = null;
43
44
 
45
+ this.cursorCaptureInterval = null;
46
+ this.cursorCaptureFile = null;
47
+ this.cursorCaptureStartTime = null;
48
+ this.cursorCaptureFirstWrite = true;
49
+ this.lastCapturedData = null;
50
+ this.cursorDisplayInfo = null;
51
+ this.recordingDisplayInfo = null;
52
+ this.cursorCaptureSessionTimestamp = null;
53
+ this.sessionTimestamp = null;
54
+ this.syncTimestamp = null;
55
+
44
56
  this.options = {
45
57
  includeMicrophone: false,
46
58
  includeSystemAudio: false,
@@ -85,10 +97,8 @@ class ElectronSafeMacRecorder extends EventEmitter {
85
97
  throw new Error("Output path is required");
86
98
  }
87
99
 
88
- // Update options
89
100
  this.setOptions(options);
90
101
 
91
- // Ensure output directory exists
92
102
  const outputDir = path.dirname(outputPath);
93
103
  if (!fs.existsSync(outputDir)) {
94
104
  fs.mkdirSync(outputDir, { recursive: true });
@@ -96,63 +106,123 @@ class ElectronSafeMacRecorder extends EventEmitter {
96
106
 
97
107
  this.outputPath = outputPath;
98
108
 
99
- return new Promise((resolve, reject) => {
100
- try {
101
- console.log("🎬 Starting Electron-safe recording...");
102
- console.log("📁 Output path:", outputPath);
103
- console.log("⚙️ Options:", this.options);
104
-
105
- // Call native function with timeout protection
106
- const startTimeout = setTimeout(() => {
107
- this.isRecording = false;
108
- reject(new Error("Recording start timeout - Electron protection"));
109
- }, 10000); // 10 second timeout
110
-
111
- const success = electronSafeNativeBinding.startRecording(
112
- outputPath,
113
- this.options
114
- );
115
- clearTimeout(startTimeout);
116
-
117
- if (success) {
118
- this.isRecording = true;
119
- this.recordingStartTime = Date.now();
120
-
121
- // Start progress timer
122
- this.recordingTimer = setInterval(() => {
123
- const elapsed = Math.floor(
124
- (Date.now() - this.recordingStartTime) / 1000
125
- );
126
- this.emit("timeUpdate", elapsed);
127
- }, 1000);
128
-
129
- // Emit started event
130
- setTimeout(() => {
131
- this.emit("recordingStarted", {
132
- outputPath: this.outputPath,
133
- timestamp: this.recordingStartTime,
134
- options: this.options,
135
- electronSafe: true,
136
- });
137
- }, 100);
138
-
139
- this.emit("started", this.outputPath);
140
- console.log("✅ Electron-safe recording started successfully");
141
- resolve(this.outputPath);
142
- } else {
143
- console.error("❌ Failed to start Electron-safe recording");
144
- reject(new Error("Failed to start recording - check permissions"));
145
- }
146
- } catch (error) {
147
- console.error("❌ Exception during recording start:", error);
148
- this.isRecording = false;
149
- if (this.recordingTimer) {
150
- clearInterval(this.recordingTimer);
151
- this.recordingTimer = null;
152
- }
153
- reject(error);
109
+ console.log("🎬 Starting Electron-safe recording...");
110
+ console.log("📁 Output path:", outputPath);
111
+ console.log("⚙️ Options:", this.options);
112
+
113
+ const startTimeout = setTimeout(() => {
114
+ this.isRecording = false;
115
+ }, 10000);
116
+
117
+ let success = false;
118
+ try {
119
+ success = electronSafeNativeBinding.startRecording(
120
+ outputPath,
121
+ this.options,
122
+ );
123
+ } finally {
124
+ clearTimeout(startTimeout);
125
+ }
126
+
127
+ if (!success) {
128
+ console.error("❌ Failed to start Electron-safe recording");
129
+ throw new Error("Failed to start recording - check permissions");
130
+ }
131
+
132
+ this.isRecording = true;
133
+ this.recordingStartTime = Date.now();
134
+
135
+ this.recordingTimer = setInterval(() => {
136
+ const elapsed = Math.floor(
137
+ (Date.now() - this.recordingStartTime) / 1000,
138
+ );
139
+ this.emit("timeUpdate", elapsed);
140
+ }, 1000);
141
+
142
+ const sessionTs =
143
+ options.sessionTimestamp ||
144
+ this.sessionTimestamp ||
145
+ this.recordingStartTime;
146
+ this.sessionTimestamp = sessionTs;
147
+ const cursorFilePath = path.join(
148
+ outputDir,
149
+ `temp_cursor_${sessionTs}.json`,
150
+ );
151
+
152
+ let recordingDisplayInfo = null;
153
+ try {
154
+ const displays = await this.getDisplays();
155
+ const did = this.options.displayId;
156
+ let target = null;
157
+ if (did != null && did !== undefined) {
158
+ target = displays.find((d) => d.id === did);
159
+ }
160
+ if (!target) {
161
+ target = displays.find((d) => d.isPrimary) || displays[0];
162
+ }
163
+ if (target) {
164
+ recordingDisplayInfo = {
165
+ displayId: target.id,
166
+ x: target.x || 0,
167
+ y: target.y || 0,
168
+ width: target.width,
169
+ height: target.height,
170
+ logicalWidth: target.width,
171
+ logicalHeight: target.height,
172
+ };
154
173
  }
155
- });
174
+ } catch {
175
+ recordingDisplayInfo = null;
176
+ }
177
+ this.recordingDisplayInfo = recordingDisplayInfo;
178
+
179
+ const syncTimestamp = Date.now();
180
+ this.syncTimestamp = syncTimestamp;
181
+
182
+ try {
183
+ await cursorCapturePolling.startCursorCapture(
184
+ this,
185
+ electronSafeNativeBinding,
186
+ cursorFilePath,
187
+ {
188
+ videoRelative: !!recordingDisplayInfo,
189
+ displayInfo: recordingDisplayInfo,
190
+ recordingType: this.options.windowId
191
+ ? "window"
192
+ : this.options.captureArea
193
+ ? "area"
194
+ : "display",
195
+ captureArea: this.options.captureArea || null,
196
+ windowId: this.options.windowId || null,
197
+ startTimestamp: syncTimestamp,
198
+ },
199
+ );
200
+ } catch (cursorError) {
201
+ console.warn(
202
+ "⚠️ Cursor tracking failed to start:",
203
+ cursorError.message,
204
+ );
205
+ }
206
+
207
+ const startPayloadTs = this.syncTimestamp || this.recordingStartTime;
208
+ const fileTimestampPayload = this.sessionTimestamp;
209
+
210
+ setTimeout(() => {
211
+ this.emit("recordingStarted", {
212
+ outputPath: this.outputPath,
213
+ timestamp: startPayloadTs,
214
+ options: this.options,
215
+ electronSafe: true,
216
+ cursorOutputPath: cursorFilePath,
217
+ sessionTimestamp: fileTimestampPayload,
218
+ syncTimestamp: startPayloadTs,
219
+ fileTimestamp: fileTimestampPayload,
220
+ });
221
+ }, 100);
222
+
223
+ this.emit("started", this.outputPath);
224
+ console.log("✅ Electron-safe recording started successfully");
225
+ return this.outputPath;
156
226
  }
157
227
 
158
228
  /**
@@ -163,64 +233,66 @@ class ElectronSafeMacRecorder extends EventEmitter {
163
233
  throw new Error("No recording in progress");
164
234
  }
165
235
 
166
- return new Promise((resolve, reject) => {
167
- try {
168
- console.log("🛑 Stopping Electron-safe recording...");
169
-
170
- // Call native function with timeout protection
171
- const stopTimeout = setTimeout(() => {
172
- this.isRecording = false;
173
- if (this.recordingTimer) {
174
- clearInterval(this.recordingTimer);
175
- this.recordingTimer = null;
176
- }
177
- reject(new Error("Recording stop timeout - forced cleanup"));
178
- }, 10000); // 10 second timeout
179
-
180
- const success = electronSafeNativeBinding.stopRecording();
181
- clearTimeout(stopTimeout);
236
+ try {
237
+ console.log("🛑 Stopping Electron-safe recording...");
238
+
239
+ if (this.cursorCaptureInterval) {
240
+ try {
241
+ await cursorCapturePolling.stopCursorCapture(this);
242
+ } catch (cursorErr) {
243
+ console.warn(
244
+ "⚠️ Cursor capture stop:",
245
+ cursorErr.message,
246
+ );
247
+ }
248
+ }
182
249
 
183
- // Always cleanup
250
+ const stopTimeout = setTimeout(() => {
184
251
  this.isRecording = false;
185
252
  if (this.recordingTimer) {
186
253
  clearInterval(this.recordingTimer);
187
254
  this.recordingTimer = null;
188
255
  }
256
+ }, 10000);
189
257
 
190
- const result = {
191
- code: success ? 0 : 1,
192
- outputPath: this.outputPath,
193
- electronSafe: true,
194
- };
258
+ const success = electronSafeNativeBinding.stopRecording();
259
+ clearTimeout(stopTimeout);
195
260
 
196
- this.emit("stopped", result);
197
-
198
- if (success) {
199
- // Check if file exists
200
- setTimeout(() => {
201
- if (fs.existsSync(this.outputPath)) {
202
- this.emit("completed", this.outputPath);
203
- console.log("✅ Recording completed successfully");
204
- } else {
205
- console.warn("⚠️ Recording completed but file not found");
206
- }
207
- }, 1000);
208
- }
261
+ this.isRecording = false;
262
+ if (this.recordingTimer) {
263
+ clearInterval(this.recordingTimer);
264
+ this.recordingTimer = null;
265
+ }
209
266
 
210
- resolve(result);
211
- } catch (error) {
212
- console.error("❌ Exception during recording stop:", error);
267
+ const result = {
268
+ code: success ? 0 : 1,
269
+ outputPath: this.outputPath,
270
+ electronSafe: true,
271
+ };
213
272
 
214
- // Force cleanup
215
- this.isRecording = false;
216
- if (this.recordingTimer) {
217
- clearInterval(this.recordingTimer);
218
- this.recordingTimer = null;
219
- }
273
+ this.emit("stopped", result);
274
+
275
+ if (success) {
276
+ setTimeout(() => {
277
+ if (fs.existsSync(this.outputPath)) {
278
+ this.emit("completed", this.outputPath);
279
+ console.log("✅ Recording completed successfully");
280
+ } else {
281
+ console.warn("⚠️ Recording completed but file not found");
282
+ }
283
+ }, 1000);
284
+ }
220
285
 
221
- reject(error);
286
+ return result;
287
+ } catch (error) {
288
+ console.error("❌ Exception during recording stop:", error);
289
+ this.isRecording = false;
290
+ if (this.recordingTimer) {
291
+ clearInterval(this.recordingTimer);
292
+ this.recordingTimer = null;
222
293
  }
223
- });
294
+ throw error;
295
+ }
224
296
  }
225
297
 
226
298
  /**
@@ -380,6 +452,26 @@ class ElectronSafeMacRecorder extends EventEmitter {
380
452
  }
381
453
  }
382
454
 
455
+ async startCursorCapture(intervalOrFilepath, options = {}) {
456
+ if (!loadElectronSafeModule()) {
457
+ throw new Error("Failed to load Electron-safe native module");
458
+ }
459
+ return cursorCapturePolling.startCursorCapture(
460
+ this,
461
+ electronSafeNativeBinding,
462
+ intervalOrFilepath,
463
+ options,
464
+ );
465
+ }
466
+
467
+ async stopCursorCapture() {
468
+ loadElectronSafeModule();
469
+ if (!electronSafeNativeBinding) {
470
+ return false;
471
+ }
472
+ return cursorCapturePolling.stopCursorCapture(this);
473
+ }
474
+
383
475
  /**
384
476
  * Get module information
385
477
  */