node-mac-recorder 2.24.5 → 2.24.7

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,44 @@ class MacRecorder extends EventEmitter {
1412
1416
  };
1413
1417
  }
1414
1418
 
1419
+ /**
1420
+ * Pencere kaydında hedef pencerenin uygulamasını aktive eder.
1421
+ *
1422
+ * Kayıt başlarken kaydedilen uygulama pasif kalırsa, odağını kaybedince
1423
+ * gizlenen pencereler (iTerm2 hotkey window vb.) kendini gizler ve kayıtta
1424
+ * görünmez. Kayıt komutu verildiğinde odak bizde olduğu için bu adım şart.
1425
+ *
1426
+ * @param {number} windowId CGWindowID
1427
+ * @returns {boolean}
1428
+ */
1429
+ activateWindowOwnerApp(windowId) {
1430
+ const id = Number(windowId);
1431
+ if (!Number.isFinite(id) || id <= 0) return false;
1432
+ if (typeof nativeBinding.activateWindowOwnerApp !== "function") return false;
1433
+ try {
1434
+ return !!nativeBinding.activateWindowOwnerApp(id);
1435
+ } catch (error) {
1436
+ console.warn(
1437
+ "[MacRecorder] activateWindowOwnerApp başarısız:",
1438
+ error?.message || error,
1439
+ );
1440
+ return false;
1441
+ }
1442
+ }
1443
+
1444
+ /**
1445
+ * Encoder'ın gerçek zamanlı yetişip yetişmediğini gösteren kare sayaçları.
1446
+ * Kayıt sırasında canlı, bittikten sonra son oturumun değerlerini döndürür.
1447
+ * dropRatio > 0.02 ise çözünürlük x FPS x bitrate bu makine için ağırdır ve
1448
+ * görüntü kalitesi sessizce düşüyor demektir.
1449
+ */
1450
+ getCaptureFrameStats() {
1451
+ if (typeof nativeBinding.getCaptureFrameStats !== "function") {
1452
+ return { appendedFrames: 0, droppedFrames: 0, targetFps: 0, dropRatio: 0 };
1453
+ }
1454
+ return nativeBinding.getCaptureFrameStats();
1455
+ }
1456
+
1415
1457
  /**
1416
1458
  * macOS'ta kayıt izinlerini kontrol eder
1417
1459
  */
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.7",
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
 
@@ -1482,6 +1600,7 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1482
1600
 
1483
1601
  // Find display containing this window to get scale factor
1484
1602
  CGFloat scaleFactor = 1.0;
1603
+ SCDisplay *windowDisplay = nil;
1485
1604
  CGPoint windowCenter = CGPointMake(
1486
1605
  targetWindow.frame.origin.x + windowLogicalWidth / 2.0,
1487
1606
  targetWindow.frame.origin.y + windowLogicalHeight / 2.0
@@ -1490,31 +1609,52 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1490
1609
  CGRect dispBounds = CGRectMake(disp.frame.origin.x, disp.frame.origin.y,
1491
1610
  disp.frame.size.width, disp.frame.size.height);
1492
1611
  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);
1612
+ windowDisplay = disp;
1613
+ scaleFactor = SCKBackingScaleForDisplay(disp);
1498
1614
  break;
1499
1615
  }
1500
1616
  }
1501
1617
  // Fallback: use main display scale factor
1502
1618
  if (scaleFactor == 1.0 && content.displays.count > 0) {
1503
1619
  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);
1620
+ if (!windowDisplay) windowDisplay = mainDisp;
1621
+ scaleFactor = SCKBackingScaleForDisplay(mainDisp);
1622
+ }
1623
+
1624
+ // DOGRUDAN pencere yakalama. Ekrandan kirpma/kompozit YOK: bu
1625
+ // filtre pencereyi kendi basina yakalar, pencere tasinsa da takip
1626
+ // eder. (Display kompozit alternatifi denendi ve reddedildi.)
1627
+ filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
1628
+
1629
+ CGFloat displayScale = scaleFactor;
1630
+ CGFloat filterScaleLog = -1.0;
1631
+ if (@available(macOS 14.0, *)) {
1632
+ // filter.pointPixelScale, SCK'nin bu filtre icin GERCEKTEN
1633
+ // uretecegi point->pixel oranidir; tek yetkili kaynak odur.
1634
+ //
1635
+ // Eskiden MAX(displayScale, filterScale) aliniyordu. Bu yanlis:
1636
+ // olculdu ki harici 3440x1440 (1x) ekranda display heuristigi
1637
+ // 2.00 donerken filtre dogru sekilde 1.00 donuyor. MAX ile 2x
1638
+ // istenince SCK yine 1x uretiyor ve icerik karenin sol ust
1639
+ // ceyregine sikisip geri kalani siyah kaliyordu.
1640
+ // Retina'da filtre zaten 2.00 donuyor (olculdu), yani Retina
1641
+ // keskinligi kaybi YOK.
1642
+ CGFloat filterScale = filter.pointPixelScale;
1643
+ filterScaleLog = filterScale;
1644
+ if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
1645
+ scaleFactor = filterScale;
1646
+ }
1509
1647
  }
