node-mac-recorder 2.24.8 → 2.24.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.8",
3
+ "version": "2.24.10",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -123,6 +123,10 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
123
123
  MRLog(@"🔍 Scale factor: %.1fx → Standard display", scaleFactor);
124
124
  }
125
125
 
126
+ // H.264 4:2:0 CIFT boyut ister — bkz. screen_capture_kit.mm'deki ayni guard.
127
+ recordingSize.width = MAX(2.0, floor(recordingSize.width / 2.0) * 2.0);
128
+ recordingSize.height = MAX(2.0, floor(recordingSize.height / 2.0) * 2.0);
129
+
126
130
  MRLog(@"🎯 Recording size: %.0fx%.0f (using actual physical dimensions for Retina fix)", recordingSize.width, recordingSize.height);
127
131
 
128
132
  NSString *normalizedQuality = @"high";
@@ -140,7 +144,6 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
140
144
 
141
145
  NSString *codecKey = AVVideoCodecTypeH264;
142
146
  NSInteger bitrate = kAVFoundationHighQualityVideoBitrate;
143
- NSNumber *qualityHint = @1.0;
144
147
 
145
148
  if ([normalizedQuality isEqualToString:@"low"]) {
146
149
  NSInteger multiplier = 10;
@@ -149,7 +152,6 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
149
152
  bitrate = (NSInteger)(recordingSize.width * recordingSize.height * multiplier);
150
153
  bitrate = MAX(bitrate, minBitrate);
151
154
  bitrate = MIN(bitrate, maxBitrate);
152
- qualityHint = @0.85;
153
155
  } else if ([normalizedQuality isEqualToString:@"medium"]) {
154
156
  NSInteger multiplier = 18;
155
157
  NSInteger minBitrate = 18 * 1000 * 1000;
@@ -157,7 +159,6 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
157
159
  bitrate = (NSInteger)(recordingSize.width * recordingSize.height * multiplier);
158
160
  bitrate = MAX(bitrate, minBitrate);
159
161
  bitrate = MIN(bitrate, maxBitrate);
160
- qualityHint = @0.9;
161
162
  } else { // high - çözünürlüğe DUYARLI yüksek kalite (eski sabit 50 Mbps yerine)
162
163
  NSInteger multiplier = 32; // ~0.53 bpp @60fps
163
164
  NSInteger minBitrate = kAVFoundationHighQualityVideoBitrate; // 100 Mbps taban
@@ -165,7 +166,6 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
165
166
  bitrate = (NSInteger)(recordingSize.width * recordingSize.height * multiplier);
166
167
  bitrate = MAX(bitrate, minBitrate);
167
168
  bitrate = MIN(bitrate, maxBitrate);
168
- qualityHint = @1.0;
169
169
  }
170
170
 
171
171
  MRLog(@"🎬 AVFoundation encoder (%@): %dx%d, codec=H.264 High Profile, bitrate=%.2fMbps",
@@ -183,14 +183,65 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
183
183
  AVVideoMaxKeyFrameIntervalKey: @((int)fps),
184
184
  AVVideoAllowFrameReorderingKey: @YES,
185
185
  AVVideoExpectedSourceFrameRateKey: @((int)fps),
186
- AVVideoQualityKey: qualityHint,
186
+ // AVVideoQualityKey BILEREK YOK — bkz. screen_capture_kit.mm:
187
+ // avc1 icin gecersiz, yazilim encoder yolunda AVAssetWriterInput
188
+ // istisna atip kaydi sessizce bos birakiyor.
187
189
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
188
190
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
189
191
  }
190
192
  };
191
193
 
