node-mac-recorder 2.15.13 โ†’ 2.16.1

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.
@@ -42,7 +42,9 @@
42
42
  "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐Ÿ Final stability test with all features'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function finalTest() {\n try {\n const outputPath = ''./test-output/final-stability-test.mov'';\n console.log(''๐Ÿ“น Starting final stability test...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: true,\n includeMicrophone: false,\n includeSystemAudio: true,\n captureArea: {\n x: 200,\n y: 200,\n width: 600,\n height: 400\n }\n });\n \n if (success) {\n console.log(''โœ… Recording started - waiting 4 seconds'');\n await new Promise(resolve => setTimeout(resolve, 4000));\n \n console.log(''๐Ÿ›‘ Stopping recording...'');\n await recorder.stopRecording();\n console.log(''๐ŸŽ‰ FINAL TEST COMPLETED SUCCESSFULLY - NO CRASH!'');\n } else {\n console.log(''โŒ Recording failed to start'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n }\n}\n\nfinalTest();\n\")",
43
43
  "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐ŸŽต Testing both microphone and system audio'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function testBothAudio() {\n try {\n const outputPath = ''./test-output/both-audio-test.mov'';\n console.log(''๐Ÿ“น Starting recording with both audio sources...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: true,\n includeMicrophone: true, // Mikrophone Aร‡IK\n includeSystemAudio: true // Sistem sesi Aร‡IK\n });\n \n if (success) {\n console.log(''โœ… Recording started - waiting 5 seconds'');\n await new Promise(resolve => setTimeout(resolve, 5000));\n \n console.log(''๐Ÿ›‘ Stopping recording...'');\n await recorder.stopRecording();\n console.log(''โœ… Both audio test completed!'');\n } else {\n console.log(''โŒ Recording failed to start'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n console.log(''Stack:'', error.stack);\n }\n}\n\ntestBothAudio();\n\")",
44
44
  "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐ŸŽค Testing microphone only'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function testMicrophoneOnly() {\n try {\n const outputPath = ''./test-output/microphone-only-test.mov'';\n console.log(''๐Ÿ“น Starting microphone only recording...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: false,\n includeMicrophone: true, // Sadece mikrofon Aร‡IK\n includeSystemAudio: false // Sistem sesi KAPALI\n });\n \n if (success) {\n console.log(''โœ… Microphone recording started - waiting 3 seconds'');\n await new Promise(resolve => setTimeout(resolve, 3000));\n \n console.log(''๐Ÿ›‘ Stopping microphone recording...'');\n await recorder.stopRecording();\n console.log(''โœ… Microphone only test completed!'');\n } else {\n console.log(''โŒ Recording failed to start'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n }\n}\n\ntestMicrophoneOnly();\n\")",
