node-mac-recorder 2.4.0 → 2.4.2

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/index.js CHANGED
@@ -5,16 +5,25 @@ const fs = require("fs");
5
5
  // Native modülü yükle
6
6
  let nativeBinding;
7
7
  try {
8
- nativeBinding = require("./build/Release/mac_recorder.node");
8
+ // Prefer prebuild on arm64
9
+ if (process.platform === "darwin" && process.arch === "arm64") {
10
+ nativeBinding = require("./prebuilds/darwin-arm64/node.napi.node");
11
+ } else {
12
+ nativeBinding = require("./build/Release/mac_recorder.node");
13
+ }
9
14
  } catch (error) {
10
15
  try {
11
- nativeBinding = require("./build/Debug/mac_recorder.node");
12
- } catch (debugError) {
13
- throw new Error(
14
- 'Native module not found. Please run "npm run build" to compile the native module.\n' +
15
- "Original error: " +
16
- error.message
17
- );
16
+ nativeBinding = require("./build/Release/mac_recorder.node");
17
+ } catch (_) {
18
+ try {
19
+ nativeBinding = require("./build/Debug/mac_recorder.node");
20
+ } catch (debugError) {
21
+ throw new Error(
22
+ 'Native module not found. Please run "npm run build" to compile the native module.\n' +
23
+ "Original error: " +
24
+ error.message
25
+ );
26
+ }
18
27
  }
19
28
  }
20
29
 
package/install.js CHANGED
@@ -2,7 +2,7 @@ const { spawn } = require("child_process");
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
 
5
- console.log("🔨 Building native macOS recorder module...\n");
5
+ console.log("🔨 Installing node-mac-recorder...\n");
6
6
 
7
7
  // Check if we're on macOS
8
8
  if (process.platform !== "darwin") {
@@ -10,7 +10,24 @@ if (process.platform !== "darwin") {
10
10
  process.exit(1);
11
11
  }
12
12
 
13
- // Check if Xcode Command Line Tools are installed
13
+ // Prefer prebuilds on supported platforms
14
+ const prebuildPath = path.join(
15
+ __dirname,
16
+ "prebuilds",
17
+ `darwin-${process.arch}`,
18
+ "node.napi.node"
19
+ );
20
+ if (
21
+ process.platform === "darwin" &&
22
+ process.arch === "arm64" &&
23
+ fs.existsSync(prebuildPath)
24
+ ) {
25
+ console.log("✅ Using prebuilt binary:", prebuildPath);
26
+ console.log("🎉 node-mac-recorder is ready to use (no compilation needed)");
27
+ process.exit(0);
28
+ }
29
+
30
+ // Fallback to building from source
14
31
  console.log("🔍 Checking Xcode Command Line Tools...");
15
32
  const xcodebuild = spawn("xcode-select", ["--print-path"], { stdio: "pipe" });
16
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -37,6 +37,9 @@
37
37
  "install": "node install.js",
38
38
  "build": "node-gyp build",
39
39
  "rebuild": "node-gyp rebuild",
40
+ "prebuild:node-arm64": "prebuildify --platform darwin --arch arm64 --napi --strip",
41
+ "prebuild:electron-arm64": "prebuildify --platform darwin --arch arm64 --napi --strip --targets electron@27.0.0",
42
+ "prebuild:all-arm64": "npm run prebuild:node-arm64 && npm run prebuild:electron-arm64",
40
43
  "clean": "node-gyp clean",
41
44
  "test:window-selector": "node window-selector-test.js",
42
45
  "example:window-selector": "node examples/window-selector-example.js"
@@ -45,7 +48,8 @@
45
48
  "node-addon-api": "^7.0.0"
46
49
  },
47
50
  "devDependencies": {
48
- "node-gyp": "^10.0.0"
51
+ "node-gyp": "^10.0.0",
52
+ "prebuildify": "^6.0.1"
49
53
  },
50
54
  "gypfile": true
51
55
  }
@@ -124,29 +124,71 @@ API_AVAILABLE(macos(12.3))
124
124
  static SCStream *g_scStream = nil;