192
- // Create video input
193
- g_avVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
194
+ // Create video input — kademeli geri dusus (bkz. screen_capture_kit.mm).
195
+ // AVFoundation reddettigi sikistirma ozelliginde NSError degil ISTISNA
196
+ // atiyor; yakalanmazsa kayit sessizce bos cikiyor.
197
+ NSArray<NSDictionary *> *settingsTiers = @[
198
+ videoSettings,
199
+ @{
200
+ AVVideoCodecKey: codecKey,
201
+ AVVideoWidthKey: @((int)recordingSize.width),
202
+ AVVideoHeightKey: @((int)recordingSize.height),
203
+ AVVideoCompressionPropertiesKey: @{
204
+ AVVideoAverageBitRateKey: @(bitrate),
205
+ AVVideoMaxKeyFrameIntervalKey: @((int)fps),
206
+ AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
207
+ }
208
+ },
209
+ @{
210
+ AVVideoCodecKey: codecKey,
211
+ AVVideoWidthKey: @((int)recordingSize.width),
212
+ AVVideoHeightKey: @((int)recordingSize.height),
213
+ AVVideoCompressionPropertiesKey: @{ AVVideoAverageBitRateKey: @(bitrate) }
214
+ },
215
+ @{
216
+ AVVideoCodecKey: codecKey,
217
+ AVVideoWidthKey: @((int)recordingSize.width),
218
+ AVVideoHeightKey: @((int)recordingSize.height)
219
+ }
220
+ ];
221
+ g_avVideoInput = nil;
222
+ NSString *lastRejection = nil;
223
+ for (NSUInteger tier = 0; tier < settingsTiers.count; tier++) {
224
+ @try {
225
+ g_avVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
226
+ outputSettings:settingsTiers[tier]];
227
+ } @catch (NSException *exception) {
228
+ g_avVideoInput = nil;
229
+ lastRejection = exception.reason;
230
+ NSLog(@"⚠️ AVFoundation encoder settings tier %lu rejected: %@", (unsigned long)tier, exception.reason);
231
+ continue;
232
+ }
233
+ if (g_avVideoInput) {
234
+ if (tier > 0) {
235
+ NSLog(@"⚠️ AVFoundation encoder fell back to settings tier %lu; last rejection: %@",
236
+ (unsigned long)tier, lastRejection);
237
+ }
238
+ break;
239
+ }
240
+ }
241
+ if (!g_avVideoInput) {
242
+ NSLog(@"❌ AVFoundation: no accepted H.264 encoder settings (%@)", lastRejection ?: @"unknown");
243
+ return false;
244
+ }
194
245
  g_avVideoInput.expectsMediaDataInRealTime = YES;
195
246
 
196
247
  // Create pixel buffer adaptor with compatibility
@@ -865,9 +865,30 @@ static bool g_leftMouseDown = false;
865
865
  static bool g_rightMouseDown = false;
866
866
  static NSString *g_lastEventType = @"move";
867
867
 
868
+ // Erisilebilirlik izni durumu (istem GOSTERMEDEN).
869
+ // NEDEN: izin yokken bile AX cagrilari yapiliyordu; macOS bunun uzerine
870
+ // "Erisilebilirlik" izin dialogunu aciyor ve bu fonksiyon kayit boyunca
871
+ // yuksek frekansta cagrildigi icin istem tekrar tekrar cikiyor. Durum
872
+ // saniyede bir tazelenir: kullanici izni verdigi anda yol kendiliginden
873
+ // devreye girer.
874
+ static bool accessibilityTrustedCached(void) {
875
+ static CFAbsoluteTime lastCheck = 0;
876
+ static bool trusted = false;
877
+ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
878
+ if (lastCheck == 0 || now - lastCheck > 1.0) {
879
+ trusted = AXIsProcessTrusted();
880
+ lastCheck = now;
881
+ }
882
+ return trusted;
883
+ }
884
+
868
885
  // Accessibility tabanlı cursor tip tespiti
869
886
  static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