45
- "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐Ÿš€ Full stress test - All features + Both audio sources'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function fullStressTest() {\n try {\n const outputPath = ''./test-output/full-stress-test.mov'';\n console.log(''๐Ÿ“น Starting full featured recording (stress test)...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: true, // Cursor kayฤฑt\n includeMicrophone: true, // Mikrofon kayฤฑt\n includeSystemAudio: true, // Sistem sesi kayฤฑt\n captureArea: { // Crop area\n x: 300,\n y: 300,\n width: 800,\n height: 500\n }\n });\n \n if (success) {\n console.log(''โœ… Full stress test started - waiting 6 seconds'');\n await new Promise(resolve => setTimeout(resolve, 6000));\n \n console.log(''๐Ÿ›‘ Stopping full stress test...'');\n await recorder.stopRecording();\n console.log(''๐ŸŽ‰ FULL STRESS TEST COMPLETED WITHOUT CRASH!'');\n } else {\n console.log(''โŒ Recording failed to start'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n console.log(''Stack:'', error.stack);\n }\n}\n\nfullStressTest();\n\")"
45
+ "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐Ÿš€ Full stress test - All features + Both audio sources'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function fullStressTest() {\n try {\n const outputPath = ''./test-output/full-stress-test.mov'';\n console.log(''๐Ÿ“น Starting full featured recording (stress test)...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: true, // Cursor kayฤฑt\n includeMicrophone: true, // Mikrofon kayฤฑt\n includeSystemAudio: true, // Sistem sesi kayฤฑt\n captureArea: { // Crop area\n x: 300,\n y: 300,\n width: 800,\n height: 500\n }\n });\n \n if (success) {\n console.log(''โœ… Full stress test started - waiting 6 seconds'');\n await new Promise(resolve => setTimeout(resolve, 6000));\n \n console.log(''๐Ÿ›‘ Stopping full stress test...'');\n await recorder.stopRecording();\n console.log(''๐ŸŽ‰ FULL STRESS TEST COMPLETED WITHOUT CRASH!'');\n } else {\n console.log(''โŒ Recording failed to start'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n console.log(''Stack:'', error.stack);\n }\n}\n\nfullStressTest();\n\")",
46
+ "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐Ÿงช Testing version detection and fallback system'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function testVersionDetection() {\n try {\n const outputPath = ''./test-output/version-detection-test.mov'';\n console.log(''๐Ÿ“น Starting recording with version detection...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: true,\n includeMicrophone: false,\n includeSystemAudio: true\n });\n \n if (success) {\n console.log(''โœ… Recording started - waiting 3 seconds'');\n await new Promise(resolve => setTimeout(resolve, 3000));\n \n console.log(''๐Ÿ›‘ Stopping recording...'');\n await recorder.stopRecording();\n console.log(''โœ… Version detection test completed successfully!'');\n } else {\n console.log(''โŒ Recording failed to start'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n }\n}\n\ntestVersionDetection();\n\")",
47
+ "Bash(ELECTRON_RUN_AS_NODE=1 node -e \"\nconsole.log(''๐Ÿงช Testing Electron safety measures'');\nconst MacRecorder = require(''./index'');\nconst recorder = new MacRecorder();\n\nasync function testElectronSafety() {\n try {\n const outputPath = ''./test-output/electron-safe-test.mov'';\n console.log(''๐Ÿ“น Starting recording in Electron environment...'');\n \n const success = await recorder.startRecording(outputPath, {\n captureCursor: true,\n includeMicrophone: false,\n includeSystemAudio: true\n });\n \n if (success) {\n console.log(''โœ… Recording started safely in Electron - waiting 3 seconds'');\n await new Promise(resolve => setTimeout(resolve, 3000));\n \n console.log(''๐Ÿ›‘ Stopping recording...'');\n await recorder.stopRecording();\n console.log(''โœ… Electron safety test completed successfully!'');\n } else {\n console.log(''โš ๏ธ Recording failed to start (expected in some cases)'');\n }\n } catch (error) {\n console.log(''โŒ Error:'', error.message);\n }\n}\n\ntestElectronSafety();\n\")"
46
48
  ],
47
49
  "deny": []
48
50
  }