125
125
  static SCKRecorderDelegate *g_scDelegate = nil;
126
126
  static bool g_isRecording = false;
127
+ static BOOL g_screenOutputAttached = NO;
128
+ static BOOL g_audioOutputAttached = NO;
129
+ static dispatch_queue_t g_outputQueue = NULL; // use a dedicated serial queue for sample handling
127
130
 
128
131
  // Helper function to cleanup ScreenCaptureKit recording resources
129
132
  void cleanupSCKRecording() {
130
133
  NSLog(@"🛑 Cleaning up ScreenCaptureKit recording");
131
-
134
+
135
+ // Detach outputs first to prevent further callbacks into the delegate
136
+ if (g_scStream && g_scDelegate) {
137
+ NSError *rmError = nil;
138
+ if (g_screenOutputAttached) {
139
+ [g_scStream removeStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen error:&rmError];
140
+ g_screenOutputAttached = NO;
141
+ }
142
+ if (g_audioOutputAttached) {
143
+ rmError = nil;
144
+ [g_scStream removeStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio error:&rmError];
145
+ g_audioOutputAttached = NO;
146
+ }
147
+ }
148
+
132
149
  if (g_scStream) {
133
150
  NSLog(@"🛑 Stopping SCStream");
134
- [g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
151
+ SCStream *streamToStop = g_scStream; // keep local until stop completes
152
+ [streamToStop stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
135
153
  if (error) {
136
154
  NSLog(@"❌ Error stopping SCStream: %@", error.localizedDescription);
137
155
  } else {
138
156
  NSLog(@"✅ SCStream stopped successfully");
139
157
  }
158
+
159
+ // Finish writer after stream has stopped to ensure no further buffers arrive
160
+ if (g_scDelegate && g_scDelegate.assetWriter && g_scDelegate.isWriting) {
161
+ NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
162
+ g_scDelegate.isWriting = NO;
163
+
164
+ if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
165
+ if (g_scDelegate.videoInput) {
166
+ [g_scDelegate.videoInput markAsFinished];
167
+ }
168
+ if (g_scDelegate.audioInput) {
169
+ [g_scDelegate.audioInput markAsFinished];
170
+ }
171
+
172
+ [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
173
+ NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
174
+ if (g_scDelegate.assetWriter.error) {
175
+ NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
176
+ }
177
+ }];
178
+ } else if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
179
+ NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
180
+ }
181
+ }
182
+
183
+ g_isRecording = false;
184
+ g_scStream = nil;
185
+ g_scDelegate = nil;
140
186
  }];
141
- g_scStream = nil;
142
- }
143
-
144
- if (g_scDelegate) {
145
- if (g_scDelegate.assetWriter && g_scDelegate.isWriting) {
187
+ } else {
188
+ // No stream, just finalize writer if needed
189
+ if (g_scDelegate && g_scDelegate.assetWriter && g_scDelegate.isWriting) {
146
190
  NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
147
191
  g_scDelegate.isWriting = NO;
148
-
149
- // Only mark inputs as finished if asset writer is actually writing
150
192
  if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
151
193
  if (g_scDelegate.videoInput) {
152
194
  [g_scDelegate.videoInput markAsFinished];
@@ -154,23 +196,12 @@ void cleanupSCKRecording() {
154
196
  if (g_scDelegate.audioInput) {
155
197
  [g_scDelegate.audioInput markAsFinished];
156
198
  }
157
-
158
- [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
159
- NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
160
- if (g_scDelegate.assetWriter.error) {
161
- NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
162
- }
163
- }];
164
- } else {
165
- NSLog(@"⚠️ Asset writer not in writing status, cannot finish normally");
166
- if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
167
- NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
168
- }
199
+ [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{}];
169
200
  }
170
201
  }
202
+ g_isRecording = false;
171
203
  g_scDelegate = nil;
172
204
  }
173
- g_isRecording = false;
174
205
  }
175
206
 
176
207
  // Check if ScreenCaptureKit is available