870
887
  @autoreleasepool {
888
+ if (!accessibilityTrustedCached()) {
889
+ return nil;
890
+ }
891
+
871
892
  AXUIElementRef systemWide = AXUIElementCreateSystemWide();
872
893
  if (!systemWide) {
873
894
  return nil;
@@ -936,8 +936,6 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
936
936
  SCKQualityBitrateForDimensions(normalizedQuality, width, height, encoderFPS,
937
937
  &bitrate, &bpp, &minBitrate, &maxBitrate);
938
938
 
939
- NSNumber *qualityHint = [normalizedQuality isEqualToString:@"high"] ? @1.0 : ([normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85);
940
-
941
939
  MRLog(@"🎬 Screen encoder (%@): %ldx%ld@%ldfps, codec=H.264 High Profile, bitrate=%.2fMbps (%.2f bpp, min=%ldMbps max=%ldMbps)",
942
940
  normalizedQuality,
943
941
  (long)width,
@@ -957,7 +955,14 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
957
955
  // edilebilir. Kapatmak fansiz makinelerde kare dusmesini azaltir.
958
956
  AVVideoAllowFrameReorderingKey: @NO,
959
957
  AVVideoExpectedSourceFrameRateKey: @(encoderFPS),
960
- AVVideoQualityKey: qualityHint,
958
+ // AVVideoQualityKey BILEREK YOK: Apple belgesine gore yalnizca JPEG ve
959
+ // ProRes icin gecerli. H.264'te (avc1) donanim encoder'i onu sessizce
960
+ // yok sayiyordu, ama encoder yazilim yoluna dustugunde AVFoundation
961
+ // dogrulamasi sertlesiyor ve AVAssetWriterInput ISTISNA atiyor:
962
+ // "Compression property Quality is not supported for video codec type
963
+ // avc1". Istisna tum kayit kurulumunu iptal ettigi icin kayit "basladi"
964
+ // gorunup dosya hic olusmuyordu (editorde bos canvas). Kaliteyi zaten
965
+ // AVVideoAverageBitRateKey belirliyor.
961
966
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
962
967
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
963
968
  AVVideoAverageNonDroppableFrameRateKey: @(encoderFPS),
@@ -982,7 +987,75 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
982
987
  AVVideoCompressionPropertiesKey: compressionProps
983
988
  };
984
989
 
985
- g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
990
+ // AVFoundation, avc1 icin hangi sikistirma ozelligini kabul ettigine
991
+ // encoder yoluna gore karar veriyor ve reddettiginde NSError DEGIL ISTISNA
992
+ // atiyor. Istisna tum kayit kurulumunu iptal ettigi icin kayit "basladi"
993
+ // gorunup dosya hic olusmuyor; kullanici ancak editorde bos canvas gorunce
994
+ // anliyor. OLCULDU (macOS 26, 1x harici ekran, iTerm penceresi): once
995
+ // "Compression property Quality is not supported for video codec type avc1",
996
+ // o kaldirilinca "AverageNonDroppableFrameRate is not supported" — yani tek
997
+ // tek anahtar elemek guvenilir degil. Ayni ozelliklerle ayni ekrandaki baska
998
+ // pencereler sorunsuz calisiyor.
999
+ //
1000
+ // Bu yuzden ayarlar KADEMELI denenir: once tam set, sonra yalnizca her
1001
+ // encoder'in destekledigi cekirdek set, en sonda codec+boyut. Ilk kabul
1002
+ // edilen kazanir; kayit hicbir kosulda sessizce bos cikmaz.
1003
+ NSMutableArray<NSDictionary *> *settingsTiers = [NSMutableArray array];
1004
+ [settingsTiers addObject:videoSettings];
1005
+ [settingsTiers addObject:@{
1006
+ AVVideoCodecKey: AVVideoCodecTypeH264,
1007
+ AVVideoWidthKey: @(width),
1008
+ AVVideoHeightKey: @(height),
1009
+ AVVideoColorPropertiesKey: colorProps,
1010
+ AVVideoCompressionPropertiesKey: @{
1011
+ AVVideoAverageBitRateKey: @(bitrate),
1012
+ AVVideoMaxKeyFrameIntervalKey: @(encoderFPS),
1013
+ AVVideoAllowFrameReorderingKey: @NO,
1014
+ AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
1015
+ }
1016
+ }];
1017
+ [settingsTiers addObject:@{
1018
+ AVVideoCodecKey: AVVideoCodecTypeH264,
1019
+ AVVideoWidthKey: @(width),
1020
+ AVVideoHeightKey: @(height),
1021
+ AVVideoCompressionPropertiesKey: @{ AVVideoAverageBitRateKey: @(bitrate) }
1022
+ }];
1023
+ [settingsTiers addObject:@{
1024
+ AVVideoCodecKey: AVVideoCodecTypeH264,
1025
+ AVVideoWidthKey: @(width),
1026
+ AVVideoHeightKey: @(height)
1027
+ }];
1028
+
1029
+ g_videoInput = nil;
1030
+ NSString *lastRejection = nil;
1031
+ for (NSUInteger tier = 0; tier < settingsTiers.count; tier++) {
1032
+ @try {
1033
+ g_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
1034
+ outputSettings:settingsTiers[tier]];
1035
+ } @catch (NSException *exception) {
1036
+ g_videoInput = nil;
1037
+ lastRejection = exception.reason;
1038
+ NSLog(@"⚠️ Video encoder settings tier %lu rejected: %@", (unsigned long)tier, exception.reason);
1039
+ continue;
1040
+ }
1041
+ if (g_videoInput) {
1042
+ if (tier > 0) {
1043
+ NSLog(@"⚠️ Video encoder fell back to settings tier %lu (%ldx%ld); last rejection: %@",
1044
+ (unsigned long)tier, (long)width, (long)height, lastRejection);
1045
+ }
1046
+ break;
1047
+ }
1048
+ }
1049
+ if (!g_videoInput) {
1050
+ MRLog(@"❌ Video writer failed: no accepted encoder settings (%@)", lastRejection ?: @"unknown");
1051
+ if (error) {
1052
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-101 userInfo:@{
1053
+ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No accepted H.264 encoder settings: %@",
1054
+ lastRejection ?: @"unknown"]
1055
+ }];
1056
+ }
1057
+ return NO;
1058
+ }
986
1059
  g_videoInput.expectsMediaDataInRealTime = YES;
