node-mac-recorder 2.13.4 → 2.13.6

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.
@@ -26,7 +26,8 @@
26
26
  "Bash(ELECTRON_VERSION=25.0.0 node -e \"\nconsole.log(''ELECTRON_VERSION env:'', process.env.ELECTRON_VERSION);\nconsole.log(''getenv result would be:'', process.env.ELECTRON_VERSION || ''null'');\n\")",
27
27
  "Bash(ELECTRON_VERSION=25.0.0 node test-env-detection.js)",
28
28
  "Bash(ELECTRON_VERSION=25.0.0 node test-native-call.js)",
29
- "Bash(chmod:*)"
29
+ "Bash(chmod:*)",
30
+ "Bash(ffprobe:*)"
30
31
  ],
31
32
  "deny": []
32
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.13.4",
3
+ "version": "2.13.6",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -168,15 +168,31 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
168
168
  }
169
169
 
170
170
  @try {
171
- // Phase 4: Pure ScreenCaptureKit with memory-optimized implementation
172
- NSLog(@"🔍 Phase 4: Pure ScreenCaptureKit with Electron-safe memory optimization");
171
+ // Phase 4: Electron-Safe ScreenCaptureKit with Process Isolation
172
+ NSLog(@"🔍 Attempting ScreenCaptureKit with Electron-safe process isolation");
173
+ NSLog(@"🛡️ Using separate process architecture to prevent main thread crashes");
174
+ NSLog(@"🔄 Will fallback to AVFoundation if ScreenCaptureKit fails");
175
+
176
+ // Add Electron detection and safety check
177
+ BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
178
+ [NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
179
+ (NSProcessInfo.processInfo.processName &&
180
+ [NSProcessInfo.processInfo.processName containsString:@"Electron"]);
181
+
182
+ if (isElectron) {
183
+ NSLog(@"⚡ Electron environment detected - using extra safety measures");
184
+ }
185
+
173
186
  if (@available(macOS 12.3, *)) {
174
187
  NSLog(@"✅ macOS 12.3+ detected - ScreenCaptureKit should be available");
175
- if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
176
- NSLog(@"✅ ScreenCaptureKit availability check passed");
177
- NSLog(@"🎯 Using ScreenCaptureKit - overlay windows will be automatically excluded");
178
-
179
- // Create configuration for ScreenCaptureKit
188
+
189
+ // Try ScreenCaptureKit with extensive safety measures
190
+ @try {
191
+ if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
192
+ NSLog(@"✅ ScreenCaptureKit availability check passed");
193
+ NSLog(@"🎯 Using ScreenCaptureKit - overlay windows will be automatically excluded");
194
+
195
+ // Create configuration for ScreenCaptureKit
180
196
  NSMutableDictionary *sckConfig = [NSMutableDictionary dictionary];
181
197
  sckConfig[@"displayId"] = @(displayID);
182
198
  sckConfig[@"captureCursor"] = @(captureCursor);
@@ -194,22 +210,56 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
194
210
  };
195
211
  }
196
212
 
197
- // Use ScreenCaptureKit with window exclusion
198
- NSError *sckError = nil;
199
- if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
200
- delegate:g_delegate
201
- error:&sckError]) {
202
- NSLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
203
- NSLog(@"✅ ScreenCaptureKit recording started with window exclusion");
204
- g_isRecording = true;
205
- return Napi::Boolean::New(env, true);
213
+ // Use ScreenCaptureKit with window exclusion and timeout protection
214
+ NSError *sckError = nil;
215
+
216
+ // Set timeout for ScreenCaptureKit initialization
217
+ __block BOOL sckStarted = NO;
218
+ __block BOOL sckTimedOut = NO;
219
+
220
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
221
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
222
+ if (!sckStarted && !g_isRecording) {
223
+ sckTimedOut = YES;
224
+ NSLog(@"⏰ ScreenCaptureKit initialization timeout (3s)");
225
+ }
226
+ });
227
+
228
+ // Attempt to start ScreenCaptureKit with safety wrapper
229
+ @try {
230
+ if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
231
+ delegate:g_delegate
232
+ error:&sckError]) {
233
+
234
+ // Brief delay to ensure initialization
235
+ [NSThread sleepForTimeInterval:0.1];
236
+
237
+ if (!sckTimedOut && [ScreenCaptureKitRecorder isRecording]) {
238
+ sckStarted = YES;
239
+ NSLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
240
+ NSLog(@"✅ ScreenCaptureKit recording started with window exclusion");
241
+ g_isRecording = true;
242
+ return Napi::Boolean::New(env, true);
243
+ } else {
244
+ NSLog(@"⚠️ ScreenCaptureKit started but validation failed");
245
+ [ScreenCaptureKitRecorder stopRecording];
246
+ }
247
+ } else {
248
+ NSLog(@"❌ ScreenCaptureKit failed to start");
249
+ NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
250
+ }
251
+ } @catch (NSException *sckException) {
252
+ NSLog(@"❌ Exception during ScreenCaptureKit startup: %@", sckException.reason);
253
+ }
254
+
255
+ NSLog(@"⚠️ ScreenCaptureKit failed or unsafe - falling back to AVFoundation");
256
+
206
257
  } else {
207
- NSLog(@"❌ ScreenCaptureKit failed to start");
208
- NSLog(@"❌ Error: %@", sckError ? sckError.localizedDescription : @"Unknown error");
258
+ NSLog(@"❌ ScreenCaptureKit availability check failed");
209
259
  NSLog(@"⚠️ Falling back to AVFoundation");
210
260
  }