package/binding.gyp CHANGED
@@ -5,6 +5,7 @@
5
5
  "sources": [
6
6
  "src/mac_recorder.mm",
7
7
  "src/screen_capture_kit.mm",
8
+ "src/avfoundation_recorder.mm",
8
9
  "src/audio_capture.mm",
9
10
  "src/cursor_tracker.mm",
10
11
  "src/window_selector.mm"
@@ -30,6 +31,10 @@
30
31
  "-framework Foundation",
31
32
  "-framework AppKit",
32
33
  "-framework ScreenCaptureKit",
34
+ "-framework AVFoundation",
35
+ "-framework CoreMedia",
36
+ "-framework CoreVideo",
37
+ "-framework QuartzCore",
33
38
  "-framework ApplicationServices",
34
39
  "-framework Carbon",
35
40
  "-framework Accessibility",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.15.13",
3
+ "version": "2.16.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,222 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <AVFoundation/AVFoundation.h>
3
+ #import <CoreMedia/CoreMedia.h>
4
+ #import <CoreVideo/CoreVideo.h>
5
+ #import <QuartzCore/QuartzCore.h>
6
+ #import <AppKit/AppKit.h>
7
+ #include <string>
8
+
9
+ static AVAssetWriter *g_avWriter = nil;
10
+ static AVAssetWriterInput *g_avVideoInput = nil;
11
+ static AVAssetWriterInputPixelBufferAdaptor *g_avPixelBufferAdaptor = nil;
12
+ static dispatch_source_t g_avTimer = nil;
13
+ static CGDirectDisplayID g_avDisplayID = 0;
14
+ static CGRect g_avCaptureRect = CGRectZero;
15
+ static bool g_avIsRecording = false;
16
+ static int64_t g_avFrameNumber = 0;
17
+ static CMTime g_avStartTime;
18
+
19
+ // AVFoundation screen recording implementation
20
+ bool startAVFoundationRecording(const std::string& outputPath,
21
+ CGDirectDisplayID displayID,
22
+ uint32_t windowID,
23
+ CGRect captureRect,
24
+ bool captureCursor,
25
+ bool includeMicrophone,
26
+ bool includeSystemAudio,
27
+ NSString* audioDeviceId) {
28
+
29
+ if (g_avIsRecording) {
30
+ NSLog(@"โŒ AVFoundation recording already in progress");
31
+ return false;
32
+ }
33
+
34
+ @try {
35
+ // Create output URL
36
+ NSString *outputPathStr = [NSString stringWithUTF8String:outputPath.c_str()];
37
+ NSURL *outputURL = [NSURL fileURLWithPath:outputPathStr];
38
+
39
+ // Remove existing file
40
+ [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
41
+
42
+ // Create asset writer
43
+ NSError *error = nil;
44
+ g_avWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&error];
45
+ if (!g_avWriter || error) {
46
+ NSLog(@"โŒ Failed to create AVAssetWriter: %@", error);
47
+ return false;
48
+ }
49
+
50
+ // Get display dimensions
51
+ CGRect displayBounds = CGDisplayBounds(displayID);
52
+ CGSize recordingSize = captureRect.size.width > 0 ? captureRect.size : displayBounds.size;
53
+
54
+ // Video settings
55
+ NSDictionary *videoSettings = @{
56
+ AVVideoCodecKey: AVVideoCodecTypeH264,
57
+ AVVideoWidthKey: @((int)recordingSize.width),
58
+ AVVideoHeightKey: @((int)recordingSize.height),
59
+ AVVideoCompressionPropertiesKey: @{
60
+ AVVideoAverageBitRateKey: @(recordingSize.width * recordingSize.height * 8),
61
+ AVVideoMaxKeyFrameIntervalKey: @30
62
+ }
63
+ };
64
+
65
+ // Create video input
66
+ g_avVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
67
+ g_avVideoInput.expectsMediaDataInRealTime = YES;
68
+
69
+ // Create pixel buffer adaptor
70
+ NSDictionary *pixelBufferAttributes = @{
71
+ (NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB),
72
+ (NSString*)kCVPixelBufferWidthKey: @((int)recordingSize.width),
73
+ (NSString*)kCVPixelBufferHeightKey: @((int)recordingSize.height),
74
+ (NSString*)kCVPixelBufferCGImageCompatibilityKey: @YES,
75
+ (NSString*)kCVPixelBufferCGBitmapContextCompatibilityKey: @YES
76
+ };
77
+
78
+ g_avPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_avVideoInput sourcePixelBufferAttributes:pixelBufferAttributes];
79
+
80
+ // Add input to writer
81
+ if (![g_avWriter canAddInput:g_avVideoInput]) {
82
+ NSLog(@"โŒ Cannot add video input to AVAssetWriter");
83
+ return false;
84
+ }
85
+ [g_avWriter addInput:g_avVideoInput];
86
+
87
+ // Start writing
88
+ if (![g_avWriter startWriting]) {
89
+ NSLog(@"โŒ Failed to start AVAssetWriter: %@", g_avWriter.error);
90
+ return false;
91
+ }
92
+
93
+ g_avStartTime = CMTimeMakeWithSeconds(CACurrentMediaTime(), 600);
94
+ [g_avWriter startSessionAtSourceTime:g_avStartTime];
95
+
96
+ // Store recording parameters
97
+ g_avDisplayID = displayID;
98
+ g_avCaptureRect = captureRect;
99
+ g_avFrameNumber = 0;
100
+
101
+ // Start capture timer (15 FPS for compatibility)
102
+ dispatch_queue_t captureQueue = dispatch_queue_create("AVFoundationCaptureQueue", DISPATCH_QUEUE_SERIAL);
103
+ g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
104
+
105
+ uint64_t interval = NSEC_PER_SEC / 15; // 15 FPS
106
+ dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
107
+
108
+ dispatch_source_set_event_handler(g_avTimer, ^{
109
+ if (!g_avIsRecording) return;
110
+
111
+ @autoreleasepool {
112
+ // Capture screen
113
+ CGImageRef screenImage = nil;
114
+ if (CGRectIsEmpty(g_avCaptureRect)) {
115
+ screenImage = CGDisplayCreateImage(g_avDisplayID);
116
+ } else {
117
+ CGImageRef fullScreen = CGDisplayCreateImage(g_avDisplayID);
118
+ if (fullScreen) {
119
+ screenImage = CGImageCreateWithImageInRect(fullScreen, g_avCaptureRect);
120
+ CGImageRelease(fullScreen);
121
+ }
122
+ }
123
+
124
+ if (!screenImage) return;
125
+
126
+ // Convert to pixel buffer
127
+ CVPixelBufferRef pixelBuffer = nil;
128
+ CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(NULL, g_avPixelBufferAdaptor.pixelBufferPool, &pixelBuffer);
129
+
130
+ if (cvRet == kCVReturnSuccess && pixelBuffer) {
131
+ CVPixelBufferLockBaseAddress(pixelBuffer, 0);
132
+
133
+ void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
134
+ size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
135
+
136
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
137
+ CGContextRef context = CGBitmapContextCreate(pixelData,
138
+ CVPixelBufferGetWidth(pixelBuffer),
139
+ CVPixelBufferGetHeight(pixelBuffer),
140
+ 8, bytesPerRow, colorSpace,
141
+ kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
142
+
143
+ if (context) {
144
+ CGContextDrawImage(context, CGRectMake(0, 0, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)), screenImage);
145
+ CGContextRelease(context);
146
+ }
147
+ CGColorSpaceRelease(colorSpace);
148
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
149
+
150
+ // Write frame
151
+ if (g_avVideoInput.readyForMoreMediaData) {
152
+ CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(g_avFrameNumber / 15.0, 600));
153
+ [g_avPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
154
+ g_avFrameNumber++;
155
+ }
156
+
157
+ CVPixelBufferRelease(pixelBuffer);
158
+ }
159
+
160
+ CGImageRelease(screenImage);
161
+ }
162
+ });
163
+
164
+ dispatch_resume(g_avTimer);
165
+ g_avIsRecording = true;
166
+
167
+ NSLog(@"๐ŸŽฅ AVFoundation recording started: %dx%d @ 15fps",
168
+ (int)recordingSize.width, (int)recordingSize.height);
169
+
170
+ return true;
171
+
172
+ } @catch (NSException *exception) {
173
+ NSLog(@"โŒ Exception in AVFoundation recording: %@", exception.reason);
174
+ return false;
175
+ }
176
+ }
177
+
178
+ bool stopAVFoundationRecording() {
179
+ if (!g_avIsRecording) {
180
+ return true;
181
+ }
182
+
183
+ g_avIsRecording = false;
184
+
185
+ @try {
186
+ // Stop timer
187
+ if (g_avTimer) {
188
+ dispatch_source_cancel(g_avTimer);
189
+ g_avTimer = nil;
190
+ }
191
+
192
+ // Finish writing
193
+ if (g_avVideoInput) {
194
+ [g_avVideoInput markAsFinished];
195
+ }
196
+
197
+ if (g_avWriter && g_avWriter.status == AVAssetWriterStatusWriting) {
198
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
199
+ [g_avWriter finishWritingWithCompletionHandler:^{
200
+ dispatch_semaphore_signal(semaphore);
201
+ }];
202
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
203
+ }
204
+
205
+ // Cleanup
206
+ g_avWriter = nil;
207
+ g_avVideoInput = nil;
208
+ g_avPixelBufferAdaptor = nil;
209
+ g_avFrameNumber = 0;
210
+
211
+ NSLog(@"โœ… AVFoundation recording stopped");
212
+ return true;
213
+
214
+ } @catch (NSException *exception) {
215
+ NSLog(@"โŒ Exception stopping AVFoundation recording: %@", exception.reason);
216
+ return false;
217
+ }
218
+ }
219
+
220
+ bool isAVFoundationRecording() {
221
+ return g_avIsRecording;
222
+ }
@@ -8,6 +8,20 @@
8
8
  // Import screen capture (ScreenCaptureKit only)