987
1060
 
988
1061
  AVAssetWriterInputPixelBufferAdaptor *pixelAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:g_videoInput sourcePixelBufferAttributes:@{
@@ -1786,6 +1859,18 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1786
1859
  (long)recordingHeight);
1787
1860
  }
1788
1861
 
1862
+ // H.264 4:2:0 luma duzlemi CIFT boyut ister; tek sayili genislik/yukseklik
1863
+ // encoder'i gereksiz yere yazilim yoluna itebiliyor. 1x harici ekranda
1864
+ // pencere boyutlari dogrudan piksele esitlendigi icin (Retina'da 2x ile
1865
+ // her zaman cift olur) tek sayilar yalnizca orada ortaya cikiyor.
1866
+ // En fazla 1 piksel kirpilir; olcek/konum degismez.
1867
+ // NOT: bu tek basina "bos kayit" hatasini cozmez — asil koruma asagidaki
1868
+ // kademeli encoder ayari geri dususudur.
1869
+ if (recordingWidth % 2 != 0) recordingWidth -= 1;
1870
+ if (recordingHeight % 2 != 0) recordingHeight -= 1;
1871
+ recordingWidth = MAX(2, recordingWidth);
1872
+ recordingHeight = MAX(2, recordingHeight);
1873
+
1789
1874
  SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
1790
1875
  streamConfig.width = recordingWidth;
1791
1876
  streamConfig.height = recordingHeight;
@@ -898,10 +898,57 @@ bool activateWindowOwnerApp(int windowId) {
898
898
  }
899
899
  }
900
900
 