1648
+ scaleFactor = MIN(4.0, MAX(1.0, scaleFactor));
1510
1649
 
1511
- NSInteger physicalWindowWidth = (NSInteger)(windowLogicalWidth * scaleFactor);
1512
- NSInteger physicalWindowHeight = (NSInteger)(windowLogicalHeight * scaleFactor);
1650
+ NSInteger physicalWindowWidth = (NSInteger)llround(windowLogicalWidth * scaleFactor);
1651
+ NSInteger physicalWindowHeight = (NSInteger)llround(windowLogicalHeight * scaleFactor);
1513
1652
 
1514
- MRLog(@"🪟 Recording window: %@ logical=%ux%u, physical=%ldx%ld, scale=%.2fx",
1653
+ MRLog(@"🪟 Recording window: %@ logical=%ux%u, physical=%ldx%ld, scale=%.2fx "
1654
+ @"(displayScale=%.2f filterScale=%.2f)",
1515
1655
  targetWindow.title, (unsigned)windowLogicalWidth, (unsigned)windowLogicalHeight,
1516
- (long)physicalWindowWidth, (long)physicalWindowHeight, scaleFactor);
1517
- filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
1656
+ (long)physicalWindowWidth, (long)physicalWindowHeight, scaleFactor,
1657
+ displayScale, filterScaleLog);
1518
1658
  recordingWidth = physicalWindowWidth;
1519
1659
  recordingHeight = physicalWindowHeight;
1520
1660
  } else {
@@ -1550,22 +1690,40 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1550
1690
  }
1551
1691
 
1552
1692
  if (targetDisplay) {
1553
- // Use physical pixel dimensions for Retina displays (not logical points)
1554
1693
  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;
1694
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1559
1695
 
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);
1696
+ // CGDisplayPixelsWide/High reports the current Quartz coordinate
1697
+ // resolution on scaled Retina modes (for example 1800x1169), not
1698
+ // necessarily the backing pixels ScreenCaptureKit can deliver.
1699
+ // The filter exposes the authoritative point -> pixel scale and
1700
+ // content rect, so derive the requested stream size from those.
1701
+ CGFloat logicalWidth = targetDisplay.frame.size.width;
1702
+ CGFloat logicalHeight = targetDisplay.frame.size.height;
1703
+ CGFloat scaleFactor = SCKBackingScaleForDisplay(targetDisplay);
1704
+ if (@available(macOS 14.0, *)) {
1705
+ CGRect filterRect = filter.contentRect;
1706
+ CGFloat filterScale = filter.pointPixelScale;
1707
+ if (filterRect.size.width > 0 && filterRect.size.height > 0) {
1708
+ logicalWidth = filterRect.size.width;
1709
+ logicalHeight = filterRect.size.height;
1710
+ }
1711
+ // Pencere dalindaki ile ayni kural: SCK'nin uretecegi olcek
1712
+ // filtreden okunur. MAX(display, filter) yanlisti — harici
1713
+ // 3440x1440 (1x) ekranda display heuristigi 2.00 donuyor ve
1714
+ // istenen boyut SCK'nin urettiginin iki kati oluyordu.
1715
+ if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
1716
+ scaleFactor = filterScale;
1717
+ }
1718
+ }
1719
+ scaleFactor = MIN(4.0, MAX(1.0, scaleFactor));
1563
1720
 
1564
- MRLog(@"🖥️ Recording display %u: logical=%dx%d, physical=%ldx%ld, scale=%.2fx",
1565
- displayID, (int)logicalWidth, (int)logicalHeight,
1721
+ NSInteger physicalWidth = (NSInteger)llround(logicalWidth * scaleFactor);
1722
+ NSInteger physicalHeight = (NSInteger)llround(logicalHeight * scaleFactor);
1723
+ MRLog(@"🖥️ Recording display %u: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx (filter-backed)",
1724
+ displayID, logicalWidth, logicalHeight,
1566
1725
  (long)physicalWidth, (long)physicalHeight, scaleFactor);
1567
1726
 
1568
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1569
1727
  recordingWidth = physicalWidth;
1570
1728
  recordingHeight = physicalHeight;
1571
1729
  } else {
@@ -1575,19 +1733,29 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1575
1733
  }
1576
1734
  }
1577
1735
 
