node-mac-recorder 2.24.5 → 2.24.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.
package/index.js CHANGED
@@ -835,8 +835,12 @@ class MacRecorder extends EventEmitter {
835
835
  };
836
836
  }
837
837
 
838
- // Manuel captureArea varsa onu kullan
839
- if (this.options.captureArea) {
838
+ // Gerçek alan kaydında crop'u native tarafa ilet. Pencere kaydında
839
+ // captureArea yalnızca cursor koordinat metadata'sıdır; ScreenCaptureKit
840
+ // desktopIndependentWindow zaten doğru pencereyi filtreler. Bunu ayrıca
841
+ // crop olarak göndermek native tarafta Retina 2x boyutu tekrar 1x
842
+ // logical pencere ölçüsüne indiriyordu.
843
+ if (this.options.captureArea && !this.options.windowId) {
840
844
  recordingOptions.captureArea = {
841
845
  x: this.options.captureArea.x,
842
846
  y: this.options.captureArea.y,
@@ -1412,6 +1416,19 @@ class MacRecorder extends EventEmitter {
1412
1416
  };
1413
1417
  }
1414
1418
 
1419
+ /**
1420
+ * Encoder'ın gerçek zamanlı yetişip yetişmediğini gösteren kare sayaçları.
1421
+ * Kayıt sırasında canlı, bittikten sonra son oturumun değerlerini döndürür.
1422
+ * dropRatio > 0.02 ise çözünürlük x FPS x bitrate bu makine için ağırdır ve
1423
+ * görüntü kalitesi sessizce düşüyor demektir.
1424
+ */
1425
+ getCaptureFrameStats() {
1426
+ if (typeof nativeBinding.getCaptureFrameStats !== "function") {
1427
+ return { appendedFrames: 0, droppedFrames: 0, targetFps: 0, dropRatio: 0 };
1428
+ }
1429
+ return nativeBinding.getCaptureFrameStats();
1430
+ }
1431
+
1415
1432
  /**
1416
1433
  * macOS'ta kayıt izinlerini kontrol eder
1417
1434
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.5",
3
+ "version": "2.24.6",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -43,6 +43,7 @@ extern "C" {
43
43
  bool hasAudioPermission();
44
44
 
45
45
  NSString *ScreenCaptureKitCurrentAudioPath(void);
46
+ void ScreenCaptureKitGetFrameStats(long *appendedOut, long *droppedOut, long *targetFPSOut);
46
47
  }
47
48
 
48
49
  // Cursor tracker function declarations
@@ -1400,6 +1401,29 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
1400
1401
  return Napi::Boolean::New(env, g_isRecording);
1401
1402
  }
1402
1403
 
1404
+ // NAPI Function: Yakalama sirasinda encoder'in dusurdugu kare istatistikleri.
1405
+ // dropRatio > ~2 ise cozunurluk x FPS x bitrate bu makine icin surdurulebilir
1406
+ // degildir ve "video kalitesiz" sikayetinin olculebilir kanitidir.
1407
+ Napi::Value GetCaptureFrameStats(const Napi::CallbackInfo& info) {
1408
+ Napi::Env env = info.Env();
1409
+ Napi::Object result = Napi::Object::New(env);
1410
+
1411
+ long appended = 0;
1412
+ long dropped = 0;
1413
+ long targetFPS = 0;
1414
+ if (@available(macOS 12.3, *)) {
1415
+ ScreenCaptureKitGetFrameStats(&appended, &dropped, &targetFPS);
1416
+ }
1417
+
1418
+ long total = appended + dropped;
1419
+ result.Set("appendedFrames", Napi::Number::New(env, (double)appended));
1420
+ result.Set("droppedFrames", Napi::Number::New(env, (double)dropped));
1421
+ result.Set("targetFps", Napi::Number::New(env, (double)targetFPS));
1422
+ result.Set("dropRatio",
1423
+ Napi::Number::New(env, total > 0 ? (double)dropped / (double)total : 0.0));
1424
+ return result;
1425
+ }
1426
+
1403
1427
  // NAPI Function: Get Window Thumbnail
1404
1428
  Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
1405
1429
  Napi::Env env = info.Env();
@@ -1738,6 +1762,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1738
1762
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1739
1763
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1740
1764
  exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
1765
+ exports.Set(Napi::String::New(env, "getCaptureFrameStats"), Napi::Function::New(env, GetCaptureFrameStats));
1741
1766
  exports.Set(Napi::String::New(env, "prewarmScreenCapture"), Napi::Function::New(env, PrewarmScreenCapture));
1742
1767
  exports.Set(Napi::String::New(env, "getVideoStartTimestamp"), Napi::Function::New(env, GetVideoStartTimestamp));
1743
1768
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
@@ -165,7 +165,17 @@ static NSInteger g_targetFPS = 60;
165
165
  static NSString *g_qualityPreset = @"high";
166
166
  static NSInteger g_frameCount = 0;
167
167
  static CFAbsoluteTime g_firstFrameTime = 0;
168
- static const NSInteger kSCKHighQualityVideoBitrate = 100 * 1000 * 1000;
168
+
169
+ // Encoder gercek zamanli yetisemezse kare SESSIZCE dusuyordu (readyForMoreMediaData
170
+ // NO -> return). Fansiz makinelerde (MacBook Air) 60 FPS Retina yakalamada bu
171
+ // gorunmez kalite kaybinin ana kaynagi; sayilmadan teshis edilemiyor.
172
+ static NSInteger g_videoFramesAppended = 0;
173
+ static NSInteger g_videoFramesDroppedNotReady = 0;
174
+ // CleanupWriters sayaclari sifirladigi icin JS tarafi kayit bittikten SONRA
175
+ // okuyabilsin diye son oturumun degerleri ayrica saklanir.
176
+ static NSInteger g_lastVideoFramesAppended = 0;
177
+ static NSInteger g_lastVideoFramesDropped = 0;
178
+ static NSInteger g_lastVideoTargetFPS = 0;
169
179
 
170
180
  // ---- Prewarm edilmis SCShareableContent onbellegi ----
171
181
  // Kayit baslatilirken envanter cagrisini tamamen atlayabilmek icin saklanir.
@@ -232,42 +242,88 @@ static CGFloat SCKQualityScaleForPreset(NSString *preset) {
232
242
  return 1.0; // High = full resolution
233
243
  }
234
244
 
245
+ // SCDisplay.width/height ve CGDisplayPixelsWide/High ölçekli Retina modlarında
246
+ // mantıksal (Quartz) çözünürlüğü döndürebiliyor. Zoom sırasında gerçek piksel
247
+ // detayını korumak için aktif display mode'un backing pixel ölçüsünü, global
248
+ // point-space frame'e böl. ScreenCaptureKit filter scale'i bazı bağımsız pencere
249
+ // filtrelerinde 1.0 raporladığından bu değer güvenilir alt sınırdır.
250
+ static CGFloat SCKBackingScaleForDisplay(SCDisplay *display) API_AVAILABLE(macos(12.3)) {
251
+ if (!display) return 1.0;
252
+
253
+ CGFloat logicalWidth = display.frame.size.width;
254
+ CGFloat logicalHeight = display.frame.size.height;
255
+ NSInteger backingWidth = 0;
256
+ NSInteger backingHeight = 0;
257
+
258
+ CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display.displayID);
259
+ if (mode) {
260
+ backingWidth = (NSInteger)CGDisplayModeGetPixelWidth(mode);
261
+ backingHeight = (NSInteger)CGDisplayModeGetPixelHeight(mode);
262
+ CGDisplayModeRelease(mode);
263
+ }
264
+
265
+ // Eski/alışılmadık display mode'larda pixel ölçüsü yoksa Quartz değerine
266
+ // düş; bu en azından non-Retina ekranlarda doğru 1x sonucu verir.
267
+ if (backingWidth <= 0) {
268
+ backingWidth = (NSInteger)CGDisplayPixelsWide(display.displayID);
269
+ }
270
+ if (backingHeight <= 0) {
271
+ backingHeight = (NSInteger)CGDisplayPixelsHigh(display.displayID);
272
+ }
273
+
274
+ CGFloat scaleX = (logicalWidth > 0 && backingWidth > 0)
275
+ ? (CGFloat)backingWidth / logicalWidth
276
+ : 1.0;
277
+ CGFloat scaleY = (logicalHeight > 0 && backingHeight > 0)
278
+ ? (CGFloat)backingHeight / logicalHeight
279
+ : 1.0;
280
+ CGFloat scale = MAX(scaleX, scaleY);
281
+ if (!isfinite(scale)) scale = 1.0;
282
+ return MIN(4.0, MAX(1.0, scale));
283
+ }
284
+
285
+ // Bitrate hesabi FPS'e DUYARLI olmali. Eski formul `w*h*multiplier` idi ve fps'i
286
+ // hic gormuyordu: kayit 30 -> 60 FPS'e cikarildiginda toplam bitrate ayni kaldigi
287
+ // icin kare basina bit yariya dustu, ustelik 180+ Mbps CABAC akisi fansiz bir
288
+ // makinede gercek zamanli encoder'i doyurup kare dusurmeye basladi. Artik hedef
289
+ // "kare basina piksel basina bit" (bpp) olarak ifade edilir; fps degisince toplam
290
+ // bitrate onunla birlikte olceklenir ve kare basina kalite SABIT kalir.
235
291
  static void SCKQualityBitrateForDimensions(NSString *preset,
236
292
  NSInteger width,
237
293
  NSInteger height,
294
+ NSInteger fps,
238
295
  NSInteger *bitrateOut,
239
- NSInteger *multiplierOut,
296
+ double *bppOut,
240
297
  NSInteger *minOut,
241
298
  NSInteger *maxOut) {
242
299
  NSString *normalized = SCKNormalizeQualityPreset(preset);
243
300
 
244
- NSInteger multiplier = 30;
245
- NSInteger minBitrate = 30 * 1000 * 1000;
246
- NSInteger maxBitrate = 120 * 1000 * 1000;
301
+ // H.264 High/CABAC 4:2:0 ekran icerigi icin 0.28 bpp zaten gorsel olarak
302
+ // doygunluk bolgesidir; asil kalite tavani bitrate degil 4:2:0 kroma alt
303
+ // ornekleme. Eski 0.53 bpp'nin ikinci yarisi kalite getirmiyor, sadece
304
+ // entropy coding + disk yazma yuku olarak realtime encoder'i zorluyordu.
305
+ double bpp = 0.28;
306
+ NSInteger minBitrate = 40 * 1000 * 1000;
307
+ NSInteger maxBitrate = 200 * 1000 * 1000;
247
308
 
248
309
  if ([normalized isEqualToString:@"low"]) {
249
- multiplier = 10;
310
+ bpp = 0.09;
250
311
  minBitrate = 10 * 1000 * 1000;
251
312
  maxBitrate = 45 * 1000 * 1000;
252
313
  } else if ([normalized isEqualToString:@"medium"]) {
253
- multiplier = 18;
254
- minBitrate = 18 * 1000 * 1000;
255
- maxBitrate = 80 * 1000 * 1000;
256
- } else { // high/default - çözünürlüğe DUYARLI yüksek kalite
257
- // Eski hali sabit 50 Mbps idi: Retina/4K kayıtta çok düşük (hatta medium
258
- // yüksek çözünürlükte high'ı geçebiliyordu). Artık çözünürlükle ölçeklenir.
259
- multiplier = 32; // ~0.53 bpp @60fps
260
- minBitrate = kSCKHighQualityVideoBitrate; // 100 Mbps taban
261
- maxBitrate = 200 * 1000 * 1000; // 200 Mbps tavan (realtime güvenli)
262
- }
263
-
264
- double base = ((double)MAX(1, width)) * ((double)MAX(1, height)) * (double)multiplier;
314
+ bpp = 0.16;
315
+ minBitrate = 20 * 1000 * 1000;
316
+ maxBitrate = 90 * 1000 * 1000;
317
+ }
318
+
319
+ double base = ((double)MAX(1, width)) * ((double)MAX(1, height)) *
320
+ ((double)MAX(1, fps)) * bpp;
265
321
  NSInteger bitrate = (NSInteger)base;
266
322
  if (bitrate < minBitrate) bitrate = minBitrate;
267
323
  if (bitrate > maxBitrate) bitrate = maxBitrate;
268
324
 
269
325
  if (bitrateOut) *bitrateOut = bitrate;
270
- if (multiplierOut) *multiplierOut = multiplier;
326
+ if (bppOut) *bppOut = bpp;
271
327
  if (minOut) *minOut = minBitrate;
272
328
  if (maxOut) *maxOut = maxBitrate;
273
329
  }
@@ -409,9 +465,31 @@ static void CleanupWriters(void) {
409
465
  g_videoWriterStarted = NO;
410
466
  g_videoStartTime = kCMTimeInvalid;
411
467
 
468
+ // Kayit sonu ozeti: kare dusme orani gorunur kalsin, aksi halde "video
469
+ // kalitesiz" sikayeti olcusuz kaliyor.
470
+ NSInteger totalSeen = g_videoFramesAppended + g_videoFramesDroppedNotReady;
471
+ if (totalSeen > 0) {
472
+ g_lastVideoFramesAppended = g_videoFramesAppended;
473
+ g_lastVideoFramesDropped = g_videoFramesDroppedNotReady;
474
+ g_lastVideoTargetFPS = MAX(1, g_targetFPS);
475
+ double dropRatio = 100.0 * g_videoFramesDroppedNotReady / totalSeen;
476
+ MRLog(@"📊 Kayit ozeti: %ld kare yazildi, %ld dusuruldu (%%%.1f) @%ldfps hedef",
477
+ (long)g_videoFramesAppended,
478
+ (long)g_videoFramesDroppedNotReady,
479
+ dropRatio,
480
+ (long)MAX(1, g_targetFPS));
481
+ if (dropRatio > 2.0) {
482
+ NSLog(@"⚠️ Encoder yuku surdurulebilir degil: kare dusme orani %%%.1f. "
483
+ @"Cozunurluk/FPS/bitrate kombinasyonu bu makine icin agir.",
484
+ dropRatio);
485
+ }
486
+ }
487
+
412
488
  // Reset frame counting
413
489
  g_frameCount = 0;
414
490
  g_firstFrameTime = 0;
491
+ g_videoFramesAppended = 0;
492
+ g_videoFramesDroppedNotReady = 0;
415
493
  }
416
494
 
417
495
  if (g_audioWriter) {
@@ -448,6 +526,23 @@ extern "C" BOOL MRMixAudioToSingleTrackWithGains(NSString *primaryAudioPath,
448
526
  float systemGain);
449
527
  extern "C" BOOL MRMuxAudioIntoVideo(NSString *videoPath, NSString *audioPath);
450
528
 
529
+ // Encoder'in gercek zamanli yetisip yetismedigini JS tarafina tasir. Kayit
530
+ // suruyorsa canli sayaclar, bittiyse son oturumun degerleri dondurulur.
531
+ extern "C" void ScreenCaptureKitGetFrameStats(long *appendedOut,
532
+ long *droppedOut,
533
+ long *targetFPSOut) {
534
+ BOOL live = (g_videoFramesAppended + g_videoFramesDroppedNotReady) > 0;
535
+ if (appendedOut) {
536
+ *appendedOut = (long)(live ? g_videoFramesAppended : g_lastVideoFramesAppended);
537
+ }
538
+ if (droppedOut) {
539
+ *droppedOut = (long)(live ? g_videoFramesDroppedNotReady : g_lastVideoFramesDropped);
540
+ }
541
+ if (targetFPSOut) {
542
+ *targetFPSOut = (long)(live ? MAX(1, g_targetFPS) : g_lastVideoTargetFPS);
543
+ }
544
+ }
545
+
451
546
  extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
452
547
  if (!g_audioOutputPath) {
453
548
  return nil;
@@ -545,9 +640,21 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
545
640
  }
546
641
 
547
642
  if (!g_videoInput.readyForMoreMediaData) {
643
+ // Encoder geride kaldi: bu kare kalici olarak kayboluyor. Sessizce
644
+ // dusurmek kalite sikayetlerini teshis edilemez hale getiriyordu; oran
645
+ // %2'yi asarsa encoder yuku (cozunurluk x fps x bitrate) surdurulebilir
646
+ // degil demektir.
647
+ g_videoFramesDroppedNotReady++;
648
+ NSInteger totalSeen = g_videoFramesAppended + g_videoFramesDroppedNotReady;
649
+ if (g_videoFramesDroppedNotReady == 1 || (g_videoFramesDroppedNotReady % 60) == 0) {
650
+ MRLog(@"⚠️ Encoder yetisemedi, kare dusuruldu: %ld/%ld (%.1f%%)",
651
+ (long)g_videoFramesDroppedNotReady,
652
+ (long)totalSeen,
653
+ totalSeen > 0 ? (100.0 * g_videoFramesDroppedNotReady / totalSeen) : 0.0);
654
+ }
548
655
  return;
549
656
  }
550
-
657
+
551
658
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
552
659
  if (!pixelBuffer) {
553
660
  return;
@@ -596,6 +703,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
596
703
  BOOL appended = [adaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativePresentation];
597
704
  if (!appended) {
598
705
  NSLog(@"⚠️ Failed appending pixel buffer: %@", g_videoWriter.error);
706
+ } else {
707
+ g_videoFramesAppended++;
599
708
  }
600
709
 
601
710
  // Frame rate debugging
@@ -819,30 +928,39 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
819
928
  }
820
929
 
821
930
  NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
931
+ NSInteger encoderFPS = MAX(1, g_targetFPS);
822
932
  NSInteger bitrate = 0;
933
+ double bpp = 0.0;
823
934
  NSInteger minBitrate = 0;
824
935
  NSInteger maxBitrate = 0;
825
- SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, NULL, &minBitrate, &maxBitrate);
936
+ SCKQualityBitrateForDimensions(normalizedQuality, width, height, encoderFPS,
937
+ &bitrate, &bpp, &minBitrate, &maxBitrate);
826
938
 
827
939
  NSNumber *qualityHint = [normalizedQuality isEqualToString:@"high"] ? @1.0 : ([normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85);
828
940
 
829
- MRLog(@"🎬 Screen encoder (%@): %ldx%ld, codec=H.264 High Profile, bitrate=%.2fMbps (min=%ldMbps max=%ldMbps)",
941
+ MRLog(@"🎬 Screen encoder (%@): %ldx%ld@%ldfps, codec=H.264 High Profile, bitrate=%.2fMbps (%.2f bpp, min=%ldMbps max=%ldMbps)",
830
942
  normalizedQuality,
831
943
  (long)width,
832
944
  (long)height,
945
+ (long)encoderFPS,
833
946
  bitrate / (1000.0 * 1000.0),
947
+ bpp,
834
948
  (long)(minBitrate / (1000 * 1000)),
835
949
  (long)(maxBitrate / (1000 * 1000)));
836
950
 
837
951
  NSDictionary *compressionProps = @{
838
952
  AVVideoAverageBitRateKey: @(bitrate),
839
- AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
840
- AVVideoAllowFrameReorderingKey: @YES,
841
- AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
953
+ AVVideoMaxKeyFrameIntervalKey: @(encoderFPS),
954
+ // B-frame'ler (kare yeniden siralama) gercek zamanli yakalamada encoder'a
955
+ // ek gecikme + is yukleyip readyForMoreMediaData'yi geciktiriyor; ekran
956
+ // icerigi zaten neredeyse tamamen statik/kesme oldugu icin kazanci ihmal
957
+ // edilebilir. Kapatmak fansiz makinelerde kare dusmesini azaltir.
958
+ AVVideoAllowFrameReorderingKey: @NO,
959
+ AVVideoExpectedSourceFrameRateKey: @(encoderFPS),
842
960
  AVVideoQualityKey: qualityHint,
843
961
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
844
962
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
845
- AVVideoAverageNonDroppableFrameRateKey: @(MAX(1, g_targetFPS)),
963
+ AVVideoAverageNonDroppableFrameRateKey: @(encoderFPS),
846
964
  AVVideoMaxKeyFrameIntervalDurationKey: @(1.0)
847
965
  };
848
966
 
@@ -1490,31 +1608,41 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1490
1608
  CGRect dispBounds = CGRectMake(disp.frame.origin.x, disp.frame.origin.y,
1491
1609
  disp.frame.size.width, disp.frame.size.height);
1492
1610
  if (CGRectContainsPoint(dispBounds, windowCenter)) {
1493
- NSInteger physW = (NSInteger)CGDisplayPixelsWide(disp.displayID);
1494
- NSInteger physH = (NSInteger)CGDisplayPixelsHigh(disp.displayID);
1495
- CGFloat scX = (disp.width > 0) ? (CGFloat)physW / (CGFloat)disp.width : 1.0;
1496
- CGFloat scY = (disp.height > 0) ? (CGFloat)physH / (CGFloat)disp.height : 1.0;
1497
- scaleFactor = MAX(scX, scY);
1611
+ scaleFactor = SCKBackingScaleForDisplay(disp);
1498
1612
  break;
1499
1613
  }
1500
1614
  }
1501
1615
  // Fallback: use main display scale factor
1502
1616
  if (scaleFactor == 1.0 && content.displays.count > 0) {
1503
1617
  SCDisplay *mainDisp = content.displays.firstObject;
1504
- NSInteger physW = (NSInteger)CGDisplayPixelsWide(mainDisp.displayID);
1505
- NSInteger physH = (NSInteger)CGDisplayPixelsHigh(mainDisp.displayID);
1506
- CGFloat scX = (mainDisp.width > 0) ? (CGFloat)physW / (CGFloat)mainDisp.width : 1.0;
1507
- CGFloat scY = (mainDisp.height > 0) ? (CGFloat)physH / (CGFloat)mainDisp.height : 1.0;
1508
- scaleFactor = MAX(scX, scY);
1618
+ scaleFactor = SCKBackingScaleForDisplay(mainDisp);
1509
1619
  }
1510
1620
 
1511
- NSInteger physicalWindowWidth = (NSInteger)(windowLogicalWidth * scaleFactor);
1512
- NSInteger physicalWindowHeight = (NSInteger)(windowLogicalHeight * scaleFactor);
1621
+ filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
1622
+ if (@available(macOS 14.0, *)) {
1623
+ // ScreenCaptureKit exposes the authoritative point→pixel
1624
+ // conversion for this exact filter. Prefer it over display
1625
+ // heuristics (scaled modes and windows spanning displays).
1626
+ CGFloat filterScale = filter.pointPixelScale;
1627
+ if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
1628
+ // desktopIndependentWindow filtresi bazı macOS/sürüm
1629
+ // kombinasyonlarında Retina pencerede bile 1.0 dönebiliyor.
1630
+ // Display geometrisinden bulunan 2x değeri 1x'e indirirsek
1631
+ // 1500x960 pencere gerçek 3000x1920 yerine logical boyutta
1632
+ // kaydediliyor ve zoom'da metin detayı geri dönülemez
1633
+ // biçimde kayboluyor. Filter yalnızca daha yüksek/güvenli
1634
+ // bir backing scale bildiriyorsa heuristiği yükseltsin.
1635
+ scaleFactor = MAX(scaleFactor, filterScale);
1636
+ }
1637
+ }
1638
+ scaleFactor = MIN(4.0, MAX(1.0, scaleFactor));
1639
+
1640
+ NSInteger physicalWindowWidth = (NSInteger)llround(windowLogicalWidth * scaleFactor);
1641
+ NSInteger physicalWindowHeight = (NSInteger)llround(windowLogicalHeight * scaleFactor);
1513
1642
 
1514
1643
  MRLog(@"🪟 Recording window: %@ logical=%ux%u, physical=%ldx%ld, scale=%.2fx",
1515
1644
  targetWindow.title, (unsigned)windowLogicalWidth, (unsigned)windowLogicalHeight,
1516
1645
  (long)physicalWindowWidth, (long)physicalWindowHeight, scaleFactor);
1517
- filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
1518
1646
  recordingWidth = physicalWindowWidth;
1519
1647
  recordingHeight = physicalWindowHeight;
1520
1648
  } else {
@@ -1550,22 +1678,36 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1550
1678
  }
1551
1679
 
1552
1680
  if (targetDisplay) {
1553
- // Use physical pixel dimensions for Retina displays (not logical points)
1554
1681
  CGDirectDisplayID displayID = targetDisplay.displayID;
1555
- NSInteger physicalWidth = (NSInteger)CGDisplayPixelsWide(displayID);
1556
- NSInteger physicalHeight = (NSInteger)CGDisplayPixelsHigh(displayID);
1557
- NSInteger logicalWidth = targetDisplay.width;
1558
- NSInteger logicalHeight = targetDisplay.height;
1682
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1559
1683
 
1560
- CGFloat scaleX = (logicalWidth > 0) ? (CGFloat)physicalWidth / (CGFloat)logicalWidth : 1.0;
1561
- CGFloat scaleY = (logicalHeight > 0) ? (CGFloat)physicalHeight / (CGFloat)logicalHeight : 1.0;
1562
- CGFloat scaleFactor = MAX(scaleX, scaleY);
1684
+ // CGDisplayPixelsWide/High reports the current Quartz coordinate
1685
+ // resolution on scaled Retina modes (for example 1800x1169), not
1686
+ // necessarily the backing pixels ScreenCaptureKit can deliver.
1687
+ // The filter exposes the authoritative point -> pixel scale and
1688
+ // content rect, so derive the requested stream size from those.
1689
+ CGFloat logicalWidth = targetDisplay.frame.size.width;
1690
+ CGFloat logicalHeight = targetDisplay.frame.size.height;
1691
+ CGFloat scaleFactor = SCKBackingScaleForDisplay(targetDisplay);
1692
+ if (@available(macOS 14.0, *)) {
1693
+ CGRect filterRect = filter.contentRect;
1694
+ CGFloat filterScale = filter.pointPixelScale;
1695
+ if (filterRect.size.width > 0 && filterRect.size.height > 0) {
1696
+ logicalWidth = filterRect.size.width;
1697
+ logicalHeight = filterRect.size.height;
1698
+ }
1699
+ if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
1700
+ scaleFactor = MAX(scaleFactor, filterScale);
1701
+ }
1702
+ }
1703
+ scaleFactor = MIN(4.0, MAX(1.0, scaleFactor));
1563
1704
 
1564
- MRLog(@"🖥️ Recording display %u: logical=%dx%d, physical=%ldx%ld, scale=%.2fx",
1565
- displayID, (int)logicalWidth, (int)logicalHeight,
1705
+ NSInteger physicalWidth = (NSInteger)llround(logicalWidth * scaleFactor);
1706
+ NSInteger physicalHeight = (NSInteger)llround(logicalHeight * scaleFactor);
1707
+ MRLog(@"🖥️ Recording display %u: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx (filter-backed)",
1708
+ displayID, logicalWidth, logicalHeight,
1566
1709
  (long)physicalWidth, (long)physicalHeight, scaleFactor);
1567
1710
 
1568
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1569
1711
  recordingWidth = physicalWidth;
1570
1712
  recordingHeight = physicalHeight;
1571
1713
  } else {
@@ -1575,19 +1717,28 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1575
1717
  }
1576
1718
  }
1577
1719
 
1578
- if (captureRect && captureRect[@"width"] && captureRect[@"height"]) {
1720
+ // desktopIndependentWindow kendi içerik sınırını zaten uygular. JS
1721
+ // katmanındaki window captureArea cursor metadata'sı olarak tutulabilir;
1722
+ // burada tekrar crop boyutu gibi kullanılırsa yukarıda hesaplanan Retina
1723
+ // physicalWindowWidth/Height logical 1x ölçülerle ezilir. Bu blok yalnızca
1724
+ // gerçek display/area kayıtlarında çıktı ölçüsünü değiştirmeli.
1725
+ BOOL isWindowCapture = windowId && [windowId integerValue] != 0;
1726
+ if (!isWindowCapture && captureRect && captureRect[@"width"] && captureRect[@"height"]) {
1579
1727
  CGFloat cropWidth = [captureRect[@"width"] doubleValue];
1580
1728
  CGFloat cropHeight = [captureRect[@"height"] doubleValue];
1581
1729
  if (cropWidth > 0 && cropHeight > 0) {
1582
1730
  // Scale crop dimensions for Retina displays
1583
1731
  CGFloat cropScaleFactor = 1.0;
1584
1732
  if (targetDisplay) {
1585
- NSInteger physW = (NSInteger)CGDisplayPixelsWide(targetDisplay.displayID);
1586
- NSInteger physH = (NSInteger)CGDisplayPixelsHigh(targetDisplay.displayID);
1587
- CGFloat scX = (targetDisplay.width > 0) ? (CGFloat)physW / (CGFloat)targetDisplay.width : 1.0;
1588
- CGFloat scY = (targetDisplay.height > 0) ? (CGFloat)physH / (CGFloat)targetDisplay.height : 1.0;
1589
- cropScaleFactor = MAX(scX, scY);
1733
+ cropScaleFactor = SCKBackingScaleForDisplay(targetDisplay);
1734
+ if (@available(macOS 14.0, *)) {
1735
+ CGFloat filterScale = filter.pointPixelScale;
1736
+ if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
1737
+ cropScaleFactor = MAX(cropScaleFactor, filterScale);
1738
+ }
1739
+ }
1590
1740
  }
1741
+ cropScaleFactor = MIN(4.0, MAX(1.0, cropScaleFactor));
1591
1742
  NSInteger physicalCropWidth = (NSInteger)(cropWidth * cropScaleFactor);
1592
1743
  NSInteger physicalCropHeight = (NSInteger)(cropHeight * cropScaleFactor);
1593
1744
  MRLog(@"🔲 Crop area: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx at (%.0f,%.0f)",