901
+ // Hedef pencerenin sahibi uygulamayi ONE ALIR (Accessibility GEREKTIRMEZ).
902
+ // bringWindowToFront'un AX yolu izin isterken bu yol yalnizca
903
+ // NSRunningApplication kullanir; izin yokken tek calisan yol budur.
904
+ static bool activateOwnerAppForWindow(int windowId) {
905
+ @autoreleasepool {
906
+ // Method 2: Light activation fallback (minimal app activation)
907
+ NSLog(@" 🔄 Trying minimal activation for window %d", windowId);
908
+
909
+ // Get window info to find the process
910
+ CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
911
+ if (cgWindowList) {
912
+ NSArray *windowArray = (__bridge NSArray *)cgWindowList;
913
+
914
+ for (NSDictionary *windowInfo in windowArray) {
915
+ NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
916
+ if ([cgWindowId intValue] == windowId) {
917
+ // Get process ID
918
+ NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
919
+ if (processId) {
920
+ // Light activation - only bring app to front, don't activate all windows
921
+ NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:[processId intValue]];
922
+ if (app) {
923
+ // Use NSApplicationActivateIgnoringOtherApps only (no NSApplicationActivateAllWindows)
924
+ [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
925
+ NSLog(@" ✅ App minimally activated: PID %d (specific window should be frontmost)", [processId intValue]);
926
+ CFRelease(cgWindowList);
927
+ return true;
928
+ }
929
+ }
930
+ break;
931
+ }
932
+ }
933
+ CFRelease(cgWindowList);
934
+ }
935
+ return false;
936
+ }
937
+ }
938
+
901
939
  // Bring window to front using Accessibility API
902
940
  bool bringWindowToFront(int windowId) {
903
941
  @autoreleasepool {
904
942
  @try {
943
+ // TCC istemi CIKMASIN: untrusted bir surecin systemWide element
944
+ // uzerindeki AX cagrilari macOS'a "Erisilebilirlik" dialogunu
945
+ // actiriyor — pencere kaydi HER baslatildiginda yeniden. Izin yoksa
946
+ // AX yolunu hic deneme; hafif aktivasyon zaten ayni isi goruyor.
947
+ // AXIsProcessTrusted() kendisi istem GOSTERMEZ.
948
+ if (!AXIsProcessTrusted()) {
949
+ return activateOwnerAppForWindow(windowId);
950
+ }
951
+
905
952
  // Method 1: Using Accessibility API (most reliable)
906
953
  AXUIElementRef systemWide = AXUIElementCreateSystemWide();
907
954
  if (!systemWide) return false;
@@ -965,35 +1012,8 @@ bool bringWindowToFront(int windowId) {
965
1012
 
966
1013
  CFRelease(systemWide);
967
1014
 
968
- // Method 2: Light activation fallback (minimal app activation)
969
- NSLog(@" 🔄 Trying minimal activation for window %d", windowId);
970
-
971
- // Get window info to find the process
972
- CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
973
- if (cgWindowList) {
974
- NSArray *windowArray = (__bridge NSArray *)cgWindowList;
975
-
976
- for (NSDictionary *windowInfo in windowArray) {
977
- NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
978
- if ([cgWindowId intValue] == windowId) {
979
- // Get process ID
980
- NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
981
- if (processId) {
982
- // Light activation - only bring app to front, don't activate all windows
983
- NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:[processId intValue]];
984
- if (app) {
985
- // Use NSApplicationActivateIgnoringOtherApps only (no NSApplicationActivateAllWindows)
986
- [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
987
- NSLog(@" ✅ App minimally activated: PID %d (specific window should be frontmost)", [processId intValue]);
988
- CFRelease(cgWindowList);
989
- return true;
990
- }
991
- }
992
- break;
993
- }
994
- }
995
- CFRelease(cgWindowList);
996
- }
1015
+ // AX yolu pencereyi bulamadi izinsiz de calisan hafif aktivasyon.
1016
+ if (activateOwnerAppForWindow(windowId)) return true;
997
1017
 
998
1018
  return false;
999
1019