@@ -184,7 +215,7 @@ bool isScreenCaptureKitAvailable() {
184
215
  // NAPI Function: Start Recording with ScreenCaptureKit
185
216
  Napi::Value StartRecording(const Napi::CallbackInfo& info) {
186
217
  Napi::Env env = info.Env();
187
-
218
+ @autoreleasepool {
188
219
  if (!isScreenCaptureKitAvailable()) {
189
220
  NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
190
221
  return Napi::Boolean::New(env, false);
@@ -472,19 +503,22 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
472
503
  }
473
504
  }
474
505
 
475
- // Create callback queue for the delegate
476
- dispatch_queue_t delegateQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
477
-
506
+ // Create a dedicated serial queue for output callbacks
507
+ if (g_outputQueue == NULL) {
508
+ g_outputQueue = dispatch_queue_create("com.node-mac-recorder.stream-output", DISPATCH_QUEUE_SERIAL);
509
+ }
510
+
478
511
  // Create and start stream first
479
512
  g_scStream = [[SCStream alloc] initWithFilter:contentFilter configuration:config delegate:g_scDelegate];
480
513
 
481
514
  // Attach outputs to actually receive sample buffers
482
515
  NSLog(@"✅ Setting up stream output callback for sample buffers");
483
- dispatch_queue_t outputQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
516
+ dispatch_queue_t outputQueue = g_outputQueue;
484
517
  NSError *outputError = nil;
485
518
  BOOL addedScreenOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen sampleHandlerQueue:outputQueue error:&outputError];
486
519
  if (addedScreenOutput) {
487
520
  NSLog(@"✅ Screen output attached to SCStream");
521
+ g_screenOutputAttached = YES;
488
522
  } else {
489
523
  NSLog(@"❌ Failed to attach screen output to SCStream: %@", outputError.localizedDescription);
490
524
  }
@@ -493,6 +527,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
493
527
  BOOL addedAudioOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio sampleHandlerQueue:outputQueue error:&outputError];
494
528
  if (addedAudioOutput) {
495
529
  NSLog(@"✅ Audio output attached to SCStream");
530
+ g_audioOutputAttached = YES;
496
531
  } else {
497
532
  NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
498
533
  }
@@ -546,6 +581,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
546
581
 
547
582
  NSLog(@"🎬 Recording initialized successfully");
548
583
  return Napi::Boolean::New(env, true);
584
+ }
549
585
  }
550
586
 
551
587
  // NAPI Function: Stop Recording
@@ -560,6 +596,12 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
560
596
  return Napi::Boolean::New(env, true);
561
597
  }
562
598
 
599
+ // NAPI Function: Get Recording Status (for JS compatibility)
600
+ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
601
+ Napi::Env env = info.Env();
602
+ return Napi::Boolean::New(env, g_isRecording);
603
+ }
604
+
563
605
  // NAPI Function: Get Recording Status