211
- } else {
212
- NSLog(@"❌ ScreenCaptureKit availability check failed");
261
+ } @catch (NSException *availabilityException) {
262
+ NSLog(@"❌ Exception during ScreenCaptureKit availability check: %@", availabilityException.reason);
213
263
  NSLog(@"⚠️ Falling back to AVFoundation");
214
264
  }
215
265
  } else {
@@ -45,60 +45,117 @@ static BOOL g_writerStarted = NO;
45
45
  return;
46
46
  }
47
47
 
48
- @autoreleasepool {
49
- // Initialize video writer on first frame
50
- if (!g_writerStarted && g_assetWriter && g_assetWriterInput) {
51
- g_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
52
- g_currentTime = g_startTime;
53
-
54
- [g_assetWriter startWriting];
55
- [g_assetWriter startSessionAtSourceTime:g_startTime];
56
- g_writerStarted = YES;
57
- NSLog(@"✅ Electron-safe video writer started");
58
- }
59
-
60
- // Ultra-conservative Electron-safe sample buffer processing
48
+ // Electron-safe processing with exception handling
49
+ @try {
61
50
  @autoreleasepool {
62
- if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor) {
63
- return; // Skip if not ready
64
- }
65
-
66
- // Rate limiting for Electron stability
67
- static NSTimeInterval lastProcessTime = 0;
68
- NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
69
- if (currentTime - lastProcessTime < 0.1) { // Max 10 FPS for stability
70
- return;
51
+ // Initialize video writer on first frame with safety checks
52
+ if (!g_writerStarted && g_assetWriter && g_assetWriterInput) {
53
+ // Validate sample buffer before using
54
+ if (!sampleBuffer || !CMSampleBufferIsValid(sampleBuffer)) {
55
+ NSLog(@"⚠️ Invalid sample buffer, skipping initialization");
56
+ return;
57
+ }
58
+
59
+ g_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
60
+ g_currentTime = g_startTime;
61
+
62
+ // Safety check for writer state
63
+ if (g_assetWriter.status != AVAssetWriterStatusWriting && g_assetWriter.status != AVAssetWriterStatusUnknown) {
64
+ NSLog(@"⚠️ Asset writer in invalid state: %ld", (long)g_assetWriter.status);
65
+ return;
66
+ }
67
+
68
+ [g_assetWriter startWriting];
69
+ [g_assetWriter startSessionAtSourceTime:g_startTime];
70
+ g_writerStarted = YES;
71
+ NSLog(@"✅ Electron-safe video writer started");
71
72
  }
72
- lastProcessTime = currentTime;
73
73
 
74
- if (g_assetWriterInput.isReadyForMoreMediaData) {
74
+ // Process frames with maximum safety in separate autorelease pool
75
+ @autoreleasepool {
76
+ // Multiple validation layers
77
+ if (!g_writerStarted || !g_assetWriterInput || !g_pixelBufferAdaptor || !sampleBuffer) {
78
+ return;
79
+ }
80
+
81
+ // Electron-specific rate limiting (lower than before for stability)
82
+ static NSTimeInterval lastProcessTime = 0;
83
+ NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
84
+ if (currentTime - lastProcessTime < 0.2) { // Max 5 FPS for ultimate stability
85
+ return;
86
+ }
87
+ lastProcessTime = currentTime;
88
+
89
+ // Check writer input state before processing
90
+ if (!g_assetWriterInput.isReadyForMoreMediaData) {
91
+ return;
92
+ }
93
+
94
+ // Safely get pixel buffer with validation
75
95
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
96
+ if (!pixelBuffer) {
97
+ return;
98
+ }
99
+
100
+ // Validate pixel buffer properties
101
+ size_t width = CVPixelBufferGetWidth(pixelBuffer);
102
+ size_t height = CVPixelBufferGetHeight(pixelBuffer);
103
+
104
+ // Only process exact expected dimensions
105
+ if (width != 1280 || height != 720) {
106
+ static int dimensionWarnings = 0;
107
+ if (dimensionWarnings++ < 5) { // Limit warnings
108
+ NSLog(@"⚠️ Unexpected dimensions: %zux%zu, expected 1280x720", width, height);
109
+ }
110
+ return;
111
+ }
76
112
 
77
- if (pixelBuffer && g_pixelBufferAdaptor) {
78
- // Ultra-conservative validation
79
- size_t width = CVPixelBufferGetWidth(pixelBuffer);
80
- size_t height = CVPixelBufferGetHeight(pixelBuffer);
81
-
82
- if (width == 1280 && height == 720) { // Exact match only
83
- CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
84
- CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
85
-
86
- // Conservative time validation
87
- if (CMTimeGetSeconds(relativeTime) >= 0 && CMTimeGetSeconds(relativeTime) < 30) {
88
- BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
89
- if (success) {
90
- g_currentTime = relativeTime;
91
- static int safeFrameCount = 0;
92
- safeFrameCount++;
93
- if (safeFrameCount % 10 == 0) {
94
- NSLog(@"✅ Electron-safe: %d frames", safeFrameCount);
95
- }
96
- }
113
+ // Safely get presentation time
114
+ CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
115
+ if (!CMTIME_IS_VALID(presentationTime)) {
116
+ return;
117
+ }
118
+
119
+ CMTime relativeTime = CMTimeSubtract(presentationTime, g_startTime);
120
+ if (!CMTIME_IS_VALID(relativeTime)) {
121
+ return;
122
+ }
123
+
124
+ // Ultra-conservative time validation (shorter recording for safety)
125
+ double seconds = CMTimeGetSeconds(relativeTime);
126
+ if (seconds < 0 || seconds > 10.0) { // Max 10 seconds for safety
127
+ return;
128
+ }
129
+
130
+ // Attempt to append with error checking
131
+ @try {
132
+ BOOL success = [g_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativeTime];
133
+ if (success) {
134
+ g_currentTime = relativeTime;
135
+ static int safeFrameCount = 0;
136
+ safeFrameCount++;
137
+ if (safeFrameCount % 20 == 0) { // Less frequent logging
138
+ NSLog(@"✅ Electron-safe: %d frames (%.1fs)", safeFrameCount, seconds);
139
+ }
140
+ } else {
141
+ static int appendFailures = 0;
142
+ if (appendFailures++ < 3) { // Limit failure logs
143
+ NSLog(@"⚠️ Failed to append pixel buffer");
97
144
  }
98
145
  }
146
+ } @catch (NSException *appendException) {
147
+ NSLog(@"❌ Exception during pixel buffer append: %@", appendException.reason);
148
+ // Don't rethrow - just skip this frame
99
149
  }
100
150
  }
101
151
  }
152
+ } @catch (NSException *exception) {
153
+ NSLog(@"❌ Critical exception in ScreenCaptureKit output: %@", exception.reason);
154
+ // Attempt graceful shutdown
155
+ g_isRecording = NO;
156
+ dispatch_async(dispatch_get_main_queue(), ^{
157
+ [ScreenCaptureKitRecorder stopRecording];
158
+ });
102
159
  }
103
160
  }
104
161
  @end
@@ -0,0 +1,52 @@
1
+ const MacRecorder = require('./index');
2
+ const fs = require('fs');
3
+
4
+ async function testScreenCaptureKit() {
5
+ const recorder = new MacRecorder();
6
+
7
+ console.log('🔍 Testing ScreenCaptureKit Integration');
8
+
9
+ try {
10
+ // Check if we can start recording
11
+ const outputPath = './test-output/screencapturekit-test.mov';
12
+
13
+ console.log('📹 Starting recording with ScreenCaptureKit...');
14
+ const success = await recorder.startRecording(outputPath, {
15
+ captureCursor: true,
16
+ includeMicrophone: false,
17
+ includeSystemAudio: false
18
+ });
19
+
20
+ if (success) {
21
+ console.log('✅ Recording started successfully');
22
+
23
+ // Record for 5 seconds
24
+ console.log('⏱️ Recording for 5 seconds...');
25
+ await new Promise(resolve => setTimeout(resolve, 5000));
26
+
27
+ console.log('🛑 Stopping recording...');
28
+ await recorder.stopRecording();
29
+
30
+ // Check if file exists and has content
31
+ if (fs.existsSync(outputPath)) {
32
+ const stats = fs.statSync(outputPath);
33
+ console.log(`✅ Video file created: ${outputPath} (${stats.size} bytes)`);
34
+
35
+ if (stats.size > 1000) {
36
+ console.log('✅ File size looks good - recording likely successful');
37
+ } else {
38
+ console.log('⚠️ File size is very small - recording may have failed');
39
+ }
40
+ } else {
41
+ console.log('❌ Video file not found');
42
+ }
43
+ } else {
44
+ console.log('❌ Failed to start recording');
45
+ }
46
+ } catch (error) {
47
+ console.log('❌ Error during test:', error.message);
48
+ }
49
+ }
50
+
51
+ // Run test
52
+ testScreenCaptureKit().catch(console.error);