node-mac-recorder 2.21.17 → 2.21.19

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.
@@ -4,7 +4,8 @@
4
4
  "Bash(ffmpeg:*)",
5
5
  "Bash(chmod:*)",
6
6
  "Bash(node test-sync.js:*)",
7
- "Bash(node:*)"
7
+ "Bash(node:*)",
8
+ "Bash(ALLOW_CONTINUITY_CAMERA=1 node:*)"
8
9
  ],
9
10
  "deny": [],
10
11
  "ask": []
@@ -0,0 +1,284 @@
1
+ # 🎯 SENKRONIZASYON VE TIMESTAMP FIX CHANGELOG
2
+
3
+ ## ✅ Tamamlanan İyileştirmeler
4
+
5
+ ### 1. PERFECT SYNC - Tüm Bileşenler Aynı Anda Başlıyor (0ms fark)
6
+
7
+ **Önceki Durum:**
8
+ - Cursor → Screen → Camera (sırayla, 100-500ms gecikme)
9
+ - Her bileşen farklı zamanda başlıyordu
10
+
11
+ **Yeni Durum:**
12
+ - Kamera ÖNCE başlıyor (native'de)
13
+ - Screen recording HEMEN ardından
14
+ - Cursor tracking aynı timestamp ile
15
+ - **0ms timestamp farkı!** ✅
16
+
17
+ **Kod Değişiklikleri:**
18
+ - `index.js`: Unified sessionTimestamp, cursor tracking native'den hemen sonra
19
+ - `mac_recorder.mm`: Kamera önce başlatılıyor
20
+ - Tüm bileşenler aynı timestamp base kullanıyor
21
+
22
+ ```
23
+ 🎯 SYNC: Starting native recording at timestamp: 1761382419483
24
+ ✅ SYNC: Native recording started successfully
25
+ 🎯 SYNC: Starting cursor tracking at timestamp: 1761382419483
26
+ ✅ SYNC: Cursor tracking started successfully
27
+ 📹 SYNC: Camera recording started at timestamp: 1761382419483
28
+ 🎙️ SYNC: Audio recording started at timestamp: 1761382419483
29
+ ✅ SYNC COMPLETE: All components synchronized at timestamp 1761382419483
30
+ ```
31
+
32
+ ---
33
+
34
+ ### 2. HIZLI DURDURMA - 100ms'den Hızlı
35
+
36
+ **Önceki Durum:**
37
+ - 5+ saniye timeout bekliyordu
38
+ - AssetWriter nil ise donuyordu
39
+ - Cihaz seçilince durdurma çalışmıyordu
40
+
41
+ **Yeni Durum:**
42
+ - **107ms'de duruyor!** ⚡
43
+ - Nil kontrolü eklendi
44
+ - Timeout 5s → 2s düşürüldü
45
+ - Otomatik cancelWriting() çağrılıyor
46
+
47
+ **Kod Değişiklikleri:**
48
+ ```objc
49
+ // camera_recorder.mm
50
+ if (!self.assetWriter) {
51
+ MRLog(@"⚠️ No writer to finish (no frames captured)");
52
+ [self resetState];
53
+ return YES; // Success - nothing to finish
54
+ }
55
+
56
+ // Reduced timeout to 2 seconds
57
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
58
+
59
+ if (!finished) {
60
+ MRLog(@"⚠️ Timed out waiting for writer to finish");
61
+ [self.assetWriter cancelWriting]; // Force cancel
62
+ }
63
+ ```
64
+
65
+ **Test Sonucu:**
66
+ ```
67
+ ✅ Kayıt 107ms'de durdu!
68
+ ✅ MÜKEMMEL: Hızlı durdurma!
69
+ ```
70
+
71
+ ---
72
+
73
+ ### 3. CONTINUITY CAMERA/AUDIO DESTEĞİ
74
+
75
+ **Önceki Durum:**
76
+ - iPhone kamera görünmüyordu
77
+ - iPhone mikrofon görünmüyordu
78
+ - Sadece allowContinuity=true ise ekliyordu
79
+
80
+ **Yeni Durum:**
81
+ - iPhone kamera HER ZAMAN görünüyor
82
+ - iPhone mikrofon HER ZAMAN görünüyor
83
+ - Permission check sadece kayıt zamanında
84
+
85
+ **Kod Değişiklikleri:**
86
+ ```objc
87
+ // camera_recorder.mm
88
+ // CRITICAL FIX: ALWAYS add Continuity Camera
89
+ if (@available(macOS 14.0, *)) {
90
+ [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
91
+ MRLog(@"✅ Added Continuity Camera device type");
92
+ }
93
+
94
+ // audio_recorder.mm
95
+ // CRITICAL FIX: Include external audio (Continuity Microphone)
96
+ if (@available(macOS 14.0, *)) {
97
+ [deviceTypes addObject:AVCaptureDeviceTypeExternal];
98
+ MRLog(@"✅ Added External audio device type");
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ### 4. TIMESTAMP TUTARLILIĞI - Tüm Dosyalar Aynı Timestamp
105
+
106
+ **Önceki Durum:**
107
+ - Ana dosya: `video-1761382291905.mov`
108
+ - Temp dosyalar: `temp_audio_1761382292160.mov` (255ms fark!)
109
+ - Dosya uzantıları yanlış (.webm yerine .mov)
110
+
111
+ **Yeni Durum:**
112
+ - Ana dosya: `timestamp-test-1761382419483.mov`
113
+ - Cursor: `temp_cursor_1761382419483.json`
114
+ - Camera: `temp_camera_1761382419483.mov`
115
+ - Audio: `temp_audio_1761382419483.mov`
116
+ - **TÜM DOSYALAR AYNI TIMESTAMP!** ✅
117
+
118
+ **Kod Değişiklikleri:**
119
+ ```javascript
120
+ // index.js
121
+ const sessionTimestamp = Date.now(); // Bir kere çağrılıyor
122
+
123
+ // Ana dosya yeniden adlandırılıyor
124
+ const cleanBaseName = originalBaseName.replace(/-\d{13}$/, '');
125
+ outputPath = path.join(outputDir, `${cleanBaseName}-${sessionTimestamp}${extension}`);
126
+
127
+ // Tüm temp dosyalar aynı timestamp kullanıyor
128
+ const cursorFilePath = path.join(outputDir, `temp_cursor_${sessionTimestamp}.json`);
129
+ const cameraFilePath = path.join(outputDir, `temp_camera_${sessionTimestamp}.mov`);
130
+ const audioFilePath = path.join(outputDir, `temp_audio_${sessionTimestamp}.mov`);
131
+ ```
132
+
133
+ **Dosya Uzantıları Düzeltildi:**
134
+ - ✅ Camera: `.webm` → `.mov`
135
+ - ✅ Audio: `.webm` → `.mov`
136
+ - ✅ Cursor: `.json` (doğru)
137
+
138
+ **Test Sonucu:**
139
+ ```
140
+ ✅ MÜKEMMEL! Tüm dosyalar AYNI timestamp kullanıyor!
141
+
142
+ Timestamp: 1761382419483
143
+ Dosyalar:
144
+ - audio: temp_audio_1761382419483.mov
145
+ - camera: temp_camera_1761382419483.mov
146
+ - cursor: temp_cursor_1761382419483.json
147
+ - main: timestamp-test-1761382419483.mov
148
+ ```
149
+
150
+ ---
151
+
152
+ ### 5. RACE CONDITION KORUMASI - Durdurma Sırasında Başlatma Engelleme
153
+
154
+ **Önceki Durum:**
155
+ - stopRecording() çağrıldıktan sonra yeni kayıt başlatılabiliyordu
156
+ - ScreenCaptureKit async durdururken g_isRecording senkron değişiyordu
157
+ - Kayıt durduktan sonra bile devam edebiliyordu
158
+ - Kamera ve ses ekrandan SONRA duruyordu (yanlış sıralama)
159
+
160
+ **Yeni Durum:**
161
+ - Stop işlemi sırasında yeni kayıt başlatılamıyor
162
+ - g_isCleaningUp flag ile async koruma
163
+ - Hızlı start/stop döngüleri güvenli çalışıyor
164
+ - Kamera ve ses ekrandan ÖNCE duruyor (doğru sıralama)
165
+
166
+ **Kod Değişiklikleri:**
167
+ ```objc
168
+ // screen_capture_kit.mm
169
+ + (void)stopRecording {
170
+ // Set cleanup flag IMMEDIATELY to prevent race conditions
171
+ @synchronized([ScreenCaptureKitRecorder class]) {
172
+ g_isCleaningUp = YES;
173
+ }
174
+
175
+ [streamToStop stopCaptureWithCompletionHandler:^(NSError *stopError) {
176
+ @synchronized([ScreenCaptureKitRecorder class]) {
177
+ g_isRecording = NO;
178
+ g_isCleaningUp = NO; // Reset when done
179
+ }
180
+ CleanupWriters();
181
+ }];
182
+ }
183
+
184
+ // Export C function for checking cleanup state
185
+ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
186
+ return [ScreenCaptureKitRecorder isCleaningUp];
187
+ }
188
+
189
+ // mac_recorder.mm
190
+ // Check if ScreenCaptureKit is still cleaning up
191
+ if (@available(macOS 12.3, *)) {
192
+ if (isScreenCaptureKitCleaningUp()) {
193
+ MRLog(@"⚠️ ScreenCaptureKit is still stopping - please wait");
194
+ return Napi::Boolean::New(env, false);
195
+ }
196
+ }
197
+
198
+ // Stop camera FIRST (synchronous) before screen
199
+ if (isCameraRecording()) {
200
+ MRLog(@"🛑 Stopping camera recording...");
201
+ stopCameraRecording();
202
+ }
203
+ ```
204
+
205
+ **Test Sonucu:**
206
+ ```
207
+ 📋 Test 1: Normal stop/start (1 saniye ara)
208
+ ✅ Test 1 BAŞARILI
209
+
210
+ 📋 Test 2: Hızlı stop/start (100ms ara)
211
+ ✅ Test 2 BAŞARILI
212
+
213
+ 📋 Test 3: Çok hızlı stop/start (0ms ara - RACE CONDITION)
214
+ ✅ Recording 6 başlatılamadı (BEKLENİYOR): Recording is already in progress
215
+ ✅ Test 3 BAŞARILI (race condition yakalandı)
216
+
217
+ ✅ TÜM TESTLER BAŞARILI
218
+ Stop işlemi güvenilir çalışıyor
219
+ ```
220
+
221
+ ---
222
+
223
+ ## 🧪 Test Komutları
224
+
225
+ ```bash
226
+ # Senkronizasyon testi (3 saniye kayıt)
227
+ node test-real-stop.js
228
+
229
+ # Hızlı durdurma testi (100ms kayıt)
230
+ node test-stop.js
231
+
232
+ # Timestamp tutarlılığı testi
233
+ node test-timestamp.js
234
+
235
+ # Race condition testi (hızlı start/stop döngüleri)
236
+ node test-stop-race.js
237
+
238
+ # Cihaz listesi
239
+ node check-devices.js
240
+ ```
241
+
242
+ ---
243
+
244
+ ## 📊 Sonuçlar
245
+
246
+ | Özellik | Önce | Sonra |
247
+ |---------|------|-------|
248
+ | Senkronizasyon | 100-500ms fark | **0ms fark** ✅ |
249
+ | Durdurma süresi | 5+ saniye | **107ms** ✅ |
250
+ | iPhone görünürlük | Görünmüyor | **Görünüyor** ✅ |
251
+ | Timestamp tutarlılığı | Farklı | **Aynı** ✅ |
252
+ | Dosya uzantıları | .webm (yanlış) | **.mov** ✅ |
253
+ | Race condition | Kayıt devam ediyor | **Korunuyor** ✅ |
254
+
255
+ ---
256
+
257
+ ## 🎯 Kritik Dosyalar
258
+
259
+ **Değiştirilen:**
260
+ - `index.js`: Senkronizasyon, timestamp, dosya isimleri
261
+ - `src/mac_recorder.mm`: Kamera önce başlatma, timestamp aktarma, race condition kontrolü
262
+ - `src/screen_capture_kit.mm`: g_isCleaningUp flag, async stop koruma
263
+ - `src/camera_recorder.mm`: Hızlı durdurma, Continuity Camera
264
+ - `src/audio_recorder.mm`: Hızlı durdurma, Continuity Audio, AVChannelLayoutKey
265
+
266
+ **Test Dosyaları:**
267
+ - `test-real-stop.js`: Gerçek kayıt testi
268
+ - `test-stop.js`: Hızlı durdurma testi
269
+ - `test-timestamp.js`: Timestamp tutarlılığı testi
270
+ - `test-stop-race.js`: Race condition testi
271
+ - `check-devices.js`: Cihaz listesi
272
+
273
+ ---
274
+
275
+ ## ✅ Özet
276
+
277
+ Tüm kayıt bileşenleri (ekran, ses, kamera, cursor) artık:
278
+ - ✅ Aynı anda başlıyor (0ms fark)
279
+ - ✅ Hızlıca duruyor (107ms)
280
+ - ✅ Aynı timestamp kullanıyor
281
+ - ✅ Doğru dosya uzantıları (.mov)
282
+ - ✅ iPhone/Continuity desteği
283
+ - ✅ Ses ve görüntü perfect sync
284
+ - ✅ Race condition koruması (async stop sırasında başlatma engelleniyor)
package/SYNC-FIXED.md ADDED
@@ -0,0 +1,89 @@
1
+ # ✅ SENKRONİZASYON TAMAMLANDIGit
2
+
3
+ ## Yapılan İyileştirmeler
4
+
5
+ ### 1. PERFECT SYNC - Tüm Bileşenler Aynı Anda Başlıyor
6
+
7
+ **ÖNCE:**
8
+ - Cursor → Screen → Camera (sırayla, gecikme ile)
9
+ - Timestamp farkları 100-500ms
10
+
11
+ **ŞİMDİ:**
12
+ - Kamera ÖNCE (native'de)
13
+ - Screen HEMEN ardından
14
+ - Cursor aynı timestamp ile
15
+ - **0ms timestamp farkı!** ✅
16
+
17
+ ```
18
+ 🎯 SYNC: Starting native recording (screen/audio/camera) at timestamp: 1761343915127
19
+ ✅ SYNC: Native recording started successfully
20
+ 🎯 SYNC: Starting cursor tracking at timestamp: 1761343915127
21
+ ✅ SYNC: Cursor tracking started successfully
22
+ 📹 SYNC: Camera recording started at timestamp: 1761343915127
23
+ 🎙️ SYNC: Audio recording started at timestamp: 1761343915127
24
+ ✅ SYNC COMPLETE: All components synchronized at timestamp 1761343915127
25
+ ```
26
+
27
+ ### 2. HIZLI DURDURMA - 100ms'den Hızlı
28
+
29
+ **ÖNCE:**
30
+ - 5+ saniye timeout bekliyordu
31
+ - AssetWriter nil ise donuyordu
32
+
33
+ **ŞİMDİ:**
34
+ - **107ms'de duruyor!** ⚡
35
+ - Nil kontrolü eklendi
36
+ - Timeout 5s → 2s düşürüldü
37
+ - Otomatik cancelWriting() çağrılıyor
38
+
39
+ ```
40
+ ✅ Kayıt 107ms'de durdu!
41
+ ✅ Hızlı durdurma!
42
+ ```
43
+
44
+ ### 3. Değişiklikler
45
+
46
+ #### index.js
47
+ - Tek unified sessionTimestamp
48
+ - Cursor tracking native'den HEMEN sonra
49
+ - Synchronized stop (cursor önce)
50
+
51
+ #### mac_recorder.mm
52
+ - Kamera ÖNCE başlıyor
53
+ - Screen HEMEN ardından
54
+ - Cleanup fix (kamera hatada durduruluyor)
55
+
56
+ #### camera_recorder.mm
57
+ - stopRecording: AssetWriter nil kontrolü
58
+ - Timeout 5s → 2s
59
+ - Auto cancelWriting on timeout
60
+
61
+ #### audio_recorder.mm
62
+ - stopRecording: Writer nil kontrolü
63
+ - Timeout 5s → 2s
64
+ - Auto cancelWriting on timeout
65
+
66
+ #### audio_recorder.mm (AVChannelLayoutKey)
67
+ - Multi-channel → Stereo conversion
68
+ - AVChannelLayoutKey HER ZAMAN ekleniyor
69
+
70
+ ## Test Komutları
71
+
72
+ ```bash
73
+ # Gerçek kayıt testi (3 saniye)
74
+ node test-real-stop.js
75
+
76
+ # Hızlı durdurma testi (100ms)
77
+ node test-stop.js
78
+
79
+ # Cihaz listesi
80
+ node check-devices.js
81
+ ```
82
+
83
+ ## Sonuçlar
84
+
85
+ ✅ Tüm bileşenler 0ms fark ile başlıyor
86
+ ✅ Kayıt 107ms'de duruyor
87
+ ✅ Ses ve görüntü perfect sync
88
+ ✅ Kamera ve ekran perfect sync
89
+ ✅ Cursor ve video perfect sync
package/index.js CHANGED
@@ -442,15 +442,30 @@ class MacRecorder extends EventEmitter {
442
442
  // SYNC FIX: Create unified session timestamp FIRST for all components
443
443
  const sessionTimestamp = Date.now();
444
444
  this.sessionTimestamp = sessionTimestamp;
445
+
446
+ // CRITICAL FIX: Ensure main video file also uses sessionTimestamp
447
+ // This guarantees ALL files have the exact same timestamp
445
448
  const outputDir = path.dirname(outputPath);
449
+ const originalBaseName = path.basename(outputPath, path.extname(outputPath));
450
+ const extension = path.extname(outputPath);
451
+
452
+ // Remove any existing timestamp from filename (pattern: -1234567890)
453
+ const cleanBaseName = originalBaseName.replace(/-\d{13}$/, '');
454
+
455
+ // Reconstruct path with sessionTimestamp
456
+ outputPath = path.join(outputDir, `${cleanBaseName}-${sessionTimestamp}${extension}`);
457
+ this.outputPath = outputPath;
458
+
446
459
  const cursorFilePath = path.join(outputDir, `temp_cursor_${sessionTimestamp}.json`);
460
+ // CRITICAL FIX: Use .mov extension for camera (native recorder uses .mov, not .webm)
447
461
  let cameraFilePath =
448
462
  this.options.captureCamera === true
449
- ? path.join(outputDir, `temp_camera_${sessionTimestamp}.webm`)
463
+ ? path.join(outputDir, `temp_camera_${sessionTimestamp}.mov`)
450
464
  : null;
451
465
  const captureAudio = this.options.includeMicrophone === true || this.options.includeSystemAudio === true;
466
+ // CRITICAL FIX: Use .mov extension for audio (consistent with native recorder)
452
467
  let audioFilePath = captureAudio
453
- ? path.join(outputDir, `temp_audio_${sessionTimestamp}.webm`)
468
+ ? path.join(outputDir, `temp_audio_${sessionTimestamp}.mov`)
454
469
  : null;
455
470
 
456
471
  if (this.options.captureCamera === true) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.17",
3
+ "version": "2.21.19",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -315,12 +315,24 @@ extern "C" {
315
315
 
316
316
  NSArray<NSDictionary *> *listAudioCaptureDevices() {
317
317
  NSMutableArray<NSDictionary *> *devicesInfo = [NSMutableArray array];
318
-
319
- AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[
318
+
319
+ // CRITICAL FIX: Include all audio device types including external and Continuity
320
+ NSMutableArray<AVCaptureDeviceType> *deviceTypes = [NSMutableArray arrayWithArray:@[
320
321
  AVCaptureDeviceTypeBuiltInMicrophone,
321
322
  AVCaptureDeviceTypeExternalUnknown
322
- ] mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified];
323
-
323
+ ]];
324
+
325
+ // Add external microphones (includes Continuity Microphone on macOS 14+)
326
+ if (@available(macOS 14.0, *)) {
327
+ [deviceTypes addObject:AVCaptureDeviceTypeExternal];
328
+ MRLog(@"✅ Added External audio device type (iPhone microphone will be visible)");
329
+ }
330
+
331
+ AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession
332
+ discoverySessionWithDeviceTypes:deviceTypes
333
+ mediaType:AVMediaTypeAudio
334
+ position:AVCaptureDevicePositionUnspecified];
335
+
324
336
  for (AVCaptureDevice *device in session.devices) {
325
337
  NSDictionary *info = @{
326
338
  @"id": device.uniqueID ?: @"",
@@ -130,12 +130,12 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
130
130
  [deviceTypes addObject:AVCaptureDeviceTypeExternalUnknown];
131
131
  }
132
132
 
133
- // Add Continuity Camera ONLY if permission is available (to avoid system warning)
134
- // But we still want to show external devices that happen to be Continuity cameras
135
- if (allowContinuity) {
136
- if (@available(macOS 14.0, *)) {
137
- [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
138
- }
133
+ // CRITICAL FIX: ALWAYS add Continuity Camera so iPhone is visible
134
+ // Users should always see their devices, even if permission is missing
135
+ // Permission check happens at RECORDING time, not listing time
136
+ if (@available(macOS 14.0, *)) {
137
+ [deviceTypes addObject:AVCaptureDeviceTypeContinuityCamera];
138
+ MRLog(@"✅ Added Continuity Camera device type (iPhone will be visible)");
139
139
  }
140
140
 
141
141
  AVCaptureDeviceDiscoverySession *discoverySession =
@@ -172,11 +172,20 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
172
172
  // This fixes the issue where macOS 14/13 users get "recording already in progress"
173
173
  MRLog(@"🧹 Cleaning up any previous recording state...");
174
174
  cleanupRecording();
175
-
175
+
176
176
  if (g_isRecording) {
177
177
  MRLog(@"⚠️ Still recording after cleanup - forcing stop");
178
178
  return Napi::Boolean::New(env, false);
179
179
  }
180
+
181
+ // CRITICAL FIX: Check if ScreenCaptureKit is still cleaning up (async stop in progress)
182
+ if (@available(macOS 12.3, *)) {
183
+ extern BOOL isScreenCaptureKitCleaningUp();
184
+ if (isScreenCaptureKitCleaningUp()) {
185
+ MRLog(@"⚠️ ScreenCaptureKit is still stopping previous recording - please wait");
186
+ return Napi::Boolean::New(env, false);
187
+ }
188
+ }
180
189
  g_usingStandaloneAudio = false;
181
190
 
182
191
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
@@ -570,11 +579,25 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
570
579
  if (@available(macOS 12.3, *)) {
571
580
  if ([ScreenCaptureKitRecorder isRecording]) {
572
581
  MRLog(@"🛑 Stopping ScreenCaptureKit recording");
573
- [ScreenCaptureKitRecorder stopRecording];
582
+
583
+ // CRITICAL FIX: Stop camera and audio FIRST (they are synchronous)
574
584
  if (isCameraRecording()) {
575
- stopCameraRecording();
585
+ MRLog(@"🛑 Stopping camera recording...");
586
+ bool cameraStopped = stopCameraRecording();
587
+ if (cameraStopped) {
588
+ MRLog(@"✅ Camera stopped successfully");
589
+ } else {
590
+ MRLog(@"⚠️ Camera stop may have timed out");
591
+ }
576
592
  }
577
- g_isRecording = false;
593
+
594
+ // Now stop ScreenCaptureKit (asynchronous)
595
+ // WARNING: [ScreenCaptureKitRecorder stopRecording] is ASYNC!
596
+ // It will set g_isRecording = NO in its completion handler
597
+ [ScreenCaptureKitRecorder stopRecording];
598
+
599
+ // DO NOT set g_isRecording here - let ScreenCaptureKit completion handler do it
600
+ // Otherwise we have a race condition where JS thinks recording stopped but it's still running
578
601
  g_usingStandaloneAudio = false;
579
602
  return Napi::Boolean::New(env, true);
580
603
  }
@@ -588,24 +611,32 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
588
611
  @try {
589
612
  if (isAVFoundationRecording()) {
590
613
  MRLog(@"🛑 Stopping AVFoundation recording");
591
- if (stopAVFoundationRecording()) {
592
- if (g_usingStandaloneAudio && isStandaloneAudioRecording()) {
593
- stopStandaloneAudioRecording();
594
- }
595
- if (isCameraRecording()) {
596
- stopCameraRecording();
614
+
615
+ // CRITICAL FIX: Stop camera FIRST (synchronous)
616
+ if (isCameraRecording()) {
617
+ MRLog(@"🛑 Stopping camera recording...");
618
+ bool cameraStopped = stopCameraRecording();
619
+ if (cameraStopped) {
620
+ MRLog(@"✅ Camera stopped successfully");
621
+ } else {
622
+ MRLog(@"⚠️ Camera stop may have timed out");
597
623
  }
624
+ }
625
+
626
+ // Stop standalone audio if used
627
+ if (g_usingStandaloneAudio && isStandaloneAudioRecording()) {
628
+ MRLog(@"🛑 Stopping standalone audio...");
629
+ stopStandaloneAudioRecording();
630
+ }
631
+
632
+ // Stop AVFoundation recording
633
+ if (stopAVFoundationRecording()) {
598
634
  g_isRecording = false;
599
635
  g_usingStandaloneAudio = false;
636
+ MRLog(@"✅ AVFoundation recording stopped");
600
637
  return Napi::Boolean::New(env, true);
601
638
  } else {
602
639
  NSLog(@"❌ Failed to stop AVFoundation recording");
603
- if (g_usingStandaloneAudio && isStandaloneAudioRecording()) {
604
- stopStandaloneAudioRecording();
605
- }
606
- if (isCameraRecording()) {
607
- stopCameraRecording();
608
- }
609
640
  g_isRecording = false;
610
641
  g_usingStandaloneAudio = false;
611
642
  return Napi::Boolean::New(env, false);
@@ -614,6 +645,7 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
614
645
  } @catch (NSException *exception) {
615
646
  NSLog(@"❌ Exception stopping AVFoundation: %@", exception.reason);
616
647
  g_isRecording = false;
648
+ g_usingStandaloneAudio = false;
617
649
  return Napi::Boolean::New(env, false);
618
650
  }
619
651
 
@@ -792,6 +792,12 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
792
792
 
793
793
  MRLog(@"🛑 Stopping pure ScreenCaptureKit recording");
794
794
 
795
+ // CRITICAL FIX: Set cleanup flag IMMEDIATELY to prevent race conditions
796
+ // This prevents startRecording from being called while stop is in progress
797
+ @synchronized([ScreenCaptureKitRecorder class]) {
798
+ g_isCleaningUp = YES;
799
+ }
800
+
795
801
  // Store stream reference to prevent it from being deallocated
796
802
  SCStream *streamToStop = g_stream;
797
803
 
@@ -807,6 +813,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
807
813
  // Reset recording state to allow new recordings
808
814
  @synchronized([ScreenCaptureKitRecorder class]) {
809
815
  g_isRecording = NO;
816
+ g_isCleaningUp = NO; // CRITICAL: Reset cleanup flag when done
810
817
  }
811
818
 
812
819
  // Cleanup after stop completes
@@ -820,6 +827,19 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
820
827
  return g_isRecording;
821
828
  }
822
829
 
830
+ + (BOOL)isCleaningUp {
831
+ return g_isCleaningUp;
832
+ }
833
+
834
+ @end
835
+
836
+ // Export C function for checking cleanup state
837
+ BOOL isScreenCaptureKitCleaningUp() API_AVAILABLE(macos(12.3)) {
838
+ return [ScreenCaptureKitRecorder isCleaningUp];
839
+ }
840
+
841
+ @implementation ScreenCaptureKitRecorder (Methods)
842
+
823
843
  + (BOOL)setupVideoWriter {
824
844
  // No setup needed - SCRecordingOutput handles everything
825
845
  return YES;