9
9
  #import "screen_capture_kit.h"
10
10
 
11
+ // AVFoundation fallback declarations
12
+ extern "C" {
13
+ bool startAVFoundationRecording(const std::string& outputPath,
14
+ CGDirectDisplayID displayID,
15
+ uint32_t windowID,
16
+ CGRect captureRect,
17
+ bool captureCursor,
18
+ bool includeMicrophone,
19
+ bool includeSystemAudio,
20
+ NSString* audioDeviceId);
21
+ bool stopAVFoundationRecording();
22
+ bool isAVFoundationRecording();
23
+ }
24
+
11
25
  // Cursor tracker function declarations
12
26
  Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
13
27
 
@@ -30,12 +44,24 @@ static bool g_isRecording = false;
30
44
 
31
45
  // Helper function to cleanup recording resources
32
46
  void cleanupRecording() {
33
- // ScreenCaptureKit cleanup only
47
+ // ScreenCaptureKit cleanup
34
48
  if (@available(macOS 12.3, *)) {
35
49
  if ([ScreenCaptureKitRecorder isRecording]) {
36
50
  [ScreenCaptureKitRecorder stopRecording];
37
51
  }
38
52
  }
53
+
54
+ // AVFoundation cleanup (only in non-Electron environments)
55
+ BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
56
+ [NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
57
+ (NSProcessInfo.processInfo.processName &&
58
+ [NSProcessInfo.processInfo.processName containsString:@"Electron"]) ||
59
+ (NSProcessInfo.processInfo.environment[@"ELECTRON_RUN_AS_NODE"] != nil);
60
+
61
+ if (!isElectron && isAVFoundationRecording()) {
62
+ stopAVFoundationRecording();
63
+ }
64
+
39
65
  g_isRecording = false;
40
66
  }
41
67
 
@@ -164,9 +190,20 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
164
190
  NSLog(@"โš ๏ธ Warning: ScreenCaptureKit in Electron may require additional stability measures");
165
191
  }