1578
- if (captureRect && captureRect[@"width"] && captureRect[@"height"]) {
1736
+ // desktopIndependentWindow kendi içerik sınırını zaten uygular. JS
1737
+ // katmanındaki window captureArea cursor metadata'sı olarak tutulabilir;
1738
+ // burada tekrar crop boyutu gibi kullanılırsa yukarıda hesaplanan Retina
1739
+ // physicalWindowWidth/Height logical 1x ölçülerle ezilir. Bu blok yalnızca
1740
+ // gerçek display/area kayıtlarında çıktı ölçüsünü değiştirmeli.
1741
+ BOOL isWindowCapture = windowId && [windowId integerValue] != 0;
1742
+ if (!isWindowCapture && captureRect && captureRect[@"width"] && captureRect[@"height"]) {
1579
1743
  CGFloat cropWidth = [captureRect[@"width"] doubleValue];
1580
1744
  CGFloat cropHeight = [captureRect[@"height"] doubleValue];
1581
1745
  if (cropWidth > 0 && cropHeight > 0) {
1582
1746
  // Scale crop dimensions for Retina displays
1583
1747
  CGFloat cropScaleFactor = 1.0;
1584
1748
  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);
1749
+ cropScaleFactor = SCKBackingScaleForDisplay(targetDisplay);
1750
+ if (@available(macOS 14.0, *)) {
1751
+ // Ekran/pencere dallariyla ayni kural: yetkili kaynak filtre.
1752
+ CGFloat filterScale = filter.pointPixelScale;
1753
+ if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
1754
+ cropScaleFactor = filterScale;
1755
+ }
1756
+ }
1590
1757
  }
1758
+ cropScaleFactor = MIN(4.0, MAX(1.0, cropScaleFactor));
1591
1759
  NSInteger physicalCropWidth = (NSInteger)(cropWidth * cropScaleFactor);
1592
1760
  NSInteger physicalCropHeight = (NSInteger)(cropHeight * cropScaleFactor);
1593
1761
  MRLog(@"🔲 Crop area: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx at (%.0f,%.0f)",
@@ -1628,11 +1796,19 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1628
1796
  streamConfig.queueDepth = 8;
1629
1797
  }
1630
1798
  if (@available(macOS 14.0, *)) {
1631
- // Use best capture resolution for maximum quality on Retina displays
1632
- streamConfig.captureResolution = SCCaptureResolutionBest;
1633
- // Make stream opaque to avoid alpha channel overhead
1799
+ // captureResolution = SCCaptureResolutionBest BILEREK KULLANILMIYOR.
1800
+ //
1801
+ // Retina kalitesi icin eklenmisti ama GPU (Metal) ile cizen
1802
+ // uygulamalarda pencere kaydinda ICERIGI TAMAMEN DUSURUYOR: iTerm2'de
1803
+ // sekme cubugu/bolme basliklari (AppKit) geliyor, oturum icerigi
1804
+ // (Metal katmani) simsiyah cikiyordu. Olculdu: ayni pencere, ayni
1805
+ // kosullar, sadece bu satir kapaliyken icerik geri geldi.
1806
+ // Cozunurluk zaten streamConfig.width/height ile isteniyor; bu ayara
1807
+ // ihtiyac yok.
1808
+ //
1809
+ // shouldBeOpaque ve colorSpaceName ayni deneyde TEST EDILDI ve
1810
+ // masum cikti (ikisi de acikken icerik geliyor) - dokunulmadi.
1634
1811
  streamConfig.shouldBeOpaque = YES;
1635
- MRLog(@"🎯 Using SCCaptureResolutionBest + shouldBeOpaque for maximum quality (macOS 14+)");
1636
1812
  }