564
606
  Napi::Value IsRecording(const Napi::CallbackInfo& info) {
565
607
  Napi::Env env = info.Env();
@@ -741,6 +783,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
741
783
  exports.Set("startRecording", Napi::Function::New(env, StartRecording));
742
784
  exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
743
785
  exports.Set("isRecording", Napi::Function::New(env, IsRecording));
786
+ exports.Set("getRecordingStatus", Napi::Function::New(env, GetRecordingStatus));
744
787
  exports.Set("getDisplays", Napi::Function::New(env, GetDisplays));
745
788
  exports.Set("getWindows", Napi::Function::New(env, GetWindows));
746
789
  exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
@@ -1,19 +1,27 @@
1
1
  const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
 
4
- // Native modülü yükle
4
+ // Native modülü yükle (arm64 prebuild öncelikli)
5
5
  let nativeBinding;
6
6
  try {
7
- nativeBinding = require("./build/Release/mac_recorder.node");
7
+ if (process.platform === "darwin" && process.arch === "arm64") {
8
+ nativeBinding = require("./prebuilds/darwin-arm64/node.napi.node");
9
+ } else {
10
+ nativeBinding = require("./build/Release/mac_recorder.node");
11
+ }
8
12
  } catch (error) {
9
13
  try {
10
- nativeBinding = require("./build/Debug/mac_recorder.node");
11
- } catch (debugError) {
12
- throw new Error(
13
- 'Native module not found. Please run "npm run build" to compile the native module.\n' +
14
- "Original error: " +
15
- error.message
16
- );
14
+ nativeBinding = require("./build/Release/mac_recorder.node");
15
+ } catch (_) {
16
+ try {
17
+ nativeBinding = require("./build/Debug/mac_recorder.node");
18
+ } catch (debugError) {
19
+ throw new Error(
20
+ 'Native module not found. Please run "npm run build" to compile the native module.\n' +
21
+ "Original error: " +
22
+ error.message
23
+ );
24
+ }
17
25
  }
18
26
  }
19
27
 
@@ -40,11 +48,11 @@ class WindowSelector extends EventEmitter {
40
48
  try {
41
49
  // Native window selection başlat
42
50
  const success = nativeBinding.startWindowSelection();
43
-
51
+
44
52
  if (success) {
45
53
  this.isSelecting = true;
46
54
  this.selectedWindow = null;
47
-
55
+
48
56
  // Status polling timer başlat (higher frequency for overlay updates)
49
57
  this.selectionTimer = setInterval(() => {
50
58
  this.checkSelectionStatus();
@@ -72,7 +80,7 @@ class WindowSelector extends EventEmitter {
72
80
  return new Promise((resolve, reject) => {
73
81
  try {
74
82
  const success = nativeBinding.stopWindowSelection();
75
-
83
+
76
84
  // Timer'ı durdur
77
85
  if (this.selectionTimer) {
78
86
  clearInterval(this.selectionTimer);
@@ -98,14 +106,14 @@ class WindowSelector extends EventEmitter {
98
106
 
99
107
  try {
100
108
  const status = nativeBinding.getWindowSelectionStatus();
101
-
109
+
102
110
  // Seçim tamamlandı mı kontrol et
103
111
  if (status.hasSelectedWindow && !this.selectedWindow) {
104
112
  const windowInfo = nativeBinding.getSelectedWindowInfo();
105
113
  if (windowInfo) {
106
114
  this.selectedWindow = windowInfo;
107
115
  this.isSelecting = false;
108
-
116
+
109
117
  // Timer'ı durdur
110
118
  if (this.selectionTimer) {
111
119
  clearInterval(this.selectionTimer);
@@ -121,17 +129,20 @@ class WindowSelector extends EventEmitter {
121
129
  if (this.lastStatus) {
122
130
  const lastWindow = this.lastStatus.currentWindow;
123
131
  const currentWindow = status.currentWindow;
124
-
132
+
125
133
  if (!lastWindow && currentWindow) {
126
134
  // Yeni pencere üstüne gelindi
127
135
  this.emit("windowEntered", currentWindow);
128
136
  } else if (lastWindow && !currentWindow) {
129
137
  // Pencere üstünden ayrıldı
130
138
  this.emit("windowLeft", lastWindow);
131
- } else if (lastWindow && currentWindow &&
132
- (lastWindow.id !== currentWindow.id ||
133
- lastWindow.title !== currentWindow.title ||
134
- lastWindow.appName !== currentWindow.appName)) {
139
+ } else if (
140
+ lastWindow &&
141
+ currentWindow &&
142
+ (lastWindow.id !== currentWindow.id ||
143
+ lastWindow.title !== currentWindow.title ||
144
+ lastWindow.appName !== currentWindow.appName)
145
+ ) {
135
146
  // Farklı bir pencereye geçildi
136
147
  this.emit("windowLeft", lastWindow);
137
148
  this.emit("windowEntered", currentWindow);
@@ -164,14 +175,14 @@ class WindowSelector extends EventEmitter {
164
175
  isSelecting: this.isSelecting && nativeStatus.isSelecting,
165
176
  hasSelectedWindow: !!this.selectedWindow,
166
177
  selectedWindow: this.selectedWindow,
167
- nativeStatus: nativeStatus
178
+ nativeStatus: nativeStatus,
168
179
  };
169
180
  } catch (error) {
170
181
  return {
171
182
  isSelecting: this.isSelecting,
172
183
  hasSelectedWindow: !!this.selectedWindow,
173
184
  selectedWindow: this.selectedWindow,
174
- error: error.message
185
+ error: error.message,
175
186
  };
176
187
  }
177
188
  }
@@ -205,7 +216,6 @@ class WindowSelector extends EventEmitter {
205
216
 
206
217
  // Seçimi başlat
207
218
  await this.startSelection();
208
-
209
219
  } catch (error) {
210
220
  this.removeAllListeners("windowSelected");
211
221
  this.removeAllListeners("error");
@@ -242,7 +252,9 @@ class WindowSelector extends EventEmitter {
242
252
  nativeBinding.setBringToFrontEnabled(enabled);
243
253
  // Only log if explicitly setting, not on startup
244
254
  if (arguments.length > 0) {
245
- console.log(`🔄 Auto bring-to-front: ${enabled ? 'ENABLED' : 'DISABLED'}`);
255
+ console.log(
256
+ `🔄 Auto bring-to-front: ${enabled ? "ENABLED" : "DISABLED"}`
257
+ );
246
258
  }
247
259
  } catch (error) {
248
260
  throw new Error(`Failed to set bring to front: ${error.message}`);
@@ -376,14 +388,14 @@ class WindowSelector extends EventEmitter {
376
388
  try {
377
389
  // Start screen selection
378
390
  await this.startScreenSelection();
379
-
391
+
380
392
  // Poll for selection completion
381
393
  return new Promise((resolve, reject) => {
382
394
  let isResolved = false;
383
-
395
+
384
396
  const checkSelection = () => {
385
397
  if (isResolved) return; // Prevent multiple resolutions
386
-
398
+
387
399
  const selectedScreen = this.getSelectedScreen();
388
400
  if (selectedScreen) {
389
401
  isResolved = true;
@@ -394,19 +406,19 @@ class WindowSelector extends EventEmitter {
394
406
  } else {
395
407
  // Selection was cancelled (probably ESC key)
396
408
  isResolved = true;
397
- reject(new Error('Screen selection was cancelled'));
409
+ reject(new Error("Screen selection was cancelled"));
398
410
  }
399
411
  };
400
-
412
+
401
413
  // Start polling
402
414
  checkSelection();
403
-
415
+
404
416
  // Timeout after 60 seconds
405
417
  setTimeout(() => {
406
418
  if (!isResolved) {
407
419
  isResolved = true;
408
420
  this.stopScreenSelection();
409
- reject(new Error('Screen selection timed out'));
421
+ reject(new Error("Screen selection timed out"));
410
422
  }
411
423
  }, 60000);
412
424
  });
@@ -430,7 +442,9 @@ class WindowSelector extends EventEmitter {
430
442
  const success = nativeBinding.showScreenRecordingPreview(screenInfo);
431
443
  return success;
432
444
  } catch (error) {
433
- throw new Error(`Failed to show screen recording preview: ${error.message}`);
445
+ throw new Error(
446
+ `Failed to show screen recording preview: ${error.message}`
447
+ );
434
448
  }
435
449
  }
436
450
 
@@ -443,7 +457,9 @@ class WindowSelector extends EventEmitter {
443
457
  const success = nativeBinding.hideScreenRecordingPreview();
444
458
  return success;
445
459
  } catch (error) {
446
- throw new Error(`Failed to hide screen recording preview: ${error.message}`);
460
+ throw new Error(
461
+ `Failed to hide screen recording preview: ${error.message}`
462
+ );
447
463
  }
448
464
  }
449
465
 
@@ -460,10 +476,10 @@ class WindowSelector extends EventEmitter {
460
476
  return {
461
477
  screenRecording: false,
462
478
  accessibility: false,
463
- error: error.message
479
+ error: error.message,
464
480
  };
465
481
  }
466
482
  }
467
483
  }
468
484
 
469
- module.exports = WindowSelector;
485
+ module.exports = WindowSelector;