166
192
 
167
- // Non-Electron: Use ScreenCaptureKit
168
- if (@available(macOS 12.3, *)) {
169
- NSLog(@"โœ… macOS 12.3+ detected - ScreenCaptureKit should be available");
193
+ // Check macOS version for ScreenCaptureKit compatibility
194
+ NSOperatingSystemVersion osVersion = [[NSProcessInfo processInfo] operatingSystemVersion];
195
+ BOOL isM15Plus = (osVersion.majorVersion > 15 ||
196
+ (osVersion.majorVersion == 15 && osVersion.minorVersion >= 0));
197
+ BOOL isM14Plus = (osVersion.majorVersion > 14 ||
198
+ (osVersion.majorVersion == 14 && osVersion.minorVersion >= 0));
199
+
200
+ NSLog(@"๐Ÿ–ฅ๏ธ macOS Version: %ld.%ld.%ld",
201
+ (long)osVersion.majorVersion, (long)osVersion.minorVersion, (long)osVersion.patchVersion);
202
+
203
+ // Try ScreenCaptureKit on macOS 14+ (with better compatibility on 15+)
204
+ if (@available(macOS 12.3, *) && isM14Plus) {
205
+ NSLog(@"โœ… macOS 14+ detected - ScreenCaptureKit available with %@ compatibility",
206
+ isM15Plus ? @"full" : @"limited");
170
207
 
171
208
  // Try ScreenCaptureKit with extensive safety measures
172
209
  @try {
@@ -243,8 +280,42 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
243
280
  return Napi::Boolean::New(env, false);
244
281
  }
245
282
 
246
- // If we get here, ScreenCaptureKit failed completely
247
- NSLog(@"โŒ ScreenCaptureKit failed to initialize - Recording not available");
283
+ // If we get here, ScreenCaptureKit failed
284
+ if (isElectron) {
285
+ NSLog(@"โŒ ScreenCaptureKit failed in Electron - AVFoundation disabled for stability");
286
+ NSLog(@"โŒ Recording not available in Electron when ScreenCaptureKit fails");
287
+ return Napi::Boolean::New(env, false);
288
+ }
289
+
290
+ // Try AVFoundation fallback (only in non-Electron environments)
291
+ NSLog(@"๐Ÿ”„ ScreenCaptureKit failed - attempting AVFoundation fallback");
292
+
293
+ @try {
294
+ // Import AVFoundation recording functions (if available)
295
+ extern bool startAVFoundationRecording(const std::string& outputPath,
296
+ CGDirectDisplayID displayID,
297
+ uint32_t windowID,
298
+ CGRect captureRect,
299
+ bool captureCursor,
300
+ bool includeMicrophone,
301
+ bool includeSystemAudio,
302
+ NSString* audioDeviceId);
303
+
304
+ if (startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
305
+ captureCursor, includeMicrophone, includeSystemAudio, audioDeviceId)) {
306
+ NSLog(@"๐ŸŽฅ RECORDING METHOD: AVFoundation (Fallback)");
307
+ NSLog(@"โœ… AVFoundation recording started successfully");
308
+ g_isRecording = true;
309
+ return Napi::Boolean::New(env, true);
310
+ } else {
311
+ NSLog(@"โŒ AVFoundation recording also failed to start");
312
+ }
313
+ } @catch (NSException *avException) {
314
+ NSLog(@"โŒ Exception during AVFoundation startup: %@", avException.reason);
315
+ }
316
+
317
+ // Both ScreenCaptureKit and AVFoundation failed
318
+ NSLog(@"โŒ All recording methods failed - no recording available");
248
319
  return Napi::Boolean::New(env, false);
249
320
 
250
321
  } @catch (NSException *exception) {
@@ -259,22 +330,47 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
259
330
 
260
331
  NSLog(@"๐Ÿ“ž StopRecording native method called");
261
332
 
262
- // ScreenCaptureKit ONLY
333
+ // Try ScreenCaptureKit first
263
334
  if (@available(macOS 12.3, *)) {
264
335
  if ([ScreenCaptureKitRecorder isRecording]) {
265
336
  NSLog(@"๐Ÿ›‘ Stopping ScreenCaptureKit recording");
266
337
  [ScreenCaptureKitRecorder stopRecording];
267
338
  g_isRecording = false;
268
339
  return Napi::Boolean::New(env, true);
269
- } else {
270
- NSLog(@"โš ๏ธ ScreenCaptureKit not recording");
271
- g_isRecording = false;
272
- return Napi::Boolean::New(env, true);
273
340
  }
274
- } else {
275
- NSLog(@"โŒ ScreenCaptureKit not available - cannot stop recording");
276
- return Napi::Boolean::New(env, false);
277
341
  }
342
+
343
+ // Try AVFoundation fallback (only in non-Electron environments)
344
+ BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
345
+ [NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
346
+ (NSProcessInfo.processInfo.processName &&
347
+ [NSProcessInfo.processInfo.processName containsString:@"Electron"]) ||
348
+ (NSProcessInfo.processInfo.environment[@"ELECTRON_RUN_AS_NODE"] != nil);
349
+
350
+ if (!isElectron) {
351
+ extern bool isAVFoundationRecording();
352
+ extern bool stopAVFoundationRecording();
353
+
354
+ @try {
355
+ if (isAVFoundationRecording()) {
356
+ NSLog(@"๐Ÿ›‘ Stopping AVFoundation recording");
357
+ if (stopAVFoundationRecording()) {
358
+ g_isRecording = false;
359
+ return Napi::Boolean::New(env, true);
360
+ } else {
361
+ NSLog(@"โŒ Failed to stop AVFoundation recording");
362
+ g_isRecording = false;
363
+ return Napi::Boolean::New(env, false);
364
+ }
365
+ }
366
+ } @catch (NSException *exception) {
367
+ NSLog(@"โŒ Exception stopping AVFoundation: %@", exception.reason);
368
+ }
369
+ }
370
+
371
+ NSLog(@"โš ๏ธ No active recording found to stop");
372
+ g_isRecording = false;
373
+ return Napi::Boolean::New(env, true);
278
374
  }
279
375
 
280
376
 
@@ -530,7 +626,28 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
530
626
  // NAPI Function: Get Recording Status
531
627
  Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
532
628
  Napi::Env env = info.Env();
533
- return Napi::Boolean::New(env, g_isRecording);
629
+
630
+ // Check recording methods
631
+ bool isRecording = g_isRecording;
632
+
633
+ if (@available(macOS 12.3, *)) {
634
+ if ([ScreenCaptureKitRecorder isRecording]) {
635
+ isRecording = true;
636
+ }
637
+ }
638
+
639
+ // Check AVFoundation only in non-Electron environments
640
+ BOOL isElectron = (NSBundle.mainBundle.bundleIdentifier &&
641
+ [NSBundle.mainBundle.bundleIdentifier containsString:@"electron"]) ||
642
+ (NSProcessInfo.processInfo.processName &&
643
+ [NSProcessInfo.processInfo.processName containsString:@"Electron"]) ||
644
+ (NSProcessInfo.processInfo.environment[@"ELECTRON_RUN_AS_NODE"] != nil);
645
+
646
+ if (!isElectron && isAVFoundationRecording()) {
647
+ isRecording = true;
648
+ }
649
+
650
+ return Napi::Boolean::New(env, isRecording);
534
651
  }
535
652
 
536
653
  // NAPI Function: Get Window Thumbnail