1637
1813
  if (@available(macOS 13.0, *)) {
1638
1814
  // Frame'leri bilinen bir renk uzayında (sRGB) iste; encoder tarafında
@@ -239,6 +239,7 @@ void updateOverlay();
239
239
  NSDictionary* getWindowUnderCursor(CGPoint point);
240
240
  NSArray* getAllSelectableWindows();
241
241
  bool bringWindowToFront(int windowId);
242
+ bool activateWindowOwnerApp(int windowId);
242
243
  void cleanupRecordingPreview();
243
244
  bool showRecordingPreview(NSDictionary *windowInfo);
244
245
  bool hideRecordingPreview();
@@ -842,6 +843,61 @@ static void ApplyBrandButtonStyle(NSButton *button) {
842
843
 
843
844
  static WindowSelectorDelegate *g_delegate = nil;
844
845
 
846
+ // Pencerenin SAHIBI UYGULAMAYI aktive eder.
847
+ //
848
+ // bringWindowToFront() yalnizca AXRaise + AXFocused yapar; bu pencereyi one
849
+ // getirir ama uygulamayi AKTIF hale getirmez. Kayit basladiginda kaydedilen
850
+ // uygulama pasif kalirsa "odagi kaybedince gizlen" davranisindaki pencereler
851
+ // (iTerm2 hotkey window vb.) kendini gizler ve kayitta gorunmez. Pencere kaydi
852
+ // baslarken hedef uygulama gercekten aktif olmalidir ki kullanici icine
853
+ // yazabilsin.
854
+ bool activateWindowOwnerApp(int windowId) {
855
+ @autoreleasepool {
856
+ @try {
857
+ CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
858
+ if (!cgWindowList) return false;
859
+
860
+ NSArray *windowArray = (__bridge NSArray *)cgWindowList;
861
+ pid_t ownerPid = 0;
862
+ for (NSDictionary *windowInfo in windowArray) {
863
+ NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
864
+ if ([cgWindowId intValue] == windowId) {
865
+ NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
866
+ ownerPid = (pid_t)[processId intValue];
867
+ break;
868
+ }
869
+ }
870
+ CFRelease(cgWindowList);
871
+
872
+ if (ownerPid <= 0) {
873
+ NSLog(@"⚠️ activateWindowOwnerApp: window %d icin PID bulunamadi", windowId);
874
+ return false;
875
+ }
876
+
877
+ NSRunningApplication *app =
878
+ [NSRunningApplication runningApplicationWithProcessIdentifier:ownerPid];
879
+ if (!app) {
880
+ NSLog(@"⚠️ activateWindowOwnerApp: PID %d icin uygulama yok", ownerPid);
881
+ return false;
882
+ }
883
+
884
+ // Once uygulamayi aktive et, sonra hedef pencereyi one kaldir.
885
+ // Ters sirada yapilirsa aktivasyon uygulamanin kendi ana penceresini
886
+ // one getirip secilen pencereyi geri plana itebiliyor.
887
+ BOOL activated = [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
888
+ NSLog(@"🔝 activateWindowOwnerApp: PID %d aktive=%d (window %d)",
889
+ ownerPid, activated, windowId);
890
+
891
+ bringWindowToFront(windowId);
892
+ return activated ? true : false;
893
+
894
+ } @catch (NSException *exception) {
895
+ NSLog(@"❌ activateWindowOwnerApp exception: %@", exception.reason);
896
+ return false;
897
+ }
898
+ }
899
+ }
900
+
845
901
  // Bring window to front using Accessibility API
846
902
  bool bringWindowToFront(int windowId) {
847
903
  @autoreleasepool {
@@ -2696,6 +2752,24 @@ Napi::Value BringWindowToFront(const Napi::CallbackInfo& info) {
2696
2752
  }
2697
2753
  }
2698
2754
 
2755
+ // NAPI Function: Pencerenin sahibi uygulamayi aktive et (pencere kaydi icin)
2756
+ Napi::Value ActivateWindowOwnerApp(const Napi::CallbackInfo& info) {
2757
+ Napi::Env env = info.Env();
2758
+
2759
+ if (info.Length() < 1 || !info[0].IsNumber()) {
2760
+ Napi::TypeError::New(env, "Window ID required").ThrowAsJavaScriptException();
2761
+ return env.Null();
2762
+ }
2763
+
2764
+ int windowId = info[0].As<Napi::Number>().Int32Value();
2765
+
2766
+ @try {
2767
+ return Napi::Boolean::New(env, activateWindowOwnerApp(windowId));
2768
+ } @catch (NSException *exception) {
2769
+ return Napi::Boolean::New(env, false);
2770
+ }
2771
+ }
2772
+
2699
2773
  // NAPI Function: Enable/Disable Auto Bring To Front
2700
2774
  Napi::Value SetBringToFrontEnabled(const Napi::CallbackInfo& info) {
2701
2775
  Napi::Env env = info.Env();
@@ -2937,6 +3011,7 @@ Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
2937
3011
  exports.Set("getSelectedWindowInfo", Napi::Function::New(env, GetSelectedWindowInfo));
2938
3012
  exports.Set("getWindowSelectionStatus", Napi::Function::New(env, GetWindowSelectionStatus));
2939
3013
  exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
3014
+ exports.Set("activateWindowOwnerApp", Napi::Function::New(env, ActivateWindowOwnerApp));
2940
3015
  exports.Set("setBringToFrontEnabled", Napi::Function::New(env, SetBringToFrontEnabled));
2941
3016
  exports.Set("showRecordingPreview", Napi::Function::New(env, ShowRecordingPreview));
2942
3017
  exports.Set("hideRecordingPreview", Napi::Function::New(env, HideRecordingPreview));